Initial commit

This commit is contained in:
Jan Schär 2024-05-09 22:45:53 +02:00
commit 968d09e362
88 changed files with 2323 additions and 0 deletions

213
os/build.py Executable file
View file

@ -0,0 +1,213 @@
#!/bin/env python3
# Build a live ISO.
# Run this from an empty working directory,
# or a directory where you have run this before to reuse caches.
import argparse
import subprocess
import pathlib
import tomllib
import hashlib
import datetime
import urllib.request
DISTRIBUTION = "bookworm"
VARIANT_LABEL = {
"contestant": "contest",
"training-live": "live",
"training-installer": "install",
}
VARIANT_BOOT_OPTIONS = {
"contestant": [
dict(label="SOI contest", cmdline="boot=live toram"),
dict(label="SOI contest, run from drive", cmdline="boot=live"),
dict(label="SOI contest, fail-safe mode", cmdline="@LB_BOOTAPPEND_LIVE_FAILSAFE@"),
],
"training-live": [
dict(label="SOI live system, run from RAM", cmdline="boot=live toram"),
dict(label="SOI live system, run from drive", cmdline="boot=live"),
dict(label="SOI live system, fail-safe mode", cmdline="@LB_BOOTAPPEND_LIVE_FAILSAFE@"),
],
"training-installer": [],
}
VARIANT_EXTRA_LB_CONFIG = {
"training-installer": [
"--debian-installer", "live",
# Linux headers are needed for VirtualBox DKMS.
"--linux-packages", "linux-image linux-headers",
],
}
VARIANT_EXTRA_BOOTSTRAP = {
# Fasttrack keyring is needed for VirtualBox.
"training-installer": ",fasttrack-archive-keyring",
}
DOWNLOADS = [
dict(
name="soi-header.tar.gz",
url="https://git.soi.ch/SOI/soi-header/archive/19ddcef24eb55bdb5ddb817c1d91bfa04c8cb8dd.tar.gz",
sha256="38042587982af4e9431aea461e5c345bde358bcc79f0a0eadcf5b3ed77aeb8ab",
),
# From https://soi.ch/wiki/soi-codeblocks/#install-the-soi-project-template
dict(
name="soi_template_codeblocks_ubuntu.zip",
url="https://soi.ch/media/files/soi_template_codeblocks_ubuntu_RzdvSho.zip",
sha256="3f4cae26bbb0cbdfd4cf9f94bfce14988e395f847948d9b349c12d0b980386e9",
),
]
def run(args, check=True, **kwargs):
print(f"> {' '.join(args)}")
subprocess.run(args, check=check, **kwargs)
def edit_file(filename, fn):
with open(filename) as f:
content = f.read()
content = fn(content)
with open(filename, "w") as f:
f.write(content)
def sha256sum(filename):
with open(filename, "rb", buffering=0) as f:
return hashlib.file_digest(f, "sha256").hexdigest()
def main():
parser = argparse.ArgumentParser()
parser.add_argument(
"variant",
choices=["contestant", "training-live", "training-installer"],
help="Variant to build.",
)
args = parser.parse_args()
script_dir = pathlib.Path(__file__).parent.resolve()
chroot_includes = pathlib.Path("config/includes.chroot")
with open(script_dir / "config/config.toml", "rb") as f:
config = tomllib.load(f)
# Remove files generated by previous build, but keep cache.
run("lb clean".split())
run("rm -rf .build config udeb-build".split())
# Download files.
run("mkdir -p downloads".split())
for download in DOWNLOADS:
filename = f'downloads/{download["name"]}'
try:
if sha256sum(filename) == download["sha256"]:
continue
except FileNotFoundError:
pass
print(f'> Downloading {download["url"]}')
urllib.request.urlretrieve(download["url"], filename)
if sha256sum(filename) != download["sha256"]:
raise Exception(f"Downloaded file {filename} has wrong hash.")
# Create base configuration directory.
run(
[
"lb", "config",
"--clean",
"--ignore-system-defaults",
"--mode", "debian",
"--distribution", DISTRIBUTION,
"--archive-areas", "main contrib non-free non-free-firmware",
"--firmware-chroot", "false",
"--firmware-binary", "false",
"--apt-recommends", "false",
# isc-dhcp-client and ifupdown are obsoleted by network-manager,
# but they still have Priority: important.
# We need ca-certificates for fetching https packages repos.
"--debootstrap-options", "--exclude=isc-dhcp-client,isc-dhcp-common,ifupdown --include=ca-certificates" +
VARIANT_EXTRA_BOOTSTRAP.get(args.variant, ""),
"--loadlin", "false",
"--iso-volume", f"SOI {VARIANT_LABEL[args.variant]} @ISOVOLUME_TS@",
"--bootappend-live", "boot=live toram",
]
+ VARIANT_EXTRA_LB_CONFIG.get(args.variant, [])
)
# Add our own configuration on top.
run(["cp", "-rT", f"{script_dir}/layers/participant", "config"])
run(["cp", "-rT", f"{script_dir}/layers/{args.variant}", "config"])
if args.variant == "training-installer":
# Insert admin password into preseed.
edit_file("config/includes.installer/preseed.cfg",
lambda s: s.replace("@install_admin_password@", config["install_admin_password"]))
# Copy inventory file.
run("mkdir -p config/includes.binary/install".split())
run(["cp", f"{script_dir}/config/installer-inventory.txt", "config/includes.binary/install/inventory.txt"])
# Insert build date in login screen logo.
edit_file("config/includes.chroot/usr/local/share/images/login-screen-logo.svg",
lambda s: s.replace("@date@", datetime.date.today().isoformat()))
# Build and install custom udeb packages for installer.
run(["cp", "-rT", f"{script_dir}/installer-udeb", "udeb-build"])
run("dpkg-buildpackage --build=all".split(), cwd="udeb-build/inventory-hostname")
run("mkdir -p config/packages.binary".split())
run("cp udeb-build/inventory-hostname_0_all.udeb config/packages.binary/".split())
# Copy the source lists. The installer deletes everything in sources.list.d,
# so we need to copy them somewhere else and restore them after the install.
for listpath in pathlib.Path('config/archives').glob('*.list.chroot'):
run(["cp", str(listpath), f"config/includes.chroot/usr/local/share/target-sources/{listpath.name.removesuffix('.chroot')}"])
elif args.variant == "contestant":
# Insert root password into hook script.
edit_file("config/hooks/live/2010-contestant.hook.chroot",
lambda s: s.replace("@contestant_root_password@", config["contestant_root_password"]))
# Copy authorized_keys.
run("mkdir -p config/includes.chroot/root/.ssh".split())
run(["cp", f"{script_dir}/config/contestant_authorized_keys", "config/includes.chroot/root/.ssh/authorized_keys"])
# Configure boot options.
grub_boot_options = '\n'.join(
f'menuentry "{option["label"]}" {{\n'
f' linux @KERNEL_LIVE@ {option["cmdline"]}\n'
f' initrd @INITRD_LIVE@\n'
f'}}\n'
for option in VARIANT_BOOT_OPTIONS[args.variant]
)
with open("/usr/share/live/build/bootloaders/grub-pc/grub.cfg") as f:
grub_cfg = f.read()
grub_cfg = grub_cfg.replace("@LINUX_LIVE@", grub_boot_options)
with open("config/bootloaders/grub-pc/grub.cfg", "w") as f:
f.write(grub_cfg)
syslinux_boot_options = ''.join(
f"label live-{i}\n"
f"\tmenu label {option['label']}\n"
+ (f"\tmenu default\n" if i == 0 else "") +
f"\tlinux @LINUX@\n"
f"\tinitrd @INITRD@\n"
f"\tappend {option['cmdline'].replace('@LB_BOOTAPPEND_LIVE_FAILSAFE@', '@APPEND_LIVE_FAILSAFE@')}\n"
f"\n"
for i, option in enumerate(VARIANT_BOOT_OPTIONS[args.variant])
)
pathlib.Path("config/bootloaders/syslinux_common").mkdir(parents=True, exist_ok=True)
with open("config/bootloaders/syslinux_common/live.cfg.in", "w") as f:
f.write(syslinux_boot_options)
# Install soi header.
(chroot_includes / "usr/local/include").mkdir(parents=True, exist_ok=True)
run(["tar", "--overwrite", "-xf", f"downloads/soi-header.tar.gz", "-C", f"{chroot_includes}/usr/local/include", "--strip-components=2", "soi-header/include/"])
# Install codeblocks template.
(chroot_includes / "usr/share/codeblocks/templates/wizard").mkdir(parents=True, exist_ok=True)
run(["unzip", "-o", f"downloads/soi_template_codeblocks_ubuntu.zip", "-d", f"{chroot_includes}/usr/share/codeblocks/templates/wizard/"])
# Start the build.
run("lb build".split())
if __name__ == "__main__":
main()

