12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121 |
- #!/bin/bash
- # Copyright 2016 The Kubernetes Authors.
- #
- # Licensed under the Apache License, Version 2.0 (the "License");
- # you may not use this file except in compliance with the License.
- # You may obtain a copy of the License at
- #
- # http://www.apache.org/licenses/LICENSE-2.0
- #
- # Unless required by applicable law or agreed to in writing, software
- # distributed under the License is distributed on an "AS IS" BASIS,
- # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- # See the License for the specific language governing permissions and
- # limitations under the License.
- set -o errexit
- set -o nounset
- set -o pipefail
- # A library of helper functions that each provider hosting Kubernetes must implement to use cluster/kube-*.sh scripts.
- KUBE_ROOT=$(dirname "${BASH_SOURCE[0]}")/../..
- # shellcheck source=./config-common.sh
- source "${KUBE_ROOT}/cluster/photon-controller/config-common.sh"
- # shellcheck source=./config-default.sh
- source "${KUBE_ROOT}/cluster/photon-controller/${KUBE_CONFIG_FILE-"config-default.sh"}"
- # shellcheck source=../common.sh
- source "${KUBE_ROOT}/cluster/common.sh"
- readonly PHOTON="photon -n"
- # Naming scheme for VMs (masters & nodes)
- readonly MASTER_NAME="${INSTANCE_PREFIX}-master"
- # shell check claims this doesn't work because you can't use a variable in a brace
- # range. It does work because we're calling eval.
- # shellcheck disable=SC2051
- readonly NODE_NAMES=($(eval echo "${INSTANCE_PREFIX}"-node-{1.."${NUM_NODES}"}))
- #####################################################################
- #
- # Public API
- #
- #####################################################################
- #
- # detect-master will query Photon Controller for the Kubernetes master.
- # It assumes that the VM name for the master is unique.
- # It will set KUBE_MASTER_ID to be the VM ID of the master
- # It will set KUBE_MASTER_IP to be the IP address of the master
- # If the silent parameter is passed, it will not print when the master
- # is found: this is used internally just to find the MASTER
- #
- function detect-master {
- local silent=${1:-""}
- local tenant_args="--tenant ${PHOTON_TENANT} --project ${PHOTON_PROJECT}"
- KUBE_MASTER=${MASTER_NAME}
- KUBE_MASTER_ID=${KUBE_MASTER_ID:-""}
- KUBE_MASTER_IP=${KUBE_MASTER_IP:-""}
- # We don't want silent failure: we check for failure
- set +o pipefail
- if [[ -z ${KUBE_MASTER_ID} ]]; then
- KUBE_MASTER_ID=$(${PHOTON} vm list ${tenant_args} | grep $'\t'"kubernetes-master"$'\t' | awk '{print $1}')
- fi
- if [[ -z ${KUBE_MASTER_ID} ]]; then
- kube::log::error "Could not find Kubernetes master node ID. Make sure you've launched a cluster with kube-up.sh"
- exit 1
- fi
- if [[ -z "${KUBE_MASTER_IP-}" ]]; then
- # Pick out the NICs that have a MAC address owned VMware (with OUI 00:0C:29)
- # Make sure to ignore lines that have a network interface but no address
- KUBE_MASTER_IP=$(${PHOTON} vm networks "${KUBE_MASTER_ID}" | grep -i $'\t'"00:0C:29" | grep -E '[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+' | head -1 | awk -F'\t' '{print $3}')
- fi
- if [[ -z "${KUBE_MASTER_IP-}" ]]; then
- kube::log::error "Could not find Kubernetes master node IP. Make sure you've launched a cluster with 'kube-up.sh'" >&2
- exit 1
- fi
- if [[ -z ${silent} ]]; then
- kube::log::status "Master: $KUBE_MASTER ($KUBE_MASTER_IP)"
- fi
- # Reset default set in common.sh
- set -o pipefail
- }
- #
- # detect-nodes will query Photon Controller for the Kubernetes nodes
- # It assumes that the VM name for the nodes are unique.
- # It assumes that NODE_NAMES has been set
- # It will set KUBE_NODE_IP_ADDRESSES to be the VM IPs of the nodes
- # It will set the KUBE_NODE_IDS to be the VM IDs of the nodes
- # If the silent parameter is passed, it will not print when the nodes
- # are found: this is used internally just to find the MASTER
- #
- function detect-nodes {
- local silent=${1:-""}
- local failure=0
- local tenant_args="--tenant ${PHOTON_TENANT} --project ${PHOTON_PROJECT}"
- KUBE_NODE_IP_ADDRESSES=()
- KUBE_NODE_IDS=()
- # We don't want silent failure: we check for failure
- set +o pipefail
- for (( i=0; i<${#NODE_NAMES[@]}; i++)); do
- local node_id
- node_id=$(${PHOTON} vm list ${tenant_args} | grep $'\t'"${NODE_NAMES[${i}]}"$'\t' | awk '{print $1}')
- if [[ -z ${node_id} ]]; then
- kube::log::error "Could not find ${NODE_NAMES[${i}]}"
- failure=1
- fi
- KUBE_NODE_IDS+=("${node_id}")
- # Pick out the NICs that have a MAC address owned VMware (with OUI 00:0C:29)
- # Make sure to ignore lines that have a network interface but no address
- node_ip=$(${PHOTON} vm networks "${node_id}" | grep -i $'\t'"00:0C:29" | grep -E '[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+' | head -1 | awk -F'\t' '{print $3}')
- KUBE_NODE_IP_ADDRESSES+=("${node_ip}")
- if [[ -z ${silent} ]]; then
- kube::log::status "Node: ${NODE_NAMES[${i}]} (${KUBE_NODE_IP_ADDRESSES[${i}]})"
- fi
- done
- if [[ ${failure} -ne 0 ]]; then
- exit 1
- fi
- # Reset default set in common.sh
- set -o pipefail
- }
- # Get node names if they are not static.
- function detect-node-names {
- echo "TODO: detect-node-names" 1>&2
- }
- #
- # Verifies that this computer has sufficient software installed
- # so that it can run the rest of the script.
- #
- function verify-prereqs {
- verify-cmd-in-path photon
- verify-cmd-in-path ssh
- verify-cmd-in-path scp
- verify-cmd-in-path ssh-add
- verify-cmd-in-path openssl
- verify-cmd-in-path mkisofs
- }
- #
- # The entry point for bringing up a Kubernetes cluster
- #
- function kube-up {
- verify-prereqs
- verify-ssh-prereqs
- verify-photon-config
- ensure-temp-dir
- find-release-tars
- find-image-id
- load-or-gen-kube-basicauth
- gen-cloud-init-iso
- gen-master-start
- create-master-vm
- install-salt-on-master
- gen-node-start
- install-salt-on-nodes
- detect-nodes -s
- install-kubernetes-on-master
- install-kubernetes-on-nodes
- wait-master-api
- wait-node-apis
- setup-pod-routes
- copy-kube-certs
- kube::log::status "Creating kubeconfig..."
- create-kubeconfig
- }
- # Delete a kubernetes cluster
- function kube-down {
- detect-master
- detect-nodes
- pc-delete-vm "${KUBE_MASTER}" "${KUBE_MASTER_ID}"
- for (( node=0; node<${#KUBE_NODE_IDS[@]}; node++)); do
- pc-delete-vm "${NODE_NAMES[${node}]}" "${KUBE_NODE_IDS[${node}]}"
- done
- }
- # Update a kubernetes cluster
- function kube-push {
- echo "TODO: kube-push" 1>&2
- }
- # Prepare update a kubernetes component
- function prepare-push {
- echo "TODO: prepare-push" 1>&2
- }
- # Update a kubernetes master
- function push-master {
- echo "TODO: push-master" 1>&2
- }
- # Update a kubernetes node
- function push-node {
- echo "TODO: push-node" 1>&2
- }
- # Execute prior to running tests to build a release if required for env
- function test-build-release {
- echo "TODO: test-build-release" 1>&2
- }
- # Execute prior to running tests to initialize required structure
- function test-setup {
- echo "TODO: test-setup" 1>&2
- }
- # Execute after running tests to perform any required clean-up
- function test-teardown {
- echo "TODO: test-teardown" 1>&2
- }
- #####################################################################
- #
- # Internal functions
- #
- #####################################################################
- #
- # Uses Photon Controller to make a VM
- # Takes two parameters:
- # - The name of the VM (Assumed to be unique)
- # - The name of the flavor to create the VM (Assumed to be unique)
- #
- # It assumes that the variables in config-common.sh (PHOTON_TENANT, etc)
- # are set correctly.
- #
- # It also assumes the cloud-init ISO has been generated
- #
- # When it completes, it sets two environment variables for use by the
- # caller: _VM_ID (the ID of the created VM) and _VM_IP (the IP address
- # of the created VM)
- #
- function pc-create-vm {
- local vm_name="${1}"
- local vm_flavor="${2}"
- local rc=0
- local i=0
- # Create the VM
- local tenant_args="--tenant ${PHOTON_TENANT} --project ${PHOTON_PROJECT}"
- local vm_args="--name ${vm_name} --image ${PHOTON_IMAGE_ID} --flavor ${vm_flavor}"
- local disk_args="disk-1 ${PHOTON_DISK_FLAVOR} boot=true"
- rc=0
- _VM_ID=$(${PHOTON} vm create ${tenant_args} ${vm_args} --disks "${disk_args}" 2>&1) || rc=$?
- if [[ ${rc} -ne 0 ]]; then
- kube::log::error "Failed to create VM. Error output:"
- echo "${_VM_ID}"
- exit 1
- fi
- kube::log::status "Created VM ${vm_name}: ${_VM_ID}"
- # Start the VM
- # Note that the VM has cloud-init in it, and we attach an ISO that
- # contains a user-data.txt file for cloud-init. When the VM starts,
- # cloud-init will temporarily mount the ISO and configure the VM
- # Our user-data will configure the 'kube' user and set up the ssh
- # authorized keys to allow us to ssh to the VM and do further work.
- run-cmd "${PHOTON} vm attach-iso -p ${KUBE_TEMP}/cloud-init.iso ${_VM_ID}"
- run-cmd "${PHOTON} vm start ${_VM_ID}"
- kube::log::status "Started VM ${vm_name}, waiting for network address..."
- # Wait for the VM to be started and connected to the network
- have_network=0
- for i in {1..120}; do
- # photon -n vm networks print several fields:
- # NETWORK MAC IP GATEWAY CONNECTED?
- # We wait until CONNECTED is True
- rc=0
- networks=$(${PHOTON} vm networks "${_VM_ID}") || rc=$?
- if [[ ${rc} -ne 0 ]]; then
- kube::log::error "'${PHOTON} vm networks ${_VM_ID}' failed. Error output: "
- echo "${networks}"
- fi
- networks=$(echo "${networks}" | grep True) || rc=$?
- if [[ ${rc} -eq 0 ]]; then
- have_network=1
- break;
- fi
- sleep 1
- done
- # Fail if the VM didn't come up
- if [[ ${have_network} -eq 0 ]]; then
- kube::log::error "VM ${vm_name} failed to start up: no IP was found"
- exit 1
- fi
- # Find the IP address of the VM
- _VM_IP=$(${PHOTON} vm networks "${_VM_ID}" | head -1 | awk -F'\t' '{print $3}')
- kube::log::status "VM ${vm_name} has IP: ${_VM_IP}"
- }
- #
- # Delete one of our VMs
- # If it is STARTED, it will be stopped first.
- #
- function pc-delete-vm {
- local vm_name="${1}"
- local vm_id="${2}"
- local rc=0
- kube::log::status "Deleting VM ${vm_name}"
- # In some cases, head exits before photon, so the pipline exits with
- # SIGPIPE. We disable the pipefile option to hide that failure.
- set +o pipefail
- ${PHOTON} vm show "${vm_id}" | head -1 | grep STARTED > /dev/null 2>&1 || rc=$?
- set +o pipefail
- if [[ ${rc} -eq 0 ]]; then
- ${PHOTON} vm stop "${vm_id}" > /dev/null 2>&1 || rc=$?
- if [[ ${rc} -ne 0 ]]; then
- kube::log::error "Error: could not stop ${vm_name} ($vm_id)"
- kube::log::error "Please investigate and stop manually"
- return
- fi
- fi
- rc=0
- ${PHOTON} vm delete "${vm_id}" > /dev/null 2>&1 || rc=$?
- if [[ ${rc} -ne 0 ]]; then
- kube::log::error "Error: could not delete ${vm_name} ($vm_id)"
- kube::log::error "Please investigate and delete manually"
- fi
- }
- #
- # Looks for the image named PHOTON_IMAGE
- # Sets PHOTON_IMAGE_ID to be the id of that image.
- # We currently assume there is exactly one image with name
- #
- function find-image-id {
- local rc=0
- PHOTON_IMAGE_ID=$(${PHOTON} image list | grep $'\t'"${PHOTON_IMAGE}"$'\t' | head -1 | grep READY | awk -F'\t' '{print $1}')
- if [[ ${rc} -ne 0 ]]; then
- kube::log::error "Cannot find image \"${PHOTON_IMAGE}\""
- fail=1
- fi
- }
- #
- # Generate an ISO with a single file called user-data.txt
- # This ISO will be used to configure cloud-init (which is already
- # on the VM). We will tell cloud-init to create the kube user/group
- # and give ourselves the ability to ssh to the VM with ssh. We also
- # allow people to ssh with the same password that was randomly
- # generated for access to Kubernetes as a backup method.
- #
- # Assumes environment variables:
- # - VM_USER
- # - KUBE_PASSWORD (randomly generated password)
- #
- function gen-cloud-init-iso {
- local password_hash
- password_hash=$(openssl passwd -1 "${KUBE_PASSWORD}")
- local ssh_key
- ssh_key=$(ssh-add -L | head -1)
- # Make the user-data file that will be used by cloud-init
- (
- echo "#cloud-config"
- echo ""
- echo "groups:"
- echo " - ${VM_USER}"
- echo ""
- echo "users:"
- echo " - name: ${VM_USER}"
- echo " gecos: Kubernetes"
- echo " primary-group: ${VM_USER}"
- echo " lock-passwd: false"
- echo " passwd: ${password_hash}"
- echo " ssh-authorized-keys: "
- echo " - ${ssh_key}"
- echo " sudo: ALL=(ALL) NOPASSWD:ALL"
- echo " shell: /bin/bash"
- echo ""
- echo "hostname:"
- echo " - hostname: kube"
- ) > "${KUBE_TEMP}/user-data.txt"
- # Make the ISO that will contain the user-data
- # The -rock option means that we'll generate real filenames (long and with case)
- run-cmd "mkisofs -rock -o ${KUBE_TEMP}/cloud-init.iso ${KUBE_TEMP}/user-data.txt"
- }
- #
- # Generate a script used to install salt on the master
- # It is placed into $KUBE_TEMP/master-start.sh
- #
- function gen-master-start {
- python "${KUBE_ROOT}/third_party/htpasswd/htpasswd.py" \
- -b -c "${KUBE_TEMP}/htpasswd" "${KUBE_USER}" "${KUBE_PASSWORD}"
- local htpasswd
- htpasswd=$(cat "${KUBE_TEMP}/htpasswd")
- # This calculation of the service IP should work, but if you choose an
- # alternate subnet, there's a small chance you'd need to modify the
- # service_ip, below. We'll choose an IP like 10.244.240.1 by taking
- # the first three octets of the SERVICE_CLUSTER_IP_RANGE and tacking
- # on a .1
- local octets
- local service_ip
- octets=($(echo "${SERVICE_CLUSTER_IP_RANGE}" | sed -e 's|/.*||' -e 's/\./ /g'))
- ((octets[3]+=1))
- service_ip=$(echo "${octets[*]}" | sed 's/ /./g')
- MASTER_EXTRA_SANS="IP:${service_ip},DNS:${MASTER_NAME},${MASTER_EXTRA_SANS}"
- (
- echo "#! /bin/bash"
- echo "readonly MY_NAME=${MASTER_NAME}"
- grep -v "^#" "${KUBE_ROOT}/cluster/photon-controller/templates/hostname.sh"
- echo "cd /home/kube/cache/kubernetes-install"
- echo "readonly MASTER_NAME='${MASTER_NAME}'"
- echo "readonly MASTER_IP_RANGE='${MASTER_IP_RANGE}'"
- echo "readonly INSTANCE_PREFIX='${INSTANCE_PREFIX}'"
- echo "readonly NODE_INSTANCE_PREFIX='${INSTANCE_PREFIX}-node'"
- echo "readonly NODE_IP_RANGES='${NODE_IP_RANGES}'"
- echo "readonly SERVICE_CLUSTER_IP_RANGE='${SERVICE_CLUSTER_IP_RANGE}'"
- echo "readonly ENABLE_NODE_LOGGING='${ENABLE_NODE_LOGGING:-false}'"
- echo "readonly LOGGING_DESTINATION='${LOGGING_DESTINATION:-}'"
- echo "readonly ENABLE_CLUSTER_DNS='${ENABLE_CLUSTER_DNS:-false}'"
- echo "readonly ENABLE_CLUSTER_UI='${ENABLE_CLUSTER_UI:-false}'"
- echo "readonly DNS_SERVER_IP='${DNS_SERVER_IP:-}'"
- echo "readonly DNS_DOMAIN='${DNS_DOMAIN:-}'"
- echo "readonly KUBE_USER='${KUBE_USER:-}'"
- echo "readonly KUBE_PASSWORD='${KUBE_PASSWORD:-}'"
- echo "readonly SERVER_BINARY_TAR='${SERVER_BINARY_TAR##*/}'"
- echo "readonly SALT_TAR='${SALT_TAR##*/}'"
- echo "readonly MASTER_HTPASSWD='${htpasswd}'"
- echo "readonly E2E_STORAGE_TEST_ENVIRONMENT='${E2E_STORAGE_TEST_ENVIRONMENT:-}'"
- echo "readonly MASTER_EXTRA_SANS='${MASTER_EXTRA_SANS:-}'"
- grep -v "^#" "${KUBE_ROOT}/cluster/photon-controller/templates/create-dynamic-salt-files.sh"
- grep -v "^#" "${KUBE_ROOT}/cluster/photon-controller/templates/install-release.sh"
- grep -v "^#" "${KUBE_ROOT}/cluster/photon-controller/templates/salt-master.sh"
- ) > "${KUBE_TEMP}/master-start.sh"
- }
- #
- # Generate the scripts for each node to install salt
- #
- function gen-node-start {
- local i
- for (( i=0; i<${#NODE_NAMES[@]}; i++)); do
- (
- echo "#! /bin/bash"
- echo "readonly MY_NAME=${NODE_NAMES[${i}]}"
- grep -v "^#" "${KUBE_ROOT}/cluster/photon-controller/templates/hostname.sh"
- echo "KUBE_MASTER=${KUBE_MASTER}"
- echo "KUBE_MASTER_IP=${KUBE_MASTER_IP}"
- echo "NODE_IP_RANGE=$NODE_IP_RANGES"
- grep -v "^#" "${KUBE_ROOT}/cluster/photon-controller/templates/salt-minion.sh"
- ) > "${KUBE_TEMP}/node-start-${i}.sh"
- done
- }
- #
- # Create a script that will run on the Kubernetes master and will run salt
- # to configure the master. We make it a script instead of just running a
- # single ssh command so that we can get logging.
- #
- function gen-master-salt {
- gen-salt "kubernetes-master"
- }
- #
- # Create scripts that will be run on the Kubernetes master. Each of these
- # will invoke salt to configure one of the nodes
- #
- function gen-node-salt {
- local i
- for (( i=0; i<${#NODE_NAMES[@]}; i++)); do
- gen-salt "${NODE_NAMES[${i}]}"
- done
- }
- #
- # Shared implementation for gen-master-salt and gen-node-salt
- # Writes a script that installs Kubernetes with salt
- # The core of the script is simple (run 'salt ... state.highstate')
- # We also do a bit of logging so we can debug problems
- #
- # There is also a funky workaround for an issue with docker 1.9
- # (elsewhere we peg ourselves to docker 1.9). It's fixed in 1.10,
- # so we should be able to remove it in the future
- # https://github.com/docker/docker/issues/18113
- # The problem is that sometimes the install (with apt-get) of
- # docker fails. Deleting a file and retrying fixes it.
- #
- # Tell shellcheck to ignore our variables within single quotes:
- # We're writing a script, not executing it, so this is normal
- # shellcheck disable=SC2016
- function gen-salt {
- node_name=${1}
- (
- echo '#!/bin/bash'
- echo ''
- echo "node=${node_name}"
- echo 'out=/tmp/${node}-salt.out'
- echo 'log=/tmp/${node}-salt.log'
- echo ''
- echo 'echo $(date) >> $log'
- echo 'salt ${node} state.highstate -t 30 --no-color > ${out}'
- echo 'grep -E "Failed:[[:space:]]+0" ${out}'
- echo 'success=$?'
- echo 'cat ${out} >> ${log}'
- echo ''
- echo 'if [[ ${success} -ne 0 ]]; then'
- echo ' # Did we try to install docker-engine?'
- echo ' attempted=$(grep docker-engine ${out} | wc -l)'
- echo ' # Is docker-engine installed?'
- echo ' installed=$(salt --output=txt ${node} pkg.version docker-engine | wc -l)'
- echo ' if [[ ${attempted} -ne 0 && ${installed} -eq 0 ]]; then'
- echo ' echo "Unwedging docker-engine install" >> ${log}'
- echo ' salt ${node} cmd.run "rm -f /var/lib/docker/network/files/local-kv.db"'
- echo ' fi'
- echo 'fi'
- echo 'exit ${success}'
- ) > "${KUBE_TEMP}/${node_name}-salt.sh"
- }
- #
- # Generate a script to add a route to a host (master or node)
- # The script will do two things:
- # 1. Add the route immediately with the route command
- # 2. Persist the route by saving it in /etc/network/interfaces
- # This was done with a script because it was easier to get the quoting right
- # and make it clear.
- #
- function gen-add-route {
- route=${1}
- gateway=${2}
- (
- echo '#!/bin/bash'
- echo ''
- echo '# Immediately add route'
- echo "sudo route add -net ${route} gw ${gateway}"
- echo ''
- echo '# Persist route so it lasts over restarts'
- echo 'sed -in "s|^iface eth0.*|&\n post-up route add -net' "${route} gw ${gateway}|"'" /etc/network/interfaces'
- ) > "${KUBE_TEMP}/add-route.sh"
- }
- #
- # Create the Kubernetes master VM
- # Sets global variables:
- # - KUBE_MASTER (Name)
- # - KUBE_MASTER_ID (Photon VM ID)
- # - KUBE_MASTER_IP (IP address)
- #
- function create-master-vm {
- kube::log::status "Starting master VM..."
- pc-create-vm "${MASTER_NAME}" "${PHOTON_MASTER_FLAVOR}"
- KUBE_MASTER=${MASTER_NAME}
- KUBE_MASTER_ID=${_VM_ID}
- KUBE_MASTER_IP=${_VM_IP}
- }
- #
- # Install salt on the Kubernetes master
- # Relies on the master-start.sh script created in gen-master-start
- #
- function install-salt-on-master {
- kube::log::status "Installing salt on master..."
- upload-server-tars "${MASTER_NAME}" "${KUBE_MASTER_IP}"
- run-script-remotely "${KUBE_MASTER_IP}" "${KUBE_TEMP}/master-start.sh"
- }
- #
- # Installs salt on Kubernetes nodes in parallel
- # Relies on the node-start script created in gen-node-start
- #
- function install-salt-on-nodes {
- kube::log::status "Creating nodes and installing salt on them..."
- # Start each of the VMs in parallel
- # In the future, we'll batch this because it doesn't scale well
- # past 10 or 20 nodes
- local node
- for (( node=0; node<${#NODE_NAMES[@]}; node++)); do
- (
- pc-create-vm "${NODE_NAMES[${node}]}" "${PHOTON_NODE_FLAVOR}"
- run-script-remotely "${_VM_IP}" "${KUBE_TEMP}/node-start-${node}.sh"
- ) &
- done
- # Wait for the node VM startups to complete
- local fail=0
- local job
- for job in $(jobs -p); do
- wait "${job}" || fail=$((fail + 1))
- done
- if (( fail != 0 )); then
- kube::log::error "Failed to start ${fail}/${NUM_NODES} nodes"
- exit 1
- fi
- }
- #
- # Install Kubernetes on the master.
- # This uses the kubernetes-master-salt.sh script created by gen-master-salt
- # That script uses salt to install Kubernetes
- #
- function install-kubernetes-on-master {
- # Wait until salt-master is running: it may take a bit
- try-until-success-ssh "${KUBE_MASTER_IP}" \
- "Waiting for salt-master to start on ${KUBE_MASTER}" \
- "pgrep salt-master"
- gen-master-salt
- copy-file-to-vm "${_VM_IP}" "${KUBE_TEMP}/kubernetes-master-salt.sh" "/tmp/kubernetes-master-salt.sh"
- try-until-success-ssh "${KUBE_MASTER_IP}" \
- "Installing Kubernetes on ${KUBE_MASTER} via salt" \
- "sudo /bin/bash /tmp/kubernetes-master-salt.sh"
- }
- #
- # Install Kubernetes on the the nodes in parallel
- # This uses the kubernetes-master-salt.sh script created by gen-node-salt
- # That script uses salt to install Kubernetes
- #
- function install-kubernetes-on-nodes {
- gen-node-salt
- # Run in parallel to bring up the cluster faster
- # TODO: Batch this so that we run up to N in parallel, so
- # we don't overload this machine or the salt master
- local node
- for (( node=0; node<${#NODE_NAMES[@]}; node++)); do
- (
- copy-file-to-vm "${_VM_IP}" "${KUBE_TEMP}/${NODE_NAMES[${node}]}-salt.sh" "/tmp/${NODE_NAMES[${node}]}-salt.sh"
- try-until-success-ssh "${KUBE_NODE_IP_ADDRESSES[${node}]}" \
- "Waiting for salt-master to start on ${NODE_NAMES[${node}]}" \
- "pgrep salt-minion"
- try-until-success-ssh "${KUBE_MASTER_IP}" \
- "Installing Kubernetes on ${NODE_NAMES[${node}]} via salt" \
- "sudo /bin/bash /tmp/${NODE_NAMES[${node}]}-salt.sh"
- ) &
- done
- # Wait for the Kubernetes installations to complete
- local fail=0
- local job
- for job in $(jobs -p); do
- wait "${job}" || fail=$((fail + 1))
- done
- if (( fail != 0 )); then
- kube::log::error "Failed to start install Kubernetes on ${fail} out of ${NUM_NODES} nodess"
- exit 1
- fi
- }
- #
- # Upload the Kubernetes tarballs to the master
- #
- function upload-server-tars {
- vm_name=${1}
- vm_ip=${2}
- run-ssh-cmd "${vm_ip}" "mkdir -p /home/kube/cache/kubernetes-install"
- local tar
- for tar in "${SERVER_BINARY_TAR}" "${SALT_TAR}"; do
- local base_tar
- base_tar=$(basename "${tar}")
- kube::log::status "Uploading ${base_tar} to ${vm_name}..."
- copy-file-to-vm "${vm_ip}" "${tar}" "/home/kube/cache/kubernetes-install/${tar##*/}"
- done
- }
- #
- # Wait for the Kubernets healthz API to be responsive on the master
- #
- function wait-master-api {
- local curl_creds="--insecure --user ${KUBE_USER}:${KUBE_PASSWORD}"
- local curl_output="--fail --output /dev/null --silent"
- local curl_net="--max-time 1"
- try-until-success "Waiting for Kubernetes API on ${KUBE_MASTER}" \
- "curl ${curl_creds} ${curl_output} ${curl_net} https://${KUBE_MASTER_IP}/healthz"
- }
- #
- # Wait for the Kubernetes healthz API to be responsive on each node
- #
- function wait-node-apis {
- local curl_output="--fail --output /dev/null --silent"
- local curl_net="--max-time 1"
- for (( i=0; i<${#NODE_NAMES[@]}; i++)); do
- try-until-success "Waiting for Kubernetes API on ${NODE_NAMES[${i}]}..." \
- "curl ${curl_output} ${curl_net} http://${KUBE_NODE_IP_ADDRESSES[${i}]}:10250/healthz"
- done
- }
- #
- # Configure the nodes so the pods can communicate
- # Each node will have a bridge named cbr0 for the NODE_IP_RANGES
- # defined in config-default.sh. This finds the IP subnet (assigned
- # by Kubernetes) to nodes and configures routes so they can communicate
- #
- # Also configure the master to be able to talk to the nodes. This is
- # useful so that you can get to the UI from the master.
- #
- function setup-pod-routes {
- local node
- KUBE_NODE_BRIDGE_NETWORK=()
- for (( node=0; node<${#NODE_NAMES[@]}; node++)); do
- # This happens in two steps (wait for an address, wait for a non 172.x.x.x address)
- # because it's both simpler and more clear what's happening.
- try-until-success-ssh "${KUBE_NODE_IP_ADDRESSES[${node}]}" \
- "Waiting for cbr0 bridge on ${NODE_NAMES[${node}]} to have an address" \
- 'sudo ifconfig cbr0 | grep -oP "inet addr:\K\S+"'
- try-until-success-ssh "${KUBE_NODE_IP_ADDRESSES[${node}]}" \
- "Waiting for cbr0 bridge on ${NODE_NAMES[${node}]} to have correct address" \
- 'sudo ifconfig cbr0 | grep -oP "inet addr:\K\S+" | grep -v "^172."'
- run-ssh-cmd "${KUBE_NODE_IP_ADDRESSES[${node}]}" 'sudo ip route show | grep -E "dev cbr0" | cut -d " " -f1'
- KUBE_NODE_BRIDGE_NETWORK+=(${_OUTPUT})
- kube::log::status "cbr0 on ${NODE_NAMES[${node}]} is ${_OUTPUT}"
- done
- local i
- local j
- for (( i=0; i<${#NODE_NAMES[@]}; i++)); do
- kube::log::status "Configuring pod routes on ${NODE_NAMES[${i}]}..."
- gen-add-route "${KUBE_NODE_BRIDGE_NETWORK[${i}]}" "${KUBE_NODE_IP_ADDRESSES[${i}]}"
- run-script-remotely "${KUBE_MASTER_IP}" "${KUBE_TEMP}/add-route.sh"
- for (( j=0; j<${#NODE_NAMES[@]}; j++)); do
- if [[ "${i}" != "${j}" ]]; then
- gen-add-route "${KUBE_NODE_BRIDGE_NETWORK[${j}]}" "${KUBE_NODE_IP_ADDRESSES[${j}]}"
- run-script-remotely "${KUBE_NODE_IP_ADDRESSES[${i}]}" "${KUBE_TEMP}/add-route.sh"
- fi
- done
- done
- }
- #
- # Copy the certificate/key from the Kubernetes master
- # These are used to create the kubeconfig file, which allows
- # users to use kubectl easily
- #
- # We also set KUBE_CERT, KUBE_KEY, CA_CERT, and CONTEXT because they
- # are needed by create-kubeconfig from common.sh to generate
- # the kube config file.
- #
- function copy-kube-certs {
- local cert="kubecfg.crt"
- local key="kubecfg.key"
- local ca="ca.crt"
- local cert_dir="/srv/kubernetes"
- kube::log::status "Copying credentials from ${KUBE_MASTER}"
- # Set global environment variables: needed by create-kubeconfig
- # in common.sh
- export KUBE_CERT="${KUBE_TEMP}/${cert}"
- export KUBE_KEY="${KUBE_TEMP}/${key}"
- export CA_CERT="${KUBE_TEMP}/${ca}"
- export CONTEXT="photon-${INSTANCE_PREFIX}"
- run-ssh-cmd "${KUBE_MASTER_IP}" "sudo chmod 644 ${cert_dir}/${cert}"
- run-ssh-cmd "${KUBE_MASTER_IP}" "sudo chmod 644 ${cert_dir}/${key}"
- run-ssh-cmd "${KUBE_MASTER_IP}" "sudo chmod 644 ${cert_dir}/${ca}"
- copy-file-from-vm "${KUBE_MASTER_IP}" "${cert_dir}/${cert}" "${KUBE_CERT}"
- copy-file-from-vm "${KUBE_MASTER_IP}" "${cert_dir}/${key}" "${KUBE_KEY}"
- copy-file-from-vm "${KUBE_MASTER_IP}" "${cert_dir}/${ca}" "${CA_CERT}"
- run-ssh-cmd "${KUBE_MASTER_IP}" "sudo chmod 600 ${cert_dir}/${cert}"
- run-ssh-cmd "${KUBE_MASTER_IP}" "sudo chmod 600 ${cert_dir}/${key}"
- run-ssh-cmd "${KUBE_MASTER_IP}" "sudo chmod 600 ${cert_dir}/${ca}"
- }
- #
- # Copies a script to a VM and runs it
- # Parameters:
- # - IP of VM
- # - Path to local file
- #
- function run-script-remotely {
- local vm_ip=${1}
- local local_file="${2}"
- local base_file
- local remote_file
- base_file=$(basename "${local_file}")
- remote_file="/tmp/${base_file}"
- copy-file-to-vm "${vm_ip}" "${local_file}" "${remote_file}"
- run-ssh-cmd "${vm_ip}" "chmod 700 ${remote_file}"
- run-ssh-cmd "${vm_ip}" "nohup sudo ${remote_file} < /dev/null 1> ${remote_file}.out 2>&1 &"
- }
- #
- # Runs an command on a VM using ssh
- # Parameters:
- # - (optional) -i to ignore failure
- # - IP address of the VM
- # - Command to run
- # Assumes environment variables:
- # - VM_USER
- # - SSH_OPTS
- #
- function run-ssh-cmd {
- local ignore_failure=""
- if [[ "${1}" = "-i" ]]; then
- ignore_failure="-i"
- shift
- fi
- local vm_ip=${1}
- shift
- local cmd=${1}
- run-cmd ${ignore_failure} "ssh ${SSH_OPTS} $VM_USER@${vm_ip} $1"
- }
- #
- # Uses scp to copy file to VM
- # Parameters:
- # - IP address of the VM
- # - Path to local file
- # - Path to remote file
- # Assumes environment variables:
- # - VM_USER
- # - SSH_OPTS
- #
- function copy-file-to-vm {
- local vm_ip=${1}
- local local_file=${2}
- local remote_file=${3}
- run-cmd "scp ${SSH_OPTS} ${local_file} ${VM_USER}@${vm_ip}:${remote_file}"
- }
- function copy-file-from-vm {
- local vm_ip=${1}
- local remote_file=${2}
- local local_file=${3}
- run-cmd "scp ${SSH_OPTS} ${VM_USER}@${vm_ip}:${remote_file} ${local_file}"
- }
- #
- # Run a command, print nice error output
- # Used by copy-file-to-vm and run-ssh-cmd
- #
- function run-cmd {
- local rc=0
- local ignore_failure=""
- if [[ "${1}" = "-i" ]]; then
- ignore_failure=${1}
- shift
- fi
- local cmd=$1
- local output
- output=$(${cmd} 2>&1) || rc=$?
- if [[ ${rc} -ne 0 ]]; then
- if [[ -z "${ignore_failure}" ]]; then
- kube::log::error "Failed to run command: ${cmd} Output:"
- echo "${output}"
- exit 1
- fi
- fi
- _OUTPUT=${output}
- return ${rc}
- }
- #
- # After the initial VM setup, we use SSH with keys to access the VMs
- # This requires an SSH agent, so we verify that it's running
- #
- function verify-ssh-prereqs {
- kube::log::status "Validating SSH configuration..."
- local rc
- rc=0
- ssh-add -L 1> /dev/null 2> /dev/null || rc=$?
- # "Could not open a connection to your authentication agent."
- if [[ "${rc}" -eq 2 ]]; then
- # ssh agent wasn't running, so start it and ensure we stop it
- eval "$(ssh-agent)" > /dev/null
- trap-add "kill ${SSH_AGENT_PID}" EXIT
- fi
- rc=0
- ssh-add -L 1> /dev/null 2> /dev/null || rc=$?
- # "The agent has no identities."
- if [[ "${rc}" -eq 1 ]]; then
- # Try adding one of the default identities, with or without passphrase.
- ssh-add || true
- fi
- # Expect at least one identity to be available.
- if ! ssh-add -L 1> /dev/null 2> /dev/null; then
- kube::log::error "Could not find or add an SSH identity."
- kube::log::error "Please start ssh-agent, add your identity, and retry."
- exit 1
- fi
- }
- #
- # Verify that Photon Controller has been configured in the way we expect. Specifically
- # - Have the flavors been created?
- # - Has the image been uploaded?
- # TODO: Check the tenant and project as well.
- function verify-photon-config {
- kube::log::status "Validating Photon configuration..."
- # We don't want silent failure: we check for failure
- set +o pipefail
- verify-photon-flavors
- verify-photon-image
- verify-photon-tenant
- # Reset default set in common.sh
- set -o pipefail
- }
- #
- # Verify that the VM and disk flavors have been created
- #
- function verify-photon-flavors {
- local rc=0
- ${PHOTON} flavor list | awk -F'\t' '{print $2}' | grep -q "^${PHOTON_MASTER_FLAVOR}$" > /dev/null 2>&1 || rc=$?
- if [[ ${rc} -ne 0 ]]; then
- kube::log::error "ERROR: Cannot find VM flavor named ${PHOTON_MASTER_FLAVOR}"
- exit 1
- fi
- if [[ "${PHOTON_MASTER_FLAVOR}" != "${PHOTON_NODE_FLAVOR}" ]]; then
- rc=0
- ${PHOTON} flavor list | awk -F'\t' '{print $2}' | grep -q "^${PHOTON_NODE_FLAVOR}$" > /dev/null 2>&1 || rc=$?
- if [[ ${rc} -ne 0 ]]; then
- kube::log::error "ERROR: Cannot find VM flavor named ${PHOTON_NODE_FLAVOR}"
- exit 1
- fi
- fi
- ${PHOTON} flavor list | awk -F'\t' '{print $2}' | grep -q "^${PHOTON_DISK_FLAVOR}$" > /dev/null 2>&1 || rc=$?
- if [[ ${rc} -ne 0 ]]; then
- kube::log::error "ERROR: Cannot find disk flavor named ${PHOTON_DISK_FLAVOR}"
- exit 1
- fi
- }
- #
- # Verify that we have the image we need, and it's not in error state or
- # multiple copies
- #
- function verify-photon-image {
- local rc
- rc=0
- ${PHOTON} image list | grep -q $'\t'"${PHOTON_IMAGE}"$'\t' > /dev/null 2>&1 || rc=$?
- if [[ ${rc} -ne 0 ]]; then
- kube::log::error "ERROR: Cannot find image \"${PHOTON_IMAGE}\""
- exit 1
- fi
- rc=0
- ${PHOTON} image list | grep $'\t'"${PHOTON_IMAGE}"$'\t' | grep ERROR > /dev/null 2>&1 || rc=$?
- if [[ ${rc} -eq 0 ]]; then
- echo "Warning: You have at least one ${PHOTON_IMAGE} image in the ERROR state. You may want to investigate."
- echo "Images in the ERROR state will be ignored."
- fi
- rc=0
- num_images=$(${PHOTON} image list | grep $'\t'"${PHOTON_IMAGE}"$'\t' | grep -c READY)
- if [[ "${num_images}" -gt 1 ]]; then
- echo "ERROR: You have more than one READY ${PHOTON_IMAGE} image. Ensure there is only one"
- exit 1
- fi
- }
- function verify-photon-tenant {
- local rc
- rc=0
- ${PHOTON} tenant list | grep -q $'\t'"${PHOTON_TENANT}" > /dev/null 2>&1 || rc=$?
- if [[ ${rc} -ne 0 ]]; then
- echo "ERROR: Cannot find tenant \"${PHOTON_TENANT}\""
- exit 1
- fi
- ${PHOTON} project list --tenant "${PHOTON_TENANT}" | grep -q $'\t'"${PHOTON_PROJECT}"$'\t' > /dev/null 2>&1 || rc=$?
- if [[ ${rc} -ne 0 ]]; then
- echo "ERROR: Cannot find project \"${PHOTON_PROJECT}\""
- exit 1
- fi
- }
- #
- # Verifies that a given command is in the PATH
- #
- function verify-cmd-in-path {
- cmd=${1}
- which "${cmd}" >/dev/null || {
- kube::log::error "Can't find ${cmd} in PATH, please install and retry."
- exit 1
- }
- }
- #
- # Checks that KUBE_TEMP is set, or sets it
- # If it sets it, it also creates the temporary directory
- # and sets up a trap so that we delete it when we exit
- #
- function ensure-temp-dir {
- if [[ -z ${KUBE_TEMP-} ]]; then
- KUBE_TEMP=$(mktemp -d -t kubernetes.XXXXXX)
- trap-add "rm -rf '${KUBE_TEMP}'" EXIT
- fi
- }
- #
- # Repeatedly try a command over ssh until it succeeds or until five minutes have passed
- # The timeout isn't exact, since we assume the command runs instantaneously, and
- # it doesn't.
- #
- function try-until-success-ssh {
- local vm_ip=${1}
- local cmd_description=${2}
- local cmd=${3}
- local timeout=600
- local sleep_time=5
- local max_attempts
- ((max_attempts=timeout/sleep_time))
- kube::log::status "${cmd_description} for up to 10 minutes..."
- local attempt=0
- while true; do
- local rc=0
- run-ssh-cmd -i "${vm_ip}" "${cmd}" || rc=1
- if [[ ${rc} != 0 ]]; then
- if (( attempt == max_attempts )); then
- kube::log::error "Failed, cannot proceed: you may need to retry to log into the VM to debug"
- exit 1
- fi
- else
- break
- fi
- attempt=$((attempt+1))
- sleep ${sleep_time}
- done
- }
- function try-until-success {
- local cmd_description=${1}
- local cmd=${2}
- local timeout=600
- local sleep_time=5
- local max_attempts
- ((max_attempts=timeout/sleep_time))
- kube::log::status "${cmd_description} for up to 10 minutes..."
- local attempt=0
- while true; do
- local rc=0
- run-cmd -i "${cmd}" || rc=1
- if [[ ${rc} != 0 ]]; then
- if (( attempt == max_attempts )); then
- kube::log::error "Failed, cannot proceed"
- exit 1
- fi
- else
- break
- fi
- attempt=$((attempt+1))
- sleep ${sleep_time}
- done
- }
- #
- # Sets up a trap handler
- #
- function trap-add {
- local handler="${1}"
- local signal="${2-EXIT}"
- local cur
- cur="$(eval "sh -c 'echo \$3' -- $(trap -p ${signal})")"
- if [[ -n "${cur}" ]]; then
- handler="${cur}; ${handler}"
- fi
- # We want ${handler} to expand now, so tell shellcheck
- # shellcheck disable=SC2064
- trap "${handler}" ${signal}
- }
|