#!/bin/bash

set -eu
set -o pipefail

# Library of functions for the kayobe development environment.

# Configuration

function config_defaults {
    # Set default values for kayobe development configuration.

    PARENT="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
    KAYOBE_SOURCE_PATH_DEFAULT="$(dirname ${PARENT})"

    # Path to the kayobe source code repository. Typically this will be the
    # Vagrant shared directory.
    export KAYOBE_SOURCE_PATH="${KAYOBE_SOURCE_PATH:-$KAYOBE_SOURCE_PATH_DEFAULT}"

    # Path to the kayobe-config repository checkout.
    export KAYOBE_CONFIG_SOURCE_PATH="${KAYOBE_CONFIG_SOURCE_PATH:-${KAYOBE_SOURCE_PATH}/config/src/kayobe-config}"

    # Path to the kayobe virtual environment.
    export KAYOBE_VENV_PATH="${KAYOBE_VENV_PATH:-${HOME}/kayobe-venv}"

    # Whether to provision a VM for the seed host.
    export KAYOBE_SEED_VM_PROVISION=${KAYOBE_SEED_VM_PROVISION:-1}

    # Whether to configure the seed host.
    export KAYOBE_SEED_HOST_CONFIGURE=${KAYOBE_SEED_HOST_CONFIGURE:-1}

    # Whether to build container images for the seed services. If 0, they will
    # be pulled.
    export KAYOBE_SEED_CONTAINER_IMAGE_BUILD=${KAYOBE_SEED_CONTAINER_IMAGE_BUILD:-0}

    # Whether to deploy seed services.
    export KAYOBE_SEED_SERVICE_DEPLOY=${KAYOBE_SEED_SERVICE_DEPLOY:-1}

    # Whether to provision a VM for the infra VM host.
    export KAYOBE_INFRA_VM_PROVISION=${KAYOBE_INFRA_VM_PROVISION:-1}

    # Whether to configure the infra VM host.
    export KAYOBE_INFRA_VM_HOST_CONFIGURE=${KAYOBE_INFRA_VM_HOST_CONFIGURE:-1}

    # Whether to deploy infra VM services.
    export KAYOBE_INFRA_VM_SERVICE_DEPLOY=${KAYOBE_INFRA_VM_SERVICE_DEPLOY:-1}

    # Whether to use the 'kolla-ansible certificates' command to generate X.509
    # certificates.
    export KAYOBE_OVERCLOUD_GENERATE_CERTIFICATES=${KAYOBE_OVERCLOUD_GENERATE_CERTIFICATES:-0}

    # Whether to build container images for the overcloud services. If 0, they
    # will be pulled if $KAYOBE_OVERCLOUD_CONTAINER_IMAGE_PULL is 1.
    export KAYOBE_OVERCLOUD_CONTAINER_IMAGE_BUILD=${KAYOBE_OVERCLOUD_CONTAINER_IMAGE_BUILD:-0}

    # Whether to pull container images for the overcloud services if
    # $KAYOBE_OVERCLOUD_CONTAINER_IMAGE_BUILD is 0.
    export KAYOBE_OVERCLOUD_CONTAINER_IMAGE_PULL=${KAYOBE_OVERCLOUD_CONTAINER_IMAGE_PULL:-1}

    # Whether to deploy overcloud services.
    export KAYOBE_OVERCLOUD_SERVICE_DEPLOY=${KAYOBE_OVERCLOUD_SERVICE_DEPLOY:-1}

    # Whether to perform overcloud post configuration.
    export KAYOBE_OVERCLOUD_POST_CONFIGURE=${KAYOBE_OVERCLOUD_POST_CONFIGURE:-1}

    # Additional arguments to pass to kayobe commands.
    export KAYOBE_EXTRA_ARGS=${KAYOBE_EXTRA_ARGS:-}

    # Use .gitreview as the key to determine the appropriate branch to clone
    # for tests. Inspired by OSA code.
    PARENT="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
    if [ -f "${PARENT}/../.gitreview" ]; then
      BRANCH=$(awk -F'=' '/defaultbranch/ {print $2}' "${PARENT}/../.gitreview")
      if [[ "${BRANCH}" == "" ]]; then
        SERIES="master"
      else
        SERIES="$(echo ${BRANCH} | sed -E 's,(stable|unmaintained)/,,')"
      fi

      # Upper constraints to use when installing Python packages.
      export UPPER_CONSTRAINTS_FILE="${UPPER_CONSTRAINTS_FILE:-https://releases.openstack.org/constraints/upper/${SERIES}}"
    fi

    # Path to the Tenks virtual environment.
    export TENKS_VENV_PATH="${TENKS_VENV_PATH:-${HOME}/tenks-test-venv}"

    # Path to a Tenks YAML configuration file. If unset,
    # tenks-deploy-config-overcloud.yml or tenks-deploy-config-compute.yml will
    # be used.
    export TENKS_CONFIG_PATH=${TENKS_CONFIG_PATH:-}

    # Log directory in case of errors
    export LOGDIR=${LOGDIR:-}
}