View file

@ -0,0 +1,6 @@
# Example password: soi
# Create a hash with mkpasswd
install_admin_password = "$y$j9T$h5VhMd4KkdmbxdZD1gO0N/$1hvwZgO8pQw13Xd6jaNXbtkbqVOC4W/ia/KXOcCGYvB"
# Example password: soi
contestant_root_password = "$y$j9T$h5VhMd4KkdmbxdZD1gO0N/$1hvwZgO8pQw13Xd6jaNXbtkbqVOC4W/ia/KXOcCGYvB"

View file

@ -0,0 +1 @@
# Add your ssh authorized keys here

View file

@ -0,0 +1,2 @@
DEMO123 debian01
SOME_SERIAL_NUMBER some-hostname

View file

@ -0,0 +1,5 @@
inventory-hostname (0) UNRELEASED; urgency=low
* Initial release.
-- Jan Schär <jan@soi.ch> Fri, 02 Feb 2024 21:37:43 +0000

View file

@ -0,0 +1,12 @@
Source: inventory-hostname
Section: debian-installer
Priority: optional
Maintainer: Jan Schär <jan@soi.ch>
Build-Depends: debhelper-compat (= 13)
Package: inventory-hostname
Package-Type: udeb
Architecture: all
Depends: ${misc:Depends}
XB-Installer-Menu-Item: 1650
Description: Configure hostname using inventory list.

View file

@ -0,0 +1,43 @@
#!/bin/sh -e
. /usr/share/debconf/confmodule
db_capb backup
log() {
logger -t inventory-hostname "$@"
}
INVENTORY_FILE=/cdrom/install/inventory.txt
SERIAL=$(cat /sys/class/dmi/id/product_serial)
if [ -z "$SERIAL" ]; then
log "Warning: No serial number found, skipping."
exit 0
fi
if [ ! -f "$INVENTORY_FILE" ]; then
log "Warning: No inventory file found at $INVENTORY_FILE, skipping."
exit 0
fi
SET_HOSTNAME=$(sed -n -e "s/^${SERIAL}\s\+\([0-9A-Za-z.-]\+\)\$/\1/p" "$INVENTORY_FILE")
if [ -z "$SET_HOSTNAME" ]; then
db_subst inventory-hostname/get_hostname SERIAL "$SERIAL"
if ! db_input high inventory-hostname/get_hostname; then
# question not asked
exit 0
fi
if ! db_go; then
exit 10 # back up
fi
db_get inventory-hostname/get_hostname
SET_HOSTNAME="$RET"
elif [ "$(echo "$SET_HOSTNAME" | wc -l)" != "1" ]; then
log "Warning: Multiple inventory entries found for serial number $SERIAL, skipping."
exit 0
fi
db_set netcfg/get_hostname "$SET_HOSTNAME"
db_fset netcfg/get_hostname seen true

View file

@ -0,0 +1,15 @@
Template: debian-installer/inventory-hostname/title
Type: text
Description: Set hostname from inventory list
Template: inventory-hostname/get_hostname
Type: string
Default: debian
Description: Hostname:
WARNING: The serial number of this machine was not found in the inventory list.
Are you sure you want to install on this machine?
If yes, you may want to add this machine to the inventory.
.
Serial number: ${SERIAL}
.
You can manually enter a hostname.

View file

@ -0,0 +1,3 @@
#! /usr/bin/make -f
%:
dh $@

View file

@ -0,0 +1,30 @@
#!/bin/bash
set -eu
# Set root password.
chpasswd --encrypted <<< 'root:@contestant_root_password@'
# Set chromium homepage.
sed -i 's|"homepage": ".*"|"homepage": "https://contest.soi.ch/"|' /etc/chromium/master_preferences
sed -i 's|"homepage_is_newtabpage": true,|"homepage_is_newtabpage": false,|' /etc/chromium/master_preferences
# Disable Bluetooth.
systemctl disable bluetooth.service
# Disable sleep.
systemctl mask sleep.target suspend.target hibernate.target hybrid-sleep.target
# Disable panels in gnome-control-center.
DISABLE_DESKTOP="dpkg-statoverride --force-statoverride-add --update --add root root 640"
$DISABLE_DESKTOP /usr/share/applications/gnome-bluetooth-panel.desktop
$DISABLE_DESKTOP /usr/share/applications/gnome-online-accounts-panel.desktop
$DISABLE_DESKTOP /usr/share/applications/gnome-sharing-panel.desktop
# Enable the live system configuration script at boot.
systemctl enable live-config.service
# Disable kexec-tools services.
# We want to load kexec manually, and execution of kexec is already done by systemd.
systemctl disable kexec-load.service
systemctl disable kexec.service

View file

@ -0,0 +1,3 @@
# Disable automount
[org/gnome/desktop/media-handling]
automount = false

View file

@ -0,0 +1,3 @@
# Disable blank screen
[org/gnome/desktop/session]
idle-delay = uint32 0

View file

@ -0,0 +1,3 @@
# Disable lock on blank screen
[org/gnome/desktop/screensaver]
lock-enabled = false

View file

@ -0,0 +1,4 @@
# Disable suspend when inactive
[org/gnome/settings-daemon/plugins/power]
sleep-inactive-ac-type = 'nothing'
sleep-inactive-battery-type = 'nothing'

View file

@ -0,0 +1,5 @@
# Disable "Updates available" notifications and auto updates.
# Updates which require reboot are useless on live systems,
# and other updates would be installed on each boot.
[org/gnome/software]
allow-updates = false

View file

@ -0,0 +1,2 @@
[org/gnome/shell]
enabled-extensions = ['contest-lock@soi.ch', 'user-indicator@soi.ch']

View file

@ -0,0 +1,3 @@
# Disable locking the screen
[org/gnome/desktop/lockdown]
disable-lock-screen = true

View file

@ -0,0 +1,23 @@
{
"policies": {
"OverrideFirstRunPage": "",
"NoDefaultBookmarks": true,
"DisableProfileImport": true,
"Preferences": {
"datareporting.policy.dataSubmissionPolicyBypassNotification": true,
"security.default_personal_cert": "Select Automatically"
},
"Homepage": {
"URL": "https://contest.soi.ch/",
"StartPage": "homepage"
},
"DisplayBookmarksToolbar": true,
"Bookmarks": [
{
"Title": "Contest",
"URL": "https://contest.soi.ch/",
"Placement": "toolbar"
}
]
}
}

