Stage 6: server image with a console installer (0.4.0)
- server/ profile and build-iso.sh [desktop|server]; make iso-server, make qemu-server, run-qemu.sh --serial; login on tty and ttyS0 - das-server: SSH, chrony, NetworkManager/dhcpcd, Docker with Compose, nftables (closed except SSH, own table), fail2ban reading socklog, sshd without root passwords, Docker log rotation, tools - das-server-install: dialog installer in Ukrainian and English, or unattended with --config; GPT + Btrfs subvolumes, GRUB for UEFI and BIOS, serial console, SSH key with optional keys-only logins - das-install-common: mount-source and postinstall shared with Calamares - dsv: runit service manager (list, enable, disable, start, stop, restart, status, logs) with bash completion, also on the desktop - das-update split into a subpackage without GTK; the window and tray icon move to /usr/libexec/das/das-update-gui - das-snapper-setup enables dbus, which snapper needs - TODO: ISO size review with measured numbers and options Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2026 Das
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
+14
-7
@@ -1,6 +1,7 @@
|
||||
#!/bin/sh
|
||||
# Finish a Das OS installation. Calamares runs this inside the target system
|
||||
# after users, locale and display manager are configured, before GRUB.
|
||||
# Finish a Das OS installation. The installers (Calamares, das-server-install)
|
||||
# run this inside the target system after users and locale are configured,
|
||||
# before GRUB.
|
||||
set -eu
|
||||
|
||||
msg() { echo "das-postinstall: $*"; }
|
||||
@@ -33,16 +34,22 @@ case $machine in
|
||||
*QEMU* | *KVM*) unused="virtualbox-ose-guest" ;;
|
||||
*) unused="spice-vdagent qemu-ga virtualbox-ose-guest" ;;
|
||||
esac
|
||||
msg "removing guest tools not needed here: $unused"
|
||||
case $unused in
|
||||
installed=""
|
||||
for pkg in $unused; do
|
||||
xbps-query "$pkg" >/dev/null 2>&1 && installed="$installed $pkg"
|
||||
done
|
||||
case $installed in
|
||||
*spice-vdagent*) rm -f /etc/runit/runsvdir/default/spice-vdagentd ;;
|
||||
esac
|
||||
case $unused in
|
||||
case $installed in
|
||||
# The live image replaces this autostart file, so xbps keeps it as modified
|
||||
*virtualbox*) rm -f /etc/xdg/autostart/vboxclient.desktop ;;
|
||||
esac
|
||||
# shellcheck disable=SC2086
|
||||
xbps-remove -Ry $unused || true
|
||||
if [ -n "$installed" ]; then
|
||||
msg "removing guest tools not needed here:$installed"
|
||||
# shellcheck disable=SC2086
|
||||
xbps-remove -Ry $installed || true
|
||||
fi
|
||||
|
||||
# Snapper, hourly snapshots and the GRUB snapshot menu (Btrfs only).
|
||||
das-snapper-setup
|
||||
@@ -0,0 +1,15 @@
|
||||
# Template file for 'das-install-common'
|
||||
pkgname=das-install-common
|
||||
version=0.1.0
|
||||
revision=1
|
||||
depends="das-snapshots dracut rsync"
|
||||
short_desc="Das OS installer steps shared by the desktop and server installers"
|
||||
maintainer="Das <anry.das@gmail.com>"
|
||||
license="MIT"
|
||||
homepage="https://git.das.org.ua/das/Das-OS"
|
||||
|
||||
do_install() {
|
||||
vinstall ${FILESDIR}/mount-source 755 usr/libexec/das/installer
|
||||
vinstall ${FILESDIR}/postinstall 755 usr/libexec/das/installer
|
||||
vlicense ${FILESDIR}/LICENSE
|
||||
}
|
||||
@@ -8,3 +8,4 @@ update_system: false
|
||||
operations:
|
||||
- try_remove:
|
||||
- das-installer
|
||||
- das-install-common
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
# Template file for 'das-installer'
|
||||
pkgname=das-installer
|
||||
version=0.2.0
|
||||
version=0.3.0
|
||||
revision=1
|
||||
depends="calamares das-snapshots das-theme sudo"
|
||||
depends="calamares das-install-common das-theme sudo"
|
||||
short_desc="Das OS installer: Calamares configuration and branding for the live ISO"
|
||||
maintainer="Das <anry.das@gmail.com>"
|
||||
license="MIT"
|
||||
@@ -13,8 +13,6 @@ do_install() {
|
||||
vmkdir usr/share/calamares
|
||||
vcopy "${FILESDIR}/calamares/*" usr/share/calamares
|
||||
vbin ${FILESDIR}/das-install
|
||||
vinstall ${FILESDIR}/postinstall 755 usr/libexec/das/installer
|
||||
vinstall ${FILESDIR}/mount-source 755 usr/libexec/das/installer
|
||||
vinstall ${FILESDIR}/installer-icon 755 usr/libexec/das/installer
|
||||
vinstall ${FILESDIR}/das-installer.desktop 644 usr/share/applications
|
||||
vinstall ${FILESDIR}/das-installer-icon.desktop 644 etc/xdg/autostart
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2026 Das
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
+582
@@ -0,0 +1,582 @@
|
||||
#!/bin/bash
|
||||
# das-server-install - console installer for Das OS Server.
|
||||
#
|
||||
# Usage: das-server-install [--config FILE]
|
||||
#
|
||||
# Asks its questions with dialog and installs the live image on a disk:
|
||||
# GPT with an EFI or BIOS boot partition and Btrfs with the same subvolumes
|
||||
# as the desktop installer, so snapshots and rollback work the same way.
|
||||
# With --config it asks nothing and takes the answers from FILE (see
|
||||
# /usr/share/examples/das-server-installer/install.conf).
|
||||
set -Eeuo pipefail
|
||||
|
||||
log=/tmp/das-server-install.log
|
||||
target=/mnt/das-target
|
||||
source_root=/run/das-installer/root
|
||||
libexec=/usr/libexec/das/installer
|
||||
|
||||
# Answers; a config file sets the same variables
|
||||
DISK=""
|
||||
HOST_NAME=dasos-server
|
||||
USER_NAME=""
|
||||
USER_PASSWORD=""
|
||||
SSH_KEY=""
|
||||
SSH_PASSWORDS=yes
|
||||
TIMEZONE=Europe/Kyiv
|
||||
KEYMAP=us
|
||||
LOCALE=en_US.UTF-8
|
||||
NETWORK=dhcpcd
|
||||
ERASE_DISK=no
|
||||
|
||||
lang=en
|
||||
[[ ${LANG:-} == uk* ]] && lang=uk
|
||||
|
||||
# t KEY: the message in the installer's language
|
||||
t() {
|
||||
local key=$1
|
||||
if [[ $lang == uk ]]; then
|
||||
case $key in
|
||||
title) echo "Встановлення Das OS Server" ;;
|
||||
lang) echo "Мова встановлення:" ;;
|
||||
disk) echo "Диск для встановлення. Усі дані на ньому буде знищено." ;;
|
||||
nodisk) echo "Не знайдено дисків для встановлення." ;;
|
||||
host) echo "Ім'я сервера (латинські літери, цифри, дефіс):" ;;
|
||||
badhost) echo "Неправильне ім'я сервера." ;;
|
||||
user) echo "Ім'я користувача-адміністратора (вхід через sudo, root вимкнено):" ;;
|
||||
baduser) echo "Неправильне ім'я: малі латинські літери, цифри, _ та -, не root." ;;
|
||||
pass) echo "Пароль користувача:" ;;
|
||||
pass2) echo "Ще раз той самий пароль:" ;;
|
||||
badpass) echo "Паролі порожні або не збігаються." ;;
|
||||
key) echo "Публічний ключ SSH для входу (вставте рядок ssh-ed25519 ... або залиште порожнім):" ;;
|
||||
badkey) echo "Це не схоже на публічний ключ SSH." ;;
|
||||
nopass) echo "Заборонити вхід по SSH за паролем (лише за ключем)?" ;;
|
||||
tz) echo "Часовий пояс (наприклад, Europe/Kyiv, UTC):" ;;
|
||||
badtz) echo "Немає такого часового поясу." ;;
|
||||
keymap) echo "Розкладка клавіатури в консолі:" ;;
|
||||
locale) echo "Мова системи:" ;;
|
||||
net) echo "Мережа:" ;;
|
||||
net_dhcpcd) echo "dhcpcd: DHCP на всіх інтерфейсах (просто)" ;;
|
||||
net_nm) echo "NetworkManager: налаштування через nmtui (Wi-Fi, статичні адреси)" ;;
|
||||
summary) echo "Перевірте налаштування:" ;;
|
||||
erase) echo "УСІ ДАНІ НА ДИСКУ %s БУДЕ ЗНИЩЕНО. Продовжити?" ;;
|
||||
cancel) echo "Встановлення скасовано." ;;
|
||||
s_source) echo "Підготовка образу системи..." ;;
|
||||
s_disk) echo "Розмітка диска %s..." ;;
|
||||
s_copy) echo "Копіювання системи (кілька хвилин)..." ;;
|
||||
s_config) echo "Налаштування системи..." ;;
|
||||
s_post) echo "Локалі, знімки, initramfs (кілька хвилин)..." ;;
|
||||
s_boot) echo "Встановлення завантажувача..." ;;
|
||||
finished) echo "Das OS Server встановлено. Вийміть інсталяційний носій і перезавантажте сервер.\n\nВхід: %s, далі sudo. Служби: dsv list. Оновлення: das-update." ;;
|
||||
reboot) echo "Перезавантажити зараз?" ;;
|
||||
failed) echo "Помилка встановлення. Журнал: %s" ;;
|
||||
root) echo "Інсталятор треба запускати від root (sudo das-server-install)." ;;
|
||||
yes) echo "так" ;;
|
||||
no) echo "ні" ;;
|
||||
esac
|
||||
else
|
||||
case $key in
|
||||
title) echo "Das OS Server installation" ;;
|
||||
lang) echo "Installation language:" ;;
|
||||
disk) echo "Disk to install to. All data on it will be erased." ;;
|
||||
nodisk) echo "No disks to install to were found." ;;
|
||||
host) echo "Server name (Latin letters, digits, hyphen):" ;;
|
||||
badhost) echo "Invalid server name." ;;
|
||||
user) echo "Administrator user name (uses sudo, root login is disabled):" ;;
|
||||
baduser) echo "Invalid name: lowercase Latin letters, digits, _ and -, not root." ;;
|
||||
pass) echo "User password:" ;;
|
||||
pass2) echo "The same password again:" ;;
|
||||
badpass) echo "The passwords are empty or do not match." ;;
|
||||
key) echo "SSH public key to log in with (paste the ssh-ed25519 ... line, or leave empty):" ;;
|
||||
badkey) echo "This does not look like an SSH public key." ;;
|
||||
nopass) echo "Disable SSH password login (keys only)?" ;;
|
||||
tz) echo "Time zone (for example Europe/Kyiv, UTC):" ;;
|
||||
badtz) echo "No such time zone." ;;
|
||||
keymap) echo "Console keyboard layout:" ;;
|
||||
locale) echo "System language:" ;;
|
||||
net) echo "Network:" ;;
|
||||
net_dhcpcd) echo "dhcpcd: DHCP on all interfaces (simple)" ;;
|
||||
net_nm) echo "NetworkManager: set up with nmtui (Wi-Fi, static addresses)" ;;
|
||||
summary) echo "Check the settings:" ;;
|
||||
erase) echo "ALL DATA ON %s WILL BE ERASED. Continue?" ;;
|
||||
cancel) echo "Installation cancelled." ;;
|
||||
s_source) echo "Preparing the system image..." ;;
|
||||
s_disk) echo "Partitioning %s..." ;;
|
||||
s_copy) echo "Copying the system (a few minutes)..." ;;
|
||||
s_config) echo "Configuring the system..." ;;
|
||||
s_post) echo "Locales, snapshots, initramfs (a few minutes)..." ;;
|
||||
s_boot) echo "Installing the boot loader..." ;;
|
||||
finished) echo "Das OS Server is installed. Remove the installation medium and reboot.\n\nLog in as %s, then use sudo. Services: dsv list. Updates: das-update." ;;
|
||||
reboot) echo "Reboot now?" ;;
|
||||
failed) echo "Installation failed. Log: %s" ;;
|
||||
root) echo "The installer must run as root (sudo das-server-install)." ;;
|
||||
yes) echo "yes" ;;
|
||||
no) echo "no" ;;
|
||||
esac
|
||||
fi
|
||||
}
|
||||
|
||||
interactive=1
|
||||
dialog_ui() {
|
||||
local labels=()
|
||||
if [[ $lang == uk ]]; then
|
||||
labels=(--ok-label "Гаразд" --cancel-label "Скасувати" --yes-label "Так" --no-label "Ні")
|
||||
fi
|
||||
dialog --backtitle "Das OS Server" --title "$(t title)" "${labels[@]}" "$@" 3>&1 1>&2 2>&3
|
||||
}
|
||||
|
||||
# Shows a step in the dialog, or prints it when installing from a config file
|
||||
step() {
|
||||
if [[ -n $interactive ]]; then
|
||||
dialog --backtitle "Das OS Server" --title "$(t title)" --infobox "$1" 5 70
|
||||
echo "==> $1" >>"$log"
|
||||
else
|
||||
echo "==> $1" | tee -a "$log"
|
||||
fi
|
||||
}
|
||||
|
||||
fail() {
|
||||
trap - ERR
|
||||
local msg
|
||||
# shellcheck disable=SC2059
|
||||
msg=$(printf "$(t failed)" "$log")
|
||||
if [[ -n $interactive ]]; then
|
||||
dialog --backtitle "Das OS Server" --title "$(t title)" --msgbox "$msg\n\n$(tail -n 12 "$log" | sed 's/\\/\\\\/g')" 22 76 || true
|
||||
clear
|
||||
fi
|
||||
echo "das-server-install: $msg" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
cancelled() {
|
||||
[[ -n $interactive ]] && clear
|
||||
t cancel
|
||||
exit 1
|
||||
}
|
||||
|
||||
# --- Questions -------------------------------------------------------------
|
||||
|
||||
# Disks that can be installed to: not read-only, not the live medium
|
||||
list_disks() {
|
||||
local live="" dev
|
||||
if dev=$(blkid -L VOID_LIVE 2>/dev/null); then
|
||||
live=$(lsblk -no PKNAME "$dev" 2>/dev/null | head -n 1)
|
||||
[[ -n $live ]] && live=/dev/$live || live=$dev
|
||||
fi
|
||||
lsblk -dpno NAME,TYPE,RO,SIZE,MODEL | while read -r name type ro size model; do
|
||||
[[ $type == disk && $ro == 0 && $name != "$live" ]] || continue
|
||||
[[ $name == /dev/zram* || $name == /dev/loop* ]] && continue
|
||||
printf '%s\t%s %s\n' "$name" "$size" "${model:-}"
|
||||
done
|
||||
}
|
||||
|
||||
ask_language() {
|
||||
local choice
|
||||
choice=$(dialog --backtitle "Das OS Server" --title "Das OS Server" \
|
||||
--default-item "$lang" --menu "Language / Мова:" 10 50 2 \
|
||||
en "English" uk "Українська" 3>&1 1>&2 2>&3) || cancelled
|
||||
lang=$choice
|
||||
if [[ $lang == uk ]]; then
|
||||
LOCALE=uk_UA.UTF-8
|
||||
# The default console font has no Cyrillic (a font needs a Unicode table,
|
||||
# .psfu); there is no font to set on serial or ssh
|
||||
case $(tty) in /dev/tty[0-9]*) setfont cyr-sun16 2>/dev/null || true ;; esac
|
||||
fi
|
||||
}
|
||||
|
||||
ask_disk() {
|
||||
local items=() name desc
|
||||
while IFS=$'\t' read -r name desc; do
|
||||
items+=("$name" "$desc")
|
||||
done < <(list_disks)
|
||||
if [[ ${#items[@]} -eq 0 ]]; then
|
||||
dialog_ui --msgbox "$(t nodisk)" 7 60 || true
|
||||
cancelled
|
||||
fi
|
||||
DISK=$(dialog_ui --menu "$(t disk)" 15 70 6 "${items[@]}") || cancelled
|
||||
}
|
||||
|
||||
valid_host() {
|
||||
[[ $1 =~ ^[a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?$ ]]
|
||||
}
|
||||
|
||||
valid_user() {
|
||||
[[ $1 =~ ^[a-z_][a-z0-9_-]{0,31}$ ]] || return 1
|
||||
case $1 in root | live | nobody | daemon | bin | sys) return 1 ;; esac
|
||||
}
|
||||
|
||||
valid_key() {
|
||||
[[ -z $1 || $1 =~ ^(ssh-(ed25519|rsa|dss)|ecdsa-sha2-[a-z0-9]+|sk-[a-z0-9@.-]+)\ [A-Za-z0-9+/=]+ ]]
|
||||
}
|
||||
|
||||
ask_host() {
|
||||
while :; do
|
||||
HOST_NAME=$(dialog_ui --inputbox "$(t host)" 8 60 "$HOST_NAME") || cancelled
|
||||
valid_host "$HOST_NAME" && return
|
||||
dialog_ui --msgbox "$(t badhost)" 6 50 || true
|
||||
done
|
||||
}
|
||||
|
||||
ask_user() {
|
||||
local p1 p2
|
||||
while :; do
|
||||
USER_NAME=$(dialog_ui --inputbox "$(t user)" 9 60 "$USER_NAME") || cancelled
|
||||
valid_user "$USER_NAME" && break
|
||||
dialog_ui --msgbox "$(t baduser)" 7 60 || true
|
||||
done
|
||||
while :; do
|
||||
p1=$(dialog_ui --insecure --passwordbox "$(t pass)" 8 60) || cancelled
|
||||
p2=$(dialog_ui --insecure --passwordbox "$(t pass2)" 8 60) || cancelled
|
||||
if [[ -n $p1 && $p1 == "$p2" ]]; then
|
||||
USER_PASSWORD=$p1
|
||||
return
|
||||
fi
|
||||
dialog_ui --msgbox "$(t badpass)" 6 50 || true
|
||||
done
|
||||
}
|
||||
|
||||
ask_ssh() {
|
||||
while :; do
|
||||
SSH_KEY=$(dialog_ui --inputbox "$(t key)" 10 76 "$SSH_KEY") || cancelled
|
||||
valid_key "$SSH_KEY" && break
|
||||
dialog_ui --msgbox "$(t badkey)" 6 50 || true
|
||||
done
|
||||
SSH_PASSWORDS=yes
|
||||
if [[ -n $SSH_KEY ]] && dialog_ui --yesno "$(t nopass)" 7 60; then
|
||||
SSH_PASSWORDS=no
|
||||
fi
|
||||
}
|
||||
|
||||
ask_system() {
|
||||
while :; do
|
||||
TIMEZONE=$(dialog_ui --inputbox "$(t tz)" 8 60 "$TIMEZONE") || cancelled
|
||||
[[ -f /usr/share/zoneinfo/$TIMEZONE ]] && break
|
||||
dialog_ui --msgbox "$(t badtz)" 6 50 || true
|
||||
done
|
||||
KEYMAP=$(dialog_ui --default-item "$KEYMAP" --menu "$(t keymap)" 15 50 7 \
|
||||
us "English (US)" ua-utf "Українська" uk "English (UK)" de-latin1 "Deutsch" \
|
||||
fr-latin1 "Français" pl2 "Polski") || cancelled
|
||||
LOCALE=$(dialog_ui --default-item "$LOCALE" --menu "$(t locale)" 10 50 2 \
|
||||
en_US.UTF-8 "English" uk_UA.UTF-8 "Українська") || cancelled
|
||||
NETWORK=$(dialog_ui --default-item "$NETWORK" --menu "$(t net)" 10 76 2 \
|
||||
dhcpcd "$(t net_dhcpcd)" NetworkManager "$(t net_nm)") || cancelled
|
||||
}
|
||||
|
||||
confirm() {
|
||||
local key pw msg
|
||||
key=$(t no)
|
||||
pw=$(t yes)
|
||||
[[ -n $SSH_KEY ]] && key=$(t yes)
|
||||
[[ $SSH_PASSWORDS == no ]] && pw=$(t no)
|
||||
dialog_ui --no-collapse --msgbox "$(t summary)
|
||||
|
||||
Disk: $DISK
|
||||
Host: $HOST_NAME
|
||||
User: $USER_NAME
|
||||
SSH key: $key
|
||||
SSH passwords: $pw
|
||||
Time zone: $TIMEZONE
|
||||
Keyboard: $KEYMAP
|
||||
Language: $LOCALE
|
||||
Network: $NETWORK" 18 60 || cancelled
|
||||
# shellcheck disable=SC2059
|
||||
msg=$(printf "$(t erase)" "$DISK")
|
||||
dialog_ui --defaultno --yesno "$msg" 7 60 || cancelled
|
||||
ERASE_DISK=yes
|
||||
}
|
||||
|
||||
# --- Installation ----------------------------------------------------------
|
||||
|
||||
# part N: the device of partition N on $DISK
|
||||
part() {
|
||||
lsblk -lnpo NAME,TYPE "$DISK" | awk '$2 == "part" { print $1 }' | sed -n "${1}p"
|
||||
}
|
||||
|
||||
in_target() {
|
||||
chroot "$target" "$@"
|
||||
}
|
||||
|
||||
# Undo mounts from an earlier attempt or a failure
|
||||
unmount_target() {
|
||||
if mountpoint -q "$target"; then
|
||||
umount -R "$target" || umount -Rl "$target"
|
||||
fi
|
||||
}
|
||||
|
||||
uefi() {
|
||||
[[ -d /sys/firmware/efi ]]
|
||||
}
|
||||
|
||||
serial_console() {
|
||||
grep -q 'console=ttyS0' /proc/cmdline
|
||||
}
|
||||
|
||||
partition() {
|
||||
local efi root
|
||||
unmount_target
|
||||
wipefs -a "$DISK"
|
||||
sgdisk --zap-all "$DISK"
|
||||
if uefi; then
|
||||
sgdisk -n 1:0:+512M -t 1:ef00 -c 1:EFI -n 2:0:0 -t 2:8300 -c 2:dasos "$DISK"
|
||||
else
|
||||
# GRUB on BIOS keeps its core image in a small partition on GPT
|
||||
sgdisk -n 1:0:+1M -t 1:ef02 -c 1:bios -n 2:0:0 -t 2:8300 -c 2:dasos "$DISK"
|
||||
fi
|
||||
partprobe "$DISK" 2>/dev/null || true
|
||||
udevadm settle
|
||||
efi=$(part 1)
|
||||
root=$(part 2)
|
||||
[[ -b $root ]] || return 1
|
||||
|
||||
uefi && mkfs.vfat -F 32 -n EFI "$efi"
|
||||
mkfs.btrfs -f -L dasos "$root"
|
||||
|
||||
# The same subvolumes as the Calamares configuration (mount.conf)
|
||||
mkdir -p "$target"
|
||||
mount "$root" "$target"
|
||||
for sv in @ @home @snapshots @var_log @var_cache_xbps; do
|
||||
btrfs subvolume create "$target/$sv"
|
||||
done
|
||||
umount "$target"
|
||||
|
||||
local opts=noatime,compress=zstd:1
|
||||
mount -o "$opts,subvol=@" "$root" "$target"
|
||||
mkdir -p "$target"/{home,.snapshots,var/log,var/cache/xbps}
|
||||
mount -o "$opts,subvol=@home" "$root" "$target/home"
|
||||
mount -o "$opts,subvol=@snapshots" "$root" "$target/.snapshots"
|
||||
mount -o "$opts,subvol=@var_log" "$root" "$target/var/log"
|
||||
mount -o "$opts,subvol=@var_cache_xbps" "$root" "$target/var/cache/xbps"
|
||||
if uefi; then
|
||||
mkdir -p "$target/boot/efi"
|
||||
mount -o umask=0077 "$efi" "$target/boot/efi"
|
||||
fi
|
||||
}
|
||||
|
||||
copy_system() {
|
||||
# No --sparse: GRUB can't read kernels with Btrfs holes (see postinstall)
|
||||
rsync -aHAX --numeric-ids "$source_root/" "$target/"
|
||||
}
|
||||
|
||||
write_fstab() {
|
||||
local root_uuid efi_uuid opts=noatime,compress=zstd:1
|
||||
root_uuid=$(blkid -s UUID -o value "$(part 2)")
|
||||
{
|
||||
echo "# <file system> <dir> <type> <options> <dump> <pass>"
|
||||
echo "UUID=$root_uuid / btrfs $opts,subvol=@ 0 0"
|
||||
echo "UUID=$root_uuid /home btrfs $opts,subvol=@home 0 0"
|
||||
echo "UUID=$root_uuid /.snapshots btrfs $opts,subvol=@snapshots 0 0"
|
||||
echo "UUID=$root_uuid /var/log btrfs $opts,subvol=@var_log 0 0"
|
||||
echo "UUID=$root_uuid /var/cache/xbps btrfs $opts,subvol=@var_cache_xbps 0 0"
|
||||
if uefi; then
|
||||
efi_uuid=$(blkid -s UUID -o value "$(part 1)")
|
||||
echo "UUID=$efi_uuid /boot/efi vfat defaults,umask=0077 0 2"
|
||||
fi
|
||||
} >"$target/etc/fstab"
|
||||
}
|
||||
|
||||
mount_chroot() {
|
||||
mount -t proc proc "$target/proc"
|
||||
mount -t sysfs sys "$target/sys"
|
||||
mount --rbind /dev "$target/dev"
|
||||
mount -t tmpfs tmpfs "$target/run"
|
||||
if uefi; then
|
||||
mount -t efivarfs efivarfs "$target/sys/firmware/efi/efivars" 2>/dev/null || true
|
||||
fi
|
||||
}
|
||||
|
||||
# set_rc VAR VALUE: sets VAR in /etc/rc.conf, uncommenting Void's example line
|
||||
set_rc() {
|
||||
local rc=$target/etc/rc.conf
|
||||
if grep -q "^#\?$1=" "$rc"; then
|
||||
sed -i "s|^#\?$1=.*|$1=\"$2\"|" "$rc"
|
||||
else
|
||||
echo "$1=\"$2\"" >>"$rc"
|
||||
fi
|
||||
}
|
||||
|
||||
enable_service() {
|
||||
ln -sfn "/etc/sv/$1" "$target/etc/runit/runsvdir/default/$1"
|
||||
}
|
||||
|
||||
disable_service() {
|
||||
rm -f "$target/etc/runit/runsvdir/default/$1"
|
||||
}
|
||||
|
||||
configure() {
|
||||
local home
|
||||
|
||||
# The live image's /etc/issue tells how to log in to the live system
|
||||
printf 'Das OS Server \\r (\\l)\n\n' >"$target/etc/issue"
|
||||
echo "$HOST_NAME" >"$target/etc/hostname"
|
||||
if ! grep -q "[[:space:]]$HOST_NAME\$" "$target/etc/hosts"; then
|
||||
printf '127.0.1.1\t%s\n' "$HOST_NAME" >>"$target/etc/hosts"
|
||||
fi
|
||||
echo "LANG=$LOCALE" >"$target/etc/locale.conf"
|
||||
ln -sfn "/usr/share/zoneinfo/$TIMEZONE" "$target/etc/localtime"
|
||||
set_rc KEYMAP "$KEYMAP"
|
||||
set_rc HARDWARECLOCK UTC
|
||||
[[ $LOCALE == uk* || $KEYMAP == ua* ]] && set_rc FONT cyr-sun16
|
||||
|
||||
# The administrator: sudo through wheel, root has no password
|
||||
in_target useradd -m -s /bin/bash -G wheel,docker,socklog "$USER_NAME"
|
||||
# -c: Void's PAM stack for chpasswd only has pam_permit for passwords, so
|
||||
# without it nothing is written (Void's own installer does the same)
|
||||
printf '%s:%s\n' "$USER_NAME" "$USER_PASSWORD" | in_target chpasswd -c SHA512
|
||||
in_target passwd -l root >/dev/null
|
||||
echo "%wheel ALL=(ALL:ALL) ALL" >"$target/etc/sudoers.d/10-wheel"
|
||||
chmod 440 "$target/etc/sudoers.d/10-wheel"
|
||||
|
||||
if [[ -n $SSH_KEY ]]; then
|
||||
home=$target/home/$USER_NAME
|
||||
install -d -m 700 "$home/.ssh"
|
||||
echo "$SSH_KEY" >"$home/.ssh/authorized_keys"
|
||||
chmod 600 "$home/.ssh/authorized_keys"
|
||||
in_target chown -R "$USER_NAME:$USER_NAME" "/home/$USER_NAME/.ssh"
|
||||
fi
|
||||
if [[ $SSH_PASSWORDS == no ]]; then
|
||||
printf '%s\n' "# Written by das-server-install: SSH keys only" \
|
||||
"PasswordAuthentication no" "KbdInteractiveAuthentication no" \
|
||||
>"$target/etc/ssh/sshd_config.d/40-das-keys-only.conf"
|
||||
fi
|
||||
|
||||
for sv in sshd chronyd nftables fail2ban docker socklog-unix nanoklogd; do
|
||||
enable_service "$sv"
|
||||
done
|
||||
if [[ $NETWORK == NetworkManager ]]; then
|
||||
disable_service dhcpcd
|
||||
enable_service dbus
|
||||
enable_service NetworkManager
|
||||
else
|
||||
enable_service dhcpcd
|
||||
fi
|
||||
serial_console && enable_service agetty-ttyS0
|
||||
return 0
|
||||
}
|
||||
|
||||
install_bootloader() {
|
||||
local grub=$target/etc/default/grub cmdline="loglevel=4"
|
||||
serial_console && cmdline+=" console=tty0 console=ttyS0,115200"
|
||||
sed -i -e '/^GRUB_DISTRIBUTOR=/d' -e '/^GRUB_CMDLINE_LINUX_DEFAULT=/d' -e '/^GRUB_TIMEOUT=/d' \
|
||||
-e '/^GRUB_TERMINAL/d' -e '/^GRUB_SERIAL_COMMAND=/d' -e '/^GRUB_DISABLE_OS_PROBER=/d' "$grub"
|
||||
{
|
||||
echo 'GRUB_DISTRIBUTOR="Das OS"'
|
||||
echo "GRUB_CMDLINE_LINUX_DEFAULT=\"$cmdline\""
|
||||
echo 'GRUB_TIMEOUT=5'
|
||||
echo 'GRUB_DISABLE_OS_PROBER=true'
|
||||
if serial_console; then
|
||||
echo 'GRUB_TERMINAL="console serial"'
|
||||
echo 'GRUB_SERIAL_COMMAND="serial --unit=0 --speed=115200"'
|
||||
fi
|
||||
} >>"$grub"
|
||||
|
||||
if uefi; then
|
||||
in_target grub-install --target=x86_64-efi --efi-directory=/boot/efi --bootloader-id=dasos
|
||||
# Also at the fallback path, for firmware that forgets boot entries
|
||||
in_target grub-install --target=x86_64-efi --efi-directory=/boot/efi --removable --no-nvram
|
||||
else
|
||||
in_target grub-install --target=i386-pc "$DISK"
|
||||
fi
|
||||
}
|
||||
|
||||
finish() {
|
||||
# The first snapshot, also shown in the boot menu (like Calamares' shellprocess@snapshot)
|
||||
in_target snapper --no-dbus -c root create -d "Fresh install" -c number --userdata important=yes || true
|
||||
in_target grub-mkconfig -o /boot/grub/grub.cfg
|
||||
# The installer is not needed on the installed system
|
||||
in_target xbps-remove -y das-server-installer das-install-common || true
|
||||
sync
|
||||
}
|
||||
|
||||
# Any failing command stops the installation with the log (set -E: also
|
||||
# inside functions; "f || fail" would switch set -e off within f).
|
||||
install_system() {
|
||||
: >"$log"
|
||||
trap fail ERR
|
||||
step "$(t s_source)"
|
||||
"$libexec/mount-source" >>"$log" 2>&1
|
||||
# shellcheck disable=SC2059
|
||||
step "$(printf "$(t s_disk)" "$DISK")"
|
||||
partition >>"$log" 2>&1
|
||||
step "$(t s_copy)"
|
||||
copy_system >>"$log" 2>&1
|
||||
step "$(t s_config)"
|
||||
write_fstab >>"$log" 2>&1
|
||||
mount_chroot >>"$log" 2>&1
|
||||
configure >>"$log" 2>&1
|
||||
step "$(t s_post)"
|
||||
in_target "$libexec/postinstall" >>"$log" 2>&1
|
||||
step "$(t s_boot)"
|
||||
install_bootloader >>"$log" 2>&1
|
||||
finish >>"$log" 2>&1
|
||||
unmount_target >>"$log" 2>&1
|
||||
trap - ERR
|
||||
}
|
||||
|
||||
load_config() {
|
||||
local file=$1
|
||||
[[ -r $file ]] || {
|
||||
echo "das-server-install: cannot read $file" >&2
|
||||
exit 1
|
||||
}
|
||||
# shellcheck source=/dev/null
|
||||
. "$file"
|
||||
interactive=""
|
||||
local bad=""
|
||||
[[ -b $DISK ]] || bad+=" DISK"
|
||||
valid_host "$HOST_NAME" || bad+=" HOST_NAME"
|
||||
valid_user "$USER_NAME" || bad+=" USER_NAME"
|
||||
[[ -n $USER_PASSWORD ]] || bad+=" USER_PASSWORD"
|
||||
valid_key "$SSH_KEY" || bad+=" SSH_KEY"
|
||||
[[ $SSH_PASSWORDS == yes || ( $SSH_PASSWORDS == no && -n $SSH_KEY ) ]] || bad+=" SSH_PASSWORDS"
|
||||
[[ -f /usr/share/zoneinfo/$TIMEZONE ]] || bad+=" TIMEZONE"
|
||||
[[ $NETWORK == dhcpcd || $NETWORK == NetworkManager ]] || bad+=" NETWORK"
|
||||
[[ $ERASE_DISK == yes ]] || bad+=" ERASE_DISK"
|
||||
if [[ -n $bad ]]; then
|
||||
echo "das-server-install: invalid or missing in $file:$bad" >&2
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
main() {
|
||||
if [[ $EUID -ne 0 ]]; then
|
||||
exec sudo --preserve-env=LANG "$0" "$@"
|
||||
fi
|
||||
case ${1:-} in
|
||||
--config)
|
||||
load_config "${2:?--config needs a file}"
|
||||
;;
|
||||
-h | --help)
|
||||
sed -n '2,11s/^# \{0,1\}//p' "$0"
|
||||
exit 0
|
||||
;;
|
||||
"") ;;
|
||||
*)
|
||||
echo "das-server-install: unknown option $1" >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
if [[ -n $interactive ]]; then
|
||||
ask_language
|
||||
ask_disk
|
||||
ask_host
|
||||
ask_user
|
||||
ask_ssh
|
||||
ask_system
|
||||
confirm
|
||||
fi
|
||||
install_system
|
||||
|
||||
if [[ -n $interactive ]]; then
|
||||
local msg
|
||||
# shellcheck disable=SC2059
|
||||
msg=$(printf "$(t finished)" "$USER_NAME")
|
||||
if dialog_ui --yesno "$msg\n\n$(t reboot)" 12 70; then
|
||||
reboot
|
||||
fi
|
||||
clear
|
||||
else
|
||||
echo "==> done, log: $log"
|
||||
fi
|
||||
}
|
||||
|
||||
main "$@"
|
||||
@@ -0,0 +1,20 @@
|
||||
# Answers for an unattended Das OS Server installation:
|
||||
# sudo das-server-install --config install.conf
|
||||
# The file is read by bash. Every value is checked before anything is done.
|
||||
|
||||
# The disk to install to; it is erased completely
|
||||
DISK=/dev/vda
|
||||
ERASE_DISK=no # must be "yes" to install
|
||||
|
||||
HOST_NAME=dasos-server
|
||||
USER_NAME=admin # gets sudo; root login stays disabled
|
||||
USER_PASSWORD=change-me
|
||||
|
||||
# A public key for SSH logins (optional), and whether passwords stay allowed
|
||||
SSH_KEY=""
|
||||
SSH_PASSWORDS=yes # "no" requires SSH_KEY
|
||||
|
||||
TIMEZONE=Europe/Kyiv
|
||||
KEYMAP=us # console layout: us, ua-utf, uk, de-latin1, ...
|
||||
LOCALE=en_US.UTF-8 # or uk_UA.UTF-8
|
||||
NETWORK=dhcpcd # or NetworkManager
|
||||
@@ -0,0 +1,16 @@
|
||||
# Template file for 'das-server-installer'
|
||||
pkgname=das-server-installer
|
||||
version=0.1.0
|
||||
revision=1
|
||||
depends="bash dialog das-install-common btrfs-progs dosfstools gptfdisk parted
|
||||
rsync util-linux grub grub-x86_64-efi efibootmgr"
|
||||
short_desc="Das OS Server console installer for the live ISO"
|
||||
maintainer="Das <anry.das@gmail.com>"
|
||||
license="MIT"
|
||||
homepage="https://git.das.org.ua/das/Das-OS"
|
||||
|
||||
do_install() {
|
||||
vbin ${FILESDIR}/das-server-install
|
||||
vsconf ${FILESDIR}/install.conf
|
||||
vlicense ${FILESDIR}/LICENSE
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2026 Das
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"log-driver": "local"
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
# Das OS: ban addresses that keep failing SSH logins, through nftables.
|
||||
[DEFAULT]
|
||||
banaction = nftables-multiport
|
||||
banaction_allports = nftables-allports
|
||||
|
||||
[sshd]
|
||||
enabled = true
|
||||
backend = polling
|
||||
# socklog-void collects auth messages here. svlogd starts each line with a
|
||||
# UTC time stamp; the pattern also consumes the facility and the syslog date
|
||||
# that follow it, so the sshd filter sees "sshd-session[pid]: ...":
|
||||
# 2026-10-19T17:25:46.09500 auth.info: Oct 19 20:25:46 sshd-session[1154]: Invalid user ...
|
||||
logpath = /var/log/socklog/secure/current
|
||||
datepattern = ^%%Y-%%m-%%dT%%H:%%M:%%S\.%%f(?: \S+: \S+ +\d+ \S+)?
|
||||
logtimezone = UTC
|
||||
@@ -0,0 +1,31 @@
|
||||
#!/usr/sbin/nft -f
|
||||
# Das OS server firewall: incoming connections are closed except SSH.
|
||||
#
|
||||
# Only the "das" table is replaced, so loading this file again
|
||||
# (dsv restart nftables) keeps the rules of Docker and fail2ban.
|
||||
# Ports that Docker containers publish (-p 8080:80) pass through Docker's
|
||||
# own forwarding rules, not the input chain below.
|
||||
|
||||
table inet das
|
||||
delete table inet das
|
||||
|
||||
table inet das {
|
||||
chain input {
|
||||
type filter hook input priority filter; policy drop;
|
||||
|
||||
ct state established,related accept
|
||||
ct state invalid drop
|
||||
iif lo accept
|
||||
meta l4proto { icmp, ipv6-icmp } accept
|
||||
# DHCPv6 answers come from the router's link-local address
|
||||
ip6 saddr fe80::/10 udp dport dhcpv6-client accept
|
||||
# Containers may reach services on the host (DNS, a database...)
|
||||
iifname "docker0" accept
|
||||
iifname "br-*" accept
|
||||
|
||||
tcp dport ssh accept
|
||||
|
||||
# Open more ports here, then: dsv restart nftables
|
||||
# tcp dport { http, https } accept
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
# Das OS server defaults, see sshd_config(5). sshd keeps the first value it
|
||||
# reads, so a file sorted before this one (40-local.conf) overrides it.
|
||||
PermitRootLogin prohibit-password
|
||||
X11Forwarding no
|
||||
@@ -0,0 +1,24 @@
|
||||
# Template file for 'das-server'
|
||||
pkgname=das-server
|
||||
version=0.1.0
|
||||
revision=1
|
||||
depends="openssh chrony dhcpcd NetworkManager docker docker-compose nftables
|
||||
fail2ban socklog-void python3 curl lazydocker lazygit mc tmux htop
|
||||
das-snapshots das-update dsv"
|
||||
short_desc="Das OS server: packages and secure defaults for SSH, firewall and Docker"
|
||||
maintainer="Das <anry.das@gmail.com>"
|
||||
license="MIT"
|
||||
homepage="https://git.das.org.ua/das/Das-OS"
|
||||
conf_files="
|
||||
/etc/nftables.conf
|
||||
/etc/fail2ban/jail.d/das.conf
|
||||
/etc/ssh/sshd_config.d/50-das.conf
|
||||
/etc/docker/daemon.json"
|
||||
|
||||
do_install() {
|
||||
vinstall ${FILESDIR}/nftables.conf 644 etc
|
||||
vinstall ${FILESDIR}/fail2ban-das.conf 644 etc/fail2ban/jail.d das.conf
|
||||
vinstall ${FILESDIR}/sshd-das.conf 644 etc/ssh/sshd_config.d 50-das.conf
|
||||
vinstall ${FILESDIR}/docker-daemon.json 644 etc/docker daemon.json
|
||||
vlicense ${FILESDIR}/LICENSE
|
||||
}
|
||||
@@ -43,7 +43,9 @@ if [ -f "$cfg" ] && ! grep -q '^GRUB_BTRFS_SUBMENUNAME=' "$cfg"; then
|
||||
fi
|
||||
|
||||
# Hourly snapshots come from cron, and grub-btrfs adds them to the boot menu.
|
||||
for sv in cronie grub-btrfs; do
|
||||
# snapper talks to snapperd over the system bus (the server has no desktop
|
||||
# that would enable dbus).
|
||||
for sv in dbus cronie grub-btrfs; do
|
||||
ln -sfn "/etc/sv/$sv" "/etc/runit/runsvdir/default/$sv"
|
||||
done
|
||||
msg "snapshots are enabled"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Template file for 'das-snapshots'
|
||||
pkgname=das-snapshots
|
||||
version=0.1.0
|
||||
version=0.2.0
|
||||
revision=1
|
||||
depends="btrfs-progs cronie dracut grub-btrfs snapper snapper-rollback"
|
||||
short_desc="Das OS Btrfs snapshots: snapper setup, boot menu entries and rollback"
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
// Command das-update-gui is the desktop part of das-update, installed as
|
||||
// /usr/libexec/das/das-update-gui by das-tools. das-update starts it when a
|
||||
// display is available, so servers get das-update without GTK.
|
||||
//
|
||||
// das-update-gui window that lists and installs updates
|
||||
// das-update-gui --tray background check, shows a tray icon when updates are available
|
||||
package main
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"os"
|
||||
|
||||
"dasos/tools/internal/i18n"
|
||||
)
|
||||
|
||||
func main() {
|
||||
tray := flag.Bool("tray", false, "check for updates in the background")
|
||||
lang := flag.String("lang", "", "message language, e.g. uk_UA.UTF-8")
|
||||
flag.Parse()
|
||||
if *lang != "" {
|
||||
i18n.SetLang(*lang)
|
||||
}
|
||||
if *tray {
|
||||
os.Exit(runTray())
|
||||
}
|
||||
os.Exit(runWindow())
|
||||
}
|
||||
+9
-8
@@ -16,6 +16,7 @@ import (
|
||||
|
||||
"dasos/tools/internal/i18n"
|
||||
"dasos/tools/internal/system"
|
||||
"dasos/tools/internal/update"
|
||||
"dasos/tools/internal/xbps"
|
||||
)
|
||||
|
||||
@@ -265,20 +266,20 @@ func (w *window) startInstall() {
|
||||
for sc.Scan() {
|
||||
line := sc.Text()
|
||||
switch {
|
||||
case strings.HasPrefix(line, markStep):
|
||||
line = "\n==> " + strings.TrimPrefix(line, markStep)
|
||||
case strings.HasPrefix(line, markSnapshot):
|
||||
n, _ := strconv.Atoi(strings.TrimPrefix(line, markSnapshot))
|
||||
case strings.HasPrefix(line, update.MarkStep):
|
||||
line = "\n==> " + strings.TrimPrefix(line, update.MarkStep)
|
||||
case strings.HasPrefix(line, update.MarkSnapshot):
|
||||
n, _ := strconv.Atoi(strings.TrimPrefix(line, update.MarkSnapshot))
|
||||
glib.IdleAdd(func() { w.snapshot = n })
|
||||
line = i18n.T("update.snapshot", n)
|
||||
case line == markReboot:
|
||||
case line == update.MarkReboot:
|
||||
reboot = true
|
||||
continue
|
||||
case line == markUpToDate:
|
||||
case line == update.MarkUpToDate:
|
||||
upToDate = true
|
||||
continue
|
||||
case strings.HasPrefix(line, markError):
|
||||
failure = strings.TrimPrefix(line, markError)
|
||||
case strings.HasPrefix(line, update.MarkError):
|
||||
failure = strings.TrimPrefix(line, update.MarkError)
|
||||
}
|
||||
glib.IdleAdd(func() { w.appendLog(line + "\n") })
|
||||
}
|
||||
@@ -1,10 +1,13 @@
|
||||
// Command das-update updates Das OS with a snapper snapshot taken before
|
||||
// the update, so a broken update can be rolled back from the boot menu.
|
||||
//
|
||||
// das-update window that lists and installs updates
|
||||
// das-update the update window on a desktop, otherwise the same as --cli
|
||||
// das-update --tray background check, shows a tray icon when updates are available
|
||||
// das-update --cli the same in a terminal (servers, ssh); -y skips the question
|
||||
// das-update --cli update in a terminal (servers, ssh); -y skips the question
|
||||
// das-update --apply the privileged part, started by the window through pkexec
|
||||
//
|
||||
// The window and the tray icon are a separate program from das-tools
|
||||
// (guiPath), so this one has no GTK dependency.
|
||||
package main
|
||||
|
||||
import (
|
||||
@@ -42,27 +45,42 @@ func main() {
|
||||
case *cli:
|
||||
os.Exit(runCLI(*yes))
|
||||
case *tray:
|
||||
os.Exit(runTray())
|
||||
// Nothing to show without the desktop part
|
||||
if haveGUI() {
|
||||
execGUI()
|
||||
}
|
||||
default:
|
||||
os.Exit(runWindow())
|
||||
if haveGUI() && haveDisplay() {
|
||||
execGUI()
|
||||
}
|
||||
os.Exit(runCLI(*yes))
|
||||
}
|
||||
}
|
||||
|
||||
// Machine-readable lines that --apply prints for the window, among the
|
||||
// plain output of xbps.
|
||||
const (
|
||||
markStep = "@@step "
|
||||
markSnapshot = "@@snapshot "
|
||||
markReboot = "@@reboot"
|
||||
markUpToDate = "@@uptodate"
|
||||
markError = "@@error "
|
||||
)
|
||||
// guiPath is the window and tray icon program from das-tools.
|
||||
const guiPath = "/usr/libexec/das/das-update-gui"
|
||||
|
||||
func haveGUI() bool {
|
||||
_, err := os.Stat(guiPath)
|
||||
return err == nil
|
||||
}
|
||||
|
||||
func haveDisplay() bool {
|
||||
return os.Getenv("DISPLAY") != "" || os.Getenv("WAYLAND_DISPLAY") != ""
|
||||
}
|
||||
|
||||
// execGUI replaces this process with the graphical part, passing the
|
||||
// arguments on. It returns only when that fails.
|
||||
func execGUI() {
|
||||
err := syscall.Exec(guiPath, append([]string{guiPath}, os.Args[1:]...), os.Environ())
|
||||
fmt.Fprintln(os.Stderr, "das-update:", err)
|
||||
}
|
||||
|
||||
type markReporter struct{ w io.Writer }
|
||||
|
||||
func (r markReporter) Step(title string) { fmt.Fprintln(r.w, markStep+title) }
|
||||
func (r markReporter) Step(title string) { fmt.Fprintln(r.w, update.MarkStep+title) }
|
||||
func (r markReporter) Output() io.Writer { return r.w }
|
||||
func (r markReporter) Snapshot(n int) { fmt.Fprintf(r.w, "%s%d\n", markSnapshot, n) }
|
||||
func (r markReporter) Snapshot(n int) { fmt.Fprintf(r.w, "%s%d\n", update.MarkSnapshot, n) }
|
||||
|
||||
func runApply() int {
|
||||
if os.Geteuid() != 0 {
|
||||
@@ -74,13 +92,13 @@ func runApply() int {
|
||||
r := markReporter{os.Stdout}
|
||||
res, err := update.System().Apply(context.Background(), r)
|
||||
if err != nil {
|
||||
fmt.Println(markError + err.Error())
|
||||
fmt.Println(update.MarkError + err.Error())
|
||||
return 1
|
||||
}
|
||||
if len(res.Updates) == 0 {
|
||||
fmt.Println(markUpToDate)
|
||||
fmt.Println(update.MarkUpToDate)
|
||||
} else if res.Reboot {
|
||||
fmt.Println(markReboot)
|
||||
fmt.Println(update.MarkReboot)
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
package update
|
||||
|
||||
// Machine-readable lines that "das-update --apply" prints among the plain
|
||||
// output of xbps, for the window that runs it through pkexec.
|
||||
const (
|
||||
MarkStep = "@@step "
|
||||
MarkSnapshot = "@@snapshot "
|
||||
MarkReboot = "@@reboot"
|
||||
MarkUpToDate = "@@uptodate"
|
||||
MarkError = "@@error "
|
||||
)
|
||||
@@ -1,18 +1,19 @@
|
||||
# Template file for 'das-tools'
|
||||
# In-tree package: Go sources are in files/src (FILESDIR).
|
||||
pkgname=das-tools
|
||||
version=0.2.0
|
||||
version=0.3.0
|
||||
revision=1
|
||||
create_wrksrc=yes
|
||||
# Like build_style=go: the Go linker decides about PIE, xbps-src must not check it
|
||||
nopie=yes
|
||||
hostmakedepends="go pkg-config"
|
||||
makedepends="gtk+3-devel"
|
||||
depends="dbus libnotify polkit sudo xdg-user-dirs xdg-utils"
|
||||
short_desc="Das OS tools: das-update (updates with snapshots) and das-welcome"
|
||||
depends="das-update>=${version}_${revision} dbus libnotify polkit xdg-user-dirs xdg-utils"
|
||||
short_desc="Das OS desktop tools: update window and tray icon, das-welcome"
|
||||
maintainer="Das <anry.das@gmail.com>"
|
||||
license="MIT"
|
||||
homepage="https://git.das.org.ua/das/Das-OS"
|
||||
subpackages="das-update"
|
||||
|
||||
do_build() {
|
||||
cp -rL "${FILESDIR}/src/." .
|
||||
@@ -24,6 +25,7 @@ do_build() {
|
||||
|
||||
do_install() {
|
||||
vbin bin/das-update
|
||||
vinstall bin/das-update-gui 755 usr/libexec/das
|
||||
vbin bin/das-welcome
|
||||
vinstall "${FILESDIR}/data/das-update.desktop" 644 usr/share/applications
|
||||
vinstall "${FILESDIR}/data/das-welcome.desktop" 644 usr/share/applications
|
||||
@@ -32,3 +34,13 @@ do_install() {
|
||||
vinstall "${FILESDIR}/data/ua.org.das.update.policy" 644 usr/share/polkit-1/actions
|
||||
vlicense "${FILESDIR}/LICENSE"
|
||||
}
|
||||
|
||||
# Without GTK, for servers: the window and the tray icon stay in das-tools.
|
||||
das-update_package() {
|
||||
short_desc="Das OS updates with a snapper snapshot to roll back to"
|
||||
depends="sudo"
|
||||
pkg_install() {
|
||||
vmove usr/bin/das-update
|
||||
vmove usr/share/polkit-1
|
||||
}
|
||||
}
|
||||
|
||||
Symlink
+1
@@ -0,0 +1 @@
|
||||
das-tools
|
||||
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2026 Das
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
Executable
+279
@@ -0,0 +1,279 @@
|
||||
#!/bin/bash
|
||||
# dsv - manage runit services on Das OS, the way systemctl does elsewhere.
|
||||
#
|
||||
# A service is a directory /etc/sv/<name>; it is enabled (started at boot) by
|
||||
# linking it into the default runlevel, which runsvdir watches through
|
||||
# /var/service. Logs come from the service's own svlogd directory or, for
|
||||
# services that log to syslog, from socklog.
|
||||
set -euo pipefail
|
||||
|
||||
sv_dir=/etc/sv
|
||||
runlevel=/etc/runit/runsvdir/default
|
||||
# Outside a running system (a chroot, an image) /var/service may not exist
|
||||
service_dir=/var/service
|
||||
[[ -d $service_dir ]] || service_dir=$runlevel
|
||||
socklog=/var/log/socklog
|
||||
|
||||
if [[ ${LANG:-} == uk* || ${LC_ALL:-} == uk* || ${LC_MESSAGES:-} == uk* ]]; then
|
||||
uk=1
|
||||
else
|
||||
uk=""
|
||||
fi
|
||||
|
||||
# say UKRAINIAN ENGLISH: prints the message in the user's language
|
||||
say() {
|
||||
if [[ -n $uk ]]; then echo "$1"; else echo "$2"; fi
|
||||
}
|
||||
|
||||
die() {
|
||||
say "dsv: $1" "dsv: $2" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
usage() {
|
||||
if [[ -n $uk ]]; then
|
||||
cat <<'EOF'
|
||||
Використання: dsv КОМАНДА [СЛУЖБА...]
|
||||
|
||||
list усі служби: чи ввімкнена, стан, PID, час роботи
|
||||
status СЛУЖБА... стан служби (sv status)
|
||||
enable СЛУЖБА... запускати під час завантаження й запустити зараз
|
||||
disable СЛУЖБА... не запускати під час завантаження й зупинити
|
||||
start|stop|restart СЛУЖБА...
|
||||
запустити, зупинити, перезапустити
|
||||
logs СЛУЖБА [-f] журнал служби (-f: стежити за новими рядками)
|
||||
|
||||
Служби — каталоги в /etc/sv. Без команди виконується list.
|
||||
EOF
|
||||
else
|
||||
cat <<'EOF'
|
||||
Usage: dsv COMMAND [SERVICE...]
|
||||
|
||||
list all services: enabled or not, state, PID, uptime
|
||||
status SERVICE... service state (sv status)
|
||||
enable SERVICE... start at boot and start now
|
||||
disable SERVICE... do not start at boot and stop now
|
||||
start|stop|restart SERVICE...
|
||||
start, stop, restart
|
||||
logs SERVICE [-f] the service log (-f: follow new lines)
|
||||
|
||||
Services are the directories in /etc/sv. Without a command, runs list.
|
||||
EOF
|
||||
fi
|
||||
}
|
||||
|
||||
# Service states live in root-only supervise directories
|
||||
need_root() {
|
||||
if [[ $EUID -ne 0 ]]; then
|
||||
exec sudo "$0" "$@"
|
||||
fi
|
||||
}
|
||||
|
||||
check_service() {
|
||||
[[ -d $sv_dir/$1 ]] || die "немає служби '$1' (див. ls $sv_dir)" "no service '$1' (see ls $sv_dir)"
|
||||
}
|
||||
|
||||
is_enabled() {
|
||||
[[ -e $runlevel/$1 || -L $runlevel/$1 ]]
|
||||
}
|
||||
|
||||
# Waits up to 10 s for runsvdir to pick up a new link (it rescans every 5 s)
|
||||
wait_supervised() {
|
||||
local i
|
||||
for ((i = 0; i < 20; i++)); do
|
||||
[[ -e $service_dir/$1/supervise/ok ]] && return 0
|
||||
sleep 0.5
|
||||
done
|
||||
return 1
|
||||
}
|
||||
|
||||
# row NAME ENABLED STATE PID UPTIME: one line of the list. printf pads by
|
||||
# bytes, ${#s} counts characters, so Cyrillic columns stay aligned.
|
||||
row() {
|
||||
local s widths=(24 9 6 8) i=0
|
||||
for s in "$1" "$2" "$3" "$4"; do
|
||||
printf '%s%*s ' "$s" $((widths[i] - ${#s})) ''
|
||||
i=$((i + 1))
|
||||
done
|
||||
printf '%s\n' "$5"
|
||||
}
|
||||
|
||||
cmd_list() {
|
||||
local dir name enabled state pid time line
|
||||
row "$(say СЛУЖБА SERVICE)" "$(say УВІМКН. ENABLED)" "$(say СТАН STATE)" PID "$(say ПРАЦЮЄ UPTIME)"
|
||||
for dir in "$sv_dir"/*/; do
|
||||
name=$(basename "$dir")
|
||||
enabled=$(say ні no)
|
||||
state=-
|
||||
pid=-
|
||||
time=-
|
||||
if is_enabled "$name"; then
|
||||
enabled=$(say так yes)
|
||||
# "run: /var/service/sshd: (pid 812) 3600s; run: log: ..."
|
||||
line=$(sv status "$service_dir/$name" 2>/dev/null || true)
|
||||
state=${line%%:*}
|
||||
[[ $line =~ \(pid\ ([0-9]+)\) ]] && pid=${BASH_REMATCH[1]}
|
||||
[[ $line =~ \ ([0-9]+)s ]] && time=$(human_time "${BASH_REMATCH[1]}")
|
||||
[[ -n $state ]] || state='?'
|
||||
fi
|
||||
row "$name" "$enabled" "$state" "$pid" "$time"
|
||||
done
|
||||
}
|
||||
|
||||
# human_time SECONDS: 3d4h, 2h5m, 7m, 42s
|
||||
human_time() {
|
||||
local s=$1
|
||||
if ((s >= 86400)); then
|
||||
echo "$((s / 86400))d$((s % 86400 / 3600))h"
|
||||
elif ((s >= 3600)); then
|
||||
echo "$((s / 3600))h$((s % 3600 / 60))m"
|
||||
elif ((s >= 60)); then
|
||||
echo "$((s / 60))m"
|
||||
else
|
||||
echo "${s}s"
|
||||
fi
|
||||
}
|
||||
|
||||
cmd_status() {
|
||||
local name rc=0
|
||||
for name in "$@"; do
|
||||
check_service "$name"
|
||||
if is_enabled "$name"; then
|
||||
sv status "$service_dir/$name" || rc=1
|
||||
else
|
||||
say "$name: вимкнена (dsv enable $name)" "$name: disabled (dsv enable $name)"
|
||||
fi
|
||||
done
|
||||
return $rc
|
||||
}
|
||||
|
||||
cmd_enable() {
|
||||
local name
|
||||
for name in "$@"; do
|
||||
check_service "$name"
|
||||
if is_enabled "$name"; then
|
||||
say "$name: уже ввімкнена" "$name: already enabled"
|
||||
continue
|
||||
fi
|
||||
ln -s "$sv_dir/$name" "$runlevel/$name"
|
||||
say "$name: увімкнена" "$name: enabled"
|
||||
# runsvdir starts it within 5 s. Right after "dsv disable" its runsv is
|
||||
# still there with the service stopped, so ask for "up" as well.
|
||||
if [[ $service_dir == /var/service ]] && wait_supervised "$name"; then
|
||||
sv -v up "$service_dir/$name" || true
|
||||
fi
|
||||
done
|
||||
}
|
||||
|
||||
cmd_disable() {
|
||||
local name
|
||||
for name in "$@"; do
|
||||
check_service "$name"
|
||||
if ! is_enabled "$name"; then
|
||||
say "$name: уже вимкнена" "$name: already disabled"
|
||||
continue
|
||||
fi
|
||||
# Stop it first: runsv only notices the removed link at runsvdir's next scan
|
||||
[[ $service_dir == /var/service ]] && sv down "$service_dir/$name" 2>/dev/null || true
|
||||
rm "$runlevel/$name"
|
||||
say "$name: вимкнена" "$name: disabled"
|
||||
done
|
||||
}
|
||||
|
||||
cmd_control() {
|
||||
local action=$1 name rc=0
|
||||
shift
|
||||
for name in "$@"; do
|
||||
check_service "$name"
|
||||
if ! is_enabled "$name"; then
|
||||
say "$name: вимкнена, спершу: dsv enable $name" "$name: disabled, first: dsv enable $name" >&2
|
||||
rc=1
|
||||
continue
|
||||
fi
|
||||
# -v waits for the new state (up to 7 s) and prints it
|
||||
sv -v "$action" "$service_dir/$name" || rc=1
|
||||
done
|
||||
return $rc
|
||||
}
|
||||
|
||||
# The log directory of a service whose log/run starts svlogd, if any
|
||||
svlogd_dir() {
|
||||
local run=$sv_dir/$1/log/run words w
|
||||
[[ -r $run ]] || return 1
|
||||
grep -q svlogd "$run" || return 1
|
||||
read -ra words < <(grep -m1 svlogd "$run")
|
||||
for w in "${words[@]}"; do
|
||||
[[ $w == /* && -d $w ]] && echo "$w" && return 0
|
||||
done
|
||||
return 1
|
||||
}
|
||||
|
||||
cmd_logs() {
|
||||
local name=${1:-} follow="" dir
|
||||
[[ -n $name ]] || die "вкажіть службу: dsv logs СЛУЖБА [-f]" "name a service: dsv logs SERVICE [-f]"
|
||||
[[ ${2:-} == -f ]] && follow=1
|
||||
check_service "$name"
|
||||
|
||||
if dir=$(svlogd_dir "$name"); then
|
||||
if [[ -n $follow ]]; then
|
||||
exec tail -n 50 -F "$dir/current"
|
||||
fi
|
||||
cat "$dir"/@*.[su] "$dir/current" 2>/dev/null | tail -n 200
|
||||
return
|
||||
fi
|
||||
|
||||
# Services without their own log write to syslog (vlogger -t <name>, or the
|
||||
# daemon itself as "<name>[pid]:" or "<name>-<helper>[pid]:", like
|
||||
# sshd-session), which socklog sorts into directories by facility.
|
||||
if ! compgen -G "$socklog/*/current" >/dev/null; then
|
||||
die "у $name немає власного журналу, а syslog не записується; увімкніть socklog: dsv enable socklog-unix nanoklogd" \
|
||||
"$name has no log of its own and syslog is not recorded; enable socklog: dsv enable socklog-unix nanoklogd"
|
||||
fi
|
||||
local pattern="[[:space:]]${name}(-[a-z]+)?(\[[0-9]+\])?:"
|
||||
if [[ -n $follow ]]; then
|
||||
tail -q -n 0 -F "$socklog"/*/current | grep --line-buffered -E "$pattern"
|
||||
else
|
||||
# A message can be in several directories (e.g. "everything" and "secure")
|
||||
cat "$socklog"/*/@*.[su] "$socklog"/*/current 2>/dev/null | grep -E "$pattern" | sort -u | tail -n 200 ||
|
||||
say "$name: у syslog немає записів (служба може писати лише в консоль runit)" \
|
||||
"$name: no syslog entries (the service may only write to the runit console)"
|
||||
fi
|
||||
}
|
||||
|
||||
cmd=${1:-list}
|
||||
[[ $# -gt 0 ]] && shift
|
||||
|
||||
case $cmd in
|
||||
list | ls)
|
||||
need_root list "$@"
|
||||
cmd_list
|
||||
;;
|
||||
status)
|
||||
[[ $# -gt 0 ]] || die "вкажіть службу" "name a service"
|
||||
need_root status "$@"
|
||||
cmd_status "$@"
|
||||
;;
|
||||
enable | disable | start | stop | restart)
|
||||
[[ $# -gt 0 ]] || die "вкажіть службу" "name a service"
|
||||
need_root "$cmd" "$@"
|
||||
case $cmd in
|
||||
enable) cmd_enable "$@" ;;
|
||||
disable) cmd_disable "$@" ;;
|
||||
start) cmd_control up "$@" ;;
|
||||
stop) cmd_control down "$@" ;;
|
||||
restart) cmd_control restart "$@" ;;
|
||||
esac
|
||||
;;
|
||||
logs | log)
|
||||
# socklog's logs are readable by root and the socklog group only
|
||||
[[ -r $socklog/messages/current ]] || need_root logs "$@"
|
||||
cmd_logs "$@"
|
||||
;;
|
||||
-h | --help | help)
|
||||
usage
|
||||
;;
|
||||
*)
|
||||
usage >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
@@ -0,0 +1,32 @@
|
||||
# bash completion for dsv
|
||||
# shellcheck shell=bash
|
||||
_dsv() {
|
||||
local cur=${COMP_WORDS[COMP_CWORD]} cmd=${COMP_WORDS[1]:-}
|
||||
if ((COMP_CWORD == 1)); then
|
||||
mapfile -t COMPREPLY < <(compgen -W "list status enable disable start stop restart logs help" -- "$cur")
|
||||
return
|
||||
fi
|
||||
case $cmd in
|
||||
enable)
|
||||
# services that are not enabled yet
|
||||
local all=() s
|
||||
for s in /etc/sv/*/; do
|
||||
s=${s%/}
|
||||
s=${s##*/}
|
||||
[[ -e /etc/runit/runsvdir/default/$s ]] || all+=("$s")
|
||||
done
|
||||
mapfile -t COMPREPLY < <(compgen -W "${all[*]}" -- "$cur")
|
||||
;;
|
||||
disable | start | stop | restart)
|
||||
mapfile -t COMPREPLY < <(compgen -W "$(ls /etc/runit/runsvdir/default 2>/dev/null)" -- "$cur")
|
||||
;;
|
||||
status | logs | log)
|
||||
if [[ $cmd == logs* ]] && ((COMP_CWORD == 3)); then
|
||||
mapfile -t COMPREPLY < <(compgen -W "-f" -- "$cur")
|
||||
else
|
||||
mapfile -t COMPREPLY < <(compgen -W "$(ls /etc/sv 2>/dev/null)" -- "$cur")
|
||||
fi
|
||||
;;
|
||||
esac
|
||||
}
|
||||
complete -F _dsv dsv
|
||||
@@ -0,0 +1,15 @@
|
||||
# Template file for 'dsv'
|
||||
pkgname=dsv
|
||||
version=0.1.0
|
||||
revision=1
|
||||
depends="bash runit sudo"
|
||||
short_desc="Das OS service manager: enable, start and read logs of runit services"
|
||||
maintainer="Das <anry.das@gmail.com>"
|
||||
license="MIT"
|
||||
homepage="https://git.das.org.ua/das/Das-OS"
|
||||
|
||||
do_install() {
|
||||
vbin ${FILESDIR}/dsv
|
||||
vcompletion ${FILESDIR}/dsv.bash-completion bash
|
||||
vlicense ${FILESDIR}/LICENSE
|
||||
}
|
||||
Reference in New Issue
Block a user