function config_set {
    # Source the configuration file, config.sh

    PARENT="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"

    source "${PARENT}/config.sh"
}

function config_check {
    # Check the configuration environment variables.

    if [[ ! -e "$KAYOBE_CONFIG_SOURCE_PATH" ]]; then
        if [[ ${KAYOBE_CONFIG_REQUIRED:-1} -eq 1 ]]; then
            echo "Kayobe configuration path $KAYOBE_CONFIG_SOURCE_PATH does not exist"
            return 1
        fi
    fi

    if [[ ! -e "$KAYOBE_SOURCE_PATH" ]]; then
        echo "Kayobe source path $KAYOBE_SOURCE_PATH does not exist"
        return 1
    fi
}

function config_init {
    config_defaults
    config_set
    config_check
}

# Installation

function is_dnf {
    if [[ -e /etc/centos-release || -e /etc/rocky-release ]]; then
        /usr/bin/which dnf >/dev/null 2>&1
    else
        return 1
    fi
}

function is_yum {
    if [[ -e /etc/centos-release || -e /etc/rocky-release ]]; then
        /usr/bin/which yum >/dev/null 2>&1
    else
        return 1
    fi
}

function install_dependencies {
    echo "Installing package dependencies for kayobe"
    if is_dnf; then
        sudo dnf -y install gcc git vim python3-devel python3-pyyaml libffi-devel
    elif is_yum; then
        echo "CentOS 7 is no longer supported"
        exit 1
    else
        sudo apt update
        sudo apt install -y python-is-python3 python3-dev python3-venv gcc git libffi-dev
    fi
}

function install_venv {
    # Install a virtualenv at $1. The rest of the arguments are passed
    # directly to pip.
    venv_path="$1"
    shift
    pip_paths="$@"
    local venv_parent="$(dirname ${venv_path})"
    if [[ ! -d "$venv_parent" ]]; then
        mkdir -p "$venv_parent"
    fi
    if [[ ! -f "${venv_path}/bin/activate" ]]; then
        echo "Creating virtual environment in ${venv_path}"
        python3 -m venv "${venv_path}"
        # NOTE: Virtualenv's activate and deactivate scripts reference an
        # unbound variable.
        set +u
        source "${venv_path}/bin/activate"
        pip install -U pip
        pip install $pip_paths
        deactivate
        set -u
    else
        echo "Using existing virtual environment in ${venv_path}"
    fi
}

function install_venv_system_site_packages {
    # Install a virtualenv at $1. The rest of the arguments are passed
    # directly to pip.
    venv_path="$1"
    shift
    pip_paths="$@"
    local venv_parent="$(dirname ${venv_path})"
    if [[ ! -d "$venv_parent" ]]; then
        mkdir -p "$venv_parent"
    fi
    if [[ ! -f "${venv_path}/bin/activate" ]]; then
        echo "Creating virtual environment in ${venv_path}"
        python3 -m venv --system-site-packages "${venv_path}"
        # NOTE: Virtualenv's activate and deactivate scripts reference an
        # unbound variable.
        set +u
        source "${venv_path}/bin/activate"
        pip install -U pip
        pip install $pip_paths
        deactivate
        set -u
    else
        echo "Using existing virtual environment in ${venv_path}"
    fi
}

function install_kayobe_venv {
    # Install the Kayobe venv.
    install_venv "${KAYOBE_VENV_PATH}" "${KAYOBE_SOURCE_PATH}"
}

function install_kayobe_dev_venv {
    # Install Kayobe in the venv in editable mode.
    install_venv "${KAYOBE_VENV_PATH}" -e "${KAYOBE_SOURCE_PATH}"
}

function upgrade_kayobe_venv {
    echo "Upgrading kayobe virtual environment in ${KAYOBE_VENV_PATH}"
    python3 -m venv "${KAYOBE_VENV_PATH}"
    # NOTE: Virtualenv's activate and deactivate scripts reference an
    # unbound variable.
    set +u
    source "${KAYOBE_VENV_PATH}/bin/activate"
    pip install -U pip
    pip install -U "${KAYOBE_SOURCE_PATH}"
    deactivate
    set -u
}

# Deployment

function is_deploy_image_built_locally {
    ipa_build_images=$(kayobe configuration dump --host localhost --var-name ipa_build_images)
    to_bool "$ipa_build_images"
}

function is_ironic_enabled {
    ironic_enabled=$(kayobe configuration dump --host localhost --var-name kolla_enable_ironic)
    to_bool "$ironic_enabled"
}