View file

@ -0,0 +1,17 @@
// Show a root password prompt for these actions:
// - change network settings
// - hibernate
// - change package download proxy
// - mount removable storage, perform other disk operations
polkit.addRule(function (action, subject) {
if (
action.id.indexOf("org.freedesktop.ModemManager1.") === 0 ||
action.id.indexOf("org.freedesktop.NetworkManager.") === 0 ||
action.id === "org.freedesktop.login1.hibernate" ||
action.id === "org.freedesktop.packagekit.system-network-proxy-configure" ||
action.id.indexOf("org.freedesktop.udisks2.") === 0
) {
return polkit.Result.AUTH_ADMIN;
}
});

View file

@ -0,0 +1,2 @@
PasswordAuthentication no
AllowUsers root

View file

@ -0,0 +1,14 @@
[Unit]
Description=custom configuration of live system during boot.
Before=basic.target
After=local-fs.target systemd-tmpfiles-setup.service
DefaultDependencies=no
ConditionKernelCommandLine=boot=live
[Service]
Type=oneshot
RemainAfterExit=yes
ExecStart=/usr/local/bin/live-config
[Install]
WantedBy=basic.target

View file

@ -0,0 +1,2 @@
[Time]
NTP=contest.soi.ch

View file

@ -0,0 +1,49 @@
#!/bin/bash
# This tool installs the client certificate in Firefox and Chromium.
username="$1"
userhome="/home/$username"
certificate="$userhome/.config/clientcert.p12"
runuser -u "$username" -- mkdir -p "$userhome/.config"
mv "$userhome/clientcert.p12" "$certificate"
chown "$username:$username" "$certificate"
# Delete all Firefox data
rm -rf "$userhome/.mozilla/"
# Create an empty profile
runuser -u "$username" -- mkdir -p "$userhome/.mozilla/firefox/main"
# Tell Firefox to use this profile
cat > "$userhome/.mozilla/firefox/profiles.ini" <<EOF
[Profile0]
Name=main
IsRelative=1
Path=main
[General]
StartWithLastProfile=1
Version=2
[Install3B6073811A6ABF12]
Default=main
Locked=1
EOF
chown "$username:$username" "$userhome/.mozilla/firefox/profiles.ini"
# Create a certificate database
runuser -u "$username" -- certutil -d "sql:$userhome/.mozilla/firefox/main/" -N --empty-password
# Import the client certificate
runuser -u "$username" -- pk12util -d "sql:$userhome/.mozilla/firefox/main/" -i "$certificate" -K "" -W ""
# Do the same for the NSS shared certificate database, used by Chromium
rm -rf "$userhome/.pki/"
runuser -u "$username" -- mkdir -p "$userhome/.pki/nssdb"
runuser -u "$username" -- certutil -d "sql:$userhome/.pki/nssdb/" -N --empty-password
runuser -u "$username" -- pk12util -d "sql:$userhome/.pki/nssdb/" -i "$certificate" -K "" -W ""

View file

@ -0,0 +1,36 @@
#!/bin/bash
set -eu
LIVE_HOSTNAME=debian
LIVE_USERNAME=contestant
LIVE_USER_FULLNAME="Contestant"
# Set hostname.
echo "${LIVE_HOSTNAME}" > /etc/hostname
hostname "${LIVE_HOSTNAME}"
# Create hosts file.
cat > /etc/hosts <<EOF
127.0.0.1 localhost ${LIVE_HOSTNAME}
::1 localhost ip6-localhost ip6-loopback
fe00::0 ip6-localnet
ff00::0 ip6-mcastprefix
ff02::1 ip6-allnodes
ff02::2 ip6-allrouters
EOF
# Create ssh host key.
ssh-keygen -q -f /etc/ssh/ssh_host_ed25519_key -N "" -t ed25519
# Create user.
adduser --disabled-password --gecos "$LIVE_USER_FULLNAME" "$LIVE_USERNAME"
# Enable auto login.
sed -i \
-e "s/^[# ]*AutomaticLoginEnable *=.*/AutomaticLoginEnable = true/g" \
-e "s/^[# ]*AutomaticLogin *=.*/AutomaticLogin = $LIVE_USERNAME/g" \
-e "s/^[# ]*TimedLoginEnable *=.*/TimedLoginEnable = true/g" \
-e "s/^[# ]*TimedLogin *=.*/TimedLogin = $LIVE_USERNAME/g" \
-e "s/^[# ]*TimedLoginDelay *=.*/TimedLoginDelay = 5/g" \
/etc/gdm3/daemon.conf

View file

@ -0,0 +1,16 @@
#!/bin/bash
set -eu
# Reboot with kexec.
# This has the advantage that we don't need to go through the system boot menu,
# which is especially useful when the boot menu is password protected.
# However, we currently can't preserve the squashfs in RAM across kexec,
# so the boot USB stick needs to be plugged in before rebooting.
kexec --kexec-file-syscall --load /vmlinuz --initrd=/initrd.img --append="$(cat /proc/cmdline)"
if XDG_RUNTIME_DIR="/run/user/$(id -u contestant)" runuser -u contestant -- zenity --question --title="Reboot?" --text="Press Enter after inserting the boot USB stick."
then
reboot
fi

View file

@ -0,0 +1,10 @@
<!DOCTYPE NETSCAPE-Bookmark-file-1>
<META HTTP-EQUIV="Content-Type" CONTENT="text/html; charset=UTF-8">
<TITLE>Bookmarks</TITLE>
<H1>Bookmarks</H1>
<DL><p>
<DT><H3 PERSONAL_TOOLBAR_FOLDER="true">Bookmarks Bar</H3>
<DL><p>
<DT><A HREF="https://contest.soi.ch/" ICON="data:image/png;base64,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">Contest</A>
</DL><p>
</DL><p>

View file

