#!/bin/sh -e
#
# SatNOGS station setup script
#
# Copyright (C) 2024-2026 Libre Space Foundation
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see .
{
DEFAULT_SATNOGS_SETUP_SATNOGS_ANSIBLE_CHECKOUT_REF="stable"
DEFAULT_SATNOGS_SETUP_SATNOGS_ANSIBLE_PLAYBOOK_URL="https://gitlab.com/librespacefoundation/satnogs/satnogs-ansible.git"
DEFAULT_SATNOGS_SETUP_DOCKER_ANSIBLE_IMAGE="librespace/ansible:9.13.0"
DOCKER_BINDMOUNTS_DIR="/var/lib/docker-bindmounts"
DOCKER_ANSIBLE_NAME="ansible"
DOCKER_ANSIBLE_CONTAINER_NAME="ansible_ansible"
DOCKER_SATNOGS_CONFIG_CONTAINER_NAME="ansible_satnogs-config"
DOCKER_ANSIBLE_CONFIG_DIR="/etc/ansible"
DOCKER_ANSIBLE_PULL_DIR="/root/.ansible/pull"
DOCKER_SATNOGS_CONFIG_UID="500"
INSTALL_PACKAGES="
docker-ce:Docker
git:Git
"
DOCKER_CE_APT_BASE_URL="https://download.docker.com/linux/$(. /etc/os-release; echo "$ID")"
DOCKER_CE_APT_KEY_URL="$DOCKER_CE_APT_BASE_URL/gpg"
DOCKER_CE_APT_KEYRING_FILE="/etc/apt/keyrings/docker.asc"
DOCKER_CE_APT_REPO_URL="deb [arch=$(dpkg --print-architecture) signed-by=$DOCKER_CE_APT_KEYRING_FILE] $DOCKER_CE_APT_BASE_URL $(. /etc/os-release; echo "$VERSION_CODENAME") stable"
DOCKER_CE_APT_REPO_FILE="/etc/apt/sources.list.d/docker.list"
DEFAULT_CONF_FILE="/etc/satnogs-setup.conf"
REQUIREMENTS="
apt-get
awk
grep
sudo
gpg
curl
"
requirements() {
for req in $REQUIREMENTS; do
if ! which "$req" >/dev/null; then
if [ -z "$has_missing" ]; then
echo "satnogs-setup: Missing script requirements!" 1>&2
echo "Please install:" 1>&2
has_missing=1
fi
echo " - '$req'" 1>&2
fi
done
if [ -n "$has_missing" ]; then
exit 1
fi
}
usage() {
cat 1>&2 </dev/null
}
configure_docker_ce() {
cat </dev/null
{
"log-driver": "journald"
}
EOF
root_or_sudo systemctl reload docker
}
install_packages() {
unset packages
installed_packages="$(dpkg --get-selections | awk '/[ \t]install$/ { print $1 }')"
while read -r install_package; do
if [ -z "$install_package" ]; then
continue
fi
package_name="${install_package%%:*}";install_package="${install_package#*:}"
package_desc="$install_package"
package_desc="${package_desc:-$package_name}"
if ! echo "$installed_packages" | grep -q "^$package_name$"; then
package_names="${package_names}${package_names:+ }${package_name}"
package_descs="${package_descs}${package_descs:+, }${package_desc}"
fi
done </dev/null
all:
hosts:
${DOCKER_ANSIBLE_NAME}:
ansible_connection: 'community.docker.nsenter'
satnogses:
hosts:
${DOCKER_ANSIBLE_NAME}:
EOF
root_or_sudo tee "${DOCKER_BINDMOUNTS_DIR}/${DOCKER_ANSIBLE_CONTAINER_NAME}${DOCKER_ANSIBLE_CONFIG_DIR}/ansible.cfg" </dev/null
[defaults]
interpreter_python = auto_silent
EOF
if root_or_sudo /bin/sh -c '[ -f "'"${DOCKER_BINDMOUNTS_DIR}/${DOCKER_ANSIBLE_NAME}${DOCKER_ANSIBLE_CONFIG_DIR}/host_vars/${DOCKER_ANSIBLE_NAME}"'" ]'; then
echo "Migrating configuration path..."
root_or_sudo mv "${DOCKER_BINDMOUNTS_DIR}/${DOCKER_ANSIBLE_NAME}${DOCKER_ANSIBLE_CONFIG_DIR}/host_vars/${DOCKER_ANSIBLE_NAME}" "${DOCKER_BINDMOUNTS_DIR}/${DOCKER_ANSIBLE_CONTAINER_NAME}${DOCKER_ANSIBLE_CONFIG_DIR}/host_vars/${DOCKER_ANSIBLE_NAME}/config.yml"
root_or_sudo chown ${DOCKER_SATNOGS_CONFIG_UID}:${DOCKER_SATNOGS_CONFIG_UID} "${DOCKER_BINDMOUNTS_DIR}/${DOCKER_ANSIBLE_CONTAINER_NAME}${DOCKER_ANSIBLE_CONFIG_DIR}/host_vars/${DOCKER_ANSIBLE_NAME}/config.yml"
root_or_sudo rm -rf "${DOCKER_BINDMOUNTS_DIR}/${DOCKER_ANSIBLE_NAME}"
fi
echo
}
needs_update() {
applied_ref="$1"
satnogs_ansible_playbook_url="$2"
satnogs_ansible_checkout_ref="$3"
remote_ref="$(git ls-remote -q -h "$satnogs_ansible_playbook_url" "$satnogs_ansible_checkout_ref" 2>/dev/null | awk '{ print $1 }' || true)"
if [ -z "$remote_ref" ] || [ "$applied_ref" = "$remote_ref" ]; then
return 1
fi
return 0
}
provision_station() {
docker_ansible_image="$1"
satnogs_ansible_playbook_url="$2"
satnogs_ansible_checkout_ref="$3"
offline="$4"
if [ -z "$offline" ]; then
root_or_sudo mkdir -p "${DOCKER_BINDMOUNTS_DIR}/${DOCKER_ANSIBLE_CONTAINER_NAME}${DOCKER_ANSIBLE_PULL_DIR}"
root_or_sudo rm -rf "${DOCKER_BINDMOUNTS_DIR}/${DOCKER_ANSIBLE_CONTAINER_NAME}${DOCKER_ANSIBLE_PULL_DIR}/ansible"
root_or_sudo git clone \
-b "${satnogs_ansible_checkout_ref}" \
--depth 1 \
--recurse-submodules \
--shallow-submodules \
-o origin \
"${satnogs_ansible_playbook_url}" \
"${DOCKER_BINDMOUNTS_DIR}/${DOCKER_ANSIBLE_CONTAINER_NAME}${DOCKER_ANSIBLE_PULL_DIR}/ansible"
fi
for image_archive in $(root_or_sudo find /var/lib/docker-archives -maxdepth 1 -type f -name "*.tar.gz" 2>/dev/null); do
echo "Loading '$(basename "$image_archive")' image archive..."
root_or_sudo docker load -i "$image_archive"
root_or_sudo rm "$image_archive"
done
echo "Provisioning SatNOGS station..."
root_or_sudo docker run \
--rm \
--read-only \
${offline:+--pull never} \
--name "${DOCKER_ANSIBLE_NAME}" \
--hostname "${DOCKER_ANSIBLE_NAME}" \
--privileged \
--pid=host \
--tmpfs "/tmp" \
--tmpfs "/root/.ansible/tmp" \
-v "${DOCKER_BINDMOUNTS_DIR}/${DOCKER_ANSIBLE_CONTAINER_NAME}${DOCKER_ANSIBLE_CONFIG_DIR}:${DOCKER_ANSIBLE_CONFIG_DIR}:ro" \
-v "${DOCKER_BINDMOUNTS_DIR}/${DOCKER_ANSIBLE_CONTAINER_NAME}${DOCKER_ANSIBLE_PULL_DIR}:${DOCKER_ANSIBLE_PULL_DIR}:ro" \
"${docker_ansible_image}" \
/bin/sh -c '\
cd "'"${DOCKER_ANSIBLE_PULL_DIR}/ansible"'" && \
ansible-playbook \
-i "'"${DOCKER_ANSIBLE_CONFIG_DIR}"'" \
-e local_ansible_enable=true \
'"${offline:+-e docker_pull_policy=never --skip-tags=packages}"' \
local.yml'
root_or_sudo git ls-remote -q "${DOCKER_BINDMOUNTS_DIR}/${DOCKER_ANSIBLE_CONTAINER_NAME}${DOCKER_ANSIBLE_PULL_DIR}/ansible" 2>/dev/null | grep -v refs | awk '{ print $1 }' | root_or_sudo tee "${DOCKER_BINDMOUNTS_DIR}/${DOCKER_ANSIBLE_CONTAINER_NAME}${DOCKER_ANSIBLE_PULL_DIR}/.applied-ref" >/dev/null
}
configure_station() {
echo "Launching SatNOGS configuration tool..."
root_or_sudo docker exec \
-ti \
"$DOCKER_SATNOGS_CONFIG_CONTAINER_NAME" \
satnogs-config < /dev/tty
}
main() {
requirements
debian_version="$(get_debian_version || true)"
case $debian_version in
buster|bullseye|bookworm|trixie)
:
;;
*)
echo "ERROR: Unsupported distribution!" >&2
exit 1
;;
esac
parse_args "$@"
load_conf
pidfile=/var/run/satnogs-setup.pid
if [ -f "$pidfile" ] && root_or_sudo kill -0 "$(root_or_sudo cat "$pidfile")" 2>/dev/null; then
echo "Another instance of 'satnogs-setup' is already running!" >&2
echo "If this is the first boot, wait until initial provisioning is completed." >&2
echo "Otherwise, close any other 'satnogs-setup' you are already running and try again." >&2
exit 1
fi
echo "$$" | root_or_sudo tee "$pidfile" >/dev/null
# Assume installation is requested when file is sourced
if [ "$(basename "$0")" != "satnogs-setup" ]; then
do_install=1
fi
applied_ref="$(root_or_sudo cat "${DOCKER_BINDMOUNTS_DIR}/${DOCKER_ANSIBLE_CONTAINER_NAME}${DOCKER_ANSIBLE_PULL_DIR}/.applied-ref" 2>/dev/null || true)"
if [ -z "$offline" ]; then
install_docker_ce_repository
install_packages
fi
configure_docker_ce
configure_ansible
if [ -z "$do_install" ]; then
if [ -n "$applied_ref" ]; then
if [ -z "$offline" ] && needs_update \
"$applied_ref" \
"$satnogs_ansible_playbook_url" \
"$satnogs_ansible_checkout_ref"; then
if [ -z "$do_update" ]; then
while true; do
echo "A new version has been detected. Do you wish to update now? [y/n]"
read -r yesno