function is_overcloud_host_image_built_by_dib {
    overcloud_dib_build_host_images=$(kayobe configuration dump --host localhost --var-name overcloud_dib_build_host_images)
    to_bool "$overcloud_dib_build_host_images"
}

function is_cinder_enabled {
    flag="$(run_kayobe configuration dump --host localhost --var-name kolla_enable_cinder)"
    to_bool "$flag"
}

function is_octavia_enabled {
    octavia_enabled=$(kayobe configuration dump --host localhost --var-name kolla_enable_octavia)
    to_bool "$octavia_enabled"
}

function are_octavia_certificates_present {
    [[ -f ${KAYOBE_CONFIG_PATH}/kolla/config/octavia/client_ca.cert.pem &&
       -f ${KAYOBE_CONFIG_PATH}/kolla/config/octavia/client.cert-and-key.pem &&
       -f ${KAYOBE_CONFIG_PATH}/kolla/config/octavia/server_ca.cert.pem &&
       -f ${KAYOBE_CONFIG_PATH}/kolla/config/octavia/server_ca.key.pem ]]
}

function environment_setup {
    # NOTE: Virtualenv's activate script references an unbound variable.
    set +u
    source "${KAYOBE_VENV_PATH}/bin/activate"
    set -u
    source "${KAYOBE_CONFIG_SOURCE_PATH}/kayobe-env" "$@"
}

function run_kayobe {
    # Run a kayobe command, including extra arguments provided via
    # $KAYOBE_EXTRA_ARGS.
    kayobe ${KAYOBE_EXTRA_ARGS} "${@}"
}

function control_host_bootstrap {
    attempts=10
    interval=5
    echo "Bootstrapping the Ansible control host"
    for i in $(seq 1 $attempts); do
        if run_kayobe control host bootstrap; then
            chb_success=1
            break
        fi
        echo "Control host bootstrap failed - likely Ansible Galaxy flakiness. Sleeping $interval seconds before retrying"
        sleep $interval
    done
    if [[ -z ${chb_success+x} ]]; then
        die $LINENO "Failed to bootstrap control host"
        exit 1
    fi
    echo "Bootstrapped control host after $i attempts"
}

function control_host_upgrade {
    attempts=10
    interval=5
    echo "Upgrading the Ansible control host"
    for i in $(seq 1 $attempts); do
        if run_kayobe control host upgrade; then
            chu_success=1
            break
        fi
        echo "Control host upgrade failed - likely Ansible Galaxy flakiness. Sleeping $interval seconds before retrying"
        sleep $interval
    done
    if [[ -z ${chu_success+x} ]]; then
        die $LINENO "Failed to upgrade control host"
        exit 1
    fi
    echo "Upgraded control host after $i attempts"
}

function seed_hypervisor_deploy {
    # Deploy a seed hypervisor.
    environment_setup

    control_host_bootstrap

    echo "Configuring the seed hypervisor"
    run_kayobe seed hypervisor host configure
}

function seed_deploy {
    # Deploy a kayobe seed in a VM.
    environment_setup

    control_host_bootstrap

    if [[ ${KAYOBE_SEED_VM_PROVISION} = 1 ]]; then
        echo "Provisioning the seed VM"
        run_kayobe seed vm provision
    fi

    if [[ ${KAYOBE_SEED_HOST_CONFIGURE} = 1 ]]; then
        echo "Configuring the seed host"
        run_kayobe seed host configure
    fi

    if [[ ${KAYOBE_SEED_CONTAINER_IMAGE_BUILD} = 1 ]]; then
        echo "Building seed container images"
        run_kayobe seed container image build
    else
        echo "Not pulling seed container images - no such command yet"
        #run_kayobe seed container image pull
    fi

    if [[ ${KAYOBE_SEED_SERVICE_DEPLOY} = 1 ]]; then
        echo "Deploying containerised seed services"
        run_kayobe seed service deploy
    fi

    if is_deploy_image_built_locally; then
        echo "Building seed deployment images"
        run_kayobe seed deployment image build
    else
        echo "Not building seed deployment images"
    fi

    if is_overcloud_host_image_built_by_dib; then
        echo "Building overcloud host images"
        run_kayobe overcloud host image build
    else
        echo "Not building overcloud host images"
    fi
}

function seed_upgrade {
    # Upgrade a kayobe seed in a VM.
    echo "Upgrading Kayobe"
    upgrade_kayobe_venv

    environment_setup

    control_host_upgrade

    echo "Upgrading the seed host"
    run_kayobe seed host upgrade

    if is_deploy_image_built_locally; then
        echo "Building seed deployment images"
        run_kayobe seed deployment image build --force-rebuild
    else
        echo "Not building seed deployment images"
    fi

    if [[ ${KAYOBE_SEED_CONTAINER_IMAGE_BUILD} = 1 ]]; then
        echo "Building seed container images"
        run_kayobe seed container image build
    else
        echo "Not pulling seed container images - no such command yet"
        #run_kayobe seed container image pull
    fi

    echo "Upgrading containerised seed services"
    run_kayobe seed service upgrade
}