@ -0,0 +1,428 @@
// Portions of this file are taken from GNOME Shell and adapted.
// Because of that, this gnome extension is distributed under
// the terms of the GNU General Public License, version 2 or later.
const {
AccountsService, Atk, Clutter, Gio,
GLib, Graphene, Meta, Shell, St,
} = imports.gi;
const Background = imports.ui.background;
const Layout = imports.ui.layout;
const Main = imports.ui.main;
// half of the time for which a frame is displayed
const HALF_FRAME_TIME_MS = 8;
const BLUR_BRIGHTNESS = 0.55;
const BLUR_SIGMA = 60;
const POINTER_HIDE_TIMEOUT = 10 * GLib.USEC_PER_SEC;
let actor;
let lockDialog;
let labelCountdown;
let labelTitle;
let labelMessage;
let labelUser;
const bgManagers = [];
let backgroundGroup;
let cursorTracker;
let motionId = 0;
let lastMotionTime = 0;
let pointerHidden = false;
let pointerHideId = 0;
let user;
let grab;
let countdownTimeoutId = 0;
let configFile;
let configMonitor;
let config;
let startTime;
let isExtensionEnabled = false;
let isActive = false;
let isShellReady = false;
let isActiveChanging = false;
function extLog (msg) {
log(`[contest-lock] ${msg}`)
}
function extLogError (msg) {
printerr(`[contest-lock] Error: ${msg}`);
}
function loadConfig () {
configFile.load_contents_async(null, (obj, res) => {
// If there is a poblem with the config file, log an error and keep
// using the old config.
let newConfig;
try {
const [ok, bytes] = configFile.load_contents_finish(res);
// TextDecoder is used in upstream gnome-shell, but not yet
// supported in current Debian.
const contentStr = imports.byteArray.toString(bytes);
//const contentStr = new TextDecoder().decode(bytes);
newConfig = JSON.parse(contentStr);
} catch (err) {
logError(err, '[contest-lock] config file');
return;
}
if (!(typeof newConfig === 'object' && newConfig != null)) {
extLogError('config file: invalid format');
return;
}
if (typeof newConfig.title !== 'string') {
extLogError('config file: "title" must be a string');
return;
}
if (typeof newConfig.message !== 'string') {
extLogError('config file: "message" must be a string');
return;
}
if (
typeof newConfig.startTime !== 'string' ||
!/^\d{4,}-\d\d-\d\dT\d\d:\d\d:\d\d\+\d\d:\d\d$/.test(newConfig.startTime)
) {
extLogError('config file: "startTime" must be a string with format 0000-00-00T00:00:00+00:00');
return;
}
extLog('Loaded new config.')
config = newConfig;
startTime = (new Date(newConfig.startTime)).getTime();
updateConfig();
syncActive();
});
}
function syncActive () {
if (isActiveChanging) return;
let beforeStart = false;
if (startTime != null) {
const now = new Date();
const timeToStart = startTime - now.getTime() - HALF_FRAME_TIME_MS;
beforeStart = timeToStart > 0;
}
// ignore disable event when active
if (beforeStart && isShellReady && (isExtensionEnabled || isActive)) {
activate();
} else {
deactivate();
}
}
function updateConfig () {
if (labelTitle != null) {
labelTitle.text = config.title;
}
if (labelMessage != null) {
labelMessage.text = config.message;
}
}
function updateUser () {
if (labelUser != null) {
const realName = user.get_real_name();
if (realName != null) labelUser.text = realName;
}
}
function updateCountdown () {
countdownTimeoutId = 0;
const now = new Date();
const nowTime = now.getTime() + HALF_FRAME_TIME_MS;
const timeToStart = startTime - nowTime;
const beforeStart = timeToStart > 0;
if (!beforeStart) {
deactivate();
return GLib.SOURCE_REMOVE;
}
const allSecondsToStart = Math.floor(timeToStart / 1000);
const secondsToStart = allSecondsToStart % 60
const allMinutesToStart = Math.floor(allSecondsToStart / 60);
const minutesToStart = allMinutesToStart % 60;
const hoursToStart = Math.floor(allMinutesToStart / 60);
let hoursString = '';
if (hoursToStart !== 0) hoursString = `${hoursToStart}`;
labelCountdown.text = hoursString +
minutesToStart.toString().padStart(2, '0') + '' +
secondsToStart.toString().padStart(2, '0');
// Force a redraw of the entire label widget. Without this, there sometimes
// appears a small artifact to the right of the text, which is only visible
// every other second. This seems to be a bug in the rendering engine itself.
labelCountdown.queue_redraw();
const nextUpdateTime = 1000 - nowTime % 1000
countdownTimeoutId = GLib.timeout_add(
GLib.PRIORITY_HIGH,
nextUpdateTime,
updateCountdown
);
GLib.Source.set_name_by_id(countdownTimeoutId, '[contest-lock] updateCountdown');
return GLib.SOURCE_REMOVE;
}
function updateBackgrounds () {
if (!isActive) return;
while (bgManagers.length) bgManagers.pop().destroy();
backgroundGroup.destroy_all_children();
for (let monitorIndex = 0; monitorIndex < Main.layoutManager.monitors.length; monitorIndex++) {
const monitor = Main.layoutManager.monitors[monitorIndex];
const widget = new St.Widget({
style_class: 'screen-shield-background',
x: monitor.x,
y: monitor.y,
width: monitor.width,
height: monitor.height,
effect: new Shell.BlurEffect({
name: 'blur',
brightness: BLUR_BRIGHTNESS,
sigma: BLUR_SIGMA,
}),
});
const bgManager = new Background.BackgroundManager({
container: widget,
monitorIndex,
controlPosition: false,
});
bgManagers.push(bgManager);
backgroundGroup.add_child(widget);
}
}
function pointerHideTimer () {
if (pointerHideId !== 0) {
GLib.source_remove(pointerHideId);
pointerHideId = 0;
}
if (!isActive) return GLib.SOURCE_REMOVE;
const timeToHide = lastMotionTime + POINTER_HIDE_TIMEOUT - GLib.get_monotonic_time();
if (timeToHide <= 0) {
cursorTracker.set_pointer_visible(false);
pointerHidden = true;
return GLib.SOURCE_REMOVE;
}
pointerHideId = GLib.timeout_add(
GLib.PRIORITY_HIGH,
timeToHide / 1000 + 20,
pointerHideTimer
);
GLib.Source.set_name_by_id(pointerHideId, '[contest-lock] pointerHide');
return GLib.SOURCE_REMOVE;
}
function activate () {
if (isActive) return;
isActiveChanging = true;
isActive = true;
grab = Main.pushModal(Main.uiGroup, { actionMode: Shell.ActionMode.LOCK_SCREEN });
if (typeof grab === 'boolean') { // gnome 38
if (!grab) {
grab = Main.pushModal(Main.uiGroup, {
options: Meta.ModalOptions.POINTER_ALREADY_GRABBED,
actionMode: Shell.ActionMode.LOCK_SCREEN
});
}
if (!grab) {
extLogError('Failed to activate: Could not obtain keyboard grab.');
return;
}
grab = Main.uiGroup;
} else if ((grab.get_seat_state() & Clutter.GrabState.KEYBOARD) === 0) {
Main.popModal(grab);
grab = null;
extLogError('Failed to activate: Could not obtain keyboard grab.');
return;
}
actor.show();
Main.sessionMode.pushMode('unlock-dialog');
backgroundGroup = new Clutter.Actor();
motionId = global.stage.connect('captured-event', (stage, event) => {
if (event.type() === Clutter.EventType.MOTION) {
lastMotionTime = GLib.get_monotonic_time();
if (pointerHidden) {
cursorTracker.set_pointer_visible(true);
pointerHidden = false;
pointerHideTimer();
}
}
return Clutter.EVENT_PROPAGATE;
});
cursorTracker.set_pointer_visible(false);
pointerHidden = true;
labelCountdown = new St.Label({
style_class: 'contest-lock-countdown',
x_align: Clutter.ActorAlign.CENTER,
});
labelTitle = new St.Label({
style_class: 'contest-lock-title',
x_align: Clutter.ActorAlign.CENTER,
});
labelMessage = new St.Label({
style_class: 'contest-lock-message',
x_align: Clutter.ActorAlign.CENTER,
});
labelUser = new St.Label({
style_class: 'contest-lock-user',
x_align: Clutter.ActorAlign.CENTER,
});
const stack = new St.BoxLayout({
style_class: 'contest-lock-stack',
vertical: true,
x_expand: true,
y_expand: true,
x_align: Clutter.ActorAlign.CENTER,
y_align: Clutter.ActorAlign.CENTER,
});
stack.add_child(labelUser);
stack.add_child(labelCountdown);
stack.add_child(labelTitle);
stack.add_child(labelMessage);
const mainBox = new St.BoxLayout();
mainBox.add_constraint(new Layout.MonitorConstraint({ primary: true }));
mainBox.add_child(stack);
lockDialog = new St.Widget({
name: 'contestLockDialog',
accessible_role: Atk.Role.WINDOW,
visible: false,
reactive: true,
can_focus: true,
x_expand: true,
y_expand: true,
pivot_point: new Graphene.Point({ x: 0.5, y: 0.5 }),
});
lockDialog.add_child(backgroundGroup);
lockDialog.add_child(mainBox);
updateConfig();
updateUser();
updateCountdown();
updateBackgrounds();
// countdown may have just expired before we called updateCountdown
if (!isActive) return;
actor.add_child(lockDialog);
lockDialog.show();
extLog('Activated.')
isActiveChanging = false;
}
function deactivate () {
if (!isActive) return;
isActiveChanging = true;
isActive = false;
if (Main.sessionMode.currentMode === 'unlock-dialog') {
Main.sessionMode.popMode('unlock-dialog');
}
Main.popModal(grab);
grab = null;
if (countdownTimeoutId !== 0) {
GLib.source_remove(countdownTimeoutId);
countdownTimeoutId = 0;
}
actor.hide();
labelCountdown = null;
labelTitle = null;
labelMessage = null;
labelUser = null;
while (bgManagers.length) bgManagers.pop().destroy();
lockDialog.destroy();
lockDialog = null;
if (motionId) {
global.stage.disconnect(motionId);
motionId = 0;
}
cursorTracker.set_pointer_visible(true);
extLog('Deactivated.');
isActiveChanging = false;
}
function init (extension) {
actor = Main.layoutManager.screenShieldGroup;
const userName = GLib.get_user_name();
user = AccountsService.UserManager.get_default().get_user(userName);
if (!user) return;
user.connect('notify::is-loaded', updateUser);
user.connect('changed', updateUser);
updateUser();
Main.layoutManager.connect('monitors-changed', updateBackgrounds);
cursorTracker = Meta.CursorTracker.get_for_display(global.display);
if (!Main.layoutManager._startingUp) {
isShellReady = true;
} else {
Main.layoutManager.connect('startup-complete', () => {
isShellReady = true;
syncActive();
});
}
// TODO: When we drop compatibility with gnome <42, remove this code,
// rename the stylesheet back to stylesheet.css (so that it is loaded
// by the extension system) and add a session-modes property which
// includes unlock-dialog to metadata.json.
const theme = St.ThemeContext.get_for_stage(global.stage).get_theme();
const stylesheetFile = extension.dir.get_child('stylesheet-always.css');
theme.load_stylesheet(stylesheetFile);
// TODO: When we drop compatibility with gnome <42, remove this code.
// gnome 38 has a bug that causes extensions to break when running
// `dconf update` while the screen is locked.
Main.extensionManager.reloadExtension = function () {};
configFile = Gio.File.new_for_path('/etc/contest-lock.json');
configMonitor = configFile.monitor_file(Gio.FileMonitorFlags.NONE, null);
configMonitor.set_rate_limit(1000);
configMonitor.connect('changed', loadConfig);
loadConfig();
}
function enable () {
isExtensionEnabled = true;
syncActive();
}
function disable () {
isExtensionEnabled = false;
syncActive();
}

