From 254313697c12cf7366597b6d0c9ecc31c350fda3 Mon Sep 17 00:00:00 2001 From: Andrei Jiroh Halili Date: Sat, 24 Aug 2024 02:40:59 +0000 Subject: [PATCH] feat(bootstrap): :hammer: add a bunch of bootstrap scripts for automating bootstrap post-install I'll add the Ansible playbooks next after this, see also https://github.com/andreijiroh-dev/personal-launchpad/issues/4. Signed-off-by: Andrei Jiroh Halili --- scripts/arch/00-setup-ansible | 24 +++ scripts/arch/01-run-setup-playbook | 13 ++ scripts/bootstrap.sh | 96 +++++++++++ scripts/lib/import-sh | 249 ++++++++++++++++++++++++++++ scripts/lib/logs | 0 scripts/prebootstrap.sh | 37 +++++ scripts/skel/00-setup-ansible | 18 ++ scripts/skel/01-run-setup-playbooks | 17 ++ 8 files changed, 454 insertions(+) create mode 100755 scripts/arch/00-setup-ansible create mode 100755 scripts/arch/01-run-setup-playbook create mode 100755 scripts/bootstrap.sh create mode 100644 scripts/lib/import-sh create mode 100644 scripts/lib/logs create mode 100755 scripts/prebootstrap.sh create mode 100644 scripts/skel/00-setup-ansible create mode 100644 scripts/skel/01-run-setup-playbooks diff --git a/scripts/arch/00-setup-ansible b/scripts/arch/00-setup-ansible new file mode 100755 index 0000000..ff238a3 --- /dev/null +++ b/scripts/arch/00-setup-ansible @@ -0,0 +1,24 @@ +#!/usr/bin/env bash +# Copyright (c) Andrei Jiroh Halili +# SPDX-License-Identifier: MPL-2.0 +# +# This script installs ansible and OpenSSH cleint+server on Arch +# in order to run the playbooks. +set -e + +if [[ $DEBUG != "" ]]; then + set -x +fi + +if command -v ansible >> /dev/null; then + echo "[setup-ansible] Looking like ansible package is installed, skipping script" + exit 0 +fi + +echo "[setup-ansible] Installing via pacman" +if [ $EUID != "0" ]; then + SUDO=${SUDO:-"sudo"} + $SUDO pacman -Sy ansible --noconfirm +else + pacman -Sy ansible --noconfirm +fi \ No newline at end of file diff --git a/scripts/arch/01-run-setup-playbook b/scripts/arch/01-run-setup-playbook new file mode 100755 index 0000000..cbb8cc0 --- /dev/null +++ b/scripts/arch/01-run-setup-playbook @@ -0,0 +1,13 @@ +#!/usr/bin/env bash +# Copyright (c) Andrei Jiroh Halili +# SPDX-License-Identifier: MPL-2.0 +set -e + +if [[ $DEBUG != "" ]]; then + set -x +fi + +ROOT="$( cd -- "$(dirname "$0/")/../.." >/dev/null 2>&1 ; pwd -P )" +USERNAME=${USERNAME:-"ajhalili2006"} + +ansible-playbook "$ROOT/ansible/arch/setup.yml" --extra-vars "username=$USERNAME" -vvvv diff --git a/scripts/bootstrap.sh b/scripts/bootstrap.sh new file mode 100755 index 0000000..feef4a5 --- /dev/null +++ b/scripts/bootstrap.sh @@ -0,0 +1,96 @@ +#!/usr/bin/env bash +# Copyright (c) Andrei Jiroh Halili +# SPDX-License-Identifier: MPL-2.0 +# +# OS detection adapted from Tailscale's install script at +# https://tailscale.com/install.sh and is licensed under +# BSD-3-Clause. + +set -e + +if [[ $DEBUG != "" ]]; then + set -x +fi + +main() { + BASE_DIR=$(dirname $0) + # Step 1: detect the current linux distro, version, and packaging system. + # + # We rely on a combination of 'uname' and /etc/os-release to find + # an OS name and version, and from there work out what + # installation method we should be using. + # + # The end result of this step is that the DETECTED_OS will be + # populated by default, expect in some edge cases (like in Termux). + if [ -f "/etc/os-release" ]; then + . "/etc/os-release" + case "$ID" in + ubuntu|pop|neon|zorin|tuxedo) + DETECTED_OS="ubuntu" + if [ "${UBUNTU_CODENAME:-}" != "" ]; then + VERSION="$UBUNTU_CODENAME" + else + VERSION="$VERSION_CODENAME" + fi + # Third-party keyrings became the preferred method of + # installation in Ubuntu 20.04. + if expr "$VERSION_ID" : "2.*" >/dev/null; then + APT_KEY_TYPE="keyring" + else + APT_KEY_TYPE="legacy" + fi + ;; + debian) + DETECTED_OS="$ID" + VERSION="$VERSION_CODENAME" + # Third-party keyrings became the preferred method of + # installation in Debian 11 (Bullseye). + if [ -z "${VERSION_ID:-}" ]; then + # rolling release. If you haven't kept current, that's on you. + APT_KEY_TYPE="keyring" + elif [ "$VERSION_ID" -lt 11 ]; then + APT_KEY_TYPE="legacy" + else + APT_KEY_TYPE="keyring" + fi + ;; + arch|archarm|endeavouros|blendos|garuda) + DETECTED_OS="arch" + ;; + alpine) + DETECTED_OS="alpine" + ;; + esac + fi + + if [ -z "${DETECTED_OS}" ]; then + if type uname >/dev/null 2>&1; then + case "$(uname)" in + Linux) + if [ "${TERMUX_VERSION}" == "" && "${ANDROID_ASSETS}" == "/system/app" ]; then + DETECTED_OS="android" + TERMUX=true + else + DETECTED_OS="other-linux" + fi + esac + fi + fi + + # Ideally we want to use curl, but on some installs we + # only have wget. Detect and use what's available. + "$BASE_DIR/prebootstrap.sh" + # After the preflight checks, hand off to distro-specific scripts + if [ -d "$BASE_DIR/${DETECTED_OS}" ]; then + for script in $(ls -A ${BASE_DIR}/${DETECTED_OS}); do + echo "[info] executing "${BASE_DIR}/${DETECTED_OS}/$script"" + "${BASE_DIR}/${DETECTED_OS}/$script" + done + else + echo "[error] We currently don't support your distro or OS yet. Send us" + echo "[error] a patch to make it work." + exit 1 + fi +} + +main \ No newline at end of file diff --git a/scripts/lib/import-sh b/scripts/lib/import-sh new file mode 100644 index 0000000..3edd2c5 --- /dev/null +++ b/scripts/lib/import-sh @@ -0,0 +1,249 @@ +#!/bin/sh + +import_log() { + echo "import:" "$@" >&2 +} + +import_debug() { + [ "${IMPORT_DEBUG-}" = "1" ] && import_log "$@" || true +} + +# Only `shasum` is present on MacOS by default, +# but only `sha1sum` is present on Alpine by default +__import_shasum="$(command -v sha1sum)" || __import_shasum="$(command -v shasum)" || { + r=$? + import_log "No \`shasum\` or \`sha1sum\` command present" + exit "$r" +} +import_debug "Using '$__import_shasum'" + +# Empty tracing file if it already exists, when the env var is set +[ -n "${IMPORT_TRACE-}" ] && :>| "$IMPORT_TRACE" + +import_usage() { + echo "Usage: import \"org/repo/mod.sh\"" >&2 + echo "" >&2 + echo " Documentation: https://import.sh" >&2 + echo " Core Modules: https://github.com/importpw" >&2 + echo "" >&2 + echo " Examples:" >&2 + echo " import \"assert\" # import the latest commit of the 'assert' module " >&2 + echo " import \"assert@2.1.3\" # import the tag \`2.1.3\` of the 'assert' module" >&2 + echo " import \"tootallnate/hello\" # import from the GitHub repo \`tootallnate/hello\`" >&2 + echo " import \"https://git.io/fAWiz\" # import from a fully qualified URL" >&2 + return 2 +} + +import_parse_location() { + local location="$1" + local headers="$2" + local location_header="" + + # Print `x-import-warning` headers + grep -i '^x-import-warning:' < "$headers" | while IFS='' read -r line; do + echo "import: warning - $(echo "$line" | awk -F": " '{print $2}' | tr -d \\r)" >&2 + done + + # Find the final `Location` or `Content-Location` header + location_header="$(grep -i '^location\|^content-location:' < "$headers" | tail -n1)" + if [ -n "$location_header" ]; then + location="$(echo "$location_header" | awk -F": " '{print $2}' | tr -d \\r)" + fi + echo "$location" +} + +# The base directory for the import cache. +# Defaults to `import.sh` in the user cache directory specified by `$XDG_CACHE_HOME` +# or `$LOCALAPPDATA` (falling back to `$HOME/Library/Caches` on macOS and +# `$HOME/.cache` everywhere else). +# May be configured by setting the `IMPORT_CACHE` variable. +# On AWS Lambda, `$HOME` is not defined but `~` works. +# Furthermore, make sure we can always set IMPORT_CACHE even if HOME is undefined. +import_cache_dir() { + local home="${HOME:-"$(echo ~)"}" + local ucd_fallback="$home/.cache" + [ "$(uname -s)" = "Darwin" ] && ucd_fallback="$home/Library/Caches" + echo "${XDG_CACHE_HOME:-${LOCALAPPDATA:-$ucd_fallback}}/$1" +} + +import_cache_dir_import() { + echo "${IMPORT_CACHE:-$(import_cache_dir import.sh)}" +} + +import() { + local url="$*" + local url_path="" + + if [ -z "$url" ]; then + import_usage + return + fi + + import_debug "Importing '$url'" + + # If this is a relative import than it need to be based off of + # the parent import's resolved URL location. + case "$url" in + (./*) url="$(dirname "$__import_location")/$url";; + (../*) url="$(dirname "$__import_location")/$url";; + esac + + local cache="" + cache="$(import_cache_dir_import)" + + # Apply the default server if the user is doing an implicit import + if ! echo "$url" | grep "://" > /dev/null && ! echo "$url" | awk -F/ '{print $1}' | awk -F@ '{print $1}' | grep '\.' > /dev/null; then + url="${IMPORT_SERVER-https://import.sh}/$url" + import_debug "Normalized URL '$url'" + fi + + # Print the URL to the tracing file if the env var is set + [ -n "${IMPORT_TRACE-}" ] && echo "$url" >> "$IMPORT_TRACE" + + url_path="$(echo "$url" | sed 's/\:\///')" + local cache_path="$cache/links/$url_path" + + if [ ! -e "$cache_path" ] || [ "${IMPORT_RELOAD-}" = "1" ]; then + # Ensure that the directory containing the symlink for this import exists. + local dir + dir="$(dirname "$url_path")" + + local link_dir="$cache/links/$dir" + mkdir -p "$link_dir" "$cache/data" "$cache/locations/$dir" >&2 || return + + # Resolve the cache and link dirs with `pwd` now that the directories exist. + cache="$( ( cd "$cache" && pwd ) )" || return + link_dir="$( ( cd "$link_dir" && pwd ) )" || return + cache_path="$cache/links/$url_path" + + # Download the requested file to a temporary place so that the shasum + # can be computed to determine the proper final filename. + local location="" + local tmpfile="$cache_path.tmp" + local tmpheader="$cache_path.header" + local locfile="$cache/locations/$url_path" + local qs="?" + if echo "$url" | grep '?' > /dev/null; then + qs="&" + fi + import_log "Downloading $url" + local url_with_qs="${url}${qs}format=raw" + import_retry curl -sfLS \ + --netrc-optional \ + --dump-header "$tmpheader" \ + ${IMPORT_CURL_OPTS-} \ + "$url_with_qs" > "$tmpfile" || { + local r=$? + import_log "Failed to download: $url_with_qs" >&2 + rm -f "$tmpfile" "$tmpheader" || true + return "$r" + } + + # Now that the HTTP request has been resolved, parse the "Location" + location="$(import_parse_location "$url" "$tmpheader")" || return + import_debug "Resolved location '$url' -> '$location'" + echo "$location" > "$locfile" + rm -f "$tmpheader" + + # Calculate the sha1 hash of the contents of the downloaded file. + local hash + hash="$("$__import_shasum" < "$tmpfile" | { read -r first rest; echo "$first"; })" || return + import_debug "Calculated hash '$url' -> '$hash'" + + local hash_file="$cache/data/$hash" + + # If the hashed file doesn't exist then move it into place, + # otherwise delete the temp file - it's no longer needed. + if [ -f "$hash_file" ]; then + rm -f "$tmpfile" || return + else + mv "$tmpfile" "$hash_file" || return + fi + + # Create a relative symlink for this import pointing to the hashed file. + local relative + local cache_start + cache_start="$(expr "${#cache}" + 1)" || return + relative="$(echo "$link_dir" | awk '{print substr($0,'$cache_start')}' | sed 's/\/[^/]*/..\//g')data/$hash" || return + [ -n "${IMPORT_DEBUG-}" ] && printf "import: Creating symlink " >&2 + ln -fs${IMPORT_DEBUG:+v} "$relative" "$cache_path" >&2 || return + + import_debug "Successfully downloaded '$url' -> '$hash_file'" + else + import_debug "Already cached '$url'" + fi + + # Reset the `import` command args. There's not really a good reason to pass + # the URL to the sourced script, and in fact could cause undesirable results. + # i.e. This is required to make `import.sh/kward/shunit2` work out of the box. + set -- + + # At this point, the file has been saved to the cache so + # either source it or print it. + if [ -z "${print-}" ]; then + import_debug "Sourcing '$cache_path'" + local __import_parent_location="${__import_location-}" + __import_location="$(cat "$cache/locations/$url_path")" || return + . "$cache_path" || return + __import_location="$__import_parent_location" + else + import_debug "Printing '$cache_path'" + echo "$cache_path" + fi +} + +import_file() { + print=1 import "$@" +} + +import_retry() { + local exit_code="" + local retry_count="0" + local number_of_retries="${retries:-5}" + + while [ "$retry_count" -lt "$number_of_retries" ]; do + exit_code="0" + "$@" || exit_code=$? + if [ "$exit_code" -eq 0 ]; then + break + fi + # TODO: add exponential backoff + sleep 1 + retry_count=$(( retry_count + 1 )) + done + + return "$exit_code" +} + +# For `#!/usr/bin/env import` +if [ -n "${ZSH_EVAL_CONTEXT-}" ]; then + if [ "${ZSH_EVAL_CONTEXT-}" = "toplevel" ]; then + __import_entrypoint="1" + fi +elif [ "$(echo "$0" | cut -c1)" != "-" ] && [ "$(basename "$0" .sh)" = "import" ]; then + __import_entrypoint="1" +fi + +if [ -n "${__import_entrypoint-}" ]; then + # Parse argv + while [ $# -gt 0 ]; do + case "$1" in + -s=*|--shell=*) __import_shell="${1#*=}"; shift 1;; + -s|--shell) __import_shell="$2"; shift 2;; + -c) __import_command="$2"; shift 2;; + -*) echo "import: unknown option $1" >&2 && exit 2;; + *) break;; + esac + done + + if [ -n "${__import_shell-}" ]; then + # If the script requested a specific shell, then relaunch using it + exec "$__import_shell" "$0" "$@" + elif [ -n "${__import_command-}" ]; then + eval "$__import_command" + else + __import_entrypoint="$1" + shift + . "$__import_entrypoint" + fi +fi diff --git a/scripts/lib/logs b/scripts/lib/logs new file mode 100644 index 0000000..e69de29 diff --git a/scripts/prebootstrap.sh b/scripts/prebootstrap.sh new file mode 100755 index 0000000..ef689d2 --- /dev/null +++ b/scripts/prebootstrap.sh @@ -0,0 +1,37 @@ +#!/usr/bin/env bash +# Copyright (c) Andrei Jiroh Halili +# SPDX-License-Identifier: MPL-2.0 +set -e + +if [[ $DEBUG != "" ]]; then + set -x +fi + +. "$(dirname $0)/lib/import-sh" + +# Ideally we want to use curl, but on some installs we +# only have wget. Detect and use what's available. +CURL= +if type curl >/dev/null; then + CURL="curl -fsSL" +elif type wget >/dev/null; then + CURL="wget -q -O-" +fi +if [ -z "$CURL" ]; then + echo "The installer needs either curl or wget to download files." + echo "Please install either curl or wget to proceed." + exit 1 +fi + +PING_SITE=${PING_SITE:-"https://proxyparty.recaptime.dev"} +RC=0 +TEST_OUT=$($CURL "$PING_SITE" 2>&1) || RC=$? + +if [ $RC != 0 ]; then + echo "The installer cannot reach $TEST_URL" + echo "Please make sure that your machine has internet access." + echo "Test output:" + echo $TEST_OUT + exit 1 +fi +export CURL diff --git a/scripts/skel/00-setup-ansible b/scripts/skel/00-setup-ansible new file mode 100644 index 0000000..af42ca6 --- /dev/null +++ b/scripts/skel/00-setup-ansible @@ -0,0 +1,18 @@ +#!/usr/bin/env bash +# Copyright (c) Andrei Jiroh Halili +# SPDX-License-Identifier: MPL-2.0 +# +# This script installs ansible and OpenSSH cleint+server on $DISTRO +# in order to run the playbooks. +set -e + +if [[ $DEBUG != "" ]]; then + set -x +fi + +if command -v ansible >> /dev/null; then + echo "[setup-ansible] Looking like ansible package is installed, skipping script" + exit 0 +fi + +# insert the rest of the commands here \ No newline at end of file diff --git a/scripts/skel/01-run-setup-playbooks b/scripts/skel/01-run-setup-playbooks new file mode 100644 index 0000000..a269868 --- /dev/null +++ b/scripts/skel/01-run-setup-playbooks @@ -0,0 +1,17 @@ +#!/usr/bin/env bash +# Copyright (c) Andrei Jiroh Halili +# SPDX-License-Identifier: MPL-2.0 +# +# This script installs ansible and OpenSSH cleint+server on $DISTRO +# in order to run the playbooks. +set -e + +if [[ $DEBUG != "" ]]; then + set -x +fi + +ROOT="$( cd -- "$(dirname "$0/")/../.." >/dev/null 2>&1 ; pwd -P )" +USERNAME=${ANSIBLE_VAR_USERNAME:-"ajhalili2006"} +HOSTNAME=${ANSIBLE_VAR_HOSTNAME:-"ajhalili2006-lab"} + +ansible-playbook "$ROOT/ansible/archlinux-setup.yml" --extra-vars "username=$USERNAME" -vvvv -- 2.51.2