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

5
.gitignore vendored Normal file
View File

@ -0,0 +1,5 @@
/os/config/
/osbuild/
/contestops/certs/
/contestops/local.known_hosts

15
LICENSE.txt Normal file
View File

@ -0,0 +1,15 @@
The Gnome Shell extensions, which can be found under the path
/os/layers/contestant/includes.chroot/usr/share/gnome-shell/extensions,
are distributed under the terms of the GNU General Public License, version 2 or later.
See https://gitlab.gnome.org/GNOME/gnome-shell/-/blob/main/COPYING for the license text.
The rest of the repository is distributed under the MIT license, see below.
The MIT License (MIT)
Copyright © 2024 Jan Schär
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.

20
contestops/assign-user.sh Executable file
View File

@ -0,0 +1,20 @@
#!/bin/bash
machinename="$1"
username="$2"
machineusername=contestant
userline=$(grep "^$username;" contestants.csv)
if [ $? -ne 0 ]; then
echo "User $username not found"
exit 1
fi
fullname=$(echo "$userline" | cut "-d;" -f2)
# Set real name of machine user
ssh -F local.ssh_config "$machinename" chfn --full-name "\"$fullname\"" $machineusername
# Install client certificate
scp -F local.ssh_config "certs/$username.p12" "$machinename:/home/$machineusername/clientcert.p12"
ssh -F local.ssh_config "$machinename" install-client-cert $machineusername

25
contestops/backup-create.sh Executable file
View File

@ -0,0 +1,25 @@
#!/bin/bash
sleep_secs=120
do_backup() {
for host in $(cat hostlist); do
echo $host
target=backups/$host/$(date --iso-8601=seconds)
mkdir -p $target
rsync -e "ssh -F local.ssh_config" --recursive --links --perms --times --verbose --prune-empty-dirs --exclude ".*" --exclude "/snap" --exclude "Screenshot from *" --max-size 200K $host:/home/contestant/ $target
done
}
if [ "$1" == timer ]; then
while true; do
do_backup
echo
echo "Finished, next backup in $sleep_secs seconds."
echo
echo
sleep $sleep_secs
done
else
do_backup
fi

9
contestops/config-hosts Normal file
View File

@ -0,0 +1,9 @@
127.0.0.1 localhost
::1 localhost ip6-localhost ip6-loopback
fe00::0 ip6-localnet
ff00::0 ip6-mcastprefix
ff02::1 ip6-allnodes
ff02::2 ip6-allrouters
89.58.34.6 contest.soi.ch
2a03:4000:64:8::1 contest.soi.ch

View File

@ -0,0 +1,48 @@
#!/usr/sbin/nft -f
flush ruleset
table inet filter {
chain input {
type filter hook input priority 0;
ct state invalid drop
ct state { established, related } accept
# Accept loopback
iif lo accept
# Accept ICMP
ip protocol icmp accept
ip6 nexthdr icmpv6 accept
# Accept incoming connections to these ports
tcp dport { ssh } accept
reject
}
chain forward {
type filter hook forward priority 0;
reject
}
chain output {
type filter hook output priority 0;
ct state invalid drop
ct state { established, related } accept
# Accept loopback
oif lo accept
# Accept outgoing connections to these addresses
ip daddr { 89.58.34.6 } tcp dport { https } accept
ip daddr { 89.58.34.6 } udp dport { ntp } accept
ip6 daddr { 2a03:4000:64:8::1 } tcp dport { https } accept
ip6 daddr { 2a03:4000:64:8::1 } udp dport { ntp } accept
# Accept any connections by root user
#meta skuid root accept
reject
}
}

View File

@ -0,0 +1,22 @@
#!/bin/bash
set -ex
# Disable WiFi.
parallel-ssh -x "-F local.ssh_config" -h hostlist nmcli radio wifi off
# Create hosts file so we don't need DNS.
parallel-scp -x "-F local.ssh_config" -h hostlist ./config-hosts /etc/hosts
# Configure firewall.
parallel-scp -x "-F local.ssh_config" -h hostlist ./config-nftables.conf /etc/nftables.conf
parallel-ssh -x "-F local.ssh_config" -h hostlist systemctl enable nftables.service
# For some unknown reason nft gets stuck the first time it is run.
parallel-ssh -x "-F local.ssh_config" -h hostlist --par 30 systemctl start nftables.service
# Uncomment these lines if machines have 4K displays. This scales display to 2x.
# parallel-scp -x "-F local.ssh_config" -h hostlist ./set-display-scale.py /usr/local/bin/set-display-scale.py
# parallel-ssh -x "-F local.ssh_config" -h hostlist 'DBUS_SESSION_BUS_ADDRESS="unix:path=/run/user/$(id -u contestant)/bus" runuser -u contestant -- python3 /usr/local/bin/set-display-scale.py'
# Configure contest lock screen.
parallel-scp -x "-F local.ssh_config" -h hostlist ./contest-lock.json /etc/contest-lock.json