View file

@ -0,0 +1,8 @@
{
"extension-id": "contest-lock",
"uuid": "contest-lock@soi.ch",
"name": "Contest lock screen",
"description": "A custom lock screen for contests.",
"shell-version": [ "3.38", "42", "43" ],
"url": ""
}

View file

@ -0,0 +1,24 @@
.contest-lock-stack {
color: white;
text-align: center;
spacing: 24px;
}
.contest-lock-countdown {
font-size: 64pt;
font-weight: 300;
font-feature-settings: "tnum"; /* tabular figures */
}
.contest-lock-title {
font-size: 16pt;
}
.contest-lock-user {
font-size: 20pt;
}
.contest-lock-message {
padding-top: 24px;
font-size: 16pt;
}

View file

@ -0,0 +1,39 @@
const { St, Clutter, GLib, Gio, AccountsService } = imports.gi;
const Main = imports.ui.main;
let panelBin;
let userLabel;
let user;
function updateUser () {
const realName = user.get_real_name();
if (realName != null) userLabel.text = realName;
}
function init () {
panelBin = new St.Bin({
style_class: 'panel-bin',
});
userLabel = new St.Label({
text: 'No user',
y_align: Clutter.ActorAlign.CENTER,
});
panelBin.set_child(userLabel);
const userName = GLib.get_user_name();
user = AccountsService.UserManager.get_default().get_user(userName);
if (!user) return;
user.connect('notify::is-loaded', updateUser);
user.connect('changed', updateUser);
updateUser();
}
function enable () {
Main.panel._rightBox.insert_child_at_index(panelBin, 0);
}
function disable () {
Main.panel._rightBox.remove_child(panelBin);
}

View file

@ -0,0 +1,8 @@
{
"extension-id": "user-indicator",
"uuid": "user-indicator@soi.ch",
"name": "User indicator",
"description": "Shows the user's real name in the top bar.",
"shell-version": [ "3.38", "42", "43" ],
"url": ""
}

View file

@ -0,0 +1,4 @@
.panel-bin {
padding-left: 12px;
padding-right: 12px;
}

View file

@ -0,0 +1,12 @@
# Remote access
openssh-server
rsync
# Firewall
nftables
# Reboot with kexec
kexec-tools
# For importing client certificate
libnss3-tools

View file

@ -0,0 +1,62 @@
-----BEGIN PGP PUBLIC KEY BLOCK-----
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
McvDuGKIXk111Z3nrF4DeIIc/U6ICQ==
=CCk2
-----END PGP PUBLIC KEY BLOCK-----

View file

@ -0,0 +1 @@
deb http://download.sublimetext.com/ apt/stable/

View file

@ -0,0 +1,19 @@
-----BEGIN PGP PUBLIC KEY BLOCK-----
Version: GnuPG v1.4.7 (GNU/Linux)
mQENBFYxWIwBCADAKoZhZlJxGNGWzqV+1OG1xiQeoowKhssGAKvd+buXCGISZJwT
LXZqIcIiLP7pqdcZWtE9bSc7yBY2MalDp9Liu0KekywQ6VVX1T72NPf5Ev6x6DLV
7aVWsCzUAF+eb7DC9fPuFLEdxmOEYoPjzrQ7cCnSV4JQxAqhU4T6OjbvRazGl3ag
OeizPXmRljMtUUttHQZnRhtlzkmwIrUivbfFPD+fEoHJ1+uIdfOzZX8/oKHKLe2j
H632kvsNzJFlROVvGLYAk2WRcLu+RjjggixhwiB+Mu/A8Tf4V6b+YppS44q8EvVr
M+QvY7LNSOffSO6Slsy9oisGTdfE39nC7pVRABEBAAG0N01pY3Jvc29mdCAoUmVs
ZWFzZSBzaWduaW5nKSA8Z3Bnc2VjdXJpdHlAbWljcm9zb2Z0LmNvbT6JATUEEwEC
AB8FAlYxWIwCGwMGCwkIBwMCBBUCCAMDFgIBAh4BAheAAAoJEOs+lK2+EinPGpsH
/32vKy29Hg51H9dfFJMx0/a/F+5vKeCeVqimvyTM04C+XENNuSbYZ3eRPHGHFLqe
MNGxsfb7C7ZxEeW7J/vSzRgHxm7ZvESisUYRFq2sgkJ+HFERNrqfci45bdhmrUsy
7SWw9ybxdFOkuQoyKD3tBmiGfONQMlBaOMWdAsic965rvJsd5zYaZZFI1UwTkFXV
KJt3bp3Ngn1vEYXwijGTa+FXz6GLHueJwF0I7ug34DgUkAFvAs8Hacr2DRYxL5RJ
XdNgj4Jd2/g6T9InmWT0hASljur+dJnzNiNCkbn9KbX7J/qK1IbR8y560yRmFsU+
NdCFTW7wY0Fb1fWJ+/KTsC4=
=J6gs
-----END PGP PUBLIC KEY BLOCK-----

