commit 6ede8cb2e32ba8fcbf7e88ecebf35bb75e1ad8af Author: Gianluca Arbezzano Date: Thu Aug 20 13:53:27 2020 +0200 initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..f46e644 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +deploy/state diff --git a/CODEOWNERS b/CODEOWNERS new file mode 100644 index 0000000..5d32109 --- /dev/null +++ b/CODEOWNERS @@ -0,0 +1,6 @@ +# These owners will be the default owners for everything in +# the repo. Unless a later match takes precedence, +# @global-owner1 and @global-owner2 will be requested for +# review when someone opens a pull request. + +* @gauravgahlot @gianarb diff --git a/README.md b/README.md new file mode 100644 index 0000000..66a50ec --- /dev/null +++ b/README.md @@ -0,0 +1 @@ +See proposal/0007 diff --git a/deploy/.gitignore b/deploy/.gitignore new file mode 100644 index 0000000..ff72b5c --- /dev/null +++ b/deploy/.gitignore @@ -0,0 +1 @@ +state diff --git a/deploy/db/tinkerbell-init.sql b/deploy/db/tinkerbell-init.sql new file mode 100644 index 0000000..9b27c81 --- /dev/null +++ b/deploy/db/tinkerbell-init.sql @@ -0,0 +1,76 @@ +SET ROLE tinkerbell; + +CREATE TABLE IF NOT EXISTS hardware ( + id UUID UNIQUE + , inserted_at TIMESTAMPTZ + , deleted_at TIMESTAMPTZ + , data JSONB +); + +CREATE INDEX IF NOT EXISTS idx_id ON hardware (id); +CREATE INDEX IF NOT EXISTS idx_deleted_at ON hardware (deleted_at NULLS FIRST); +CREATE INDEX IF NOT EXISTS idxgin_type ON hardware USING GIN (data JSONB_PATH_OPS); + +CREATE TABLE IF NOT EXISTS template ( + id UUID UNIQUE NOT NULL + , name VARCHAR(200) NOT NULL + , created_at TIMESTAMPTZ + , updated_at TIMESTAMPTZ + , deleted_at TIMESTAMPTZ + , data BYTEA + + CONSTRAINT CK_name CHECK (name ~ '^[a-zA-Z0-9_-]*$') +); + +CREATE INDEX IF NOT EXISTS idx_tid ON template (id); +CREATE INDEX IF NOT EXISTS idx_tdeleted_at ON template (deleted_at NULLS FIRST); + +CREATE TABLE IF NOT EXISTS workflow ( + id UUID UNIQUE NOT NULL + , template UUID NOT NULL + , devices JSONB NOT NULL + , created_at TIMESTAMPTZ + , updated_at TIMESTAMPTZ + , deleted_at TIMESTAMPTZ +); + +CREATE INDEX IF NOT EXISTS idx_wid ON workflow (id); +CREATE INDEX IF NOT EXISTS idx_wdeleted_at ON workflow (deleted_at NULLS FIRST); + +CREATE TABLE IF NOT EXISTS workflow_state ( + workflow_id UUID UNIQUE NOT NULL + , current_task_name VARCHAR(200) + , current_action_name VARCHAR(200) + , current_action_state SMALLINT + , current_worker VARCHAR(200) + , action_list JSONB + , current_action_index int + , total_number_of_actions INT +); + +CREATE INDEX IF NOT EXISTS idx_wfid ON workflow_state (workflow_id); + +CREATE TABLE IF NOT EXISTS workflow_event ( + workflow_id UUID NOT NULL + , worker_id UUID NOT NULL + , task_name VARCHAR(200) + , action_name VARCHAR(200) + , execution_time int + , message VARCHAR(200) + , status SMALLINT + , created_at TIMESTAMPTZ +); + +CREATE INDEX IF NOT EXISTS idx_event ON workflow_event (created_at); + +CREATE TABLE IF NOT EXISTS workflow_worker_map ( + workflow_id UUID NOT NULL + , worker_id UUID NOT NULL +); + +CREATE TABLE IF NOT EXISTS workflow_data ( + workflow_id UUID NOT NULL + , version INT + , metadata JSONB + , data JSONB +); diff --git a/deploy/docker-compose.yml b/deploy/docker-compose.yml new file mode 100644 index 0000000..4207992 --- /dev/null +++ b/deploy/docker-compose.yml @@ -0,0 +1,155 @@ +version: "2.1" +services: + tink-server: + image: quay.io/tinkerbell/tink:sha-adb49da + restart: unless-stopped + environment: + FACILITY: ${FACILITY:-onprem} + PACKET_ENV: ${PACKET_ENV:-testing} + PACKET_VERSION: ${PACKET_VERSION:-ignored} + ROLLBAR_TOKEN: ${ROLLBAR_TOKEN:-ignored} + ROLLBAR_DISABLE: ${ROLLBAR_DISABLE:-1} + PGDATABASE: tinkerbell + PGHOST: db + PGPASSWORD: tinkerbell + PGPORT: 5432 + PGSSLMODE: disable + PGUSER: tinkerbell + TINKERBELL_GRPC_AUTHORITY: :42113 + TINKERBELL_HTTP_AUTHORITY: :42114 + TINK_AUTH_USERNAME: ${TINKERBELL_TINK_USERNAME} + TINK_AUTH_PASSWORD: ${TINKERBELL_TINK_PASSWORD} + depends_on: + db: + condition: service_healthy + healthcheck: + test: ["CMD-SHELL", "wget -qO- 127.0.0.1:42114/cert"] # port needs to match TINKERBELL_HTTP_AUTHORITY + interval: 5s + timeout: 2s + retries: 30 + volumes: + - ./state/certs:/certs/${FACILITY:-onprem} + ports: + - 42113:42113/tcp + - 42114:42114/tcp + + db: + image: postgres:10-alpine + restart: unless-stopped + environment: + POSTGRES_DB: tinkerbell + POSTGRES_PASSWORD: tinkerbell + POSTGRES_USER: tinkerbell + volumes: + - ./db/tinkerbell-init.sql:/docker-entrypoint-initdb.d/tinkerbell-init.sql:ro + - postgres_data:/var/lib/postgresql/data:rw + ports: + - 5432:5432 + healthcheck: + test: ["CMD-SHELL", "pg_isready -U tinkerbell"] + interval: 1s + timeout: 1s + retries: 30 + + tink-cli: + image: quay.io/tinkerbell/tink-cli:sha-adb49da + restart: unless-stopped + environment: + TINKERBELL_GRPC_AUTHORITY: 127.0.0.1:42113 + TINKERBELL_CERT_URL: http://127.0.0.1:42114/cert + depends_on: + tink-server: + condition: service_healthy + db: + condition: service_healthy + network_mode: host + + registry: + build: + context: registry + args: + REGISTRY_USERNAME: $TINKERBELL_REGISTRY_USERNAME + REGISTRY_PASSWORD: $TINKERBELL_REGISTRY_PASSWORD + restart: unless-stopped + healthcheck: + test: ["CMD-SHELL", "curl --cacert /certs/ca.pem https://127.0.0.1"] + interval: 5s + timeout: 1s + retries: 5 + environment: + REGISTRY_HTTP_ADDR: 0.0.0.0:443 + REGISTRY_HTTP_TLS_CERTIFICATE: /certs/server.pem + REGISTRY_HTTP_TLS_KEY: /certs/server-key.pem + REGISTRY_AUTH: htpasswd + REGISTRY_AUTH_HTPASSWD_REALM: "Registry Realm" + REGISTRY_AUTH_HTPASSWD_PATH: /auth/htpasswd + volumes: + - ./state/certs:/certs + - ./state/registry:/var/lib/registry + network_mode: host + + boots: + image: quay.io/tinkerbell/boots:327-58ab49913b5498908b16e2607d265a61a05f73b6 + restart: unless-stopped + network_mode: host + command: -dhcp-addr 0.0.0.0:67 -tftp-addr $TINKERBELL_HOST_IP:69 -http-addr $TINKERBELL_HOST_IP:80 -log-level DEBUG + environment: + API_AUTH_TOKEN: ${PACKET_API_AUTH_TOKEN:-ignored} + API_CONSUMER_TOKEN: ${PACKET_CONSUMER_TOKEN:-ignored} + FACILITY_CODE: ${FACILITY:-onprem} + PACKET_ENV: ${PACKET_ENV:-testing} + PACKET_VERSION: ${PACKET_VERSION:-ignored} + ROLLBAR_TOKEN: ${ROLLBAR_TOKEN:-ignored} + ROLLBAR_DISABLE: ${ROLLBAR_DISABLE:-1} + MIRROR_HOST: ${TINKERBELL_NGINX_IP:-127.0.0.1} + DNS_SERVERS: 8.8.8.8 + PUBLIC_IP: $TINKERBELL_HOST_IP + BOOTP_BIND: $TINKERBELL_HOST_IP:67 + HTTP_BIND: $TINKERBELL_HOST_IP:80 + SYSLOG_BIND: $TINKERBELL_HOST_IP:514 + TFTP_BIND: $TINKERBELL_HOST_IP:69 + DOCKER_REGISTRY: $TINKERBELL_HOST_IP + REGISTRY_USERNAME: $TINKERBELL_REGISTRY_USERNAME + REGISTRY_PASSWORD: $TINKERBELL_REGISTRY_PASSWORD + TINKERBELL_GRPC_AUTHORITY: $TINKERBELL_HOST_IP:42113 + TINKERBELL_CERT_URL: http://$TINKERBELL_HOST_IP:42114/cert + ELASTIC_SEARCH_URL: $TINKERBELL_HOST_IP:9200 + DATA_MODEL_VERSION: 1 + depends_on: + db: + condition: service_healthy + ports: + - $TINKERBELL_HOST_IP:80:80/tcp + - 67:67/udp + - 69:69/udp + + nginx: + image: nginx:alpine + restart: unless-stopped + tty: true + ports: + - $TINKERBELL_NGINX_IP:80:80/tcp + volumes: + - ./state/webroot:/usr/share/nginx/html/ + + hegel: + image: quay.io/tinkerbell/hegel:196-fa897aa020769db8becb9be29adaeb6be92a7fc7 + restart: unless-stopped + network_mode: host + environment: + ROLLBAR_TOKEN: ${ROLLBAR_TOKEN-ignored} + ROLLBAR_DISABLE: 1 + PACKET_ENV: testing + PACKET_VERSION: ${PACKET_VERSION:-ignored} + GRPC_PORT: 42115 + HEGEL_FACILITY: ${FACILITY:-onprem} + HEGEL_USE_TLS: 0 + TINKERBELL_GRPC_AUTHORITY: 127.0.0.1:42113 + TINKERBELL_CERT_URL: http://127.0.0.1:42114/cert + DATA_MODEL_VERSION: 1 + depends_on: + db: + condition: service_healthy + +volumes: + postgres_data: diff --git a/deploy/registry/Dockerfile b/deploy/registry/Dockerfile new file mode 100644 index 0000000..dedd7d6 --- /dev/null +++ b/deploy/registry/Dockerfile @@ -0,0 +1,7 @@ +FROM registry:2.7.1 +RUN apk add --no-cache --update curl apache2-utils +ARG REGISTRY_USERNAME +ARG REGISTRY_PASSWORD +RUN mkdir -p /certs /auth +RUN htpasswd -Bbn ${REGISTRY_USERNAME} ${REGISTRY_PASSWORD} > /auth/htpasswd +EXPOSE 443 diff --git a/deploy/terraform/README.md b/deploy/terraform/README.md new file mode 100644 index 0000000..5801874 --- /dev/null +++ b/deploy/terraform/README.md @@ -0,0 +1 @@ +https://tinkerbell.org/docs/setup/packet-with-terraform/ diff --git a/deploy/terraform/input.tf b/deploy/terraform/input.tf new file mode 100644 index 0000000..958888c --- /dev/null +++ b/deploy/terraform/input.tf @@ -0,0 +1,17 @@ +variable "packet_api_token" { + description = "Packet user api token" +} + +variable "project_id" { + description = "Project ID" +} + +variable "facility" { + description = "Packet facility to provision in" + default = "sjc1" +} + +variable "device_type" { + description = "Type of device to provision" + default = "c3.small.x86" +} diff --git a/deploy/terraform/install_package.sh b/deploy/terraform/install_package.sh new file mode 100644 index 0000000..9992160 --- /dev/null +++ b/deploy/terraform/install_package.sh @@ -0,0 +1,66 @@ +#!/usr/bin/env bash + +YUM="yum" +APT="apt" +PIP3="pip3" +YUM_CONFIG_MGR="yum-config-manager" +WHICH_YUM=$(command -v $YUM) +WHICH_APT=$(command -v $APT) +YUM_INSTALL="$YUM install" +APT_INSTALL="$APT install" +PIP3_INSTALL="$PIP3 install" +declare -a YUM_LIST=("https://download.docker.com/linux/centos/7/x86_64/stable/Packages/containerd.io-1.2.6-3.3.el7.x86_64.rpm" + "docker-ce" + "docker-ce-cli" + "epel-release" + "python3") +declare -a APT_LIST=("docker" + "docker-compose") + +add_yum_repo() ( + $YUM_CONFIG_MGR --add-repo https://download.docker.com/linux/centos/docker-ce.repo +) + +update_yum() ( + $YUM_INSTALL -y yum-utils + add_yum_repo +) + +update_apt() ( + $APT update + DEBIAN_FRONTEND=noninteractive $APT --yes --force-yes -o Dpkg::Options::="--force-confdef" -o Dpkg::Options::="--force-confold" upgrade +) + +restart_docker_service() ( + service docker restart +) + +install_yum_packages() ( + $YUM_INSTALL "${YUM_LIST[@]}" -y +) + +install_pip3_packages() ( + $PIP3_INSTALL docker-compose +) + +install_apt_packages() ( + $APT_INSTALL "${APT_LIST[@]}" -y +) + +main() ( + if [[ -n $WHICH_YUM ]]; then + update_yum + install_yum_packages + install_pip3_packages + restart_docker_service + elif [[ -n $WHICH_APT ]]; then + update_apt + install_apt_packages + restart_docker_service + else + echo "Unknown platform. Error while installing the required packages" + exit 1 + fi +) + +main diff --git a/deploy/terraform/main.tf b/deploy/terraform/main.tf new file mode 100644 index 0000000..3fbe9d3 --- /dev/null +++ b/deploy/terraform/main.tf @@ -0,0 +1,65 @@ +# Configure the Packet Provider. +provider "packet" { + auth_token = var.packet_api_token + version = "~> 2.9" +} + +# Create a new VLAN in datacenter "ewr1" +resource "packet_vlan" "provisioning-vlan" { + description = "provisioning-vlan" + facility = var.facility + project_id = var.project_id +} + +# Create a device and add it to tf_project_1 +resource "packet_device" "tink-provisioner" { + hostname = "tink-provisioner" + plan = var.device_type + facilities = [var.facility] + operating_system = "ubuntu_18_04" + billing_cycle = "hourly" + project_id = var.project_id + network_type = "hybrid" + user_data = "${file("install_package.sh")}" +} + +} + +# Create a device and add it to tf_project_1 +resource "packet_device" "tink-worker" { + hostname = "tink-worker" + plan = var.device_type + facilities = [var.facility] + operating_system = "custom_ipxe" + ipxe_script_url = "https://boot.netboot.xyz" + always_pxe = "true" + billing_cycle = "hourly" + project_id = var.project_id + network_type = "layer2-individual" +} + +# Attach VLAN to provisioner +resource "packet_port_vlan_attachment" "provisioner" { + device_id = packet_device.tink-provisioner.id + port_name = "eth1" + vlan_vnid = packet_vlan.provisioning-vlan.vxlan +} + +# Attach VLAN to worker +resource "packet_port_vlan_attachment" "worker" { + device_id = packet_device.tink-worker.id + port_name = "eth0" + vlan_vnid = packet_vlan.provisioning-vlan.vxlan +} + +output "provisioner_dns_name" { + value = "${split("-", packet_device.tink-provisioner.id)[0]}.packethost.net" +} + +output "provisioner_ip" { + value = "${packet_device.tink-provisioner.network[0].address}" +} + +output "worker_mac_addr" { + value = "${packet_device.tink-worker.ports[1].mac}" +} diff --git a/deploy/tls/.gitignore b/deploy/tls/.gitignore new file mode 100644 index 0000000..355164c --- /dev/null +++ b/deploy/tls/.gitignore @@ -0,0 +1 @@ +*/ diff --git a/deploy/tls/Dockerfile b/deploy/tls/Dockerfile new file mode 100644 index 0000000..3162c8a --- /dev/null +++ b/deploy/tls/Dockerfile @@ -0,0 +1,7 @@ +FROM alpine:3.11 +ENTRYPOINT [ "/entrypoint.sh" ] + +RUN apk add --no-cache --update --upgrade ca-certificates postgresql-client +RUN apk add --no-cache --update --upgrade --repository=http://dl-cdn.alpinelinux.org/alpine/edge/testing cfssl + +COPY . . diff --git a/deploy/tls/ca-config.json b/deploy/tls/ca-config.json new file mode 100644 index 0000000..d6d666e --- /dev/null +++ b/deploy/tls/ca-config.json @@ -0,0 +1,17 @@ +{ + "signing": { + "default": { + "expiry": "168h" + }, + "profiles": { + "server": { + "expiry": "8760h", + "usages": ["signing", "key encipherment", "server auth"] + }, + "signing": { + "expiry": "8760h", + "usages": ["signing", "key encipherment"] + } + } + } +} diff --git a/deploy/tls/ca.in.json b/deploy/tls/ca.in.json new file mode 100644 index 0000000..0c0cf7a --- /dev/null +++ b/deploy/tls/ca.in.json @@ -0,0 +1,12 @@ +{ + "CN": "Autogenerated CA", + "key": { + "algo": "rsa", + "size": 2048 + }, + "names": [ + { + "L": "@FACILITY@" + } + ] +} diff --git a/deploy/tls/entrypoint.sh b/deploy/tls/entrypoint.sh new file mode 100755 index 0000000..a172eaa --- /dev/null +++ b/deploy/tls/entrypoint.sh @@ -0,0 +1,13 @@ +#!/usr/bin/env sh + +# set -o errexit -o nounset -o pipefail + +if [ -z "${TINKERBELL_TLS_CERT:-}" ]; then + ( + echo "creating directory" + mkdir -p "certs" + ./gencerts.sh + ) +fi + +"$@" diff --git a/deploy/tls/gencerts.sh b/deploy/tls/gencerts.sh new file mode 100755 index 0000000..66731df --- /dev/null +++ b/deploy/tls/gencerts.sh @@ -0,0 +1,30 @@ +#!/usr/bin/env sh + +set -eux + +cd /certs + +if [ ! -f ca-key.pem ]; then + cfssl gencert \ + -initca ca.json | cfssljson -bare ca +fi + +if [ ! -f server.pem ]; then + cfssl gencert \ + -ca=ca.pem \ + -ca-key=ca-key.pem \ + -config=/ca-config.json \ + -profile=server \ + server-csr.json | + cfssljson -bare server +fi + +cat server.pem ca.pem >bundle.pem.tmp + +# only "modify" the file if truly necessary since workflow will serve it with +# modtime info for client caching purposes +if ! cmp -s bundle.pem.tmp bundle.pem; then + mv bundle.pem.tmp bundle.pem +else + rm bundle.pem.tmp +fi diff --git a/deploy/tls/server-csr.in.json b/deploy/tls/server-csr.in.json new file mode 100644 index 0000000..86c1008 --- /dev/null +++ b/deploy/tls/server-csr.in.json @@ -0,0 +1,19 @@ +{ + "CN": "tinkerbell", + "hosts": [ + "tinkerbell.registry", + "tinkerbell.tinkerbell", + "tinkerbell", + "localhost", + "127.0.0.1" + ], + "key": { + "algo": "rsa", + "size": 2048 + }, + "names": [ + { + "L": "@FACILITY@" + } + ] +} diff --git a/deploy/vagrant/.gitignore b/deploy/vagrant/.gitignore new file mode 100644 index 0000000..8000dd9 --- /dev/null +++ b/deploy/vagrant/.gitignore @@ -0,0 +1 @@ +.vagrant diff --git a/deploy/vagrant/README.md b/deploy/vagrant/README.md new file mode 100644 index 0000000..74a6afd --- /dev/null +++ b/deploy/vagrant/README.md @@ -0,0 +1 @@ +https://tinkerbell.org/docs/setup/local-with-vagrant/ diff --git a/deploy/vagrant/Vagrantfile b/deploy/vagrant/Vagrantfile new file mode 100644 index 0000000..8a9f40e --- /dev/null +++ b/deploy/vagrant/Vagrantfile @@ -0,0 +1,71 @@ +ENV['VAGRANT_NO_PARALLEL'] = 'yes' + +# Returns true if `GUI` environment variable is set to a non-empty value. +# Defaults to false +def worker_gui_enabled? + ENV.fetch('VAGRANT_WORKER_GUI', '').empty? +end + +Vagrant.configure('2') do |config| + + config.vm.define :provisioner do |provisioner| + provisioner.vm.box = 'generic/ubuntu1804' + provisioner.vm.hostname = 'provisioner' + provisioner.vm.synced_folder './../../', '/vagrant' + provisioner.vm.provision :shell, path: './scripts/tinkerbell.sh' + + provisioner.vm.network :private_network, + virtualbox__intnet: "tink_network", + libvirt__dhcp_enabled: false, + libvirt__forward_mode: 'none', + auto_config: false + + provisioner.vm.network "forwarded_port", guest: 42113, host: 42113 + provisioner.vm.network "forwarded_port", guest: 42114, host: 42114 + + + provisioner.vm.provider :libvirt do |lv, override| + lv.memory = 2*1024 + lv.cpus = 2 + lv.cpu_mode = 'host-passthrough' + end + + provisioner.vm.provider :virtualbox do |vb, override| + vb.memory = 2*1024 + vb.cpus = 2 + end + end + + config.vm.define "worker" do |worker| + worker.vm.box = nil + worker.vm.network :private_network, + mac: "080027000001", + virtualbox__intnet: "tink_network", + libvirt__dhcp_enabled: false, + libvirt__forward_mode: 'none', + auto_config: false + + worker.vm.provider :libvirt do |lv| + lv.memory = 4*1024 + lv.cpus = 1 + lv.boot 'network' + lv.mgmt_attach = false + end + + worker.vm.provider :virtualbox do |vb, worker| + worker.vm.box = 'generic/alpine38' + vb.memory = 4*1024 + vb.cpus = 1 + vb.gui = worker_gui_enabled? + vb.customize [ + 'modifyvm', :id, + '--nic1', 'none', + '--boot1', 'net', + '--boot2', 'none', + '--boot3', 'none', + '--boot4', 'none', + '--macaddress1', '080027000001' + ] + end + end +end diff --git a/deploy/vagrant/scripts/tinkerbell.sh b/deploy/vagrant/scripts/tinkerbell.sh new file mode 100644 index 0000000..c9a41e8 --- /dev/null +++ b/deploy/vagrant/scripts/tinkerbell.sh @@ -0,0 +1,100 @@ +#!/bin/bash + +# abort this script on errors +set -euxo pipefail + +whoami + +cd /vagrant + +setup_docker() ( + # steps from https://docs.docker.com/engine/install/ubuntu/ + sudo apt-get install -y \ + apt-transport-https \ + ca-certificates \ + curl \ + gnupg-agent \ + software-properties-common + + curl -fsSL https://download.docker.com/linux/ubuntu/gpg | + sudo apt-key add - + + local repo + repo=$( + printf "deb [arch=amd64] https://download.docker.com/linux/ubuntu %s stable" \ + "$(lsb_release -cs)" + ) + sudo add-apt-repository "$repo" + + sudo apt-get update + sudo apt-get install -y docker-ce docker-ce-cli containerd.io +) + +setup_docker_compose() ( + # from https://docs.docker.com/compose/install/ + sudo curl -L \ + "https://github.com/docker/compose/releases/download/1.26.0/docker-compose-$(uname -s)-$(uname -m)" \ + -o /usr/local/bin/docker-compose + + sudo chmod +x /usr/local/bin/docker-compose +) + +make_certs_writable() ( + local certdir="/etc/docker/certs.d/$TINKERBELL_HOST_IP" + sudo mkdir -p "$certdir" + sudo chown -R "$USER" "$certdir" +) + +secure_certs() ( + local certdir="/etc/docker/certs.d/$TINKERBELL_HOST_IP" + sudo chown "root" "$certdir" +) + +command_exists() ( + command -v "$@" >/dev/null 2>&1 +) + +configure_vagrant_user() ( + sudo usermod -aG docker vagrant + + echo -n "$TINKERBELL_REGISTRY_PASSWORD" | + sudo -iu vagrant docker login \ + --username="$TINKERBELL_REGISTRY_USERNAME" \ + --password-stdin "$TINKERBELL_HOST_IP" +) + +main() ( + export DEBIAN_FRONTEND=noninteractive + + apt-get update + + if ! command_exists docker; then + setup_docker + fi + + if ! command_exists docker-compose; then + setup_docker_compose + fi + + if ! command_exists jq; then + sudo apt-get install -y jq + fi + + if [ ! -f ./envrc ]; then + ./generate-envrc.sh eth1 >envrc + fi + + # shellcheck disable=SC1091 + . ./envrc + + make_certs_writable + + ./setup.sh + + secure_certs + + configure_vagrant_user + +) + +main diff --git a/envrc b/envrc new file mode 100644 index 0000000..73b4b09 --- /dev/null +++ b/envrc @@ -0,0 +1,32 @@ +# Network interface for Tinkerbell's network +export TINKERBELL_NETWORK_INTERFACE="eth1" + +# Decide on a subnet for provisioning. Tinkerbell should "own" this +# network space. Its subnet should be just large enough to be able +# to provision your hardware. +export TINKERBELL_CIDR=29 + +# Host IP is used by provisioner to expose different services such as +# tink, boots, etc. +# +# The host IP should the first IP in the range, and the Nginx IP +# should be the second address. +export TINKERBELL_HOST_IP=192.168.1.1 + +# NGINX IP is used by provisioner to serve files required for iPXE boot +export TINKERBELL_NGINX_IP=192.168.1.2 + +# Tink server username and password +export TINKERBELL_TINK_USERNAME=admin +export TINKERBELL_TINK_PASSWORD="ef53a532afe700a521b4aee97dc55c402a71dce5566dc3c7ef89bea7ffd6c8ea" + +# Docker Registry's username and password +export TINKERBELL_REGISTRY_USERNAME=admin +export TINKERBELL_REGISTRY_PASSWORD="9ca5d519f54b5345fb0451a3dda58506c3a7a7f8c2ce8eb234b82018c82c0a2c" + +# Legacy options, to be deleted: +export FACILITY=onprem +export ROLLBAR_TOKEN=ignored +export ROLLBAR_DISABLE=1 + +export OSIE_DOWNLOAD_LINK=https://tinkerbell-oss.s3.amazonaws.com/osie-uploads/osie-v0-n=252,c=bc454bc,b=master.tar.gz diff --git a/generate-envrc.sh b/generate-envrc.sh new file mode 100755 index 0000000..bbc357a --- /dev/null +++ b/generate-envrc.sh @@ -0,0 +1,99 @@ +#!/usr/bin/env bash + +# stops the execution if a command or pipeline has an error +set -eu + +if command -v tput >/dev/null && tput setaf 1 >/dev/null 2>&1; then + # color codes + RED="$(tput setaf 1)" + RESET="$(tput sgr0)" +fi + +ERR="${RED:-}ERROR:${RESET:-}" + +err() ( + if [ -z "${1:-}" ]; then + cat >&2 + else + echo "$ERR " "$@" >&2 + fi +) + +candidate_interfaces() ( + ip -o link show | + awk -F': ' '{print $2}' | + sed 's/[ \t].*//;/^\(lo\|bond0\|\|\)$/d' | + sort +) + +validate_tinkerbell_network_interface() ( + local tink_interface=$1 + + if ! candidate_interfaces | grep -q "^$tink_interface$"; then + err "Invalid interface ($tink_interface) selected, must be one of:" + candidate_interfaces | err + return 1 + else + return 0 + fi +) + +generate_password() ( + head -c 12 /dev/urandom | sha256sum | cut -d' ' -f1 +) + +generate_envrc() ( + local tink_interface=$1 + + validate_tinkerbell_network_interface "$tink_interface" + + local tink_password + tink_password=$(generate_password) + local registry_password + registry_password=$(generate_password) + cat < envrc" + exit 1 + fi + + generate_envrc "$1" +) + +main "$@" diff --git a/setup.sh b/setup.sh new file mode 100755 index 0000000..e096224 --- /dev/null +++ b/setup.sh @@ -0,0 +1,518 @@ +#!/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=envrc + +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 both the host and nginx IP + if ! ip addr show "$TINKERBELL_NETWORK_INTERFACE" | + grep -q "$TINKERBELL_HOST_IP"; then + return 1 + fi + + if ! ip addr show "$TINKERBELL_NETWORK_INTERFACE" | + grep -q "$TINKERBELL_NGINX_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 +) + +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" \ + --arg nginx_ip "$TINKERBELL_NGINX_IP" \ + '{ + network: { + renderer: "networkd", + ethernets: { + ($interface): { + addresses: [ + "\($host_ip)/\($cidr)", + "\($nginx_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:0" + echo "$BLANK ifdown $TINKERBELL_NETWORK_INTERFACE:1" + echo "$BLANK ifup $TINKERBELL_NETWORK_INTERFACE:0" + echo "$BLANK ifup $TINKERBELL_NETWORK_INTERFACE:1" + exit 1 + else + generate_iface_config >>/etc/network/interfaces + ip link set "$TINKERBELL_NETWORK_INTERFACE" nomaster + ifdown "$TINKERBELL_NETWORK_INTERFACE:0" + ifdown "$TINKERBELL_NETWORK_INTERFACE:1" + ifup "$TINKERBELL_NETWORK_INTERFACE:0" + ifup "$TINKERBELL_NETWORK_INTERFACE:1" + 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/tinkerbell.crt" ]; 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 "quay.io/tinkerbell/tink-worker:sha-adb49da" "${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 -eq 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 ../envrc; 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-envrc.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