View File

@ -0,0 +1,5 @@
{
"title": "SOI Finals 2024 · Day 1",
"message": "",
"startTime": "2024-01-01T10:00:00+01:00"
}

View File

@ -0,0 +1,2 @@
stofl;Mouse Stofl
binna1;Mouse Binna
1 stofl Mouse Stofl
2 binna1 Mouse Binna

60
contestops/create-certs.sh Executable file
View File

@ -0,0 +1,60 @@
#!/usr/bin/env bash
# install cfssl
set -e
usernames=$(cat contestants.csv | cut "-d;" -f1)
mkdir -p certs
cd certs
cat <<EOF > ca.json
{
"CN": "SOI Contest Root CA",
"key": {
"algo": "rsa",
"size": 2048
}
}
EOF
if [ ! -f ca.pem ]; then
cfssl gencert -initca ca.json | cfssljson -bare ca
fi
cat <<EOF >client-config.json
{
"signing": {
"default": {
"expiry": "438000h"
},
"profiles": {
"client": {
"usages": ["signing", "key encipherment", "digital signature", "client auth"],
"expiry": "438000h"
}
}
}
}
EOF
for username in $usernames; do
cat <<EOF >client-csr-$username.json
{
"CN": "$username",
"key": {
"algo": "rsa",
"size": 2048
}
}
EOF
cfssl gencert -ca=ca.pem -ca-key=ca-key.pem -config=client-config.json -profile=client client-csr-$username.json | cfssljson --bare $username-cert
openssl pkcs12 -export -in $username-cert.pem -inkey $username-cert-key.pem -out $username.p12 -passout pass:
done

3
contestops/hostlist Normal file
View File

@ -0,0 +1,3 @@
contestant01
contestant02
contestant03

View File

@ -0,0 +1,202 @@
# ssh config for room INF3 at EPFL
Host contestant01
HostName 128.178.158.101
Host contestant02
HostName 128.178.158.102
Host contestant03
HostName 128.178.158.103
Host contestant04
HostName 128.178.158.104
Host contestant05
HostName 128.178.158.105
Host contestant06
HostName 128.178.158.106
Host contestant07
HostName 128.178.158.107
Host contestant08
HostName 128.178.158.108
Host contestant09
HostName 128.178.158.109
Host contestant10
HostName 128.178.158.110
Host contestant11
HostName 128.178.158.111
Host contestant12
HostName 128.178.158.112
Host contestant13
HostName 128.178.158.113
Host contestant14
HostName 128.178.158.114
Host contestant15
HostName 128.178.158.115
Host contestant16
HostName 128.178.158.116
Host contestant17
HostName 128.178.158.117
Host contestant18
HostName 128.178.158.118
Host contestant19
HostName 128.178.158.119
Host contestant20
HostName 128.178.158.120
Host contestant21
HostName 128.178.158.121
Host contestant22
HostName 128.178.158.122
Host contestant23
HostName 128.178.158.123
Host contestant24
HostName 128.178.158.124
Host contestant25
HostName 128.178.158.125
Host contestant26
HostName 128.178.158.126
Host contestant27
HostName 128.178.158.127
Host contestant28
HostName 128.178.158.128
Host contestant29
HostName 128.178.158.129
Host contestant30
HostName 128.178.158.130
Host contestant31
HostName 128.178.158.131
Host contestant32
HostName 128.178.158.132
Host contestant33
HostName 128.178.158.133
Host contestant34
HostName 128.178.158.134
Host contestant35
HostName 128.178.158.135
Host contestant36
HostName 128.178.158.136
Host contestant37
HostName 128.178.158.137
Host contestant38
HostName 128.178.158.138
Host contestant39
HostName 128.178.158.139
Host contestant40
HostName 128.178.158.140
Host contestant41
HostName 128.178.158.141
Host contestant42
HostName 128.178.158.142
Host contestant43
HostName 128.178.158.143
Host contestant44
HostName 128.178.158.144
Host contestant45
HostName 128.178.158.145
Host contestant46
HostName 128.178.158.146
Host contestant47
HostName 128.178.158.147
Host contestant48
HostName 128.178.158.148
Host contestant49
HostName 128.178.158.149
Host contestant50
HostName 128.178.158.150
Host contestant51
HostName 128.178.158.151
Host contestant52
HostName 128.178.158.152
Host contestant53
HostName 128.178.158.153
Host contestant54
HostName 128.178.158.154
Host contestant55
HostName 128.178.158.155
Host contestant56
HostName 128.178.158.156
Host contestant57
HostName 128.178.158.157
Host contestant58
HostName 128.178.158.158
Host contestant59
HostName 128.178.158.159
Host contestant60
HostName 128.178.158.160
Host contestant61
HostName 128.178.158.161
Host contestant62
HostName 128.178.158.162
Host contestant63
HostName 128.178.158.163
Host contestant64
HostName 128.178.158.164
Host vm
HostName localhost
Port 2222
Host *
User root
UserKnownHostsFile ./local.known_hosts
HashKnownHosts no