View file

@ -0,0 +1 @@
deb http://packages.microsoft.com/repos/code stable main

View file

@ -0,0 +1,31 @@
set timeout=5
# Everything below is copied from the default config.
set default=0
if [ x$feature_default_font_path = xy ] ; then
font=unicode
else
font=$prefix/unicode.pf2
fi
# Copied from the netinst image
if loadfont $font ; then
set gfxmode=800x600
set gfxpayload=keep
insmod efi_gop
insmod efi_uga
insmod video_bochs
insmod video_cirrus
else
set gfxmode=auto
insmod all_video
fi
insmod gfxterm
insmod png
source /boot/grub/theme.cfg
terminal_output gfxterm

View file

@ -0,0 +1,33 @@
<svg xmlns="http://www.w3.org/2000/svg" width="640" height="480">
<rect x="0" y="0" width="640" height="480" fill="#000"/>
<g transform="translate(90, 90) scale(4)">
<circle r="13.75" fill="none" stroke="#303030" stroke-width="2.5"/>
<path fill="none" stroke="#1eadf5" stroke-width="2" d="M7.6121 0 0 -7.6121 13.2727 -15.7298M-7.6121 0 7.6121 0 0 7.6121 -7.6121 0 -14.3693 13.5411"/>
<g fill="#1862ff">
<circle r="3" cy="7.6121"/>
<circle r="3" cx="7.6121"/>
<circle r="3" cy="-7.6121"/>
<circle r="3" cx="-7.6121"/>
<circle r="3" cx="13.2727" cy="-15.7298"/>
<circle r="3" cx="-14.3693" cy="13.5411"/>
</g>
</g>
<g style="font-family: 'DejaVu Sans'; fill: #fff;">
<text style="font-weight:bold;font-size:20px;"
x="191" y="48">@PROJECT@ @VERSION@ (@DISTRIBUTION@)</text>
<text style="font-weight:bold;font-size:20px;"
x="191" y="68">@ARCHITECTURE@</text>
<text style="font-weight:bold;font-size:16px;"
x="191" y="108">Built: @YEAR@-@MONTH@-@DAY@ @HOUR@:@MINUTE@:@SECOND@ @TIMEZONE@</text>
<text style="font-weight:normal;font-size:10px;"
x="191" y="140">linux: @LINUX_VERSIONS@</text>
<!--<text style="font-weight:normal;font-size:10px;"
x="191" y="156">live-build: @LIVE_BUILD_VERSION@</text>
<text style="font-weight:normal;font-size:10px;"
x="191" y="172">live-boot: @LIVE_BOOT_VERSION@</text>
<text style="font-weight:normal;font-size:10px;"
x="191" y="188">live-config: @LIVE_CONFIG_VERSION@</text>
<text style="font-weight:normal;font-size:10px;"
x="191" y="204">live-tools: @LIVE_TOOLS_VERSION@</text>-->
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.6 KiB

View file

@ -0,0 +1,47 @@
#!/bin/bash
set -eu
# Update dconf database after having put files in /etc/dconf/.
dconf update
# Configure timezone.
TIMEZONE=Europe/Zurich
echo "$TIMEZONE" > /etc/timezone
ln -sf /usr/share/zoneinfo/"$TIMEZONE" /etc/localtime
# Install VS Code extensions.
VSCODE_EXTENSIONS="ms-python.python ms-vscode.cpptools swissolyinfo.soicode"
mkdir /etc/skel/.vscode
chown nobody:nogroup /etc/skel/.vscode
for ext in $VSCODE_EXTENSIONS; do
runuser -u nobody -- code --user-data-dir=/tmp/vsc.tmp \
--extensions-dir=/etc/skel/.vscode/extensions \
--install-extension="$ext"
done
chown -R root:root /etc/skel/.vscode
rm -rf /tmp/vsc.tmp
# Enable codeblocks template.
sed -i 's|// project wizards|RegisterWizard(wizProject, _T("soi"), _T("A SOI task"), _T("Console"));|' /usr/share/codeblocks/templates/wizard/config.script
# Add a default keyring to avoid a prompt to create one when launching Chromium.
mkdir -p /etc/skel/.local/share/keyrings/
chmod og= /etc/skel/.local/share/keyrings/
echo -n "Default_keyring" > /etc/skel/.local/share/keyrings/default
cat > /etc/skel/.local/share/keyrings/Default_keyring.keyring <<EOF
[keyring]
display-name=Default keyring
ctime=0
mtime=0
lock-on-idle=false
lock-after=false
EOF
chmod og= /etc/skel/.local/share/keyrings/Default_keyring.keyring
# Patch bug in live-boot.
# This is fixed upstream: https://salsa.debian.org/live-team/live-boot/-/commit/2cb049fb7502d11f344d14c567aab2592f19e77b
# Once we pull in the fix through an upgrade, we can remove the patch here.
if [ -f /lib/live/boot/9990-toram-todisk.sh ]; then
sed -i 's|dev="/dev/shm"|dev="tmpfs"|g' /lib/live/boot/9990-toram-todisk.sh
fi

View file

@ -0,0 +1,5 @@
[org/gnome/desktop/background]
picture-uri = 'file:///usr/local/share/backgrounds/wallpaper-soi.svg'
picture-uri-dark = 'file:///usr/local/share/backgrounds/wallpaper-soi-dark.svg'
picture-options = 'zoom'
primary-color = '#e6e6e6'

View file

@ -0,0 +1,2 @@
[org/gnome/shell]
favorite-apps = ['firefox-esr.desktop', 'gnome-terminal.desktop', 'nautilus.desktop']

View file

@ -0,0 +1,5 @@
# Configure the default keyboard layouts shown in the switcher
# According to Wikipedia, the Italian-speaking part of Switzerland also uses the ch+fr layout.
# The first option is the default.
[org/gnome/desktop/input-sources]
sources = [('xkb', 'ch'), ('xkb', 'ch+fr'), ('xkb', 'us')]

View file

@ -0,0 +1,2 @@
user-db:user
system-db:local

View file

@ -0,0 +1,14 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="-80 -80 160 160" width="3840" height="3840">
<title>Swiss Olympiad in Informatics</title>
<rect fill="#2a2a2a" x="-100" y="-100" width="200" height="200"/>
<circle r="13.75" fill="none" stroke="#101010" stroke-width="2.5"/>
<path fill="none" stroke="#1eadf5" stroke-width="2" d="M7.6121 0 0 -7.6121 13.2727 -15.7298M-7.6121 0 7.6121 0 0 7.6121 -7.6121 0 -14.3693 13.5411"/>
<g fill="#1862ff">
<circle r="3" cy="7.6121"/>
<circle r="3" cx="7.6121"/>
<circle r="3" cy="-7.6121"/>
<circle r="3" cx="-7.6121"/>
<circle r="3" cx="13.2727" cy="-15.7298"/>
<circle r="3" cx="-14.3693" cy="13.5411"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 654 B

View file

@ -0,0 +1,14 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="-80 -80 160 160" width="3840" height="3840">
<title>Swiss Olympiad in Informatics</title>
<rect fill="#e6e6e6" x="-100" y="-100" width="200" height="200"/>
<circle r="13.75" fill="none" stroke="#303030" stroke-width="2.5"/>
<path fill="none" stroke="#1eadf5" stroke-width="2" d="M7.6121 0 0 -7.6121 13.2727 -15.7298M-7.6121 0 7.6121 0 0 7.6121 -7.6121 0 -14.3693 13.5411"/>
<g fill="#1862ff">
<circle r="3" cy="7.6121"/>
<circle r="3" cx="7.6121"/>
<circle r="3" cy="-7.6121"/>
<circle r="3" cx="-7.6121"/>
<circle r="3" cx="13.2727" cy="-15.7298"/>
<circle r="3" cx="-14.3693" cy="13.5411"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 654 B

