Tinkerbell.Sandbox/setup.sh
Manuel Mendez 0fff3e6d7f sh: Make use of bashisms in bash scripts
Both [[ ]] and (( )) bashisms are better than the alternative
in POSIX sh, since they are builtin and don't suffer from quoting
or number-of-args issues.

Signed-off-by: Manuel Mendez <mmendez@equinix.com>
2021-04-27 20:04:38 +00:00

512 lines
13 KiB
Bash
Executable File

#!/usr/bin/env bash
# stops the execution if a command or pipeline has an error
set -eu
# Tinkerbell stack Linux setup script
#
# See https://tinkerbell.org/setup for the installation steps.
# file to hold all environment variables
ENV_FILE=.env
SCRATCH=$(mktemp -d -t tmp.XXXXXXXXXX)
readonly SCRATCH
function finish() (
rm -rf "$SCRATCH"
)
trap finish EXIT
DEPLOYDIR=$(pwd)/deploy
readonly DEPLOYDIR
readonly STATEDIR=$DEPLOYDIR/state
if command -v tput >/dev/null && tput setaf 1 >/dev/null 2>&1; then
# color codes
RED="$(tput setaf 1)"
GREEN="$(tput setaf 2)"
YELLOW="$(tput setaf 3)"
RESET="$(tput sgr0)"
fi
INFO="${GREEN:-}INFO:${RESET:-}"
ERR="${RED:-}ERROR:${RESET:-}"
WARN="${YELLOW:-}WARNING:${RESET:-}"
BLANK=" "
NEXT="${GREEN:-}NEXT:${RESET:-}"
get_distribution() (
local lsb_dist=""
# Every system that we officially support has /etc/os-release
if [[ -r /etc/os-release ]]; then
# shellcheck disable=SC1091
lsb_dist="$(. /etc/os-release && echo "$ID")"
fi
# Returning an empty string here should be alright since the
# case statements don't act unless you provide an actual value
echo "$lsb_dist" | tr '[:upper:]' '[:lower:]'
)
get_distro_version() (
local lsb_version="0"
# Every system that we officially support has /etc/os-release
if [[ -r /etc/os-release ]]; then
# shellcheck disable=SC1091
lsb_version="$(. /etc/os-release && echo "$VERSION_ID")"
fi
echo "$lsb_version"
)
is_network_configured() (
# Require the provisioner interface have the host IP
if ! ip addr show "$TINKERBELL_NETWORK_INTERFACE" |
grep -q "$TINKERBELL_HOST_IP"; then
return 1
fi
return 0
)
identify_network_strategy() (
local distro=$1
local version=$2
case "$distro" in
ubuntu)
if jq -n --exit-status '$distro_version >= 17.10' --argjson distro_version "$version" >/dev/null 2>&1; then
echo "setup_networking_netplan"
else
echo "setup_networking_ubuntu_legacy"
fi
;;
centos)
echo "setup_networking_centos"
;;
*)
echo "setup_networking_manually"
;;
esac
)
setup_networking() (
local distro=$1
local version=$2
setup_network_forwarding
if is_network_configured; then
echo "$INFO tinkerbell network interface is already configured"
return 0
fi
local strategy
strategy=$(identify_network_strategy "$distro" "$version")
"${strategy}" "$distro" "$version" # execute the strategy
if is_network_configured; then
echo "$INFO tinkerbell network interface configured successfully"
else
echo "$ERR tinkerbell network interface configuration failed"
fi
NAT_INTERFACE=""
if [[ -r .nat_interface ]]; then
NAT_INTERFACE=$(cat .nat_interface)
fi
if [[ -n $NAT_INTERFACE ]] && ip addr show "$NAT_INTERFACE" &>/dev/null; then
# TODO(nshalman) the terraform code would just run these commands as-is once
# but it would be nice to make these more persistent based on OS
iptables -A FORWARD -i "$TINKERBELL_NETWORK_INTERFACE" -o "$NAT_INTERFACE" -j ACCEPT
iptables -A FORWARD -i "$NAT_INTERFACE" -o "$TINKERBELL_NETWORK_INTERFACE" -m state --state ESTABLISHED,RELATED -j ACCEPT
iptables -t nat -A POSTROUTING -o "$NAT_INTERFACE" -j MASQUERADE
fi
)
setup_networking_manually() (
local distro=$1
local version=$2
echo "$ERR this setup script cannot configure $distro ($version)"
echo "$BLANK please read this script's source and configure it manually."
exit 1
)
setup_network_forwarding() (
# enable IP forwarding for docker
if (($(sysctl -n net.ipv4.ip_forward) != 1)); then
if [[ -d /etc/sysctl.d ]]; then
echo "net.ipv4.ip_forward=1" >/etc/sysctl.d/99-tinkerbell.conf
elif [[ -f /etc/sysctl.conf ]]; then
echo "net.ipv4.ip_forward=1" >>/etc/sysctl.conf
fi
sysctl net.ipv4.ip_forward=1
fi
)
setup_networking_netplan() (
jq -n \
--arg interface "$TINKERBELL_NETWORK_INTERFACE" \
--arg cidr "$TINKERBELL_CIDR" \
--arg host_ip "$TINKERBELL_HOST_IP" \
'{
network: {
renderer: "networkd",
ethernets: {
($interface): {
addresses: [
"\($host_ip)/\($cidr)"
]
}
}
}
}' >"/etc/netplan/${TINKERBELL_NETWORK_INTERFACE}.yaml"
ip link set "$TINKERBELL_NETWORK_INTERFACE" nomaster
netplan apply
echo "$INFO waiting for the network configuration to be applied by systemd-networkd"
sleep 3
)
setup_networking_ubuntu_legacy() (
if ! [[ -f /etc/network/interfaces ]]; then
echo "$ERR file /etc/network/interfaces not found"
exit 1
fi
if grep -q "$TINKERBELL_NETWORK_INTERFACE" /etc/network/interfaces; then
echo "$ERR /etc/network/interfaces already has an entry for $TINKERBELL_NETWORK_INTERFACE."
echo "$BLANK To prevent breaking your network, please edit /etc/network/interfaces"
echo "$BLANK and configure $TINKERBELL_NETWORK_INTERFACE as follows:"
generate_iface_config
echo ""
echo "$BLANK Then run the following commands:"
echo "$BLANK ip link set $TINKERBELL_NETWORK_INTERFACE nomaster"
echo "$BLANK ifdown $TINKERBELL_NETWORK_INTERFACE"
echo "$BLANK ifup $TINKERBELL_NETWORK_INTERFACE"
exit 1
else
generate_iface_config >>/etc/network/interfaces
ip link set "$TINKERBELL_NETWORK_INTERFACE" nomaster
ifdown "$TINKERBELL_NETWORK_INTERFACE"
ifup "$TINKERBELL_NETWORK_INTERFACE"
fi
)
generate_iface_config() (
cat <<EOF
auto $TINKERBELL_NETWORK_INTERFACE
iface $TINKERBELL_NETWORK_INTERFACE inet static
address $TINKERBELL_HOST_IP/$TINKERBELL_CIDR
pre-up sleep 4
EOF
)
setup_networking_centos() (
local HWADDRESS
local content
HWADDRESS=$(ip addr show "$TINKERBELL_NETWORK_INTERFACE" | grep ether | awk -F 'ether' '{print $2}' | cut -d" " -f2)
content=$(
cat <<EOF
DEVICE=$TINKERBELL_NETWORK_INTERFACE
ONBOOT=yes
HWADDR=$HWADDRESS
BOOTPROTO=static
IPADDR=$TINKERBELL_HOST_IP
PREFIX=$TINKERBELL_CIDR
EOF
)
local cfgfile="/etc/sysconfig/network-scripts/ifcfg-$TINKERBELL_NETWORK_INTERFACE"
if [[ -f $cfgfile ]]; then
echo "$ERR network config already exists: $cfgfile"
echo "$BLANK Please update it to match this configuration:"
echo "$content"
echo ""
echo "$BLANK Then, run the following commands:"
echo "ip link set $TINKERBELL_NETWORK_INTERFACE nomaster"
echo "ifup $TINKERBELL_NETWORK_INTERFACE"
fi
echo "$content" >"$cfgfile"
ip link set "$TINKERBELL_NETWORK_INTERFACE" nomaster
ifup "$TINKERBELL_NETWORK_INTERFACE"
)
setup_osie() (
mkdir -p "$STATEDIR/webroot"
local osie_current=$STATEDIR/webroot/misc/osie/current
local tink_workflow=$STATEDIR/webroot/workflow/
if [[ ! -d $osie_current ]] || [[ ! -d $tink_workflow ]]; then
mkdir -p "$osie_current"
mkdir -p "$tink_workflow"
pushd "$SCRATCH"
if [[ -z ${TB_OSIE_TAR:-} ]]; then
curl "${OSIE_DOWNLOAD_LINK}" -o ./osie.tar.gz
tar -zxf osie.tar.gz
else
tar -zxf "$TB_OSIE_TAR"
fi
if pushd osie*/; then
if mv workflow-helper.sh workflow-helper-rc "$tink_workflow"; then
cp -r ./* "$osie_current"
else
echo "$ERR failed to move 'workflow-helper.sh' and 'workflow-helper-rc'"
exit 1
fi
popd
fi
else
echo "$INFO found existing osie files, skipping osie setup"
fi
)
check_container_status() (
local container_name="$1"
local container_id
container_id=$(docker-compose -f "$DEPLOYDIR/docker-compose.yml" ps -q "$container_name")
local start_moment
local current_status
start_moment=$(docker inspect "${container_id}" --format '{{ .State.StartedAt }}')
current_status=$(docker inspect "${container_id}" --format '{{ .State.Health.Status }}')
case "$current_status" in
starting)
: # move on to the events check
;;
healthy)
return 0
;;
unhealthy)
echo "$ERR $container_name is already running but not healthy. status: $current_status"
exit 1
;;
*)
echo "$ERR $container_name is already running but its state is a mystery. status: $current_status"
exit 1
;;
esac
local status
read -r status < <(docker events \
--since "$start_moment" \
--filter "container=$container_id" \
--filter "event=health_status" \
--format '{{.Status}}')
if [[ $status != "health_status: healthy" ]]; then
echo "$ERR $container_name is not healthy. status: $status"
exit 1
fi
)
generate_certificates() (
mkdir -p "$STATEDIR/certs"
if ! [[ -f "$STATEDIR/certs/ca.json" ]]; then
jq \
'.
| .names[0].L = $facility
' \
"$DEPLOYDIR/tls/ca.in.json" \
--arg ip "$TINKERBELL_HOST_IP" \
--arg facility "$FACILITY" \
>"$STATEDIR/certs/ca.json"
fi
if ! [[ -f "$STATEDIR/certs/server-csr.json" ]]; then
jq \
'.
| .hosts += [ $ip, "tinkerbell.\($facility).packet.net" ]
| .names[0].L = $facility
| .hosts = (.hosts | sort | unique)
' \
"$DEPLOYDIR/tls/server-csr.in.json" \
--arg ip "$TINKERBELL_HOST_IP" \
--arg facility "$FACILITY" \
>"$STATEDIR/certs/server-csr.json"
fi
docker build --tag "tinkerbell-certs" "$DEPLOYDIR/tls"
docker run --rm \
--volume "$STATEDIR/certs:/certs" \
--user "$UID:$(id -g)" \
tinkerbell-certs
local certs_dir="/etc/docker/certs.d/$TINKERBELL_HOST_IP"
# copy public key to NGINX for workers
if ! cmp --quiet "$STATEDIR/certs/ca.pem" "$STATEDIR/webroot/workflow/ca.pem"; then
cp "$STATEDIR/certs/ca.pem" "$STATEDIR/webroot/workflow/ca.pem"
fi
# update host to trust registry certificate
if ! cmp --quiet "$STATEDIR/certs/ca.pem" "$certs_dir/tinkerbell.crt"; then
if ! [[ -d "$certs_dir/" ]]; then
# The user will be told to create the directory
# in the next block, if copying the certs there
# fails.
mkdir -p "$certs_dir" || true >/dev/null 2>&1
fi
if ! cp "$STATEDIR/certs/ca.pem" "$certs_dir/tinkerbell.crt"; then
echo "$ERR please copy $STATEDIR/certs/ca.pem to $certs_dir/tinkerbell.crt"
echo "$BLANK and run $0 again:"
if ! [[ -d $certs_dir ]]; then
echo "sudo mkdir -p '$certs_dir'"
fi
echo "sudo cp '$STATEDIR/certs/ca.pem' '$certs_dir/tinkerbell.crt'"
exit 1
fi
fi
)
docker_login() (
echo -n "$TINKERBELL_REGISTRY_PASSWORD" | docker login -u="$TINKERBELL_REGISTRY_USERNAME" --password-stdin "$TINKERBELL_HOST_IP"
)
# This function takes an image specified as first parameter and it tags and
# push it using the second one. useful to proxy images from a repository to
# another.
docker_mirror_image() (
local from=$1
local to=$2
docker pull "$from"
docker tag "$from" "$to"
docker push "$to"
)
start_registry() (
docker-compose -f "$DEPLOYDIR/docker-compose.yml" up --build -d registry
check_container_status "registry"
)
# This function supposes that the registry is up and running.
# It configures with the required dependencies.
bootstrap_docker_registry() (
docker_login
# osie looks for tink-worker:latest, so we have to play with it a bit
# https://github.com/tinkerbell/osie/blob/master/apps/workflow-helper.sh#L66
docker_mirror_image "${TINKERBELL_TINK_WORKER_IMAGE}" "${TINKERBELL_HOST_IP}/tink-worker:latest"
)
setup_docker_registry() (
local registry_images="$STATEDIR/registry"
if ! [[ -d $registry_images ]]; then
mkdir -p "$registry_images"
fi
start_registry
bootstrap_docker_registry
)
start_components() (
local components=(db hegel tink-server boots tink-cli nginx)
for comp in "${components[@]}"; do
docker-compose -f "$DEPLOYDIR/docker-compose.yml" up --build -d "$comp"
sleep 3
check_container_status "$comp"
done
)
command_exists() (
command -v "$@" >/dev/null 2>&1
)
check_command() (
if command_exists "$1"; then
echo "$BLANK Found prerequisite: $1"
return 0
else
echo "$ERR Prerequisite command not installed: $1"
return 1
fi
)
check_prerequisites() (
distro=$1
version=$2
echo "$INFO verifying prerequisites for $distro ($version)"
failed=0
check_command docker || failed=1
check_command docker-compose || failed=1
check_command ip || failed=1
check_command jq || failed=1
strategy=$(identify_network_strategy "$distro" "$version")
case "$strategy" in
"setup_networking_netplan")
check_command netplan || failed=1
;;
"setup_networking_ubuntu_legacy")
check_command ifdown || failed=1
check_command ifup || failed=1
;;
"setup_networking_centos")
check_command ifdown || failed=1
check_command ifup || failed=1
;;
"setup_networking_manually")
echo "$WARN this script cannot automatically configure your network."
;;
*)
echo "$ERR bug: unhandled network strategy: $strategy"
exit 1
;;
esac
if ((failed == 1)); then
echo "$ERR Prerequisites not met. Please install the missing commands and re-run $0."
exit 1
fi
)
whats_next() (
echo "$NEXT 1. Enter /vagrant/deploy and run: source ../.env; docker-compose up -d"
echo "$BLANK 2. Try executing your fist workflow."
echo "$BLANK Follow the steps described in https://tinkerbell.org/examples/hello-world/ to say 'Hello World!' with a workflow."
)
do_setup() (
# perform some very rudimentary platform detection
lsb_dist=$(get_distribution)
lsb_version=$(get_distro_version)
echo "$INFO starting tinkerbell stack setup"
check_prerequisites "$lsb_dist" "$lsb_version"
if ! [[ -f $ENV_FILE ]]; then
echo "$ERR Run './generate-env.sh network-interface > \"$ENV_FILE\"' before continuing."
exit 1
fi
# shellcheck disable=SC1090
source "$ENV_FILE"
setup_networking "$lsb_dist" "$lsb_version"
setup_osie
generate_certificates
setup_docker_registry
echo "$INFO tinkerbell stack setup completed successfully on $lsb_dist server"
whats_next
)
# wrapped up in a function so that we have some protection against only getting
# half the file during "curl | sh"
do_setup