View File

@ -0,0 +1,17 @@
Host contestant01
HostName 10.42.0.101
Host contestant02
HostName 10.42.0.102
Host contestant03
HostName 10.42.0.103
Host vm
HostName localhost
Port 2222
Host *
User root
UserKnownHostsFile ./local.known_hosts
HashKnownHosts no

184
contestops/readme.md Normal file
View File

@ -0,0 +1,184 @@
# Contest ops
Here are instructions and various scripts and files for running contests.
The setup consists of a machine for each contestant, a machine running the grader, and an admin machine.
All these should be connected through a network, preferably wired.
The grader can be a machine accessible over the internet or in the local network.
## Grader setup
Install an ntp server on the grader machine.
This ensures that the contestant machine clocks are synchronized with the grader clock.
If a firewall is enabled, you may need to open the NTP port.
```bash
sudo apt install ntpsec
```
Configure the grader to accept client certificates.
The CA certificate (`certs/ca.pem`) is generated as part of the admin setup.
## Contestant machine setup
Obtain the contestant ISO, or build it yourself.
Flash the ISO to an USB stick.
All data on the stick will be lost.
For example, with the Gnome Disks utility, select the USB stick, open the menu on the right of the title bar, and click "Restore Disk Image...".
Boot the contestant machine from the USB stick.
Insert the stick and power on the machine.
Then repeatedly press a key to enter the boot menu (which key depends on the model, e.g. F12).
The boot menu may be password protected on machines in computer rooms; in that case you need to know the password.
The OS is loaded into RAM during boot, so you can remove the stick once the boot is finished and boot the next machine.
## Network setup
If there is not already an existing network, you need to set it up yourself.
Connect all contestant machines and the admin machine to a network switch with LAN cables.
If you use multiple switches, don't forget to also link the switches together.
If the grader must be accessed over the internet, you can connect the admin machine to WiFi or USB tethering with a phone.
You can then share the internet with the local network.
If you have Gnome, go to Network settings, click on the gear on the Ethernet connection, go to IPv4 tab, and select "Shared to other computers".
If you have docker installed, this doesn't work yet, because docker blocks routing.
You can fix it by running the following commands.
```bash
sudo iptables -I DOCKER-USER -i en+ -j ACCEPT
sudo iptables -I DOCKER-USER -o en+ -j ACCEPT
```
## Admin setup
This guide assumes that the admin machine is running Debian, Ubuntu or similar.
Invent a password for root on the machines.
Create a password hash for it with the following command.
Put the hash in the `contest_root_password` variable in `os/config/config.toml`.
This must be done before building the ISO.
```bash
sudo apt install whois
mkpasswd
```
Install parallel-ssh.
```bash
sudo apt install pssh
```
Edit `contestants.csv` and fill in the username and real name of each contestant.
Run the script to create a CA and client certificates.
```bash
sudo apt install golang-cfssl
./create-certs.sh
```
Edit `local.ssh_config` and create an entry with hostname and IP address for each contestant machine.
You can get the IP address by running `ip addr` in a terminal on the contestant machine.
Edit `hostlist` and add the hostnames of all contestant machines.
Get ssh host keys.
After rebooting machines, delete `local.known_hosts` and run this command again.
```bash
parallel-ssh -x "-F local.ssh_config" -h hostlist -O StrictHostKeyChecking=accept-new true
```
Test time synchronization.
```bash
parallel-ssh -x "-F local.ssh_config" -h hostlist -i date
```
Edit `config-hosts` and `config-nftables.conf` to fill in the correct IP addresses for the grader.
You can look these up with `host contest.soi.ch`.
Edit `contest-lock.json` to fill in the title and start time of the contest.
Apply the configuration to machines.
If the script gets stuck, press Ctrl+C and run it again.
```bash
./configure-machines.sh
```
Assign users to machines.
```bash
./assign-user.sh contestant01 stofl
./assign-user.sh contestant02 binna1
```
Start periodic backup of contestant machines.
```bash
./backup-create.sh timer
```
## Restore machine from backup
Because machines run from RAM, they will lose all files after rebooting.
Therefore, backups are especially important.
To restore a backup to a spare machine, use the following commands.
Prepare in advance by keeping the user to machine assignment nearby for reference, and
replacing `contestant03` in the commands below with the spare machine hostname.
```bash
./assign-user.sh contestant03 <username>
rsync -e "ssh -F local.ssh_config" -av --chown contestant:contestant backups/contestantxx/xxxx/ contestant03:/home/contestant/
```
## Contest lock screen
The contest lock screen is a gnome extension which can lock the screen and show a countdown until the contest starts.
The screen is unlocked when the contest starts.
The lock screen also displays the user name and a title.
It is configured in the file `/etc/contest-lock.json`.
It watches this file, and when it changes the new configuration is instantly applied.
If there is an error in the config file, it will continue to use the old config and print a message.
To see the logs, run this on a contestant machine:
```bash
journalctl -f -o cat /usr/bin/gnome-shell
```
An additional text can be shown with the `message` field. It can contain newlines (`\n`).
In case there is a problem with the contest lock screen and you can't fix it, the backup solution is to turn off `AutomaticLoginEnable` and set a password instead, that you announce when the contest starts.
```bash
parallel-ssh -x "-F local.ssh_config" -h hostlist 'chpasswd <<< contestant:stofl'
```
**Development notes**
Links:
- https://www.codeproject.com/Articles/5271677/How-to-Create-A-GNOME-Extension
- https://gjs.guide/
Regular lock screen (contest-lock is based on this):
- https://gitlab.gnome.org/GNOME/gnome-shell/-/blob/main/js/ui/screenShield.js
- https://gitlab.gnome.org/GNOME/gnome-shell/-/blob/main/js/ui/unlockDialog.js
Developer commands:
- Open the gnome-shell developer tools: Press Alt+F2, enter `lg`.
## Problems and solutions
Here are solutions to recurring problems.
**User indicator does not appear.**
Fixed by adding the gnome shell version from `gnome-shell --version` to the list of supported versions: `shell-version` in `os/layers/contestant/includes.chroot/usr/share/gnome-shell/extensions/user-indicator@soi.ch/metadata.json`.
The same applies for the contest-lock extension.