View file

@ -0,0 +1,3 @@
live-boot
# This file overrides a default list to remove live-config.

View file

@ -0,0 +1,41 @@
# firmware
firmware-linux amd64-microcode intel-microcode
firmware-iwlwifi firmware-brcm80211 firmware-realtek
# system
systemd-timesyncd
locales
zstd
wpasupplicant
wireless-regdb
# desktop
gnome-core
xdg-user-dirs-gtk
network-manager-gnome
# needed for ejecting UBS sticks in nautilus
eject
# shell utilities
htop unzip
# software for participants
firefox-esr chromium-l10n
codeblocks emacs geany gedit joe kate kdevelop nano vim vim-gtk3
gcc g++ gdb ddd valgrind python3 pypy3
evince gnome-terminal konsole xterm byobu make cmake
nautilus-extension-gnome-terminal
file-roller
# for drawing on screenshots
drawing
# documentation
info manpages-dev gcc-doc gdb-doc
# from third-party repositories
sublime-text code
# requested by participants (gnome-tweaks can be used e.g. to change the function of Caps Lock key)
gnome-tweaks fonts-firacode
# translations
manpages-de manpages-fr manpages-it
gcc-12-locales
firefox-esr-l10n-de firefox-esr-l10n-fr firefox-esr-l10n-it

View file

@ -0,0 +1 @@
locales locales/locales_to_be_generated multiselect en_US.UTF-8 UTF-8, de_CH.UTF-8 UTF-8, fr_CH.UTF-8 UTF-8, it_CH.UTF-8 UTF-8

View file

@ -0,0 +1,2 @@
# Fasttrack is needed for VirtualBox.
deb https://fasttrack.debian.net/debian-fasttrack/ @DISTRIBUTION@-fasttrack main contrib

View file

@ -0,0 +1 @@
ethdetect

View file

@ -0,0 +1,7 @@
#!/bin/bash
set -eu
# Install the noauth PAM profile.
groupadd noauth
pam-auth-update --enable noauth

View file

@ -0,0 +1,5 @@
#!/bin/bash
set -eu
echo "inventory-hostname" >> .disk/udeb_include

View file

@ -0,0 +1,2 @@
[org/gnome/login-screen]
logo = '/usr/local/share/images/login-screen-logo.svg'

View file

@ -0,0 +1,3 @@
user-db:user
system-db:gdm
file-db:/usr/share/gdm/greeter-dconf-defaults

View file

@ -0,0 +1,9 @@
// Connecting to a WiFi in the gnome-shell quick settings prompts for an admin
// password without this rule.
// https://gitlab.gnome.org/GNOME/gnome-shell/-/issues/7378
polkit.addRule(function (action, subject) {
if (action.id === "org.freedesktop.NetworkManager.settings.modify.system") {
return polkit.Result.YES;
}
});

View file

@ -0,0 +1,17 @@
#!/bin/sh
set -eu
# Set up apt lists.
cp -rT /usr/local/share/target-sources /etc/apt/sources.list.d
rm /etc/apt/sources.list
USERNAME=soi
USER_FULLNAME="SOI"
# Password: soi
USER_PASSWORD='$y$j9T$h5VhMd4KkdmbxdZD1gO0N/$1hvwZgO8pQw13Xd6jaNXbtkbqVOC4W/ia/KXOcCGYvB'
# Create user.
adduser --disabled-password --gecos "$USER_FULLNAME" "$USERNAME"
usermod -p "$USER_PASSWORD" "$USERNAME"
adduser "$USERNAME" noauth

View file

@ -0,0 +1,18 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="-260 -20 520 40" width="1300" height="100">
<title>Swiss Olympiad in Informatics</title>
<circle r="13.75" fill="none" stroke="#101010" stroke-width="2.5"/>
<path fill="none" stroke="#1eadf5" stroke-width="2" d="M7.6121 0 0 -7.6121 13.2727 -15.7298M-7.6121 0 7.6121 0 0 7.6121 -7.6121 0 -14.3693 13.5411"/>
<g fill="#1862ff">
<circle r="3" cy="7.6121"/>
<circle r="3" cx="7.6121"/>
<circle r="3" cy="-7.6121"/>
<circle r="3" cx="-7.6121"/>
<circle r="3" cx="13.2727" cy="-15.7298"/>
<circle r="3" cx="-14.3693" cy="13.5411"/>
</g>
<g style="font-family: 'DejaVu Sans'; font-size: 5px; fill: #fff;">
<text x="40" y="-9">This laptop is property of the Swiss Olympiad in Informatics. Contact: info@soi.ch</text>
<text x="40" y="1">Dieser Laptop ist Eigentum der Schweizer Informatikolympiade. Kontakt: info@soi.ch</text>
<text x="40" y="11">Software version: @date@</text>
</g>
</svg>

After

Width:  |  Height:  |  Size: 937 B

View file

@ -0,0 +1,11 @@
Types: deb deb-src
URIs: http://deb.debian.org/debian
Suites: bookworm bookworm-updates
Components: main contrib non-free non-free-firmware
Signed-By: /usr/share/keyrings/debian-archive-keyring.gpg
Types: deb deb-src
URIs: http://deb.debian.org/debian-security
Suites: bookworm-security
Components: main contrib non-free non-free-firmware
Signed-By: /usr/share/keyrings/debian-archive-keyring.gpg

View file

@ -0,0 +1,6 @@
Name: Accept noauth users without any authentication
Default: yes
Priority: 512
Auth-Type: Primary
Auth:
[success=end default=ignore] pam_succeed_if.so user ingroup noauth

View file

@ -0,0 +1,6 @@
#!/bin/sh
# Disable mountmedia, because that can make the cdrom mount fail.
# Not sure why there are two conflicting mounting mechanisms in Debian Installer
# (mountmedia with /media and cdrom-detect with /cdrom).
exit 1

View file

@ -0,0 +1,28 @@
#_preseed_V1
d-i debian-installer/language string en
d-i debian-installer/country string CH
d-i debian-installer/locale string en_US.UTF-8
d-i keyboard-configuration/xkb-keymap select ch
d-i hw-detect/load_firmware boolean false
d-i netcfg/enable boolean false
d-i netcfg/get_domain string
d-i passwd/root-login boolean false
d-i passwd/user-fullname string Admin
d-i passwd/username string superstofl
d-i passwd/user-password-crypted password @install_admin_password@
d-i partman-auto/method string regular
d-i partman-auto/init_automatically_partition select some_device
d-i partman-auto/choose_recipe select atomic
d-i partman/choose_partition select finish
d-i apt-setup/use_mirror boolean false
d-i grub-installer/only_debian boolean true
d-i preseed/late_command string in-target /usr/local/bin/install-config

View file

@ -0,0 +1,3 @@
# This file overrides a default list to remove live-config.
# For the installer, we don't need any live packages,
# they would just be removed again by the installer.

View file

@ -0,0 +1,16 @@
sudo
# Make Secure Boot work
grub-efi-amd64-signed
# Firmware updates through gnome-software
fwupd fwupd-signed
# Low battery charge notifications, battery info
gnome-power-manager
# Run virtual machines with Gnome Boxes
gnome-boxes qemu-system-x86 qemu-utils libvirt-daemon-system
# Run virtual machines with VirtualBox
virtualbox-qt

