#!/usr/bin/env bash # stops the execution if a command or pipeline has an error set -euxo pipefail # 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 <"$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 "$ERR Prerequisite executable command not found: $1" return 1 fi if ! [[ -s "$(which "$1")" ]]; then echo "$ERR Prerequisite command is an empty file: $1" fi echo "$BLANK Found prerequisite: $1" return 0 ) 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 /deploy and run: source ../.env; docker-compose up -d" echo "$BLANK 2. Try executing your first 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" if [[ -z $TINKERBELL_SKIP_NETWORKING ]]; then setup_networking "$lsb_dist" "$lsb_version" fi setup_osie generate_certificates setup_docker_registry echo "$INFO tinkerbell stack setup completed successfully on $lsb_dist server" whats_next | tee /tmp/post-setup-message ) # wrapped up in a function so that we have some protection against only getting # half the file during "curl | sh" do_setup