View File

@ -0,0 +1,37 @@
#!/usr/bin/python3
# This script sets the display scale factor to 2, for use on 4K displays where everything is too small without scaling.
import dbus
new_scale = 2.0
# https://gitlab.gnome.org/GNOME/mutter/-/blob/main/data/dbus-interfaces/org.gnome.Mutter.DisplayConfig.xml
bus_name = "org.gnome.Mutter.DisplayConfig"
object_path = "/org/gnome/Mutter/DisplayConfig"
session_bus = dbus.SessionBus()
display_config_object = session_bus.get_object(bus_name, object_path)
display_config_intf = dbus.Interface(display_config_object, dbus_interface=bus_name)
serial, physical_monitors, logical_monitors, properties = display_config_intf.GetCurrentState()
current_mode_id = {}
for ((connector, _, _, _), modes, monitor_properties) in physical_monitors:
for (mode_id, width, height, rate, preferred_scale, supported_scales, mode_properties) in modes:
if mode_properties.get("is-current", False):
current_mode_id[connector] = mode_id
scaled_logical_monitors = [
(layout_x, layout_y, new_scale, transform, primary, [
(connector, current_mode_id[connector], {}) for (connector, _, _, _) in monitors
])
for (layout_x, layout_y, scale, transform, primary, monitors, monitor_properties) in logical_monitors
]
apply_properties = {}
if "layout-mode" in properties and properties.get("supports-changing-layout-mode", False):
apply_properties["layout-mode"] = properties["layout-mode"]
method = 1 # temporary
display_config_intf.ApplyMonitorsConfig(serial, method, scaled_logical_monitors, apply_properties)

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

5
readme.md Normal file
View File

@ -0,0 +1,5 @@
# SOI OS
Folder `os` contains the scripts and files for building custom OS images based on Debian for training and contests.
Folder `contestops` contains various scripts and files for administrating contests where contestants use this OS.