View file

@ -0,0 +1,6 @@
#!/bin/bash
set -eu
# Enable the live system configuration script at boot.
systemctl enable live-config.service

View file

@ -0,0 +1,3 @@
# Disable lock on blank screen
[org/gnome/desktop/screensaver]
lock-enabled = false

View file

@ -0,0 +1,5 @@
# Disable "Updates available" notifications and auto updates.
# Updates which require reboot are useless on live systems,
# and other updates would be installed on each boot.
[org/gnome/software]
allow-updates = false

View file

@ -0,0 +1,14 @@
[Unit]
Description=custom configuration of live system during boot.
Before=basic.target
After=local-fs.target systemd-tmpfiles-setup.service
DefaultDependencies=no
ConditionKernelCommandLine=boot=live
[Service]
Type=oneshot
RemainAfterExit=yes
ExecStart=/usr/local/bin/live-config
[Install]
WantedBy=basic.target

View file

@ -0,0 +1,44 @@
#!/bin/bash
set -eu
LIVE_HOSTNAME=debian
LIVE_USERNAME=soi
LIVE_USER_FULLNAME="SOI live"
# Password: soi
LIVE_USER_PASSWORD='$y$j9T$h5VhMd4KkdmbxdZD1gO0N/$1hvwZgO8pQw13Xd6jaNXbtkbqVOC4W/ia/KXOcCGYvB'
# Set hostname.
echo "${LIVE_HOSTNAME}" > /etc/hostname
hostname "${LIVE_HOSTNAME}"
# Create hosts file.
cat > /etc/hosts <<EOF
127.0.0.1 localhost ${LIVE_HOSTNAME}
::1 localhost ip6-localhost ip6-loopback
fe00::0 ip6-localnet
ff00::0 ip6-mcastprefix
ff02::1 ip6-allnodes
ff02::2 ip6-allrouters
EOF
# Create user.
adduser --disabled-password --gecos "$LIVE_USER_FULLNAME" "$LIVE_USERNAME"
usermod -p "$LIVE_USER_PASSWORD" "$LIVE_USERNAME"
adduser "$LIVE_USERNAME" sudo
# Disable sudo password prompt.
cat > /etc/sudoers.d/10_customize <<EOF
# Do not ask for password
Defaults !authenticate
EOF
# Enable auto login.
sed -i \
-e "s/^[# ]*AutomaticLoginEnable *=.*/AutomaticLoginEnable = true/g" \
-e "s/^[# ]*AutomaticLogin *=.*/AutomaticLogin = $LIVE_USERNAME/g" \
-e "s/^[# ]*TimedLoginEnable *=.*/TimedLoginEnable = true/g" \
-e "s/^[# ]*TimedLogin *=.*/TimedLogin = $LIVE_USERNAME/g" \
-e "s/^[# ]*TimedLoginDelay *=.*/TimedLoginDelay = 5/g" \
/etc/gdm3/daemon.conf

View file

@ -0,0 +1,4 @@
sudo
# Show progress while copying squashfs to RAM.
rsync

128
os/readme.md Normal file
View file

@ -0,0 +1,128 @@
# OS build system
This is a system for building a customized OS for SOI, based on [Debian Live].
[Debian Live]: https://live-team.pages.debian.net/live-manual/html/live-manual/index.en.html
## Variants
There are multiple variants of the OS for different use cases.
- `training-live` is a live system for training.
- `training-installer` is an installer, which writes the OS to disk instead of running it directly.
This is intended for installing the laptops owned by SOI only.
Installation is offline and mostly automated.
- `contestant` is a live system for contests.
It has additional configuration useful for contests.
The live systems copy the entire OS to RAM while booting, so you can remove the USB stick after booting is finished.
That way, you only need a small number of USB sticks for booting many computers.
All variants support Secure Boot.
However, VirtualBox (contained in the installer variant) only works with Secure Boot disabled.
## How to build an ISO
We run the build inside a Docker container, so you need Docker installed on your host.
Building works on Linux hosts, other OSes are untested.
First, obtain the configuration files and put them in the folder `config`.
These files contain secrets and are thus not committed to the repository.
If you want to create your own config, see the folder `config-example` for examples.
Run the following commands in the repository root folder.
The `--privileged` flag is needed for mounting `/proc` and similar in the target system root.
```bash
mkdir -p osbuild/build
sudo docker pull debian:bookworm
sudo docker run --rm -it --privileged --mount type=bind,source="$(pwd)",target=/work --workdir /work debian:bookworm
```
Inside the container, run the following commands.
Replace `training-live` with the variant you want to build.
```bash
apt-get update
# python3: for build script
# ca-certificates: for downloading files over https
# rsync, cpio: used by live-build
# unzip: for codeblocks plugin
# build-essential, debhelper: for building custom udeb
apt-get install --no-install-recommends python3 ca-certificates live-build rsync cpio unzip build-essential debhelper
cd osbuild/build
python3 ../../os/build.py training-live
```
Once the build is finished, you will find the ISO at `osbuild/build/live-image-amd64.hybrid.iso`.
## Testing in a VM
During development, it's convenient to test the OS in a virtual machine.
Install QEMU on your host.
The following commands should be run outside the docker container.
```bash
# training-live, legacy and EFI boot:
kvm -m 8G -smp 4 -vga virtio -cdrom training-live.iso
kvm -m 8G -smp 4 -vga virtio -cdrom training-live.iso -bios /usr/share/ovmf/OVMF.fd
# training-installer:
qemu-img create -f qcow2 installtarget.qcow2 20G
kvm -m 8G -smp 4 -vga virtio -drive file=training-installer.iso,if=virtio,format=raw,readonly=on -drive file=installtarget.qcow2,if=virtio -bios /usr/share/ovmf/OVMF.fd -smbios type=1,serial=DEMO123
# contestant:
# Add your ssh key to os/config/contestant_authorized_keys
kvm -m 8G -smp 4 -vga virtio -cdrom contestant.iso -bios /usr/share/ovmf/OVMF.fd -nic user,model=virtio-net-pci,hostfwd=tcp:127.0.0.1:2222-:22
ssh -o "UserKnownHostsFile ./local.known_hosts" -p 2222 root@localhost
```
## Features
The configuration is split into layers, which are applied depending on the variant.
Here is a list of features.
- `participant` (all variants)
- various code editors and other tools
- VS Code extensions
- SOI header
- Code::Blocks template
- wallpaper
- default favorite apps
- default list of keyboard layouts
- timezone
- list of locales
- bootloader background image
- `training-live`
- disable lock on blank screen
- disable software update notifications
- automatic login
- sudo without password
- `training-installer`
- disable network detection to speed up install
- preseed most installer questions
- look up hostname in inventory file from serial number (inventory-hostname udeb)
- login screen logo
- login without password for `noauth` group
- create an admin user with sudo rights and password
- create a participant user without password
- install packages for firmware updates and power manager
- install Gnome Boxes and VirtualBox for running virtual machines
- `contestant`
- disable bluetooth
- disable sleep
- disable lock on blank screen
- disable software update notifications
- disable some panels in gnome-control-center
- disable automatic mounting of storage media
- polkit rules which block changing network settings and mounting storage media (it prompts for the root password)
- configure NTP
- install and configure ssh server
- set root password
- set `authorized_keys` for root
- automatic login
- set browser homepage and bookmarks to https://contest.soi.ch
- Gnome Shell extension which displays the user name in the top bar
- contest lock Gnome Shell extension
- some management scripts to be run via ssh
- some packages for contest admin