function infra_vm_deploy {
    # Deploy a kayobe infra VM.
    environment_setup

    control_host_bootstrap

    if [[ ${KAYOBE_INFRA_VM_PROVISION} = 1 ]]; then
        echo "Provisioning the infra VM"
        run_kayobe infra vm provision
    fi

    if [[ ${KAYOBE_INFRA_VM_HOST_CONFIGURE} = 1 ]]; then
        echo "Configuring the infra VM host"
        run_kayobe infra vm host configure
    fi

    if [[ ${KAYOBE_INFRA_VM_SERVICE_DEPLOY} = 1 ]]; then
        echo "Deploying containerised infra VM services"
        run_kayobe infra vm service deploy
    fi
}

function overcloud_deploy {
    # Deploy a kayobe control plane.
    echo "Deploying a kayobe development environment. This consists of a "
    echo "single node OpenStack control plane."

    environment_setup

    control_host_bootstrap

    if [[ ${KAYOBE_OVERCLOUD_GENERATE_CERTIFICATES} = 1 ]]; then
        echo "Generate TLS certificates"
        run_kayobe playbook run $KAYOBE_SOURCE_PATH/ansible/kolla-ansible.yml -t config
        # NOTE(mgoddard): There is a chicken and egg when generating libvirt
        # TLS certificates using the kolla-ansible certificates command, and
        # host libvirt. The certificates command needs to be able to gather
        # facts for all hosts, but since the host configure step hasn't been
        # run, we don't have SSH or the kolla user configured yet. However, we
        # can't run host configure without the libvirt TLS certificates.
        # Workaround: add the host to SSH known hosts and SSH as $USER.
        run_kayobe playbook run $KAYOBE_SOURCE_PATH/ansible/ssh-known-host.yml -l overcloud

        # Avoid populating the fact cache with this weird setup.
        export ANSIBLE_CACHE_PLUGIN=memory
        run_kayobe kolla ansible run certificates \
          --kolla-extra kolla_certificates_dir=${KAYOBE_CONFIG_PATH}/kolla/certificates \
          --kolla-extra ansible_user=$USER \
          --kolla-extra ansible_python_interpreter=/usr/bin/python3 \
          --skip-tags kolla-openstack
        unset ANSIBLE_CACHE_PLUGIN

        # Add CA cert to trust store.
        ca_cert=${KAYOBE_CONFIG_PATH}/kolla/certificates/ca/root.crt
        if [[ -e /etc/debian_version ]]; then
            # Ubuntu
            sudo cp $ca_cert "/usr/local/share/ca-certificates/kayobe-customca.crt"
            sudo update-ca-certificates
        elif [[ -e /etc/redhat-release ]]; then
            # CentOS
            sudo cp $ca_cert "/etc/pki/ca-trust/source/anchors/kayobe-customca.crt"
            sudo update-ca-trust
        fi
    fi

    echo "Configuring the controller host"
    run_kayobe overcloud host configure

    # FIXME(mgoddard): Perform host upgrade workarounds to ensure hostname
    # resolves to IP address of API interface for RabbitMQ. This seems to be
    # required since https://review.openstack.org/#/c/584427 was merged.
    echo "Workaround: upgrading the controller host"
    run_kayobe overcloud host upgrade

    # Note: This must currently be before host configure, because host
    # configure runs kolla-ansible.yml, which validates the presence of the
    # built deploy images.
    if is_deploy_image_built_locally; then
        echo "Building overcloud deployment images"
        run_kayobe overcloud deployment image build
    else
        echo "Not building overcloud deployment images"
    fi

    if [[ ${KAYOBE_OVERCLOUD_CONTAINER_IMAGE_BUILD} = 1 ]]; then
        echo "Building overcloud container images"
        run_kayobe overcloud container image build
    elif [[ ${KAYOBE_OVERCLOUD_CONTAINER_IMAGE_PULL} = 1 ]]; then
        echo "Pulling overcloud container images"
        run_kayobe overcloud container image pull
    fi

    if [[ ${KAYOBE_OVERCLOUD_SERVICE_DEPLOY} = 1 ]]; then
        if is_octavia_enabled && ! are_octavia_certificates_present; then
            echo "Generating Octavia certificates"
            run_kayobe kolla ansible run octavia-certificates
            mkdir -p ${KAYOBE_CONFIG_PATH}/kolla/config/octavia
            cp ${KOLLA_CONFIG_PATH:-/etc/kolla}/config/octavia/*.pem ${KAYOBE_CONFIG_PATH}/kolla/config/octavia/
        fi
        echo "Deploying containerised overcloud services"
        run_kayobe overcloud service deploy
    fi

    if [[ ${KAYOBE_OVERCLOUD_POST_CONFIGURE} = 1 ]]; then
        echo "Performing post-deployment configuration"
        source "${KOLLA_CONFIG_PATH:-/etc/kolla}/admin-openrc.sh"
        run_kayobe overcloud post configure
    fi

    echo "Control plane deployment complete"
}

function overcloud_upgrade {
    # Upgrade a kayobe control plane.
    echo "Upgrading a kayobe development environment. This consists of a "
    echo "single node OpenStack control plane."

    echo "Upgrading Kayobe"
    upgrade_kayobe_venv

    environment_setup

    control_host_upgrade

    echo "Upgrading the controller host"
    run_kayobe overcloud host upgrade

    if is_deploy_image_built_locally; then
        echo "Building overcloud deployment images"
        run_kayobe overcloud deployment image build --force-rebuild
    else
        echo "Not building overcloud deployment images"
    fi

    echo "Updating baremetal deployment images"
    (source "${KOLLA_CONFIG_PATH:-/etc/kolla}/admin-openrc.sh" &&
     run_kayobe baremetal compute update deployment image)

    if [[ ${KAYOBE_OVERCLOUD_CONTAINER_IMAGE_BUILD} = 1 ]]; then
        echo "Building overcloud container images"
        run_kayobe overcloud container image build
    else
        echo "Pulling overcloud container images"
        run_kayobe overcloud container image pull
    fi

    echo "Saving overcloud service configuration"
    # Don't copy the ironic IPA kernel and ramdisk, since these files can be
    # quite large.
    run_kayobe overcloud service configuration save --exclude 'ironic-agent.*'

    echo "Deploying containerised overcloud services"
    run_kayobe overcloud service upgrade

    echo "Control plane upgrade complete"
}

function overcloud_test_init {

    echo "Performing overcloud test init"
    environment_setup

    if [[ ! -z "$UPPER_CONSTRAINTS_FILE" ]]; then
        pip install python-openstackclient python-ironicclient -c "$UPPER_CONSTRAINTS_FILE"
    else
        pip install python-openstackclient python-ironicclient
    fi
    source "${KOLLA_CONFIG_PATH:-/etc/kolla}/admin-openrc.sh"

    # This guards init-runonce from running more than once
    if mkdir /tmp/init-runonce > /dev/null 2>&1; then
        echo "Running kolla-ansible init-runonce"
        if is_ironic_enabled; then
            # Don't create an external network, since it conflicts with the
            # ironic provision-net.
            export ENABLE_EXT_NET=${ENABLE_EXT_NET:-0}
        else
            # Use the all-in-one network as the external network. There is
            # currently no option to avoid setting a gateway, so use the
            # controller's IP.
            export EXT_NET_CIDR="${EXT_NET_CIDR:-192.168.33.0/24}"
            export EXT_NET_RANGE="${EXT_NET_RANGE:-start=192.168.33.31,end=192.168.33.127}"
            export EXT_NET_GATEWAY="${EXT_NET_GATEWAY:-192.168.33.3}"
        fi
        ${KOLLA_VENV_PATH:-$HOME/kolla-venv}/share/kolla-ansible/init-runonce

        if is_ironic_enabled; then
            unset ENABLE_EXT_NET
            # Allow provision-net to be used as an external network for
            # floating IPs.
            # Note: a provisioning network would not normally be external.
            openstack network set provision-net --external
            openstack router set demo-router --external-gateway provision-net
        fi
    else
        echo "Not running kolla-ansible init-runonce - resources exist"
    fi
}

function overcloud_test_bounce_interface {
    shift
    LIMIT=$1
    INTERFACE=$2

    environment_setup

    # Change current configuration so that a subsequent host configure will bounce the interfaces:
    if is_dnf; then
        # Rocky
        run_kayobe overcloud host command run -b --command "rm -f /etc/NetworkManager/system-connections/$INTERFACE.nmconnection" --limit "$LIMIT"
    else
	# Ubuntu
        run_kayobe overcloud host command run -b --command "rm -f /etc/systemd/network/50-kayobe-$INTERFACE.network" --limit "$LIMIT"
    fi
    # Trigger bounce
    run_kayobe overcloud host configure -t network
}

function overcloud_inspection_rule_dump {
   echo "Listing inspection rules ..."
   openstack baremetal inspection rule list
   echo "Dumping inspection rules ..."
   openstack baremetal inspection rule list -c UUID -f value | xargs -L 1 openstack baremetal inspection rule show
}

function overcloud_test_inspect {
  set -eu
  node=$1

  environment_setup

  source "${KOLLA_CONFIG_PATH:-/etc/kolla}/admin-openrc-system.sh"

  overcloud_inspection_rule_dump

  echo "Baremetal node: $node before inspection"
  openstack baremetal node show "$node"

  # NOTE(wszumski): Switch to using kayobe playbooks to manage and inspect
  # when we switch to node registration

  if [ "$(openstack baremetal node show -c provision_state -f value $node)"  != "manageable" ]; then
      openstack baremetal node manage "$node" --wait
  fi

  # Run inspection
  openstack baremetal node inspect "$node" --wait

  echo "Baremetal node: $node after inspection"
  openstack baremetal node show $node
  openstack baremetal node inventory save $node

  # Use Kayobe to save introspection data
  run_kayobe baremetal compute introspection data save --limit "baremetal-compute,controllers[0]" --output-dir /tmp/baremetal-compute-inspection-data

  # Move back to available
  openstack baremetal node provide "$node" --wait
}

function overcloud_test {
    set -eu

    # function arguments
    name="$1"
    flavor="$2"
    network="$3"

    node_config="{ 'name': '$name', 'flavor': '$flavor', 'network': '$network' }"

    overcloud_test_init

    # Perform a simple smoke test against the cloud.
    echo "Performing a simple smoke test with node config: $node_config"

    echo "$name: Creating a server"
    openstack server create --wait --image cirros --flavor "$flavor" --key-name mykey --network "$network" "$name"
    echo "$name: Server created"
    openstack server show "$name"
    status=$(openstack server show "$name" -f value -c status)
    if [[ $status != ACTIVE ]]; then
        echo "$name: Node creation failed"
        return 1
    fi

    # Test SSH connectivity. For servers attached directly to the external
    # network, use the fixed IP. Otherwise add a floating IP.
    if [[ $network = provision-net ]]; then
        port_id=$(openstack port list --server $name --network $network -f value -c ID)
        echo "port $port_id"
        fixed_ips=$(openstack port show $port_id -f json -c fixed_ips)
        echo "fixed_ips $fixed_ips"
        ip=$(echo $fixed_ips | python3 -c "import json,sys; print(json.load(sys.stdin)['fixed_ips'][0]['ip_address'])")
        echo "ip $ip"
    else
        echo "$name: Attaching floating IP"
        if is_ironic_enabled; then
            floating_net=provision-net
        else
            floating_net=public1
        fi
        ip=$(openstack floating ip create $floating_net -f value -c floating_ip_address)
        openstack server add floating ip ${name} ${ip}
    fi
    echo "$name: Waiting for ping and SSH access via ${ip}"
    attempts=12
    for i in $(seq 1 $attempts); do
        if ping -c1 -W1 $ip && ssh -v -o StrictHostKeyChecking=no -o BatchMode=yes cirros@$ip hostname; then
            break
        elif [[ $i -eq $attempts ]]; then
            echo "Failed to access server $name via SSH after $attempts attempts"
            echo "Console log:"
            openstack console log show ${name}
            return 1
        else
            echo "Cannot access server $name - retrying"
        fi
        sleep 10
    done
    echo "$name: Ping and SSH successful"
    if [[ $network != provision-net ]]; then
        echo "$name: Removing floating IP"
        openstack server remove floating ip ${name} ${ip}
        openstack floating ip delete ${ip}
    fi

    echo "$name: Deleting the Node"
    openstack server delete --wait "$name"
}

function write_bifrost_clouds_yaml {
    SEED_IP="192.168.33.5"

    # Pull clouds.yaml from Bifrost container and change certificate path.
    if [[ ! -f ~/.config/openstack/clouds.yaml ]]; then
        mkdir -p ~/.config/openstack
        scp stack@$SEED_IP:/home/stack/.config/openstack/clouds.yaml ~/.config/openstack/clouds.yaml
        sed -i 's|/home/stack/.config/openstack/bifrost.crt|~/.config/openstack/bifrost.crt|g' ~/.config/openstack/clouds.yaml
    else
        echo "Not updating clouds.yaml file because it already exists at $HOME/.config/openstack/clouds.yaml. Try removing it if authentication against Bifrost fails."
    fi
    #Pull Bifrost PEM certificate from seed.
    if [[ ! -f ~/.config/openstack/bifrost.crt ]]; then
        mkdir -p ~/.config/openstack
        scp stack@$SEED_IP:/home/stack/.config/openstack/bifrost.crt ~/.config/openstack/bifrost.crt
    else
        echo "Not updating Bifrost certificate file because it already exists at $HOME/.config/openstack/bifrost.crt. Try removing it if authentication against Bifrost fails."
    fi
}

function run_tenks_playbook {
    # Run a Tenks playbook. Arguments:
    # $1: The path to the Tenks repo.
    # $2: The name of the playbook to run.
    local tenks_path="$1"
    local tenks_playbook="$2"
    local tenks_deploy_type="${3:-default}"

    local parent="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"

    if [[ ! -f "${KOLLA_CONFIG_PATH:-/etc/kolla}/admin-openrc.sh" &&
              "${tenks_deploy_type}" = "compute" ]]; then

        die $LINENO "Missing admin-openrc.sh & tenks_deploy_type is compute."
        exit 1
    fi
    if [[ -f "${KOLLA_CONFIG_PATH:-/etc/kolla}/admin-openrc.sh" &&
              ( "${tenks_deploy_type}" = "default" ||
                    "${tenks_deploy_type}" = "compute" ) ]]; then

        # Deploys Compute from Overcloud
        default_tenks_config=tenks-deploy-config-compute.yml
        source "${KOLLA_CONFIG_PATH:-/etc/kolla}/admin-openrc.sh"

    elif [[ "${tenks_deploy_type}" = "default" ||
               "${tenks_deploy_type}" = "overcloud" ]]; then

        # Deploys Overcloud from Seed
        default_tenks_config=tenks-deploy-config-overcloud.yml
        write_bifrost_clouds_yaml
        export OS_CLOUD=bifrost

    else
        die $LINENO "Bad tenks_deploy_type: ${tenks_deploy_type}"
        exit 1
    fi

    # Allow a specific Tenks config file to be specified via
    # $TENKS_CONFIG_PATH.
    tenks_config="${TENKS_CONFIG_PATH:-$parent/$default_tenks_config}"

    ansible-playbook \
        -vvv \
        --inventory "$tenks_path/ansible/inventory" \
        --extra-vars=@"$tenks_config" \
        "$tenks_path/ansible/$tenks_playbook"
}

function tenks_deploy {
    set -eu
    # Create a simple test Tenks deployment. Assumes that a bridge named
    # 'breth1' exists.  Arguments:
    # $1: The path to the Tenks repo.
    local tenks_path="$1"
    local tenks_deploy_type="${2:-default}"

    echo "Configuring Tenks"

    environment_setup

    # We don't want to use the Kayobe venv.
    deactivate

    # Install the Tenks venv.
    install_venv_system_site_packages "${TENKS_VENV_PATH}" "$tenks_path" -c "$UPPER_CONSTRAINTS_FILE"
    source ${TENKS_VENV_PATH:-$HOME/tenks-test-venv}/bin/activate

    ${KAYOBE_SOURCE_PATH}/tools/ansible-galaxy-retried.sh install \
        --role-file="$tenks_path/requirements.yml" \
        --roles-path="$tenks_path/ansible/roles/"

    local parent="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"

    # Install a trivial script for ovs-vsctl that talks to containerised Open
    # vSwitch.
    sudo cp --no-clobber "$parent/ovs-vsctl" /usr/bin/ovs-vsctl

    run_tenks_playbook "$tenks_path" deploy.yml "$tenks_deploy_type"
}

function tenks_teardown {
    set -eu
    # Tear down a test Tenks deployment.
    # Arguments:
    # $1: The path to the Tenks repo.
    local tenks_path="$1"
    local tenks_deploy_type="${2:-default}"

    echo "Tearing down Tenks"

    environment_setup

    # We don't want to use the Kayobe venv.
    deactivate

    # Source the Tenks venv.
    source ${TENKS_VENV_PATH:-$HOME/tenks-test-venv}/bin/activate

    run_tenks_playbook "$tenks_path" teardown.yml "$tenks_deploy_type"
}

# General purpose

# Prints backtrace info
# filename:lineno:function
# backtrace level
function backtrace {
    local level=$1
    local deep
    deep=$((${#BASH_SOURCE[@]} - 1))
    echo "[Call Trace]"
    while [ $level -le $deep ]; do
        echo "${BASH_SOURCE[$deep]}:${BASH_LINENO[$deep-1]}:${FUNCNAME[$deep-1]}"
        deep=$((deep - 1))
    done
}

# Prints line number and "message" then exits
# die $LINENO "message"
function die {
    local exitcode=$?
    set +o xtrace
    local line=$1; shift
    if [ $exitcode == 0 ]; then
        exitcode=1
    fi
    backtrace 2
    err $line "$*"
    # Give buffers a second to flush
    sleep 1
    exit $exitcode
}


# Prints line number and "message" in error format
# err $LINENO "message"
function err {
    local exitcode=$?
    local xtrace
    xtrace=$(set +o | grep xtrace)
    set +o xtrace
    local msg="[ERROR] ${BASH_SOURCE[2]}:$1 $2"
    echo "$msg" 1>&2;
    if [[ -n ${LOGDIR} ]]; then
        echo "$msg" >> "${LOGDIR}/error.log"
    fi
    $xtrace
    return $exitcode
}


function die_if_module_not_loaded {
    if ! grep -q $1 /proc/modules; then
        die $LINENO "$1 kernel module is not loaded"
    fi
}

# running_in_container - Returns true otherwise false
function running_in_container {
    [[ $(systemd-detect-virt --container) != 'none' ]]
}

# enable_kernel_bridge_firewall - Enable kernel support for bridge firewalling
function enable_kernel_bridge_firewall {
    # Load bridge module. This module provides access to firewall for bridged
    # frames; and also on older kernels (pre-3.18) it provides sysctl knobs to
    # enable/disable bridge firewalling
    sudo modprobe bridge
    # For newer kernels (3.18+), those sysctl settings are split into a separate
    # kernel module (br_netfilter). Load it too, if present.
    sudo modprobe br_netfilter 2>> /dev/null || :
    # Enable bridge firewalling in case it's disabled in kernel (upstream
    # default is enabled, but some distributions may decide to change it).
    # This is at least needed for RHEL 7.2 and earlier releases.
    for proto in ip ip6; do
        sudo sysctl -w net.bridge.bridge-nf-call-${proto}tables=1
    done
}

function to_bool {
    if [[ "$1" =~ (y|Y|yes|Yes|YES|true|True|TRUE|on|On|ON) ]]; then
        true
    elif [[ "$1" =~ (n|N|no|No|NO|false|False|FALSE|off|Off|OFF) ]]; then
        false
    else
        die $LINENO "$1 was not a valid yaml boolean"
    fi
}

function is_absolute_path {
    path="$1"
    case "$path" in
    /*) true ;;
    *) false ;;
    esac
}

function configure_iptables {
    # NOTE(wszumski): adapted from the ironic devstack plugin, see:
    # https://github.com/openstack/ironic/blob/36e87dc5b472d79470b783fbba9ce396e3cbb96e/devstack/lib/ironic#L2132

    set -eu
    environment_setup

    # FIXME(wszumski): set these variables with values from kayobe-config
    HOST_IP='192.168.33.3'
    INTERNAL_VIP='192.168.33.2'
    IRONIC_TFTPSERVER_IP="$HOST_IP"
    IRONIC_SERVICE_PORT=6385
    IRONIC_INSPECTOR_PORT=5050
    IRONIC_HTTP_SERVER="$INTERNAL_VIP"
    GLANCE_SERVICE_PORT=9292
    IRONIC_HTTP_PORT=8089
    ISCSI_SERVICE_PORT=3260

    # enable tftp natting for allowing connections to HOST_IP's tftp server
    if ! running_in_container; then
        sudo modprobe nf_conntrack_tftp
        sudo modprobe nf_nat_tftp

        enable_kernel_bridge_firewall
    else
        die_if_module_not_loaded nf_conntrack_tftp
        die_if_module_not_loaded nf_nat_tftp
    fi
    # explicitly allow DHCP - packets are occasionally being dropped here
    sudo iptables -I INPUT -p udp --dport 67:68 --sport 67:68 -j ACCEPT || true
    # nodes boot from TFTP and callback to the API server listening on $HOST_IP
    sudo iptables -I INPUT -d $IRONIC_TFTPSERVER_IP -p udp --dport 69 -j ACCEPT || true

    sudo iptables -I INPUT -d $HOST_IP -p tcp --dport $IRONIC_SERVICE_PORT -j ACCEPT || true
    # open ironic API on baremetal network
    sudo iptables -I INPUT -d $IRONIC_HTTP_SERVER -p tcp --dport $IRONIC_SERVICE_PORT -j ACCEPT || true

    # Docker CE has added a default DROP policy to the FORWARD chain.
    # When nova-compute runs on the controller, kolla ansible sets the
    # net.bridge.bridge-nf-call-iptables sysctl to 1, which causes iptables to
    # process frames forwarded across bridges. Forward all frames on the main
    # bridge, breth1.
    sudo iptables -A FORWARD -i breth1 -j ACCEPT || true

    # agent ramdisk gets instance image from swift
    sudo iptables -I INPUT -d $INTERNAL_VIP -p tcp --dport ${SWIFT_DEFAULT_BIND_PORT:-8080} -j ACCEPT || true
    sudo iptables -I INPUT -d $INTERNAL_VIP -p tcp --dport $GLANCE_SERVICE_PORT -j ACCEPT || true

    sudo iptables -I INPUT -d $HOST_IP -p tcp --dport $IRONIC_HTTP_PORT -j ACCEPT || true

    if is_cinder_enabled; then
        sudo iptables -I INPUT -d $HOST_IP -p tcp --dport $ISCSI_SERVICE_PORT -j ACCEPT || true
    fi
}
