├── files ├── secrets │ └── .gitignore ├── .gitignore ├── wallpaper.png ├── scripts │ ├── enableFirewall.sh │ ├── disableFirewall.sh │ ├── reset-team-account │ ├── checkFirewall.sh │ ├── vmtouch.sh │ ├── deleteUser.sh │ ├── full_reset.sh │ ├── firstLogin.sh │ ├── makeDist.sh │ ├── version_check.sh │ ├── pcpr │ ├── self_test │ ├── whiptail.py │ └── icpc_setup ├── 99-deny-polkit-mount.pkla ├── wg-setup.service.j2 ├── ansible-pull.path ├── pam_environment ├── lightdm.conf ├── pam_sudo ├── teamreset.desktop ├── language-docs.html.j2 ├── xorg.conf ├── warm-fs-cache.service ├── 01-netcfg.yaml ├── firstboot.service ├── management-server │ ├── dsnet.service │ ├── dsnetconfig.json.j2 │ ├── coredns.service.j2 │ ├── register_wireguard_client │ ├── nginx.conf │ ├── wg-discover │ └── do-screenshots.py ├── autossh.service.j2 ├── ansible-pull.service.j2 ├── icpc-metrics ├── systemd-journald.conf ├── wg_setup.j2 ├── systemd-system.conf ├── squid │ ├── squid.conf.j2 │ └── block.html.j2 └── nginx.conf.j2 ├── tmp └── .gitignore ├── configs ├── 2004_metadata ├── 2204_metadata ├── imageadmin-ssh_key.pub ├── imageadmin-ssh_key ├── 2004_autoinstall.yaml └── 2204_autoinstall.yaml ├── output └── .gitignore ├── .gitignore ├── secrets ├── .gitignore └── gen-secrets.sh ├── playbooks ├── languages │ ├── elixir.yml │ ├── c.yml │ ├── d.yml │ ├── lua.yml │ ├── ruby.yml │ ├── rust.yml │ ├── f-sharp.yml │ ├── gnu_ada.yml │ ├── js.yml │ ├── nim.yml │ ├── clojure.yml │ ├── fortran.yml │ ├── erlang.yml │ ├── go.yml │ ├── groovy.yml │ ├── ocaml.yml │ ├── obj-c.yml │ ├── prolog.yml │ ├── c-sharp.yml │ ├── r.yml │ ├── dart.yml │ ├── pascal.yml │ ├── haskell.yml │ ├── scala.yml │ ├── python2.yml │ ├── java.yml │ ├── python3.yml │ ├── kotlin.yml │ └── cpp.yml ├── devel_tools │ ├── netbeans.yml │ ├── vscode.yml │ ├── intellij.yml │ └── eclipse.yml ├── vmtouch.yml ├── ansible-pull.yml ├── reversetunnel.yml ├── reverseproxy.yml ├── system.yml ├── vpn.yml ├── firewall.yml ├── devel_tools.yml ├── compilers.yml ├── gui.yml ├── monitoring.yml └── icpc.yml ├── efi ├── OVMF_CODE.fd └── OVMF_VARS.fd ├── fetch-secrets.sh ├── test_final.sh ├── test_final_efi.sh ├── jumphost.md ├── create_baseimg.sh ├── main.yml ├── runvm.sh ├── group_vars └── all.dist ├── readme.md └── management-server.yml /files/secrets/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | -------------------------------------------------------------------------------- /tmp/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | -------------------------------------------------------------------------------- /configs/2004_metadata: -------------------------------------------------------------------------------- 1 | hostname: icpc 2 | -------------------------------------------------------------------------------- /configs/2204_metadata: -------------------------------------------------------------------------------- 1 | hostname: icpc 2 | -------------------------------------------------------------------------------- /files/.gitignore: -------------------------------------------------------------------------------- 1 | *.tar.gz 2 | *.zip 3 | -------------------------------------------------------------------------------- /output/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.iso 2 | configs/ssh* 3 | group_vars/all 4 | -------------------------------------------------------------------------------- /secrets/.gitignore: -------------------------------------------------------------------------------- 1 | *.pub 2 | *@* 3 | *_key 4 | *_ca 5 | -------------------------------------------------------------------------------- /playbooks/languages/elixir.yml: -------------------------------------------------------------------------------- 1 | # https://elixir-lang.org/install.html#gnulinux 2 | -------------------------------------------------------------------------------- /efi/OVMF_CODE.fd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/icpc-environment/icpc-env/HEAD/efi/OVMF_CODE.fd -------------------------------------------------------------------------------- /efi/OVMF_VARS.fd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/icpc-environment/icpc-env/HEAD/efi/OVMF_VARS.fd -------------------------------------------------------------------------------- /files/wallpaper.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/icpc-environment/icpc-env/HEAD/files/wallpaper.png -------------------------------------------------------------------------------- /files/scripts/enableFirewall.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | ufw enable 3 | zenity --info --text="Firewall enabled." 4 | -------------------------------------------------------------------------------- /playbooks/languages/c.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: c 3 | apt: 4 | state: present 5 | pkg: 6 | - gcc 7 | -------------------------------------------------------------------------------- /playbooks/languages/d.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: D 3 | apt: 4 | state: present 5 | pkg: 6 | - gdc 7 | -------------------------------------------------------------------------------- /playbooks/languages/lua.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: lua 3 | apt: 4 | state: present 5 | pkg: 6 | - lua5.3 7 | -------------------------------------------------------------------------------- /playbooks/languages/ruby.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: ruby 3 | apt: 4 | state: present 5 | pkg: 6 | - ruby 7 | -------------------------------------------------------------------------------- /playbooks/languages/rust.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: rust 3 | apt: 4 | state: present 5 | pkg: 6 | - rustc 7 | -------------------------------------------------------------------------------- /playbooks/languages/f-sharp.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: F# 3 | apt: 4 | state: present 5 | pkg: 6 | - fsharp 7 | -------------------------------------------------------------------------------- /playbooks/languages/gnu_ada.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: GNU Ada 3 | apt: 4 | state: present 5 | pkg: 6 | - gnat 7 | -------------------------------------------------------------------------------- /playbooks/languages/js.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: javascript 3 | apt: 4 | state: present 5 | pkg: 6 | - nodejs 7 | -------------------------------------------------------------------------------- /playbooks/languages/nim.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Nim (~12mb) 3 | apt: 4 | state: present 5 | pkg: 6 | - nim 7 | -------------------------------------------------------------------------------- /playbooks/languages/clojure.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Clojure 3 | apt: 4 | state: present 5 | pkg: 6 | - clojure 7 | -------------------------------------------------------------------------------- /playbooks/languages/fortran.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Fortran 3 | apt: 4 | state: present 5 | pkg: 6 | - gfortran 7 | -------------------------------------------------------------------------------- /playbooks/languages/erlang.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Erlang (~115mb) 3 | apt: 4 | state: present 5 | pkg: 6 | - erlang 7 | -------------------------------------------------------------------------------- /playbooks/languages/go.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: go 3 | apt: 4 | state: present 5 | pkg: 6 | - gccgo 7 | - golang 8 | -------------------------------------------------------------------------------- /playbooks/languages/groovy.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Groovy (~45mb) 3 | apt: 4 | state: present 5 | pkg: 6 | - groovy 7 | -------------------------------------------------------------------------------- /playbooks/languages/ocaml.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: OCaml (~220mb) 3 | apt: 4 | state: present 5 | pkg: 6 | - ocaml-nox 7 | -------------------------------------------------------------------------------- /configs/imageadmin-ssh_key.pub: -------------------------------------------------------------------------------- 1 | ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIB9pjASmP4wQkhJ1VEbl0l1Vgn3lsOzctRS2m0wBVlaO ICPC ImageAdmin Key 2 | -------------------------------------------------------------------------------- /playbooks/languages/obj-c.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Objective-C (~25mb) 3 | apt: 4 | state: present 5 | pkg: 6 | - gobjc 7 | -------------------------------------------------------------------------------- /playbooks/languages/prolog.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: SWI Prolog (~35mb) 3 | apt: 4 | state: present 5 | pkg: 6 | - swi-prolog 7 | -------------------------------------------------------------------------------- /playbooks/languages/c-sharp.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: c# 3 | apt: 4 | state: present 5 | pkg: 6 | - mono-complete 7 | - monodoc-browser # 80mb 8 | -------------------------------------------------------------------------------- /files/scripts/disableFirewall.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | ufw disable 3 | zenity --warning --text="Firewall disabled. Please remember to re-enable it before the contest" 4 | -------------------------------------------------------------------------------- /playbooks/languages/r.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: R project for statistical computing (~88mb) 3 | apt: 4 | state: present 5 | pkg: 6 | - r-base 7 | - r-base-dev 8 | -------------------------------------------------------------------------------- /files/99-deny-polkit-mount.pkla: -------------------------------------------------------------------------------- 1 | [Block access to udisks for contestant] 2 | Identity=unix-user:contestant 3 | Action=org.freedesktop.udisks2.* 4 | ResultAny=no 5 | ResultInactive=no 6 | ResultActive=no 7 | -------------------------------------------------------------------------------- /files/scripts/reset-team-account: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # Used to reset the machines between the practice round and actual contest 3 | /icpc/scripts/deleteUser.sh 4 | 5 | echo "Done. Rebooting..." 6 | sleep 3 7 | reboot now 8 | -------------------------------------------------------------------------------- /playbooks/devel_tools/netbeans.yml: -------------------------------------------------------------------------------- 1 | # Install netbeans debian package 2 | - name: Install netbeans debian package 3 | apt: 4 | deb: https://dlcdn.apache.org/netbeans/netbeans-installers/20/apache-netbeans_20-1_all.deb 5 | -------------------------------------------------------------------------------- /files/wg-setup.service.j2: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=Wireguard Setup 3 | After=network.target 4 | 5 | [Service] 6 | ExecStart=/usr/local/bin/wg_setup 7 | Restart=always 8 | RestartSec=30 9 | 10 | [Install] 11 | WantedBy=multi-user.target 12 | -------------------------------------------------------------------------------- /files/scripts/checkFirewall.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | sudo ufw status | grep 'Status: active' >/dev/null 2>&1 4 | RET=$? 5 | if [[ $RET == 1 ]];then 6 | zenity --warning --text="Firewall disabled. Please remember to re-enable it before the contest" 7 | fi 8 | -------------------------------------------------------------------------------- /files/ansible-pull.path: -------------------------------------------------------------------------------- 1 | # trigger ansible-pull service if the file /icpc/trigger-update is modified 2 | # ansible should delete the file when it runs successfully 3 | [Path] 4 | PathExists=/icpc/trigger-ansible 5 | 6 | [Install] 7 | WantedBy=multi-user.target 8 | -------------------------------------------------------------------------------- /files/pam_environment: -------------------------------------------------------------------------------- 1 | http_proxy OVERRIDE="" 2 | https_proxy OVERRIDE="" 3 | ftp_proxy OVERRIDE="" 4 | no_proxy OVERRIDE="" 5 | HTTP_PROXY OVERRIDE="" 6 | HTTPS_PROXY OVERRIDE="" 7 | FTP_PROXY OVERRIDE="" 8 | NO_PROXY OVERRIDE="" 9 | -------------------------------------------------------------------------------- /files/lightdm.conf: -------------------------------------------------------------------------------- 1 | [SeatDefaults] 2 | greeter-session=lightdm-gtk-greeter 3 | 4 | # Set xfce as the default 5 | user-session=xubuntu 6 | 7 | # Autologin 8 | autologin-user=contestant 9 | autologin-timeout=0 10 | 11 | # Disable guest session 12 | allow-guest=false 13 | -------------------------------------------------------------------------------- /files/pam_sudo: -------------------------------------------------------------------------------- 1 | #%PAM-1.0 2 | 3 | session required pam_env.so readenv=1 user_readenv=1 4 | session required pam_env.so readenv=1 envfile=/etc/default/locale user_readenv=1 5 | @include common-auth 6 | @include common-account 7 | @include common-session-noninteractive 8 | -------------------------------------------------------------------------------- /files/teamreset.desktop: -------------------------------------------------------------------------------- 1 | [Desktop Entry] 2 | Version=1.0 3 | Type=Application 4 | Name=Reset Team Account 5 | Comment= 6 | Exec=xfce4-terminal --execute sudo /icpc/scripts/reset-team-account 7 | Icon=edit-clear 8 | Path= 9 | Terminal=true 10 | StartupNotify=false 11 | GenericName= 12 | -------------------------------------------------------------------------------- /files/language-docs.html.j2: -------------------------------------------------------------------------------- 1 | 2 | 3 | Language Documentation 4 | 5 | 6 | {% for k,v in lang_docs.items() %} 7 |

{{ k }}

8 | {{v['name']}} 9 | {% endfor %} 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /files/xorg.conf: -------------------------------------------------------------------------------- 1 | # Disable screen blanking/display power management(this will keep the display on all the time) 2 | Section "ServerFlags" 3 | Option "DPMS" "false" 4 | Option "BlankTime" "0" 5 | Option "StandbyTime" "0" 6 | Option "SuspendTime" "0" 7 | Option "OffTime" "0" 8 | EndSection 9 | -------------------------------------------------------------------------------- /files/warm-fs-cache.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=Warm up file system cache 3 | 4 | [Service] 5 | Type=oneshot 6 | ExecStart=/usr/bin/nice -n 19 /usr/bin/find / -path /proc -prune -o -path /sys -prune -o -print 7 | ExecStart=/usr/bin/nice -n 19 /icpc/scripts/vmtouch.sh 8 | 9 | [Install] 10 | WantedBy=multi-user.target 11 | -------------------------------------------------------------------------------- /playbooks/vmtouch.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: install vmtouch 3 | apt: pkg=vmtouch state=present 4 | 5 | - name: add init script so this runs on boot 6 | copy: src=files/warm-fs-cache.service dest=/etc/systemd/system/warm-fs-cache.service 7 | 8 | - name: enable warm-fs-cache service 9 | service: name=warm-fs-cache enabled=yes 10 | -------------------------------------------------------------------------------- /files/01-netcfg.yaml: -------------------------------------------------------------------------------- 1 | # Installed by ansible 2 | # This file describes the network interfaces available on your system 3 | # For more information, see netplan(5). 4 | network: 5 | version: 2 6 | renderer: networkd 7 | ethernets: 8 | default: 9 | match: 10 | name: e* 11 | dhcp4: yes 12 | dhcp-identifier: mac 13 | -------------------------------------------------------------------------------- /playbooks/languages/dart.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: install dart repo key 3 | apt_key: 4 | url: https://dl-ssl.google.com/linux/linux_signing_key.pub 5 | id: 7FAC5991 6 | - name: install dart repo 7 | apt_repository: 8 | repo: "deb [arch=amd64] https://storage.googleapis.com/download.dartlang.org/linux/debian stable main" 9 | state: present 10 | filename: 'google-dart' 11 | - name: install dart 12 | apt: pkg=dart state=present 13 | -------------------------------------------------------------------------------- /configs/imageadmin-ssh_key: -------------------------------------------------------------------------------- 1 | -----BEGIN OPENSSH PRIVATE KEY----- 2 | b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW 3 | QyNTUxOQAAACAfaYwEpj+MEJISdVRG5dJdVYJ95bDs3LUUtptMAVZWjgAAAJg+fZT1Pn2U 4 | 9QAAAAtzc2gtZWQyNTUxOQAAACAfaYwEpj+MEJISdVRG5dJdVYJ95bDs3LUUtptMAVZWjg 5 | AAAEBje6TLZOpsHjjm4HuXJjZcaZF5kyKQz9ieGnui13B0iB9pjASmP4wQkhJ1VEbl0l1V 6 | gn3lsOzctRS2m0wBVlaOAAAAFHViZXJnZWVrQGtqLWxpbnV4LXBjAQ== 7 | -----END OPENSSH PRIVATE KEY----- 8 | -------------------------------------------------------------------------------- /files/firstboot.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=Initial configuration of the image 3 | Before=lightdm.service 4 | After=cups.service getty@tty2.service 5 | 6 | [Service] 7 | Type=oneshot 8 | RemainAfterExit=yes 9 | TimeoutSec=0 10 | TTYReset=yes 11 | TTYVHangup=yes 12 | TTYPath=/dev/tty2 13 | StandardInput=tty 14 | StandardOutput=tty 15 | ExecStart=/bin/bash -c '/bin/chvt 2; /icpc/scripts/icpc_setup' 16 | 17 | [Install] 18 | WantedBy=graphical.target 19 | WantedBy=multi-user.target 20 | -------------------------------------------------------------------------------- /files/management-server/dsnet.service: -------------------------------------------------------------------------------- 1 | # Copy this service file to /etc/systemd/system/ to start dsnet on boot, 2 | # assuming dsnet is installed to /usr/local/bin 3 | [Unit] 4 | Description=dsnet 5 | After=network-online.target 6 | Wants=network-online.target 7 | 8 | [Service] 9 | Type=oneshot 10 | ExecStart=/usr/local/bin/dsnet up 11 | ExecStop=/usr/local/bin/dsnet down 12 | RemainAfterExit=yes 13 | ExecReload=/usr/local/bin/dsnet sync 14 | 15 | [Install] 16 | WantedBy=default.target 17 | -------------------------------------------------------------------------------- /files/autossh.service.j2: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=AutoSSH tunnel service for remote control 3 | After=network.target 4 | 5 | [Service] 6 | User=jumpy 7 | Environment="AUTOSSH_GATETIME=0" 8 | ExecStart=/usr/bin/autossh -M 0 -o "ServerAliveInterval 30" -o "ServerAliveCountMax 3" -o "StrictHostKeyChecking no" -o "UserKnownHostsFile /dev/null" -NR 0:localhost:22 jumpy@{{ jumpbox_host }} -p 443 -i /home/jumpy/.ssh/id_ed25519 9 | Restart=always 10 | RestartSec=5 11 | 12 | [Install] 13 | WantedBy=multi-user.target 14 | -------------------------------------------------------------------------------- /files/ansible-pull.service.j2: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=Execute ansible-pull to do any last minute changes to the system 3 | After=network.target 4 | 5 | [Service] 6 | Restart=on-failure 7 | RestartSec=30 8 | Type=oneshot 9 | ExecStart=/usr/bin/ansible-pull \ 10 | --private-key /root/.ssh/id_ed25519 \ 11 | --directory /root/ansible-pull \ 12 | --sleep 30 \ 13 | --clean \ 14 | --diff \ 15 | --inventory localhost, \ 16 | --url ssh://git@{{ ansible_pull_host }}:{{ ansible_pull_port }}/{{ ansible_pull_path }} 17 | 18 | [Install] 19 | WantedBy=multi-user.target 20 | -------------------------------------------------------------------------------- /files/management-server/dsnetconfig.json.j2: -------------------------------------------------------------------------------- 1 | { 2 | "ExternalHostname": "{{ wg_vpn_server_external_hostname }}", 3 | "ExternalIP": "{{ wg_vpn_server_external_ip }}", 4 | "ExternalIP6": "", 5 | "ListenPort": {{ wg_vpn_server_wg_port }}, 6 | "Domain": "icpcnet.internal", 7 | "InterfaceName": "contest", 8 | "Network6": "{{ wg_vpn_server_subnet }}", 9 | "IP6": "{{ contestmanager_ip }}", 10 | "DNS": "", 11 | "Networks": [], 12 | "ReportFile": "/var/lib/dsnetreport.json", 13 | "PrivateKey": "{{ wg_vpn_server_private_key }}", 14 | "PostUp": "", 15 | "PostDown": "", 16 | "Peers": [] 17 | } 18 | -------------------------------------------------------------------------------- /files/scripts/vmtouch.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # Load some useful things into the kernels cache 3 | VMTOUCH="/usr/bin/vmtouch -t" 4 | 5 | $VMTOUCH /opt/eclipse # 251M 6 | $VMTOUCH /usr/lib/jvm # 312M 7 | $VMTOUCH /usr/include # 33M 8 | 9 | 10 | # If we have more than 5 Gb of memory, cache some other stuff 11 | phymem=$(awk '/MemTotal/{print $2}' /proc/meminfo) 12 | if [ "$phymem" -gt "5000000" ]; then 13 | $VMTOUCH /lib # 229M 14 | $VMTOUCH /usr/lib # 2G 15 | $VMTOUCH /opt/intellij-idea-community # 968M 16 | fi 17 | 18 | # Do these last to make sure they end up in memory 19 | $VMTOUCH -f /icpc # 250K 20 | $VMTOUCH -f /bin # 12M 21 | $VMTOUCH -f /usr/bin # 365M 22 | -------------------------------------------------------------------------------- /playbooks/languages/pascal.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Pascal 3 | apt: 4 | state: present 5 | pkg: 6 | - fpc 7 | 8 | - name: pascal docs 9 | when: not devdocs 10 | block: 11 | - name: Pascal docs 12 | apt: 13 | state: present 14 | pkg: 15 | - fp-docs # 32mb 16 | - ansible.utils.update_fact: 17 | updates: 18 | - path: "lang_docs['Pascal']" 19 | value: 20 | name: FPC Documentation 21 | id: pascal 22 | path: /usr/share/doc/fp-docs/3.0.4 23 | index: fpctoc.html 24 | register: updated 25 | - set_fact: 26 | lang_docs: "{{ updated.lang_docs }}" 27 | -------------------------------------------------------------------------------- /files/scripts/deleteUser.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Kill all the user processes(and wait for them to die) 4 | killall -9 -u contestant 5 | sleep 5 6 | 7 | DISK=$(blkid -L "ICPC") 8 | if [[ $? == 0 ]]; then 9 | echo "Wiping FAT32 Partition" 10 | umount $DISK 11 | mkfs.vfat $DISK -n ICPC 12 | fi 13 | 14 | # Deletes all files owned by the contestant user, then deletes and recreates the account. 15 | echo "Deleting team files" 16 | find / -user contestant -delete 17 | 18 | echo "Deleting contestant user" 19 | userdel contestant 20 | rm -rf /home/contestant 21 | 22 | echo "Recreating contestant user" 23 | useradd -d /home/contestant -m contestant -G lpadmin -s /bin/bash 24 | passwd -d contestant 25 | -------------------------------------------------------------------------------- /playbooks/languages/haskell.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Haskell 3 | apt: 4 | state: present 5 | pkg: 6 | - ghc 7 | 8 | - name: haskell docs 9 | when: not devdocs 10 | block: 11 | - name: Haskell docs 12 | apt: 13 | state: present 14 | pkg: 15 | - ghc-doc # 146 mb 16 | - ansible.utils.update_fact: 17 | updates: 18 | - path: "lang_docs['Haskell']" 19 | value: 20 | name: Haskell Documentation 21 | id: haskell 22 | path: /usr/share/doc/ghc-doc 23 | index: index.html 24 | register: updated 25 | - set_fact: 26 | lang_docs: "{{ updated.lang_docs }}" 27 | -------------------------------------------------------------------------------- /playbooks/languages/scala.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: scala 3 | apt: 4 | state: present 5 | pkg: 6 | - scala 7 | 8 | - name: scala docs 9 | when: not devdocs 10 | block: 11 | - name: scala 12 | apt: 13 | state: present 14 | pkg: 15 | - scala-doc # 413mb somehow 16 | - ansible.utils.update_fact: 17 | updates: 18 | - path: "lang_docs['Scala']" 19 | value: 20 | name: Scala Documentation 21 | id: scala 22 | path: /usr/share/doc/scala-2.11/api/library 23 | index: index.html 24 | register: updated 25 | - set_fact: 26 | lang_docs: "{{ updated.lang_docs }}" 27 | -------------------------------------------------------------------------------- /files/management-server/coredns.service.j2: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=CoreDNS 3 | Documentation=https://coredns.io 4 | After=network-online.target 5 | StartLimitInterval=0 6 | 7 | [Service] 8 | Type=simple 9 | PermissionsStartOnly=true 10 | LimitNOFILE=1048576 11 | LimitNPROC=512 12 | CapabilityBoundingSet=CAP_NET_BIND_SERVICE 13 | AmbientCapabilities=CAP_NET_BIND_SERVICE 14 | NoNewPrivileges=true 15 | WorkingDirectory=/etc/coredns 16 | 17 | User=coredns 18 | Group=coredns 19 | ExecStart=/usr/local/bin/coredns \ 20 | -conf /etc/coredns/Corefile 21 | 22 | SyslogIdentifier=coredns 23 | ExecReload=/bin/kill -SIGUSR1 $MAINPID 24 | Restart=always 25 | RestartSec=0 26 | 27 | [Install] 28 | WantedBy=multi-user.target 29 | -------------------------------------------------------------------------------- /files/icpc-metrics: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # make site/team information available 4 | # info on how to use role: https://www.robustperception.io/how-to-have-labels-for-machine-roles 5 | echo "icpc_workstation_info{site=\"$(cat /icpc/SITE)\", team=\"$(cat /icpc/TEAMID)\", affiliation=\"$(cat /icpc/TEAMAFFILIATION)\", name=\"$(cat /icpc/TEAMNAME)\"} 1" | sponge /var/lib/prometheus/node-exporter/icpc_workstation_info.prom 6 | 7 | # information about when the image was built/last updated 8 | echo icpc_build_ts $(stat --printf=%Y /icpc/version) | sponge /var/lib/prometheus/node-exporter/icpc_build_ts.prom 9 | echo icpc_update_ts $(stat --printf=%Y /icpc/update-version) | sponge /var/lib/prometheus/node-exporter/icpc_update_ts.prom 10 | -------------------------------------------------------------------------------- /files/scripts/full_reset.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Remove the team wallpaper 4 | rm -f /icpc/teamWallpaper.png 5 | 6 | # Delete the printers/printer class 7 | for PRINTER in $(lpstat -v | cut -d ' ' -f 3 | tr -d ':') 8 | do 9 | lpadmin -x $PRINTER 10 | done 11 | lpadmin -x ContestPrinter 12 | 13 | # clear the user 14 | /icpc/scripts/deleteUser.sh 15 | 16 | # make sure the firewall is on 17 | ufw --force enable 18 | 19 | rm -f /icpc/setup-complete 20 | rm -f /icpc/TEAM* 21 | rm -f /icpc/SITE 22 | 23 | # reset squid autologin 24 | echo "# Placeholder" > /etc/squid/autologin.conf 25 | chmod 640 /etc/squid/autologin.conf 26 | chown root:root /etc/squid/autologin.conf 27 | 28 | # clear self test report 29 | rm -f /icpc/self_test_report 30 | -------------------------------------------------------------------------------- /playbooks/ansible-pull.yml: -------------------------------------------------------------------------------- 1 | --- 2 | # install ansible, configure ansible-pull to run on boot 3 | - name: install ansible (for ansible-pull) 4 | apt: pkg=ansible state=present 5 | 6 | - name: copy ansible-pull.service 7 | template: 8 | src: files/ansible-pull.service.j2 9 | dest: /etc/systemd/system/ansible-pull.service 10 | 11 | - name: enable ansible-pull.service 12 | service: name=ansible-pull.service enabled=yes 13 | 14 | - name: add ansible-pull.path (triggers on /icpc/trigger-ansible existing) 15 | copy: 16 | src: files/ansible-pull.path 17 | dest: /etc/systemd/system/ansible-pull.path 18 | 19 | - name: enable ansible-pull.path (triggers on /icpc/trigger-ansible existing) 20 | service: name=ansible-pull.path enabled=yes state=started 21 | -------------------------------------------------------------------------------- /playbooks/languages/python2.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Python 2 3 | apt: 4 | state: present 5 | pkg: 6 | - python 7 | - pypy 8 | - python2-doc # 50 mb 9 | 10 | - name: python2 docs 11 | when: not devdocs 12 | block: 13 | - name: Python 2 docs 14 | apt: 15 | state: present 16 | pkg: 17 | - python2-doc # 50 mb 18 | - ansible.utils.update_fact: 19 | updates: 20 | - path: "lang_docs['Python 2']" 21 | value: 22 | name: Python 2 Documentation 23 | id: py2 24 | path: /usr/share/doc/python2-doc/html 25 | index: index.html 26 | register: updated 27 | - set_fact: 28 | lang_docs: "{{ updated.lang_docs }}" 29 | -------------------------------------------------------------------------------- /playbooks/languages/java.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: java openjdk 3 | apt: 4 | state: present 5 | pkg: 6 | - openjdk-11-jdk 7 | - openjdk-11-source # 60 mb 8 | 9 | - name: register docs 10 | when: not devdocs 11 | block: 12 | - name: java openjdk docs 13 | apt: 14 | state: present 15 | pkg: 16 | - openjdk-11-doc # 246 mb 17 | - ansible.utils.update_fact: 18 | updates: 19 | - path: "lang_docs['Java']" 20 | value: 21 | name: Java API Documentation 22 | id: java_api 23 | path: /usr/share/doc/openjdk-11-doc/api 24 | index: index.html 25 | register: updated 26 | - set_fact: 27 | lang_docs: "{{ updated.lang_docs }}" 28 | -------------------------------------------------------------------------------- /files/management-server/register_wireguard_client: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | import os, re, secrets, subprocess, sys 3 | from subprocess import Popen, PIPE 4 | 5 | # random hostname/peer entry 6 | client_ip = os.environ.get('SSH_CLIENT').split()[0] 7 | hostname=f"{secrets.token_hex(16)}" # 128bit; if it's good enough for ipv6, it's good enough for me 8 | 9 | proc = subprocess.Popen([ 10 | '/usr/local/bin/dsnet', 'add', 11 | '--confirm', hostname, 12 | '--owner', client_ip, 13 | '--description', f"{hostname} from {client_ip}" 14 | ], stdout=PIPE, stderr=PIPE) 15 | try: 16 | out, err = proc.communicate(timeout=10) 17 | except subprocess.TimeoutExpired: 18 | proc.kill() 19 | out, err = proc.communicate() 20 | 21 | if proc.returncode != 0: 22 | raise SystemExit(f"dsnet add failed: {out.decode('utf8')}\n{err.decode('utf-8')}") 23 | 24 | print(err.decode('utf-8'), file=sys.stderr) 25 | print(out.decode('utf-8'), file=sys.stdout) 26 | -------------------------------------------------------------------------------- /fetch-secrets.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -euo pipefail 3 | usage() { 4 | echo "usage: fetch-secrets.sh " 5 | exit 1 6 | } 7 | [ $# -eq 1 ] || usage 8 | 9 | mkdir -p files/secrets 10 | 11 | # copy jumpy public key 12 | cp secrets/$1/jumpy@icpc.pub files/secrets/ 13 | # copy jumpy user private key + ca certificate signature 14 | cp secrets/$1/jumpy@icpc{,-cert.pub} files/secrets/ 15 | 16 | # copy icpcadmin@contestmanager public key 17 | cp secrets/$1/icpcadmin@contestmanager.pub files/secrets/ 18 | # and private key + ca certificate signature 19 | cp secrets/$1/icpcadmin@contestmanager{,-cert.pub} files/secrets/ 20 | 21 | # copy public+private host key for the contestant machines (and ca certificate signature) 22 | cp secrets/$1/contestant.icpcnet.internal_host_ed25519_key{,.pub,-cert.pub} files/secrets/ 23 | 24 | # copy public+private host key for the management machine (and ca certificate signature) 25 | cp secrets/$1/contestmanager.icpcnet.internal_host_ed25519_key{,.pub,-cert.pub} files/secrets/ 26 | 27 | # copy public ca 28 | cp secrets/$1/server_ca.pub files/secrets/ 29 | echo "done!" 30 | -------------------------------------------------------------------------------- /files/systemd-journald.conf: -------------------------------------------------------------------------------- 1 | # This file is part of systemd. 2 | # 3 | # systemd is free software; you can redistribute it and/or modify it 4 | # under the terms of the GNU Lesser General Public License as published by 5 | # the Free Software Foundation; either version 2.1 of the License, or 6 | # (at your option) any later version. 7 | # 8 | # Entries in this file show the compile time defaults. 9 | # You can change settings by editing this file. 10 | # Defaults can be restored by simply deleting this file. 11 | # 12 | # See journald.conf(5) for details. 13 | 14 | [Journal] 15 | Storage=volatile # no sense persisting logs to disk, we don't care about them that much 16 | #Compress=yes 17 | #Seal=yes 18 | #SplitMode=uid 19 | #SyncIntervalSec=5m 20 | #RateLimitInterval=30s 21 | #RateLimitBurst=1000 22 | #SystemMaxUse= 23 | #SystemKeepFree= 24 | #SystemMaxFileSize= 25 | #SystemMaxFiles=100 26 | #RuntimeMaxUse= 27 | #RuntimeKeepFree= 28 | #RuntimeMaxFileSize= 29 | #RuntimeMaxFiles=100 30 | #MaxRetentionSec= 31 | #MaxFileSec=1month 32 | ForwardToSyslog=no 33 | ForwardToKMsg=no 34 | ForwardToConsole=yes 35 | ForwardToWall=no 36 | TTYPath=/dev/tty1 37 | #MaxLevelStore=debug 38 | #MaxLevelSyslog=debug 39 | #MaxLevelKMsg=notice 40 | #MaxLevelConsole=info 41 | #MaxLevelWall=emerg 42 | -------------------------------------------------------------------------------- /playbooks/devel_tools/vscode.yml: -------------------------------------------------------------------------------- 1 | - name: apt key for vscode 2 | apt_key: url=https://packages.microsoft.com/keys/microsoft.asc state=present 3 | - name: apt repo for vscode 4 | apt_repository: repo="deb [arch=amd64] https://packages.microsoft.com/repos/vscode stable main" update_cache=yes filename="vscode" 5 | 6 | - name: install vscode 7 | apt: 8 | state: present 9 | pkg: code 10 | 11 | - name: make sure vscode extension directory exists 12 | file: path=/opt/vscode/extensions state=directory 13 | - name: vscode extensions 14 | shell: code --extensions-dir /opt/vscode/extensions --user-data-dir /opt/vscode --no-sandbox --install-extension {{ item }} 15 | loop: "{{ vscode_extensions }}" 16 | 17 | 18 | - name: create a script to symlink our extensions 19 | copy: 20 | dest: /usr/local/bin/vscode-extension-install 21 | mode: 0755 22 | content: | 23 | #!/bin/bash 24 | mkdir -p $HOME/.vscode 25 | ln -sf /opt/vscode/extensions $HOME/.vscode/extensions 26 | 27 | - name: add vscode extension install script to autostart 28 | copy: 29 | dest: /etc/xdg/autostart/vscode-extension-install.desktop 30 | content: | 31 | [Desktop Entry] 32 | Type=Application 33 | Exec=/usr/local/bin/vscode-extension-install 34 | NoDisplay=true 35 | -------------------------------------------------------------------------------- /test_final.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | VARIANT=${1:-""} 4 | 5 | SSHPORT=2222 6 | SSH_ICPCADMIN_KEY="files/secrets/icpcadmin@contestmanager" 7 | PIDFILE="tmp/qemu.pid" 8 | SNAPSHOT="-snapshot" 9 | ALIVE=0 10 | 11 | BASEIMG=$(ls -tr output/$VARIANT*image-amd64.img | tail -n1) 12 | echo "Booting $BASEIMG" 13 | 14 | function launchssh() { 15 | echo "Launching ssh session" 16 | ssh -o BatchMode=yes -o IdentitiesOnly=yes -o ConnectTimeout=1 -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null icpcadmin@localhost -i $SSH_ICPCADMIN_KEY -p$SSHPORT 17 | } 18 | function cleanup() { 19 | echo "Forcing shutdown(poweroff)" 20 | kill "$(cat $PIDFILE)" 21 | rm -f $PIDFILE 22 | } 23 | 24 | set -x 25 | qemu-system-x86_64 -smp 2 -m 4096 -hda $BASEIMG -global isa-fdc.driveA= --enable-kvm -net user,hostfwd=tcp::$SSHPORT-:22 -net nic --daemonize --pidfile $PIDFILE $SNAPSHOT -vnc :0 -vga qxl -spice port=5901,disable-ticketing -usbdevice tablet 26 | set +x 27 | 28 | CMD=1 29 | while [ $CMD != 0 ]; do 30 | echo "Select an action" 31 | echo " 1. Launch SSH Session" 32 | echo " 0. Halt VM" 33 | read -p "Action(Default 1): " CMD 34 | CMD=${CMD:-1} 35 | case $CMD in 36 | 0) break ;; 37 | 1) launchssh ;; 38 | *) launchssh ;; 39 | esac 40 | done 41 | 42 | echo 43 | echo 44 | read -p "Press enter to halt" 45 | 46 | cleanup 47 | exit 0 48 | -------------------------------------------------------------------------------- /playbooks/languages/python3.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Python 3 3 | apt: 4 | state: present 5 | pkg: 6 | - python3 7 | - pypy3 8 | - python-is-python3 9 | 10 | - name: ipython 11 | apt: 12 | state: present 13 | install_recommends: false # it's like 500MB with recommends vs 14MB without 14 | pkg: 15 | - ipython3 16 | 17 | # TODO: it'd be neat to get the pypy docs 18 | # https://docs.pypy.org/en/latest 19 | # it's a readthedocs site, but doesn't have any downloads :( 20 | # https://readthedocs.org/projects/pypy/downloads/ 21 | 22 | - name: python3 docs 23 | when: not devdocs 24 | block: 25 | - name: Python 3 docs 26 | apt: 27 | state: present 28 | pkg: 29 | - python3-doc # 37.2 mb 30 | - pypy3-doc # 5mb 31 | - ansible.utils.update_fact: 32 | updates: 33 | - path: "lang_docs['Python 3']" 34 | value: 35 | name: Python 3 Documentation 36 | id: py3 37 | path: /usr/share/doc/python3-doc/html 38 | index: index.html 39 | - path: "lang_docs['PyPy 3']" 40 | value: 41 | name: PyPy3 Documentation 42 | id: pypy3 43 | path: /usr/share/doc/pypy3-doc/html 44 | index: index.html 45 | register: updated 46 | - set_fact: 47 | lang_docs: "{{ updated.lang_docs }}" 48 | -------------------------------------------------------------------------------- /playbooks/reversetunnel.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: install autossh 3 | apt: pkg=autossh state=present 4 | 5 | - name: create ssh tunnel user 6 | user: name=jumpy state=present system=yes 7 | 8 | - name: create .ssh directory 9 | file: dest=/home/jumpy/.ssh state=directory 10 | 11 | - name: set up private key 12 | copy: 13 | src: files/secrets/jumpy@icpc 14 | mode: 0400 15 | owner: jumpy 16 | group: jumpy 17 | dest: /home/jumpy/.ssh/id_ed25519 18 | - name: set up private key certificate (so we can log in to remote things that know about our ssh CA) 19 | copy: 20 | src: files/secrets/jumpy@icpc-cert.pub 21 | mode: 0400 22 | owner: jumpy 23 | group: jumpy 24 | dest: /home/jumpy/.ssh/id_ed25519-cert.pub 25 | 26 | 27 | - name: create autossh service 28 | template: src=files/autossh.service.j2 dest=/etc/systemd/system/autossh.service 29 | 30 | - name: enable autossh service 31 | service: enabled=yes name=autossh 32 | 33 | - name: make sure ssh is enabled 34 | service: enabled=yes name=ssh 35 | 36 | - name: hide jumpy from login screen 37 | block: 38 | - name: ensure directory exists 39 | file: path=/var/lib/AccountsService/users state=directory 40 | - name: create file configuring it as a system account 41 | copy: 42 | dest: /var/lib/AccountsService/users/jumpy 43 | content: | 44 | [User] 45 | SystemAccount=true 46 | -------------------------------------------------------------------------------- /playbooks/languages/kotlin.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: install unzip so we can extract kotlin 3 | apt: 4 | pkg: unzip 5 | state: present 6 | - name: install kotlin 7 | unarchive: src=files/kotlin-compiler-1.8.10.zip dest=/opt creates=/opt/kotlinc 8 | - name: create symlinks for kotlin 9 | file: 10 | src: /opt/kotlinc/bin/{{ binary_name }} 11 | dest: /usr/local/bin/{{ binary_name }} 12 | state: link 13 | loop_control: 14 | loop_var: binary_name 15 | with_items: 16 | - kapt 17 | - kotlin 18 | - kotlinc 19 | - kotlinc-js 20 | - kotlinc-jvm 21 | - kotlin-dce-js 22 | 23 | - name: kotlin docs 24 | when: not devdocs 25 | block: 26 | # kotlin pdf doc (single file) 27 | - name: create directory for kotlin docs 28 | file: 29 | state: directory 30 | mode: 0755 31 | path: /opt/kotlin-docs 32 | 33 | - name: fetch kotlin pdf 34 | get_url: 35 | url: https://kotlinlang.org/docs/kotlin-reference.pdf 36 | dest: /opt/kotlin-docs/kotlin-reference.pdf 37 | mode: 0644 38 | - ansible.utils.update_fact: 39 | updates: 40 | - path: "lang_docs['Kotlin']" 41 | value: 42 | name: Kotlin PDF 43 | id: kotlin 44 | path: /opt/kotlin-docs 45 | index: kotlin-reference.pdf 46 | register: updated 47 | - set_fact: 48 | lang_docs: "{{ updated.lang_docs }}" 49 | -------------------------------------------------------------------------------- /files/scripts/firstLogin.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # Close standard output file descriptor 3 | exec 1<&- 4 | # Close standard error file descriptor 5 | exec 2<&- 6 | 7 | # Open standard output as $LOG_FILE file for read and write. 8 | exec 1<>/tmp/firstlogin.log 9 | # Redirect standard error to standard output 10 | exec 2>&1 11 | 12 | 13 | UTILDIR="/icpc" 14 | 15 | if [ -f "$UTILDIR/teamWallpaper.png" ]; then 16 | BACKGROUND="$UTILDIR/teamWallpaper.png" 17 | else 18 | # Set the wallpaper to the "template" 19 | BACKGROUND="$UTILDIR/wallpaper.png" 20 | fi 21 | 22 | # wait for xfdesktop to be loaded 23 | echo "Waiting for xfdesktop to be running" 24 | while ! pgrep xfdesktop; do 25 | sleep 1 26 | done 27 | 28 | # wait a few moments for things to load initally 29 | sleep 5 30 | 31 | echo "Update the desktop background properties" 32 | xfconf-query -c xfce4-desktop -p /backdrop/screen0 -rR 33 | xfconf-query -c xfce4-desktop -p /backdrop/screen0/monitor0/last-image --create -t string -s $BACKGROUND 34 | xfconf-query -c xfce4-desktop -p /backdrop/screen0/monitor0/image-path --create -t string -s $BACKGROUND 35 | xfconf-query -c xfce4-desktop -p /backdrop/screen0/monitor0/image-style --create -t int -s 3 36 | 37 | # Reload xfdesktop to get the background image showing (--reload doesn't work, have to --quit first...) 38 | echo "Reload xfdesktop to refresh the background" 39 | sleep 5 40 | xfdesktop --quit 41 | timeout 5 xfdesktop --reload 42 | sleep 5 43 | xfdesktop --quit 44 | timeout 5 xfdesktop --reload 45 | 46 | -------------------------------------------------------------------------------- /test_final_efi.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | SSHPORT=2222 4 | SSH_ICPCADMIN_KEY="files/secrets/icpcadmin@contestmanager" 5 | PIDFILE="tmp/qemu.pid" 6 | SNAPSHOT="-snapshot" 7 | ALIVE=0 8 | 9 | BASEIMG="*_image-amd64.img" 10 | 11 | function launchssh() { 12 | echo "Launching ssh session" 13 | ssh -o BatchMode=yes -o IdentitiesOnly=yes -o ConnectTimeout=1 -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null icpcadmin@localhost -i $SSH_ICPCADMIN_KEY -p$SSHPORT 14 | } 15 | function cleanup() { 16 | echo "Forcing shutdown(poweroff)" 17 | kill "$(cat $PIDFILE)" 18 | rm -f $PIDFILE 19 | } 20 | 21 | set -x 22 | qemu-system-x86_64 -machine q35 -smp 2 -m 4096 --enable-kvm \ 23 | -hda output/$BASEIMG \ 24 | -global driver=cfi.pflash01,property=secure,value=on \ 25 | -drive if=pflash,format=raw,unit=0,file=efi/OVMF_CODE.fd,readonly=on \ 26 | -drive if=pflash,format=raw,unit=1,file=efi/OVMF_VARS.fd \ 27 | -net user,hostfwd=tcp::$SSHPORT-:22 -net nic \ 28 | --daemonize --pidfile $PIDFILE \ 29 | $SNAPSHOT \ 30 | -vnc :0 -vga qxl -spice port=5901,disable-ticketing \ 31 | -usbdevice tablet 32 | 33 | set +x 34 | 35 | CMD=1 36 | while [ $CMD != 0 ]; do 37 | echo "Select an action" 38 | echo " 1. Launch SSH Session" 39 | echo " 0. Halt VM" 40 | read -p "Action(Default 1): " CMD 41 | CMD=${CMD:-1} 42 | case $CMD in 43 | 0) break ;; 44 | 1) launchssh ;; 45 | *) launchssh ;; 46 | esac 47 | done 48 | 49 | echo 50 | echo 51 | read -p "Press enter to halt" 52 | 53 | cleanup 54 | exit 0 55 | -------------------------------------------------------------------------------- /files/scripts/makeDist.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # Cleans up temporary files, etc and makes it ready for imaging 3 | 4 | UTILDIR="/icpc" 5 | 6 | # remove the git repositories 7 | rm -rf /etc/skel/.git 8 | rm -rf /home/icpcadmin/.git 9 | 10 | # Cleanup 'imageadmin' things 11 | killall -9 -u imageadmin 12 | sleep 5 13 | rm -rf /home/imageadmin 14 | userdel imageadmin 15 | 16 | # Delete proxy settings for apt(if any) 17 | rm -f /etc/apt/apt.conf.d/01proxy 18 | 19 | # Kill all the user processes(and wait for them to die) 20 | killall -9 -u contestant 21 | sleep 5 22 | 23 | # Reset the team(and any defaults) 24 | $UTILDIR/scripts/deleteUser.sh 25 | 26 | # reset the TEAM and SITE 27 | echo "default team" > $UTILDIR/TEAM 28 | echo "default site" > $UTILDIR/SITE 29 | 30 | # Remove the team wallpaper 31 | rm -f $UTILDIR/teamWallpaper.png 32 | 33 | # Delete the printers/printer class 34 | for PRINTER in $(lpstat -v | cut -d ' ' -f 3 | tr -d ':') 35 | do 36 | lpadmin -x $PRINTER 37 | done 38 | lpadmin -x ContestPrinter 39 | 40 | # Cleanup apt cache/unnecessary package 41 | apt-get autoremove --purge -y 42 | apt-get clean 43 | 44 | # Remove 'apt-get update' data 45 | rm -rf /var/lib/apt/lists 46 | mkdir -p /var/lib/apt/lists/partial 47 | 48 | # enable the firewall 49 | ufw --force enable 50 | 51 | # Delete /etc/machine-id so the image generates a new one on boot 52 | echo "" > /etc/machine-id 53 | 54 | # make sure wireguard config is wiped so it can be generated for each system on boot 55 | systemctl stop wg-setup 56 | wg-quick down contest 57 | rm -f /etc/wireguard/contest.conf 58 | 59 | # Free up space(just to make the image smaller) 60 | dd if=/dev/zero of=/empty bs=1M || true 61 | rm -f /empty 62 | sync 63 | -------------------------------------------------------------------------------- /files/wg_setup.j2: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -euo pipefail 3 | 4 | # Try to start wireguard if we have a config for it 5 | if [ -f /etc/wireguard/contest.conf ]; then 6 | wg-quick up contest || true 7 | fi 8 | 9 | # Nothing to do if the VPN is already up/contestmanager.icpcnet.internal is reachable 10 | if ping -w3 -c1 contestmanager.icpcnet.internal >/dev/null 2>&1 ; then 11 | exit 0 12 | fi 13 | 14 | echo "contestmanager.icpcnet.internal not reachable, reconfiguring VPN" 15 | 16 | # Network is broken/not configured yet, bring it down so we can try to re-initialize it 17 | wg-quick down contest || true 18 | 19 | wg_config=$(/usr/bin/ssh \ 20 | -o "ServerAliveInterval 30" \ 21 | -o "ServerAliveCountMax 3" \ 22 | {{ wireguard_client_user }}@{{ wireguard_host }} \ 23 | -p {{ wireguard_port }} \ 24 | -i /root/.ssh/id_ed25519) 25 | 26 | if [ $? != 0 ]; then 27 | exit 1 # failed, exit so it tries again 28 | fi 29 | 30 | echo "$wg_config" > /etc/wireguard/contest.conf 31 | wg-quick up contest 32 | 33 | # Wait a few moments 34 | sleep 5 35 | 36 | # Try to ping the contest management server 37 | if ping -w3 -c1 contestmanager.icpcnet.internal >/dev/null 2>&1 ; then 38 | exit 0 39 | fi 40 | 41 | # Bring it down so we can try wstunnel instead 42 | wg-quick down contest || true 43 | 44 | # If it's still not reachable, try using wstunnel to wrap the connection 45 | sed -i -e 's/^Endpoint=.*/Endpoint=127.0.0.10:51820/' /etc/wireguard/contest.conf 46 | 47 | # Bring it back up 48 | wg-quick up contest 49 | 50 | # Check if it's alive now 51 | sleep 5 52 | if ping -w3 -c1 contestmanager.icpcnet.internal >/dev/null 2>&1 ; then 53 | exit 0 54 | fi 55 | 56 | # Report an exit failure if it was still down at this point 57 | exit 1 58 | -------------------------------------------------------------------------------- /playbooks/languages/cpp.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: cpp 3 | apt: 4 | state: present 5 | pkg: 6 | - g++ 7 | - stl-manual # 2.5mb 8 | - unzip # for dealing with docs 9 | 10 | - name: cpp docs 11 | when: not devdocs 12 | block: 13 | # From https://en.cppreference.com/w/Cppreference:Archives 14 | - name: create directories 15 | file: 16 | state: directory 17 | path: "{{ item }}" 18 | with_items: 19 | - /opt/cppreference-docs 20 | - /opt/libc-manual 21 | - name: cppreference.com offline docs 22 | unarchive: 23 | src: files/html_book_20190607.zip 24 | dest: /opt/cppreference-docs 25 | 26 | - name: libc reference docs 27 | unarchive: 28 | src: https://www.gnu.org/software/libc/manual/html_node/libc-html_node.tar.gz 29 | dest: /opt/libc-manual 30 | remote_src: true # to get it to download from the internet 31 | - ansible.utils.update_fact: 32 | updates: 33 | - path: "lang_docs['C++']" 34 | value: 35 | name: STL Manual 36 | id: stl_docs 37 | path: /usr/share/doc/stl-manual/html 38 | index: index.html 39 | - path: "lang_docs['c/cpp reference']" 40 | value: 41 | name: cppreference.com 42 | id: cpp_reference 43 | path: /opt/cppreference-docs/reference 44 | index: en/index.html 45 | - path: "lang_docs['glibc manual']" 46 | value: 47 | name: GNU c library (glibc) manual 48 | id: libc_manual 49 | path: /opt/libc-manual/libc 50 | index: index.html 51 | register: updated 52 | - set_fact: 53 | lang_docs: "{{ updated.lang_docs }}" 54 | -------------------------------------------------------------------------------- /files/systemd-system.conf: -------------------------------------------------------------------------------- 1 | # This file is part of systemd. 2 | # 3 | # systemd is free software; you can redistribute it and/or modify it 4 | # under the terms of the GNU Lesser General Public License as published by 5 | # the Free Software Foundation; either version 2.1 of the License, or 6 | # (at your option) any later version. 7 | # 8 | # Entries in this file show the compile time defaults. 9 | # You can change settings by editing this file. 10 | # Defaults can be restored by simply deleting this file. 11 | # 12 | # See systemd-system.conf(5) for details. 13 | 14 | [Manager] 15 | ShowStatus=false 16 | #LogLevel=info 17 | #LogTarget=journal-or-kmsg 18 | #LogColor=yes 19 | #LogLocation=no 20 | #DumpCore=yes 21 | #CrashChangeVT=no 22 | #CrashShell=no 23 | #CrashReboot=no 24 | #CPUAffinity=1 2 25 | #JoinControllers=cpu,cpuacct net_cls,net_prio 26 | #RuntimeWatchdogSec=0 27 | #ShutdownWatchdogSec=10min 28 | #CapabilityBoundingSet= 29 | #SystemCallArchitectures= 30 | #TimerSlackNSec= 31 | #DefaultTimerAccuracySec=1min 32 | #DefaultStandardOutput=journal 33 | #DefaultStandardError=inherit 34 | #DefaultTimeoutStartSec=90s 35 | #DefaultTimeoutStopSec=90s 36 | #DefaultRestartSec=100ms 37 | #DefaultStartLimitInterval=10s 38 | #DefaultStartLimitBurst=5 39 | #DefaultEnvironment= 40 | #DefaultCPUAccounting=no 41 | #DefaultBlockIOAccounting=no 42 | #DefaultMemoryAccounting=no 43 | #DefaultTasksAccounting=no 44 | #DefaultTasksMax= 45 | #DefaultLimitCPU= 46 | #DefaultLimitFSIZE= 47 | #DefaultLimitDATA= 48 | #DefaultLimitSTACK= 49 | #DefaultLimitCORE= 50 | #DefaultLimitRSS= 51 | #DefaultLimitNOFILE= 52 | #DefaultLimitAS= 53 | #DefaultLimitNPROC= 54 | #DefaultLimitMEMLOCK= 55 | #DefaultLimitLOCKS= 56 | #DefaultLimitSIGPENDING= 57 | #DefaultLimitMSGQUEUE= 58 | #DefaultLimitNICE= 59 | #DefaultLimitRTPRIO= 60 | #DefaultLimitRTTIME= 61 | -------------------------------------------------------------------------------- /files/squid/squid.conf.j2: -------------------------------------------------------------------------------- 1 | # 4MB is the default, so we could omit that... 2 | # dynamic_cert_mem_cache_size=4MB 3 | http_port 3128 ssl-bump generate-host-certificates=on tls-cert=/etc/squid/squidCA.crt tls-key=/etc/squid/squidCA.pem 4 | 5 | # need to manually initialized the ssl_db 6 | #sudo /usr/lib/squid/security_file_certgen -c -s /var/spool/squid/ssl_db -M 4MB 7 | 8 | acl SSL_ports port 443 9 | acl Safe_ports port 80 # http 10 | acl Safe_ports port 21 # ftp 11 | acl Safe_ports port 443 # https 12 | acl Safe_ports port 70 # gopher 13 | acl Safe_ports port 210 # wais 14 | acl Safe_ports port 1025-65535 # unregistered ports 15 | acl Safe_ports port 280 # http-mgmt 16 | acl Safe_ports port 488 # gss-http 17 | acl Safe_ports port 591 # filemaker 18 | acl Safe_ports port 777 # multiling http 19 | acl CONNECT method CONNECT 20 | 21 | http_access deny !Safe_ports 22 | http_access deny CONNECT !SSL_ports 23 | 24 | # Deny access to the cachemgr 25 | http_access deny manager 26 | 27 | # Block everything except our reverseproxy, which will filter the allowed network destinations 28 | acl reverseproxy dst 127.0.0.1 29 | http_access allow reverseproxy 30 | http_access deny all 31 | # show a block page 32 | deny_info ICPC_ERR_ACCESS_DENIED all 33 | 34 | coredump_dir /var/spool/squid 35 | 36 | # To make it quick to restart squid, otherwise it takes ~30s to stop 37 | shutdown_lifetime 1 second 38 | 39 | # MITM everything! 40 | ssl_bump bump all 41 | 42 | # Ignore ssl errors, so we can still show our nice block page 43 | # And also because our block page has no good ssl certificate 44 | sslproxy_cert_error allow all 45 | 46 | {% if squid_autologin_urls|length > 0 %} 47 | {% for url in squid_autologin_urls %} 48 | acl autologin url_regex {{url}} 49 | {% endfor %} 50 | include /etc/squid/autologin.conf 51 | {% endif%} 52 | -------------------------------------------------------------------------------- /files/management-server/nginx.conf: -------------------------------------------------------------------------------- 1 | user www-data; 2 | worker_processes auto; # number of cores 3 | pid /run/nginx.pid; 4 | 5 | events { 6 | worker_connections 8096; 7 | multi_accept on; 8 | use epoll; 9 | } 10 | 11 | http { 12 | sendfile on; 13 | tcp_nopush on; 14 | keepalive_timeout 65; 15 | server_tokens off; 16 | 17 | tcp_nodelay on; 18 | types_hash_max_size 2048; 19 | 20 | include /etc/nginx/mime.types; 21 | default_type application/octet-stream; 22 | 23 | access_log /var/log/nginx/access.log; 24 | error_log /var/log/nginx/error.log; 25 | 26 | gzip on; 27 | gzip_disable "msie6"; 28 | 29 | include /etc/nginx/conf.d/*.conf; 30 | 31 | # this is required to proxy Grafana Live WebSocket connections. 32 | map $http_upgrade $connection_upgrade { 33 | default upgrade; 34 | '' close; 35 | } 36 | 37 | upstream grafana { 38 | server localhost:3000; 39 | } 40 | 41 | server { 42 | listen 80; 43 | 44 | location / { 45 | auth_basic "Contest Admin Area"; 46 | auth_basic_user_file /etc/nginx/contestadmin_users.htpasswd; 47 | root /srv/contestweb; 48 | } 49 | # Grafana server configuration for proxying 50 | location /grafana/ { 51 | rewrite ^/grafana/(.*) /$1 break; 52 | proxy_set_header Host $host; 53 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 54 | proxy_set_header X-Forwarded-Proto $scheme; 55 | 56 | # Expose grafana 57 | proxy_pass http://grafana; 58 | proxy_read_timeout 90; 59 | } 60 | location /grafana/api/live/ { 61 | rewrite ^/grafana/(.*) /$1 break; 62 | proxy_http_version 1.1; 63 | proxy_set_header Upgrade $http_upgrade; 64 | proxy_set_header Connection $connection_upgrade; 65 | proxy_set_header Host $http_host; 66 | proxy_pass http://grafana; 67 | } 68 | 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /playbooks/reverseproxy.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: install nginx 3 | ansible.builtin.apt: 4 | pkg: 5 | - nginx 6 | - ssl-cert # for the snakeoil stuff 7 | state: present 8 | 9 | - name: remove default nginx configuration 10 | ansible.builtin.file: 11 | path: /etc/nginx/sites-enabled/default 12 | state: absent 13 | notify: reload nginx 14 | 15 | - name: use the snakeoil cert, since squid will be proxying everything and mitms things/accepts any certificate 16 | ansible.builtin.copy: 17 | remote_src: true 18 | src: /etc/ssl/{{item.src}} 19 | dest: /etc/ssl/{{item.dest}} 20 | mode: "{{ item.mode }}" 21 | notify: reload nginx 22 | loop: 23 | - { src: "private/ssl-cert-snakeoil.key", dest: reverseproxy.key, mode: "0600" } 24 | - { src: "certs/ssl-cert-snakeoil.pem", dest: reverseproxy.cert, mode: "0644" } 25 | 26 | - name: make sure web dir exists 27 | file: state=directory path=/opt/localwww 28 | 29 | - name: install our reverse proxy config 30 | ansible.builtin.template: 31 | src: files/nginx.conf.j2 32 | dest: /etc/nginx/sites-available/reverseproxy.conf 33 | mode: 0644 34 | owner: root 35 | group: root 36 | lstrip_blocks: true 37 | trim_blocks: true 38 | notify: reload nginx 39 | 40 | - name: enable the reverseproxy nginx config 41 | ansible.builtin.file: 42 | src: /etc/nginx/sites-available/reverseproxy.conf 43 | dest: /etc/nginx/sites-enabled/reverseproxy 44 | state: link 45 | notify: reload nginx 46 | 47 | - name: update hosts file so we talk to the proxyserver instead of the real server 48 | ansible.builtin.lineinfile: 49 | path: /etc/hosts 50 | line: "127.0.0.1 {{ item }}" 51 | owner: root 52 | group: root 53 | mode: '0644' 54 | loop: "{{ reverseproxy_sites.keys() }}" 55 | 56 | - name: update hosts file so devdocs is available properly 57 | ansible.builtin.lineinfile: 58 | path: /etc/hosts 59 | line: "127.0.0.1 devdocs" 60 | owner: root 61 | group: root 62 | mode: '0644' 63 | -------------------------------------------------------------------------------- /files/scripts/version_check.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | VFILE="/tmp/versions.txt" 4 | echo "Software Versions" > $VFILE 5 | 6 | # these checks happen outside the chroot 7 | echo -e "==== javac Version ====" | tee -a $VFILE 8 | javac -version 2>&1 | tee -a $VFILE 9 | 10 | echo -e "\n\n==== GCC Version ====" | tee -a $VFILE 11 | gcc --version 2>&1 | tee -a $VFILE 12 | 13 | echo -e "\n\n==== G++ Version ====" | tee -a $VFILE 14 | g++ --version 2>&1 | tee -a $VFILE 15 | 16 | echo -e "\n\n==== FPC(Pascal) Version ====" | tee -a $VFILE 17 | fpc -version 2>&1 | tee -a $VFILE 18 | 19 | echo -e "\n\n==== Haskell Version ====" | tee -a $VFILE 20 | ghc --version 2>&1 | tee -a $VFILE 21 | 22 | echo -e "\n\n==== gmcs(mono compiler) Version ====" | tee -a $VFILE 23 | gmcs --version 2>&1 | tee -a $VFILE 24 | 25 | # There is currently no compiler setup for fortran 26 | echo -e "\n\n==== gfortran Version ====" | tee -a $VFILE 27 | gfortran --version 2>&1 | tee -a $VFILE 28 | 29 | # There is currently no compiler set up for ada 30 | echo -e "\n\n==== GNAT(ADA) Version ====" | tee -a $VFILE 31 | gnat 2>&1 | head -n1 | tee -a $VFILE 32 | 33 | # This needs to be checked in the chroot 34 | echo -e "\n\n==== Python Version ====" | tee -a $VFILE 35 | python --version 2>&1 | tee -a $VFILE 36 | 37 | echo -e "\n\n==== Java Version ====" | tee -a $VFILE 38 | java -version 2>&1 | tee -a $VFILE 39 | 40 | echo -e "\n\n==== Mono Version ====" | tee -a $VFILE 41 | mono --version 2>&1 | tee -a $VFILE 42 | 43 | echo -e "\n\n==== Scala Version ====" | tee -a $VFILE 44 | scala -version 2>&1 | tee -a $VFILE 45 | 46 | echo -e "\n\n==== Kotlin Version ====" | tee -a $VFILE 47 | kotlin -version 2>&1 | tee -a $VFILE 48 | 49 | echo -e "\n\n==== Rust Version ====" | tee -a $VFILE 50 | rustc --version 2>&1 | tee -a $VFILE 51 | 52 | echo -e "\n\n==== NodeJS Version ====" | tee -a $VFILE 53 | nodejs --version 2>&1 | tee -a $VFILE 54 | 55 | echo -e "\n\n==== OCaml Version ====" | tee -a $VFILE 56 | ocaml -version 2>&1 | tee -a $VFILE 57 | 58 | echo -e "Software versions written to: $VFILE\n\n" 59 | -------------------------------------------------------------------------------- /playbooks/system.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: useful packages for admins 3 | apt: 4 | pkg: 5 | # performance tools 6 | - htop 7 | - dstat 8 | - iotop 9 | - sysstat 10 | - dstat 11 | # misc admin tools 12 | - net-tools # for ifconfig/old tools 13 | - curl # debugging connection things 14 | - ncdu 15 | - jq 16 | # Needed for wifi 17 | - wpasupplicant 18 | - iw 19 | 20 | - name: disable udev persistent net generator 21 | file: state=link name=/etc/udev/rules.d/75-persistent-net-generator.rules src=/dev/null 22 | 23 | # https://www.freedesktop.org/wiki/Software/systemd/PredictableNetworkInterfaceNames/ 24 | - name: disable "predictable network interface names" 25 | file: state=link name=/etc/udev/rules.d/80-net-setup-link.rules src=/dev/null 26 | 27 | - name: push a better default network interfaces file 28 | copy: src=files/01-netcfg.yaml dest=/etc/netplan/01-netcfg.yaml 29 | 30 | - name: disable console blanking 31 | lineinfile: dest=/etc/default/grub regexp="^GRUB_CMDLINE_LINUX_DEFAULT" line='GRUB_CMDLINE_LINUX_DEFAULT="biosdevname=0 consoleblank=0 net.ifnames=0"' state=present 32 | notify: update grub 33 | 34 | - name: journald write to /dev/tty1 35 | copy: src=files/systemd-journald.conf dest=/etc/systemd/journald.conf 36 | 37 | - name: systemd disable status messages on console 38 | copy: src=files/systemd-system.conf dest=/etc/systemd/system.conf 39 | 40 | - name: disable avahi-daemon (and socket), because we don't need mDNS 41 | service: name={{ item }} state=stopped enabled=no 42 | with_items: 43 | - avahi-daemon.socket 44 | - avahi-daemon.service 45 | 46 | - name: disable unattended upgrades/apt cache fetching 47 | copy: 48 | dest: /etc/apt/apt.conf.d/20auto-upgrades 49 | content: | 50 | APT::Periodic::Update-Package-Lists "0"; 51 | APT::Periodic::Unattended-Upgrade "0"; 52 | 53 | - name: Configure firefox policies (trust squid cert, homepage/bookmarks/etc) 54 | copy: 55 | dest: /usr/lib/firefox/distribution/policies.json 56 | content: | 57 | { 58 | "policies": 59 | {{ firefox_default_policies|combine(firefox_policies)|to_nice_json }} 60 | } 61 | -------------------------------------------------------------------------------- /secrets/gen-secrets.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd ) 4 | 5 | 6 | usage() { 7 | echo "gen-secrets.sh " 8 | exit 1 9 | } 10 | 11 | [ $# -ge 1 ] || usage 12 | 13 | mkdir -p "$SCRIPT_DIR/$1" 14 | 15 | pushd "$SCRIPT_DIR/$1" 16 | 17 | # This is the key for contestant to jumpbox (to provision wireguard, run ansible-pull, and set up a reverse ssh tunnel) 18 | ssh-keygen -t ed25519 -N "" \ 19 | -C "jumpy@icpc (For connecting to the contestmanager)" \ 20 | -f "./jumpy@icpc" 21 | 22 | # This is the key for the icpcadmin to connect to any of the contestant machines 23 | ssh-keygen -t ed25519 -N "" \ 24 | -C "icpcadmin@contestmanager (For connecting from the contestmanager to the icpc image)" \ 25 | -f "./icpcadmin@contestmanager" 26 | 27 | # create a server CA 28 | ssh-keygen -t ed25519 -N "" -f ./server_ca 29 | 30 | # generate a host certificate for the contest image 31 | ssh-keygen -t ed25519 -N "" -f ./contestant.icpcnet.internal_host_ed25519_key 32 | 33 | # and for the contestmanager machine 34 | ssh-keygen -t ed25519 -N "" -f ./contestmanager.icpcnet.internal_host_ed25519_key 35 | 36 | # Sign the host certificates 37 | ssh-keygen -s ./server_ca -h \ 38 | -I "contestant.icpcnet.internal host key" \ 39 | -h ./contestant.icpcnet.internal_host_ed25519_key 40 | # don't specify a set of principals, so it's valid for any hostname 41 | # -n "contestant,contestant.icpcnet.internal" 42 | 43 | ssh-keygen -s ./server_ca -h \ 44 | -I "contestmanager.icpcnet.internal host key" \ 45 | -n "contestmanager,contestmanager.icpcnet.internal,icpc.cloudcontest.org" \ 46 | ./contestmanager.icpcnet.internal_host_ed25519_key 47 | 48 | # sign the icpcadmin user key (allowing to log into the icpc machine with root, icpcadmin, or contestant) 49 | ssh-keygen -s ./server_ca \ 50 | -I "icpcadmin@contestmanager user key" \ 51 | -n "icpcadmin,root,contestant" \ 52 | ./icpcadmin@contestmanager 53 | 54 | # sign the jumpy key allowing the icpc machines to connect to the contestmanager machine (as jumpy, wg_client, or git) 55 | ssh-keygen -s ./server_ca \ 56 | -I "jumpy@icpc key" \ 57 | -n "jumpy,wg_client,git" \ 58 | ./jumpy@icpc 59 | 60 | popd 61 | -------------------------------------------------------------------------------- /playbooks/vpn.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: install wireguard-tools 3 | apt: pkg=wireguard-tools state=present 4 | 5 | - name: set up private key 6 | copy: 7 | src: files/secrets/jumpy@icpc 8 | mode: 0400 9 | owner: root 10 | group: root 11 | dest: /root/.ssh/id_ed25519 12 | - name: set up private key certificate 13 | copy: 14 | src: files/secrets/jumpy@icpc-cert.pub 15 | mode: 0400 16 | owner: root 17 | group: root 18 | dest: /root/.ssh/id_ed25519-cert.pub 19 | 20 | - name: copy the wg_setup script 21 | template: 22 | src: files/wg_setup.j2 23 | dest: /usr/local/bin/wg_setup 24 | mode: 0755 25 | 26 | - name: create wireguard service to initialize vpn details (fetch credentials and whatnot) 27 | template: src=files/wg-setup.service.j2 dest=/etc/systemd/system/wg-setup.service 28 | 29 | - name: enable wg-setup service 30 | service: enabled=yes name=wg-setup 31 | 32 | - name: add some aliases for the vpn server to our /etc/hosts file 33 | lineinfile: 34 | dest: /etc/hosts 35 | line: '{{ contestmanager_ip }} contestmanager.icpcnet.internal contestmanager' 36 | state: present 37 | 38 | - name: install wstunnel (for wireguard websocket tunnel fallback) 39 | unarchive: 40 | src: https://github.com/erebe/wstunnel/releases/download/v9.2.2/wstunnel_9.2.2_linux_amd64.tar.gz 41 | dest: /usr/local/bin 42 | remote_src: true 43 | include: ['wstunnel'] 44 | - name: make wstunnel executable 45 | file: 46 | state: file 47 | path: /usr/local/bin/wstunnel 48 | mode: 0755 49 | - name: set up wstunnel service so we can access it via websocket (at 127.0.0._10_, not .1 for security 50 | copy: 51 | dest: /etc/systemd/system/wstunnel.service 52 | content: | 53 | [Unit] 54 | Description=Tunnel WG UDP over websocket to our management host 55 | After=network.target 56 | 57 | [Service] 58 | Type=simple 59 | DynamicUser=yes 60 | ExecStart=/usr/local/bin/wstunnel client \ 61 | -L udp://127.0.0.10:51820:127.0.0.1:51820 \ 62 | wss://{{ wireguard_host }} \ 63 | --http-upgrade-path-prefix /wgtunnel/ 64 | Restart=always 65 | RestartSec=5 66 | 67 | [Install] 68 | WantedBy=multi-user.target 69 | - name: enable wstunnel service 70 | service: enabled=yes name=wstunnel 71 | -------------------------------------------------------------------------------- /files/nginx.conf.j2: -------------------------------------------------------------------------------- 1 | server_names_hash_bucket_size 128; 2 | resolver 8.8.8.8 ipv6=off; 3 | 4 | server { 5 | listen 80 default_server; 6 | listen 443 ssl default_server; 7 | server_name _; 8 | ssl_certificate /etc/ssl/reverseproxy.cert; 9 | ssl_certificate_key /etc/ssl/reverseproxy.key; 10 | 11 | root /opt/localwww; 12 | } 13 | 14 | # Local devdocs server 15 | server { 16 | listen 80; 17 | server_name devdocs; 18 | return 301 https://$host$request_uri; 19 | } 20 | server { 21 | listen 443 ssl; 22 | server_name devdocs; 23 | ssl_certificate /etc/ssl/reverseproxy.cert; 24 | ssl_certificate_key /etc/ssl/reverseproxy.key; 25 | location / { 26 | proxy_pass http://127.0.0.1:9292; 27 | proxy_ssl_server_name on; 28 | proxy_set_header Host $host; 29 | } 30 | } 31 | 32 | {% for server,config in reverseproxy_sites.items() %} 33 | server { 34 | listen 80; 35 | listen 443 ssl; 36 | server_name {{ server }}; 37 | ssl_certificate /etc/ssl/reverseproxy.cert; 38 | ssl_certificate_key /etc/ssl/reverseproxy.key; 39 | 40 | # Tune some buffer settings 41 | proxy_buffer_size 16k; 42 | proxy_busy_buffers_size 24k; 43 | proxy_buffers 64 4k; 44 | 45 | # Set the hostname as a variable, this forces nginx to do a dns lookup 46 | # otherwise it only looks it up when it starts and never refreshes it 47 | set $server_host "{{config.scheme|default('https')}}://{{config.backend_host|default(server)}}{% if 'port' in config %}{{ ":" + config.port }}{%endif%}"; 48 | 49 | {% for item in config.paths %} 50 | location {% if item.exact is defined and item.exact %}={% endif %} {{ item.path }} { 51 | proxy_pass $server_host 52 | {%- if not (item.exact is defined and item.exact) and item.path != '/' -%} 53 | $request_uri 54 | {%- else -%} 55 | {%- if item.path != '/' -%} 56 | {{ item.path }} 57 | {%- endif -%} 58 | {%- endif -%} 59 | {%- if 'with_args' in item and item.with_args %}$is_args$args{% endif -%} 60 | ; 61 | proxy_ssl_server_name on; 62 | proxy_set_header Host $host; 63 | } 64 | {% endfor %} 65 | } 66 | {% endfor %} 67 | -------------------------------------------------------------------------------- /playbooks/firewall.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Install squid 3 | apt: 4 | pkg: 5 | - squid-common 6 | - squid-openssl 7 | state: present 8 | 9 | - name: configure squid 10 | template: src=files/squid/squid.conf.j2 dest=/etc/squid/squid.conf 11 | notify: restart squid 12 | 13 | - name: make sure an autologin.conf file exists or squid won't start 14 | copy: content="#placeholder\n" dest=/etc/squid/autologin.conf mode=0640 owner=root group=root 15 | 16 | - name: copy a pretty error page for squid to use 17 | template: src=files/squid/block.html.j2 dest=/usr/share/squid/errors/templates/ICPC_ERR_ACCESS_DENIED 18 | 19 | - name: generate a fake CA for squid to use 20 | shell: openssl req -new -newkey rsa:2048 -days 360 -nodes -x509 -keyout /etc/squid/squidCA.pem -out /etc/squid/squidCA.crt -subj "/O=ICPC Network Filter/CN=ICPC Network Filter" 21 | args: 22 | creates: /etc/squid/squidCA.crt 23 | 24 | - name: ensure that the squidCA is trusted by the system 25 | file: 26 | src: /etc/squid/squidCA.crt 27 | dest: /usr/local/share/ca-certificates/squid-ca.crt 28 | state: link 29 | notify: update-ca-certificates 30 | 31 | - name: initialize squid ssl cache 32 | shell: /usr/lib/squid/security_file_certgen -c -s /var/spool/squid/ssl_db -M 4MB 33 | args: 34 | creates: /var/spool/squid/ssl_db/index.txt 35 | 36 | - name: configure system to use the proxy by default 37 | lineinfile: dest=/etc/environment line="{{item}}" 38 | with_items: 39 | - http_proxy="http://localhost:3128/" 40 | - https_proxy="http://localhost:3128/" 41 | - ftp_proxy="http://localhost:3128/" 42 | - no_proxy="localhost,127.0.0.1" 43 | - HTTP_PROXY="http://localhost:3128/" 44 | - HTTPS_PROXY="http://localhost:3128/" 45 | - FTP_PROXY="http://localhost:3128/" 46 | - NO_PROXY="localhost,127.0.0.1" 47 | 48 | - name: Make sure inbound policy is deny 49 | ufw: direction=incoming policy=deny 50 | 51 | - name: allow inbound ssh 52 | ufw: rule=allow name=OpenSSH 53 | 54 | - name: enable the firewall 55 | ufw: state=enabled logging=off 56 | 57 | - name: prevent the 'contestant' user from using the network(except through the proxy or to localhost) 58 | lineinfile: dest=/etc/ufw/before.rules line="-I ufw-before-output -m owner --uid-owner contestant -j REJECT" insertbefore="^# don't delete the 'COMMIT' line" state=present 59 | - name: Allow the contestant user(and others) to talk to localhost from localhost). Fixes issue with intellij/eclipse compilation/debugging. 60 | lineinfile: dest=/etc/ufw/before.rules line="-I ufw-before-output -s 127.0.0.1 -d 127.0.0.1 -j ACCEPT" insertafter="uid-owner contestant -j REJECT" state=present 61 | -------------------------------------------------------------------------------- /jumphost.md: -------------------------------------------------------------------------------- 1 | ssh host: 2 | 3 | create jumpy user, for that user, create a keypair, private key goes in the contest image(group_vars/all) 4 | ``` 5 | root@jumpbox:~# useradd -m -U -s /bin/bash jumpy 6 | ssh-keygen -t ed25519 -f jumpbox_key 7 | ``` 8 | public key goes in the authorized_keys file for the jumpy user: 9 | command="echo 'This account can only be used for opening a reverse tunnel.'",no-agent-forwarding,no-X11-forwarding ssh-ed25519 SSH_PUBKEY jumpy@ssh.yourserver.com 10 | 11 | 12 | Now generate a second key, this will be used to actually ssh into the machines as root, and needs to be kept private/secret 13 | ``` 14 | ssh-keygen -t ed25519 -f ~/.ssh/id_ed25519 15 | ``` 16 | 17 | Make sure the jumpbox listens on port 443 for ssh connections as wel 18 | 19 | install parallel-ssh `sudo apt-get update && sudo apt-get install pssh` 20 | create discover-hosts.sh file with contents: 21 | ``` 22 | #!/bin/bash 23 | set -euo pipefail 24 | 25 | echo -n "" > ~/.ssh/config 26 | PORTS=$(sudo lsof -i4TCP -sTCP:LISTEN -P -n | sed -r 's/.*:([0-9]+).*/\1/' | tail -n +2 ) 27 | idx=0 28 | for p in $PORTS; do 29 | let idx=idx+1 30 | # Skip localhost 31 | if [[ $p == 443 || $p == 22 ]]; then continue; fi 32 | TEAM=$(ssh localhost -p $p -i ~/.ssh/id_ed25519 -l root -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no cat /icpc/TEAM 2>/dev/null| sed 's/team//') 33 | if [[ -z $TEAM ]]; then 34 | HOST=unknown-$idx 35 | else 36 | HOST=t$TEAM 37 | fi 38 | echo "$HOST - $p" 39 | cat >> ~/.ssh/config <Bulk Rename 89 | - xfce4-mail-reader.desktop # Mail Reader 90 | - thunar-volman-settings.desktop # Removable devices and media 91 | -------------------------------------------------------------------------------- /playbooks/devel_tools/intellij.yml: -------------------------------------------------------------------------------- 1 | # IntelliJ repository (unofficial ppa: https://github.com/JonasGroeger/jetbrains-ppa) 2 | - name: apt key for IntelliJ IDEA 3 | apt_key: 4 | url: https://s3.eu-central-1.amazonaws.com/jetbrains-ppa/0xA6E8698A.pub.asc 5 | state: present 6 | keyring: /etc/apt/trusted.gpg.d/jetbrains.gpg 7 | 8 | - name: apt repo for IntelliJ IDEA 9 | apt_repository: repo='deb http://jetbrains-ppa.s3-website.eu-central-1.amazonaws.com any main' update_cache=yes 10 | 11 | # IntelliJ IDEA community(~968mb installed in /opt/intellij-idea-community) 12 | - name: install IntelliJ IDEs 13 | when: "'intellij-idea' in devtools" 14 | apt: 15 | state: present 16 | pkg: 17 | - intellij-idea-community 18 | 19 | # For intellij+kotlin, we need to fetch kotlin from maven (it doesn't want to easily use the local files we have) 20 | - name: install maven 21 | apt: 22 | pkg: maven 23 | state: present 24 | - name: make sure m2 repository exists 25 | file: 26 | state: directory 27 | path: /opt/m2/repository 28 | mode: 0755 29 | owner: root 30 | group: root 31 | # TODO: figure out where 1.9.22 comes from (intellij has a default for this) 32 | # TODO: maybe from /opt/intellij-idea-community/plugins/Kotlin/kotlinc/build.txt 33 | - name: fetch some things from maven 34 | shell: mvn dependency:get -Dartifact="org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.9.22" -Dmaven.repo.local="/opt/m2/repository" 35 | 36 | # PyCharm Community(523mb installed in /opt/pycharm-community) 37 | - name: install PyCharm 38 | when: "'intellij-pycharm' in devtools" 39 | apt: 40 | state: present 41 | pkg: 42 | - pycharm-community 43 | 44 | # Clion(1028mb installed in /opt/clion) 45 | - name: install clion 46 | when: "'intellij-clion' in devtools" 47 | block: 48 | - name: install clion 49 | apt: 50 | state: present 51 | pkg: 52 | - clion 53 | 54 | # Clion likes a weirdly formatted keyfile (UCS2 encoding with a BOM) 55 | - name: create a script to configure clion license 56 | copy: 57 | dest: /usr/local/bin/clion-license-key-install 58 | mode: 0755 59 | content: | 60 | #!/bin/bash 61 | mkdir -p $HOME/.config/JetBrains/CLion2023.3 62 | ( 63 | printf '\xFF\xFF'; 64 | echo -en "\n{{ clion_license_key }}" | iconv -f UTF-8 -t UCS2 - 65 | ) > $HOME/.config/JetBrains/CLion2023.3/clion.key 66 | when: clion_license_key | length > 0 67 | 68 | - name: add licensekey script to autostart 69 | copy: 70 | dest: /etc/xdg/autostart/clion-license-key-install.desktop 71 | content: | 72 | [Desktop Entry] 73 | Type=Application 74 | Exec=/usr/local/bin/clion-license-key-install 75 | NoDisplay=true 76 | when: clion_license_key | length > 0 77 | 78 | - name: hide clion shortcut (if we don't have a license key for it) 79 | shell: mv /usr/share/applications/clion.desktop /usr/share/applications/clion.desktop.disabled 80 | args: 81 | creates: /usr/share/applications/clion.desktop.disabled 82 | when: clion_license_key | length == 0 83 | -------------------------------------------------------------------------------- /create_baseimg.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Settings 4 | ISO64="ubuntu-22.04.3-live-server-amd64.iso" 5 | OUT64="unattended-${ISO64}" 6 | IMG64="base-amd64.img" 7 | 8 | TMPDIR="tmp" 9 | USERDATA="configs/2204_autoinstall.yaml" 10 | METADATA="configs/2204_metadata" 11 | 12 | function usage() { 13 | echo "Usage: create_baseimage.sh [-s size]" 14 | echo "" 15 | echo "-s|--size n Size of the resulting image(default 14700M)" 16 | echo "--no-usb Don't create a fat32 partition for easy usb mounting" 17 | exit 1 18 | } 19 | 20 | ISO=$ISO64 21 | OUTISO=$OUT64 22 | IMG=$IMG64 23 | USB_PARTITION=1 24 | 25 | while [[ $# -ge 1 ]]; do 26 | key="$1" 27 | case $key in 28 | -s|--size) 29 | IMGSIZE=$2 30 | shift 31 | ;; 32 | --no-usb) 33 | USB_PARTITION=0 34 | ;; 35 | *) 36 | usage 37 | ;; 38 | esac 39 | shift 40 | done 41 | 42 | # Default image size 14700M(fits on an 16G flash drive) 43 | # IMGSIZE=${IMGSIZE:-14700M} 44 | # Default image size 28500M(fits on an 32G flash drive) 45 | IMGSIZE=${IMGSIZE:-28500M} 46 | 47 | 48 | function create_unattended_iso() { 49 | CONTENTSDIR="$TMPDIR/contents" 50 | rm -rf "$CONTENTSDIR" 51 | mkdir -p "$CONTENTSDIR" 52 | 53 | # Extract the efi partition out of the iso 54 | read -a EFI_PARTITION < <(parted -m $ISO unit b print | awk -F: '$1 == "2" { print $2,$3,$4}' | tr -d 'B') 55 | dd if=$ISO of=$TMPDIR/efi.img skip=${EFI_PARTITION[0]} bs=1 count=${EFI_PARTITION[2]} 56 | # # this is basically /usr/lib/grub/i386-pc/boot_hybrid.img from grub-pc-bin package (we just skip the end bits which xorriso will recreate) 57 | dd if=$ISO of=$TMPDIR/mbr.img bs=1 count=440 58 | 59 | 60 | #Use bsdtar if possible to extract(no root required) 61 | if hash bsdtar 2>/dev/null; then 62 | bsdtar xfp $ISO -C $CONTENTSDIR 63 | chmod -R u+w "$CONTENTSDIR" 64 | else 65 | # mount the iso, then copy the contents 66 | LOOPDIR="$TMPDIR/iso" 67 | mkdir -p "$LOOPDIR" 68 | sudo mount -o loop "$ISO" "$LOOPDIR" 69 | cp -rT "$LOOPDIR" "$CONTENTSDIR" 70 | sudo umount "$LOOPDIR" 71 | fi 72 | 73 | 74 | mkdir -p "$CONTENTSDIR/autoinst" 75 | cp "$USERDATA" "$CONTENTSDIR/autoinst/user-data" 76 | cp "$METADATA" "$CONTENTSDIR/autoinst/meta-data" 77 | if [[ $USB_PARTITION == 0 ]]; then 78 | # remove the ICPC partition from the user-data yaml if we aren't going to use it 79 | sed -i -e "/USB_PARTITION_ENABLED/d" "$CONTENTSDIR/autoinst/user-data" 80 | fi 81 | 82 | 83 | # Configure grub to start the autoinstall after 3 seconds 84 | cat < "$CONTENTSDIR/boot/grub/grub.cfg" 85 | set timeout=3 86 | 87 | loadfont unicode 88 | 89 | set menu_color_normal=white/black 90 | set menu_color_highlight=black/light-gray 91 | 92 | menuentry "Install Ubuntu Server (Unattended)" { 93 | set gfxpayload=keep 94 | linux /casper/vmlinuz autoinstall ds=nocloud\;seedfrom=/cdrom/autoinst/ net.ifnames=0 --- 95 | initrd /casper/initrd 96 | } 97 | EOF 98 | set -x 99 | 100 | # Finally pack up an ISO the new way 101 | xorriso -as mkisofs -r \ 102 | -V 'ATTENDLESS_UBUNTU' \ 103 | -o $OUTISO \ 104 | --grub2-mbr $TMPDIR/mbr.img \ 105 | -partition_offset 16 \ 106 | --mbr-force-bootable \ 107 | -append_partition 2 28732ac11ff8d211ba4b00a0c93ec93b $TMPDIR/efi.img \ 108 | -appended_part_as_gpt \ 109 | -iso_mbr_part_type a2a0d0ebe5b9334487c068b6b72699c7 \ 110 | -c '/boot.catalog' \ 111 | -b '/boot/grub/i386-pc/eltorito.img' \ 112 | -no-emul-boot -boot-load-size 4 -boot-info-table --grub2-boot-info \ 113 | -eltorito-alt-boot \ 114 | -e '--interval:appended_partition_2:::' \ 115 | -no-emul-boot \ 116 | $CONTENTSDIR 117 | set +x 118 | 119 | # cleanup 120 | rm -rf "$CONTENTSDIR" 121 | } 122 | 123 | create_unattended_iso 124 | 125 | # Install that base image 126 | rm -f "output/$IMG" 127 | set -x 128 | qemu-img create -f qcow2 -o size="$IMGSIZE" "output/$IMG" 129 | qemu-system-x86_64 \ 130 | --enable-kvm -m 4096 -global isa-fdc.driveA= \ 131 | -drive file="output/$IMG",index=0,media=disk,format=qcow2 \ 132 | -cdrom $OUTISO -boot order=d \ 133 | -net nic -net user,hostfwd=tcp::5222-:22,hostfwd=tcp::5280-:80 \ 134 | -vga qxl -vnc :0 \ 135 | -usbdevice tablet 136 | # -global isa-fdc.driveA= is used to disable floppy drive(gets rid of a warning message) 137 | -------------------------------------------------------------------------------- /configs/2004_autoinstall.yaml: -------------------------------------------------------------------------------- 1 | #cloud-config 2 | 3 | # Set up ssh/sudo access to the installer environment(so debugging any issues during installation is possible) 4 | users: 5 | - name: default 6 | ssh_authorized_keys: 7 | - ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIB9pjASmP4wQkhJ1VEbl0l1Vgn3lsOzctRS2m0wBVlaO ICPC ImageAdmin Key 8 | - name: imageadmin 9 | sudo: ['ALL=(ALL) NOPASSWD:ALL'] 10 | ssh_authorized_keys: 11 | - ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIB9pjASmP4wQkhJ1VEbl0l1Vgn3lsOzctRS2m0wBVlaO ICPC ImageAdmin Key 12 | 13 | # Docs on autoinstall are found here: https://ubuntu.com/server/docs/install/autoinstall-reference 14 | # Additional useful resources: 15 | # https://utcc.utoronto.ca/~cks/space/blog/linux/Ubuntu2004ISOAutoinst - how to bundle autoinstall with the cdrom 16 | # https://utcc.utoronto.ca/~cks/space/blog/linux/Ubuntu2004AutoinstFormat - good example of autoinstall setup 17 | autoinstall: 18 | version: 1 19 | # Locale and keyboard layout 20 | locale: en_US.UTF-8 21 | keyboard: 22 | layout: us 23 | variant: '' 24 | toggle: null 25 | user-data: 26 | timezone: America/New_York 27 | network: 28 | # network: # this extra level was required for ubuntu 20.04 GA (it was fixed in later point releases) 29 | version: 2 30 | ethernets: 31 | # We disable persistent network interface naming on the kernel command line, so this is always eth0 32 | eth0: {dhcp4: true} 33 | storage: 34 | # Disable swap 35 | swap: {size: 0} 36 | config: 37 | # Docs on this section are found here: https://curtin.readthedocs.io/en/latest/topics/storage.html 38 | # This makes 4 partitions 39 | # 1 - bios_grub (space for grub stage2 to live) 1MiB 40 | # 2 - EFI partition 41 | # 3 - ICPC fat32 partition (to make files easy to load after the contest ends without booting the usb drive) 42 | # 4 - linux root partition 43 | - {type: disk, id: disk0, grub_device: true, ptable: gpt, wipe: superblock-recursive} 44 | - {type: partition, device: disk0, number: 1, id: partition-bios, flag: bios_grub, size: 1M} 45 | - {type: partition, device: disk0, number: 2, id: partition-efi, flag: boot, size: 128M} 46 | - {type: partition, device: disk0, number: 3, id: partition-icpc, name: icpc, size: 192M} 47 | - {type: partition, device: disk0, number: 4, id: partition-root, flag: linux, size: -1} 48 | - {type: format, volume: partition-efi, id: format-efi, fstype: fat32, label: EFI} 49 | - {type: format, volume: partition-icpc, id: format-icpc, fstype: fat32, label: ICPC} 50 | - {type: format, volume: partition-root, id: format-root, fstype: ext4, extra_options: ['-m', '0']} 51 | - {type: mount, device: format-efi, id: mount-efi, path: /boot/efi} 52 | - {type: mount, device: format-root, id: mount-root, path: /, options: 'noatime,nodiratime,errors=remount-ro'} 53 | identity: 54 | hostname: icpc 55 | username: imageadmin 56 | # This crypted password corresponds to 'imageadmin' 57 | password: $6$D1vml7SluH/Pfw43$upy5UKqf6iZtLGXRXcAUAqCDMpFMWiZcve9tj16/5l1eD8j5YWoVYCmLvxl6eXrRmSKSngIiH5.NJBNMx.SZg0 58 | 59 | # Set up ssh with a public key we'll use to bootstrap the rest of the system 60 | ssh: 61 | install-server: yes 62 | authorized-keys: 63 | - ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIB9pjASmP4wQkhJ1VEbl0l1Vgn3lsOzctRS2m0wBVlaO ICPC ImageAdmin Key 64 | allow-pw: yes 65 | 66 | packages: [] 67 | late-commands: 68 | # This fixes a weird issue with qemu not wanting to boot the system unless you 69 | # hold down shift, and manually pick the os to boot. 70 | # Disable graphical grub console 71 | # - sed -i -e 's/#\(GRUB_TERMINAL.*\)/\1/' /target/etc/default/grub 72 | 73 | # Enable passwordless sudo 74 | - echo '%sudo ALL=(ALL) NOPASSWD:ALL' > /target/etc/sudoers.d/icpc 75 | # TODO: what was the value of GRUB_CMDLINE_LINUX before this line nukes it? 76 | - cp /target/etc/default/grub /target/etc/default/grub.orig 77 | - sed -ie 's/GRUB_CMDLINE_LINUX=.*/GRUB_CMDLINE_LINUX="net.ifnames=0"/' /target/etc/default/grub 78 | # Do these matter(or did they get nuked above?) 79 | - sed -ie 's/quiet splash//' /target/etc/default/grub 80 | 81 | - curtin in-target update-grub2 82 | # The original version of subiquity had a bug where specifying --target /target was required 83 | # - curtin in-target --target /target update-grub2 84 | 85 | # Poweroff because otherwise it'll reboot 86 | - poweroff 87 | -------------------------------------------------------------------------------- /configs/2204_autoinstall.yaml: -------------------------------------------------------------------------------- 1 | #cloud-config 2 | 3 | # Set up ssh/sudo access to the installer environment(so debugging any issues during installation is possible) 4 | users: 5 | - name: default 6 | ssh_authorized_keys: 7 | - ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIB9pjASmP4wQkhJ1VEbl0l1Vgn3lsOzctRS2m0wBVlaO ICPC ImageAdmin Key 8 | - name: imageadmin 9 | sudo: 'ALL=(ALL) NOPASSWD:ALL' 10 | ssh_authorized_keys: 11 | - ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIB9pjASmP4wQkhJ1VEbl0l1Vgn3lsOzctRS2m0wBVlaO ICPC ImageAdmin Key 12 | 13 | # Try to add a key elsewhere to enable login... 14 | ssh_authorized_keys: 15 | - ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIB9pjASmP4wQkhJ1VEbl0l1Vgn3lsOzctRS2m0wBVlaO ICPC ImageAdmin Key 16 | 17 | # Docs on autoinstall are found here: https://ubuntu.com/server/docs/install/autoinstall-reference 18 | # Additional useful resources: 19 | # https://utcc.utoronto.ca/~cks/space/blog/linux/Ubuntu2004ISOAutoinst - how to bundle autoinstall with the cdrom 20 | # https://utcc.utoronto.ca/~cks/space/blog/linux/Ubuntu2004AutoinstFormat - good example of autoinstall setup 21 | autoinstall: 22 | version: 1 23 | # Locale and keyboard layout 24 | locale: en_US.UTF-8 25 | keyboard: 26 | layout: us 27 | variant: '' 28 | toggle: null 29 | timezone: America/New_York 30 | network: 31 | version: 2 32 | ethernets: 33 | # We disable persistent network interface naming on the kernel command line, so this is always eth0 34 | eth0: {dhcp4: true} 35 | storage: 36 | version: 2 37 | # Disable swap 38 | swap: {size: 0} 39 | config: 40 | # Docs on this section are found here: https://curtin.readthedocs.io/en/latest/topics/storage.html 41 | # This makes 4 partitions 42 | # 1 - bios_grub (space for grub stage2 to live) 1MiB 43 | # 2 - EFI partition 44 | # 3 - ICPC fat32 partition (to make files easy to load after the contest ends without booting the usb drive) 45 | # 4 - linux root partition 46 | - {type: disk, id: disk0, grub_device: true, ptable: gpt, wipe: superblock-recursive} 47 | - {type: partition, device: disk0, number: 1, id: partition-bios, flag: bios_grub, size: 1M} 48 | - {type: partition, device: disk0, number: 2, id: partition-efi, flag: boot, size: 128M} 49 | - {type: partition, device: disk0, number: 3, id: partition-icpc, partition_type: EBD0A0A2-B9E5-4433-87C0-68B6B72699C7, name: icpc, size: 192M} # TAG:USB_PARTITION_ENABLED 50 | - {type: partition, device: disk0, number: 4, id: partition-root, flag: linux, size: -1} 51 | - {type: format, volume: partition-efi, id: format-efi, fstype: fat32, label: EFI} 52 | - {type: format, volume: partition-icpc, id: format-icpc, fstype: fat32, label: ICPC} # TAG:USB_PARTITION_ENABLED 53 | - {type: format, volume: partition-root, id: format-root, fstype: ext4, extra_options: ['-m', '0']} 54 | - {type: mount, device: format-efi, id: mount-efi, path: /boot/efi} 55 | # Set commit=60, to help improve performance 56 | - {type: mount, device: format-root, id: mount-root, path: /, options: 'noatime,nodiratime,errors=remount-ro,commit=60'} 57 | identity: 58 | hostname: icpc 59 | username: imageadmin 60 | # This crypted password corresponds to 'imageadmin' 61 | password: $6$D1vml7SluH/Pfw43$upy5UKqf6iZtLGXRXcAUAqCDMpFMWiZcve9tj16/5l1eD8j5YWoVYCmLvxl6eXrRmSKSngIiH5.NJBNMx.SZg0 62 | 63 | # Set up ssh with a public key we'll use to bootstrap the rest of the system 64 | ssh: 65 | install-server: yes 66 | authorized-keys: 67 | - ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIB9pjASmP4wQkhJ1VEbl0l1Vgn3lsOzctRS2m0wBVlaO ICPC ImageAdmin Key 68 | allow-pw: yes 69 | 70 | packages: [] 71 | # updates: all # install all updates after the installer finishes (default 'security') 72 | updates: security # install all updates after the installer finishes (default 'security') 73 | late-commands: 74 | # Fix the filesystem type of the icpc fat32 partition (curtain sets it to "Linux filesystem", even though we specify partition_type) 75 | - sfdisk --part-type /dev/sda 3 EBD0A0A2-B9E5-4433-87C0-68B6B72699C7 # TAG:USB_PARTITION_ENABLED 76 | 77 | # Enable passwordless sudo 78 | - echo '%sudo ALL=(ALL) NOPASSWD:ALL' > /target/etc/sudoers.d/icpc 79 | - cp /target/etc/default/grub /target/etc/default/grub.orig 80 | - sed -ie 's/GRUB_CMDLINE_LINUX=.*/GRUB_CMDLINE_LINUX="net.ifnames=0"/' /target/etc/default/grub 81 | # Do these matter(or did they get nuked above?) 82 | - sed -ie 's/quiet splash//' /target/etc/default/grub 83 | 84 | - curtin in-target update-grub2 85 | 86 | # Poweroff because otherwise it'll reboot 87 | - poweroff 88 | -------------------------------------------------------------------------------- /main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Do Everything 3 | hosts: vm 4 | become: true 5 | user: imageadmin 6 | gather_facts: true 7 | vars: 8 | ansible_python_interpreter: /usr/bin/python3 9 | tasks: 10 | 11 | - name: copy pam_environment to make sure the proxy is disabled for icpcadmin(and root) 12 | copy: src=files/pam_environment dest={{ item }}/.pam_environment 13 | with_items: 14 | - /root 15 | - /home/imageadmin 16 | 17 | - name: copy updated pam sudo config so it reads .pam_environment 18 | copy: src=files/pam_sudo dest=/etc/pam.d/sudo 19 | 20 | 21 | - name: disable fsync for dpkg 22 | copy: dest=/etc/dpkg/dpkg.cfg.d/02-dpkg-no-sync content="force-unsafe-io" 23 | - name: disable apt cache 24 | copy: 25 | dest: /etc/apt/apt.conf.d/02-fast-apt 26 | content: | 27 | # Disable some apt-caching 28 | Dir::Cache { 29 | srcpkgcache ""; 30 | pkgcache ""; 31 | } 32 | # No translations 33 | Acquire::Language "none"; 34 | 35 | - name: be sure apt cache is updated 36 | apt: update_cache=yes upgrade=dist 37 | 38 | - name: set up efi booting 39 | apt: 40 | pkg: [ grub-efi, grub-efi-amd64-signed ] 41 | state: present 42 | - name: run grub-install for efi 43 | command: grub-install --no-nvram --uefi-secure-boot --target=x86_64-efi /dev/sda 44 | 45 | 46 | - name: remove snap package 47 | apt: 48 | name: snapd 49 | purge: true 50 | state: absent 51 | - name: clean up any leftover snap data 52 | file: 53 | state: absent 54 | path: "{{item}}" 55 | with_items: 56 | - /snap 57 | - /var/snap 58 | - /var/lib/snapd 59 | - /var/cache/snapd 60 | - /run/snapd-snap.socket 61 | - /run/snapd.socket 62 | - /etc/apt/apt.conf.d/20snapd.conf 63 | - name: prevent snapd from being installed later 64 | copy: 65 | dest: /etc/apt/preferences.d/snapd-disable 66 | content: | 67 | Package: snapd 68 | Pin: release * 69 | Pin-Priority: -1 70 | 71 | # remove cloud init, because it's a security issue (a cd/other usb drive could give someone root) 72 | - name: remove cloud-init 73 | apt: 74 | pkg: cloud-init 75 | state: absent 76 | purge: yes 77 | 78 | - import_tasks: 'playbooks/gui.yml' 79 | 80 | - import_tasks: 'playbooks/reverseproxy.yml' 81 | - import_tasks: 'playbooks/compilers.yml' 82 | - import_tasks: 'playbooks/devel_tools.yml' 83 | 84 | - import_tasks: 'playbooks/icpc.yml' 85 | - import_tasks: 'playbooks/vmtouch.yml' 86 | 87 | - import_tasks: 'playbooks/firewall.yml' 88 | - import_tasks: 'playbooks/system.yml' 89 | 90 | # Management related things 91 | - import_tasks: 'playbooks/ansible-pull.yml' 92 | - import_tasks: 'playbooks/reversetunnel.yml' 93 | - import_tasks: 'playbooks/vpn.yml' 94 | - import_tasks: 'playbooks/monitoring.yml' 95 | 96 | - name: autoremove/autoclean apt 97 | block: 98 | - apt: autoremove=yes 99 | - apt: autoclean=yes 100 | - shell: apt-get clean 101 | 102 | - name: ensure systemd-timesyncd is running (to make sure ntp is working properly) 103 | # This will/should fail if ntp is installed 104 | service: name=systemd-timesyncd state=started 105 | 106 | # Copy some build information to the image 107 | - shell: 'echo "Built on $(date +"%Y-%m-%d %H:%M:%S")\nRevision: $(git rev-list --full-history --all --abbrev-commit | head -1)\n"' 108 | become: false 109 | register: git_revision 110 | delegate_to: 127.0.0.1 111 | - name: copy version info 112 | copy: content="{{git_revision.stdout}}\n" dest=/icpc/version 113 | 114 | # - name: zero out the disk so it's more optimal (this is part of the makeDist script) 115 | # shell: | 116 | # dd if=/dev/zero of=/empty bs=1M || true 117 | # rm -f /empty 118 | # sync 119 | 120 | handlers: 121 | - name: clear user password 122 | command: passwd -d contestant 123 | 124 | - name: update grub 125 | command: /usr/sbin/update-grub 126 | 127 | - name: restart squid 128 | service: name=squid state=restarted 129 | 130 | - name: update-ca-certificates 131 | command: /usr/sbin/update-ca-certificates 132 | 133 | - name: restart ssh 134 | service: name=ssh state=restarted 135 | 136 | - name: reload nginx 137 | ansible.builtin.service: 138 | name: nginx 139 | state: reloaded 140 | -------------------------------------------------------------------------------- /playbooks/monitoring.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: install node-exporter 3 | apt: pkg=prometheus-node-exporter state=present 4 | 5 | - name: also make sure moreutils is installed (for sponge) 6 | apt: pkg=moreutils state=present 7 | 8 | - name: drop a configuration file for node exporter to disable systemd metrics (it's noisy/slowish) 9 | copy: 10 | dest: /etc/default/prometheus-node-exporter 11 | content: ARGS="--no-collector.systemd" 12 | # --collector.disable-defaults 13 | # --collector.arp 14 | # --collector.bcache 15 | # --collector.bonding 16 | # # --collector.btrfs 17 | # --collector.conntrack 18 | # --collector.cpu 19 | # --collector.cpufreq 20 | # --collector.diskstats 21 | # # --collector.dmi 22 | # --collector.edac 23 | # --collector.entropy 24 | # # --collector.exec 25 | # # --collector.fiberchannel 26 | # --collector.filefd 27 | # --collector.filesystem 28 | # --collector.hwmon 29 | # --collector.infiniband 30 | # --collector.ipvs 31 | # --collector.loadavg 32 | # --collector.mdadm 33 | # --collector.meminfo 34 | # --collector.netclass 35 | # --collector.netdev 36 | # --collector.netstat 37 | # --collector.nfs 38 | # --collector.nfsd 39 | # # --collector.nvme 40 | # # --collector.os 41 | # # --collector.powersupplyclass 42 | # --collector.pressure 43 | # # --collector.rapl 44 | # # --collector.schedstat 45 | # --collector.sockstat 46 | # # --collector.softnet 47 | # --collector.stat 48 | # --collector.systemd 49 | # # --collector.tapestats 50 | # --collector.textfile 51 | # --collector.textfile.directory /var/lib/prometheus/node-exporter 52 | # # --collector.thermal 53 | # # --collector.thermal_zone 54 | # --collector.time 55 | # --collector.timex 56 | # # --collector.udp_queues 57 | # --collector.uname 58 | # --collector.vmstat 59 | # --collector.xfs 60 | # --collector.zfs 61 | 62 | - name: disable some timers that come with node-exporter that we don't want 63 | service: name={{item}} enabled=no 64 | with_items: 65 | - prometheus-node-exporter-smartmon.timer 66 | - prometheus-node-exporter-apt.timer 67 | # These are configured, but inactive (conditional false) 68 | - prometheus-node-exporter-ipmitool-sensor.timer # inactive because no /usr/bin/ipmitool binary (and /sys/class/ipmi empty) 69 | - prometheus-node-exporter-mellanox-hca-temp.timer # inactive because no /usr/bin/mget_temp_ext binary (and /sys/class/infiniband missing) 70 | 71 | - name: override node-exporter to run nice'd/idle/low io+cpu priority 72 | block: 73 | - file: 74 | path: /etc/systemd/system/prometheus-node-exporter.service.d 75 | state: directory 76 | - copy: 77 | dest: /etc/systemd/system/prometheus-node-exporter.service.d/override.conf 78 | content: | 79 | [Service] 80 | CPUSchedulingPolicy=other 81 | Nice=19 82 | IOSchedulingClass=idle 83 | 84 | - name: restart node-exporter 85 | service: name=prometheus-node-exporter state=started enabled=yes 86 | 87 | - name: add firewall rule so the contestmanagement host can access prometheus 88 | ufw: 89 | rule: allow 90 | direction: in 91 | interface: contest 92 | proto: tcp 93 | src: "{{ contestmanager_ip }}" 94 | port: 9100 95 | 96 | 97 | # add some custom "metrics" like roles/sites/etc 98 | - name: install our custom icpc-metrics script 99 | copy: 100 | src: files/icpc-metrics 101 | dest: /usr/local/bin/icpc-metrics 102 | mode: 0755 103 | - name: static metrics exporter 104 | copy: 105 | dest: /etc/systemd/system/icpc-static-node-exporter.service 106 | content: | 107 | [Unit] 108 | Description=Update static node-exporter metrics with icpc things 109 | After=network.target 110 | 111 | [Service] 112 | Restart=on-failure 113 | RestartSec=30 114 | Type=oneshot 115 | ExecStart=/usr/local/bin/icpc-metrics 116 | 117 | [Install] 118 | WantedBy=multi-user.target 119 | 120 | - name: enable icpc-static-node-exporter.service 121 | service: name=icpc-static-node-exporter.service enabled=yes 122 | 123 | - name: static metric trigger 124 | copy: 125 | dest: /etc/systemd/system/icpc-static-node-exporter.path 126 | content: | 127 | # trigger metrics update whenever TEAM/SITE/version/update-version change so we can update the values in prometheus 128 | [Path] 129 | PathModified=/icpc/TEAM 130 | PathModified=/icpc/SITE 131 | PathModified=/icpc/version 132 | PathModified=/icpc/update-version 133 | 134 | [Install] 135 | WantedBy=multi-user.target 136 | 137 | - name: enable icpc-static-node-exporter.path 138 | service: name=icpc-static-node-exporter.path enabled=yes 139 | -------------------------------------------------------------------------------- /files/squid/block.html.j2: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 67 | 68 | 69 |
70 | 71 | 72 |
73 |

The site you are trying to access cannot be displayed

74 |
%U
75 |

76 | During the programming contest general access to the internet is not provided or allowed. 77 | You may only use the materials you brought with you for reference, plus any documentation available locally on this machine.

78 |
79 |

Local Documentation

80 | 81 | {% for k,v in lang_docs.items() %} 82 | 83 | {% endfor %} 84 |
{{ k }}{{v['name']}}
85 |

A complete list of reference material available on your machine can be found here:
Local Language Reference

86 | 87 |
88 | 89 | 90 |
91 | 92 | 93 | -------------------------------------------------------------------------------- /playbooks/devel_tools/eclipse.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: install eclipse(64bit) 3 | unarchive: src=files/eclipse-java-2023-12-R-linux-gtk-x86_64.tar.gz dest=/opt creates=/opt/eclipse 4 | 5 | - name: eclipse shortcut 6 | copy: 7 | dest: /usr/share/applications/Eclipse.desktop 8 | content: | 9 | [Desktop Entry] 10 | Version=1.0 11 | Type=Application 12 | Name=Eclipse 13 | Comment=Eclipse 14 | Exec=/opt/eclipse/eclipse 15 | Icon=/opt/eclipse/icon.xpm 16 | Terminal=false 17 | StartupNotify=false 18 | Categories=Application;Development;X-Development; 19 | 20 | # Check if plugin is installed by looking for the IU in: 21 | # /opt/eclipse/eclipse -nosplash -application org.eclipse.equinox.p2.director -listInstalledRoots 22 | # output is IU/version number. last line is a timestamp 23 | - name: get installed eclipse plugins 24 | shell: | 25 | /opt/eclipse/eclipse -nosplash \ 26 | -application org.eclipse.equinox.p2.director \ 27 | -listInstalledRoots 28 | register: eclipse_installed_pkgs 29 | # Eclipse plugins(for c/c++ and python) 30 | - name: install cdt plugin 31 | when: '"org.eclipse.cdt.platform.feature.group" not in eclipse_installed_pkgs.stdout' 32 | shell: | 33 | /opt/eclipse/eclipse -nosplash \ 34 | -application org.eclipse.equinox.p2.director \ 35 | -repository http://download.eclipse.org/tools/cdt/releases/latest/,http://download.eclipse.org/releases/2023-12/ \ 36 | -destination /opt/eclipse \ 37 | -installIU org.eclipse.cdt.feature.group \ 38 | -installIU org.eclipse.cdt.platform.feature.group | grep -v DEBUG 39 | 40 | - name: install pydev plugin 41 | when: '"org.python.pydev.feature.feature.group" not in eclipse_installed_pkgs.stdout' 42 | shell: | 43 | /opt/eclipse/eclipse -nosplash \ 44 | -application org.eclipse.equinox.p2.director \ 45 | -repository http://pydev.org/updates/ \ 46 | -destination /opt/eclipse \ 47 | -installIU org.python.pydev.feature.feature.group | grep -v DEBUG 48 | 49 | # See this for how to set global eclipse configurations 50 | - name: configure eclipse 51 | copy: 52 | dest: /opt/eclipse/eclipse_plugincustomization.ini 53 | content: | 54 | # README 55 | # To find values for this file, just run eclipse, export preferences 56 | # Then make changes to some preferences, export to second file 57 | # Compare the differences, then chop off the instance/ at the beginning and put it here 58 | # Following the examples above. 59 | 60 | # don't show the splash screen 61 | org.eclipse.ui/showIntro=false 62 | 63 | # disable error reporting(It's usually a pop up on first run) 64 | org.eclipse.epp.logging.aeri.ui/action=IGNORE 65 | org.eclipse.epp.logging.aeri.ui/configured=true 66 | 67 | # default to jdk 11 compliance level (otherwise it's 17 for eclipse 2021-12) 68 | org.eclipse.jdt.core/org.eclipse.jdt.core.compiler.source=11 69 | org.eclipse.jdt.core/org.eclipse.jdt.core.compiler.compliance=11 70 | org.eclipse.jdt.core/org.eclipse.jdt.core.compiler.codegen.targetPlatform=11 71 | 72 | # set the available jdks...(this from export/import...) 73 | org.eclipse.jdt.launching/org.eclipse.jdt.launching.PREF_VM_XML=\n\n \n \n \n \n \n \n \n \n \n \n \n \n\n 74 | 75 | - name: fixup the eclipse.ini file to use our custom configuration 76 | lineinfile: dest=/opt/eclipse/eclipse.ini insertafter="-vmargs" line="-Declipse.pluginCustomization=/opt/eclipse/eclipse_plugincustomization.ini" 77 | -------------------------------------------------------------------------------- /runvm.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Default group for the VM in ansible. This lets you use group_vars/$VARIANT for site specific configuration 4 | VARIANT=${1:-all} 5 | 6 | SSHPORT=2222 7 | SSHKEY="$PWD/configs/imageadmin-ssh_key" 8 | PIDFILE="tmp/qemu.pid" 9 | SNAPSHOT="-snapshot" 10 | ALIVE=0 11 | 12 | BASEIMG="base-amd64.img" 13 | 14 | trap ctrl_c INT 15 | function ctrl_c() { 16 | cleanup 17 | exit 0 18 | } 19 | 20 | 21 | function runssh() { 22 | chmod 0400 "$SSHKEY" 23 | ssh -i "$SSHKEY" -o BatchMode=yes -o ConnectTimeout=1 -o IdentitiesOnly=yes -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null imageadmin@localhost -p$SSHPORT "$@" 2>/dev/null 24 | } 25 | 26 | function cleanup() { 27 | if [ $ALIVE -eq 1 ]; then 28 | echo "Attempting graceful shutdown" 29 | runssh sudo poweroff 30 | else 31 | echo "Forcing shutdown(poweroff)" 32 | kill "$(cat $PIDFILE)" 33 | fi 34 | rm -f $PIDFILE 35 | } 36 | 37 | function waitforssh() { 38 | # wait for it to boot 39 | echo -n "Waiting for ssh " 40 | TIMEOUT=600 41 | X=0 42 | 43 | while [[ $X -lt $TIMEOUT ]]; do 44 | let X+=1 45 | OUT=$(runssh echo "ok" 2>/dev/null) 46 | if [[ "$OUT" == "ok" ]]; then 47 | ALIVE=1 48 | break 49 | fi 50 | echo -n "." 51 | sleep 5 52 | done 53 | echo "" 54 | 55 | if [ $ALIVE -eq 0 ]; then 56 | echo "Timed out waiting for host to respond" 57 | cleanup 58 | exit 1 59 | else 60 | echo "Host is alive! You can ssh in now" 61 | fi 62 | } 63 | 64 | function runansible() { 65 | echo "Running ansible" 66 | echo "Started at $(date)" 67 | INVENTORY_FILE=$(mktemp) 68 | cat < $INVENTORY_FILE 69 | vm ansible_port=$SSHPORT ansible_host=127.0.0.1 70 | [$VARIANT] 71 | vm 72 | EOF 73 | ANSIBLE_HOST_KEY_CHECKING=False ansible-playbook -i $INVENTORY_FILE --diff --become -u imageadmin --private-key $SSHKEY --ssh-extra-args="-o IdentitiesOnly=yes -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null" main.yml 74 | rm -f $INVENTORY_FILE 75 | echo "Ansible finished at $(date)" 76 | 77 | echo "Rebooting..." 78 | runssh sudo reboot 79 | # Wait 5 seconds for reboot to happen so we don't ssh back in before it actually reboots 80 | sleep 5 81 | ALIVE=0 82 | waitforssh 83 | } 84 | function launchssh() { 85 | echo "Launching ssh session" 86 | ssh -i $SSHKEY -o BatchMode=yes -o ConnectTimeout=1 -o IdentitiesOnly=yes -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null imageadmin@localhost -p$SSHPORT 87 | } 88 | 89 | function saveuserhome() { 90 | echo "pulling contestant home directory changes from inside vm" 91 | pushd home_dirs/contestant 92 | GIT_SSH_COMMAND="ssh -i $SSHKEY -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o IdentitiesOnly=yes " git ls-remote --exit-code virtualmachine 93 | if [[ $? != 0 ]]; then 94 | git remote add virtualmachine ssh://imageadmin@localhost:$SSHPORT/home/contestant 95 | fi 96 | GIT_SSH_COMMAND="ssh -i $SSHKEY -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o IdentitiesOnly=yes " git fetch virtualmachine 97 | 98 | echo "run: 'cd home_dirs/contestant && git merge virtualmachine/master' to pull these changes in" 99 | popd 100 | } 101 | 102 | function saveadminhome() { 103 | echo "pulling admin home directory changes from inside vm" 104 | pushd home_dirs/admin 105 | GIT_SSH_COMMAND="ssh -i $SSHKEY -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o IdentitiesOnly=yes " git ls-remote --exit-code virtualmachine 106 | if [[ $? != 0 ]]; then 107 | git remote add virtualmachine ssh://imageadmin@localhost:$SSHPORT/home/icpcadmin 108 | fi 109 | GIT_SSH_COMMAND="ssh -i $SSHKEY -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o IdentitiesOnly=yes " git fetch virtualmachine 110 | 111 | echo "run: 'cd home_dirs/contestant && git merge virtualmachine/master' to pull these changes in" 112 | popd 113 | } 114 | 115 | function setresolution() { 116 | echo "Setting resolution to 1440x900(temporarily)" 117 | runssh sudo -u contestant env DISPLAY=:0 xrandr --size 1440x900 118 | } 119 | 120 | qemu-system-x86_64 -smp 2 -m 4096 -drive file="output/$BASEIMG",index=0,media=disk,format=qcow2 -global isa-fdc.driveA= --enable-kvm -net user,hostfwd=tcp::$SSHPORT-:22 -net nic --daemonize --pidfile $PIDFILE $SNAPSHOT -vnc :0 -vga qxl -spice port=5901,disable-ticketing -usbdevice tablet 121 | ALIVE=0 122 | waitforssh 123 | 124 | CMD=1 125 | while [ $CMD != 0 ]; do 126 | echo "Select an action" 127 | echo " 1. Launch SSH Session" 128 | echo " 2. Run ansible" 129 | echo " 3. Save contestant home directory" 130 | echo " 4. Save admin home directory" 131 | echo " 5. Set resolution(1440x900)" 132 | echo " 0. Halt VM" 133 | read -p "Action(Default 1): " CMD 134 | CMD=${CMD:-1} 135 | case $CMD in 136 | 0) break ;; 137 | 1) launchssh ;; 138 | 2) runansible ;; 139 | 3) saveuserhome ;; 140 | 4) saveadminhome ;; 141 | 5) setresolution ;; 142 | *) launchssh ;; 143 | esac 144 | done 145 | 146 | echo 147 | echo 148 | read -p "Press enter to halt" 149 | 150 | cleanup 151 | exit 0 152 | -------------------------------------------------------------------------------- /group_vars/all.dist: -------------------------------------------------------------------------------- 1 | --- 2 | icpc_timezone: 'America/New_York' 3 | 4 | git_user_home: 'https://github.com/icpc-environment/fuzzy-octo-dangerzone.git' 5 | git_admin_home: 'https://github.com/icpc-environment/tripping-computing-machine.git' 6 | 7 | icpcadmin_pass: icpcadmin 8 | 9 | # urls we are configured to send autologin credentials to 10 | # TODO: allow https as well :/ 11 | squid_autologin_urls: [] 12 | # - ^http://ser2019.cloudcontest.org/login 13 | 14 | # this is the wireguard server's wireguard interface ip address 15 | # i.e. the address we can use when we want to talk to the vpn server 16 | contestmanager_ip: fd00:a0a8:34d:2a00::1 17 | 18 | # This is the config server used by the icpc_setup script. It contains sites, teams, and printer configurations. 19 | # See the readme for more information on what files this server needs to return 20 | config_url: http://configs.cloudcontest.org 21 | 22 | # could be contestmanager.icpcnet.internal and run over the wireguard vpn 23 | # not sure which is likely to be more reliable... 24 | ansible_pull_host: icpc.cloudcontest.org 25 | ansible_pull_port: 443 26 | ansible_pull_path: ~/ansible 27 | 28 | # The host details for the wireguard registration script. It connects over ssh to 29 | # this and expects the output of that ssh session to be a wireguard config file. 30 | # This is then loaded and wireguard started so the client connects to the VPN. 31 | wireguard_host: icpc.cloudcontest.org 32 | wireguard_client_user: wg_client 33 | wireguard_port: 443 34 | 35 | # Generate this using `wg genkey`, it is the server's wireguard private key 36 | wg_vpn_server_private_key: wEN5iYXA8M4JD3HIa17mhtDID3+/HQbLFGVN9USC9XE= 37 | # This is the external hostname that your management server resolves to 38 | wg_vpn_server_external_hostname: icpc.cloudcontest.org 39 | # This needs to match the externally visible ip address of your management server 40 | wg_vpn_server_external_ip: 0.1.2.3 41 | # the port wireguard will use (maybe change to 443 to help bypass firewalls) 42 | wg_vpn_server_wg_port: 51820 43 | wg_vpn_server_subnet: fd00:a0a8:34d:2a00::/64 44 | 45 | 46 | # Maybe more accurately named "ssh tunnel host" 47 | jumpbox_host: icpc.cloudcontest.org 48 | 49 | languages: 50 | - c 51 | - cpp 52 | - java 53 | - kotlin 54 | - python3 55 | # - clojure 56 | # - c-sharp 57 | # - dart 58 | # - d 59 | # - elixir 60 | # - erlang 61 | # - fortran 62 | # - f-sharp 63 | # - gnu_ada 64 | # - go 65 | # - groovy 66 | # - haskell 67 | # - js 68 | # - lua 69 | # - nim 70 | # - obj-c 71 | # - ocaml 72 | # - pascal 73 | # - prolog 74 | # - python2 75 | # - ruby 76 | # - rust 77 | # - r 78 | # - scala 79 | 80 | devtools: 81 | - intellij-idea 82 | - intellij-clion 83 | - intellij-pycharm 84 | - eclipse 85 | - geany 86 | #- netbeans # netbeans isn't a thing anymore since 22.04 87 | - codeblocks 88 | # - monodevelop 89 | 90 | # Firefox policies. Documented here: 91 | # https://github.com/mozilla/policy-templates/blob/master/README.md 92 | firefox_policies: 93 | # Set (and lock) the homepage to the given url 94 | Homepage: 95 | URL: "http://contest" 96 | Locked: true 97 | StartPage: homepage-locked 98 | # Add some bookmark entries (and always show the bookmarks toolbar) 99 | DisplayBookmarksToolbar: always 100 | Bookmarks: 101 | - Title: Contest Site 102 | URL: "http://contest" 103 | Placement: "toolbar" 104 | - Title: Documentation 105 | URL: "http://localhost/" 106 | Placement: "toolbar" 107 | 108 | firefox_default_policies: 109 | # Disable internet checking (it'll fail when the proxy is on so we want to avoid the notification banner) 110 | CaptivePortal: false 111 | # Prevent updating (shouldn't happen on linux, but we want to avoid it anyway) 112 | DisableAppUpdate: true 113 | # Prevent first run page/post update pages from showing up 114 | OverrideFirstRunPage: "" 115 | OverridePostUpdatePage: "" 116 | # What it says on the tin. This is to prevent a banner on first start about what it's sharing 117 | DisableTelemetry: true 118 | # Disable DNS over HTTPS (this might get around filters/things) 119 | DNSOverHTTPS: 120 | Enabled: false 121 | # Install the squid CA so it can mitm things/properly filter the network 122 | Certificates: { 123 | ImportEnterpriseRoots: true, 124 | Install: ["/etc/squid/squidCA.crt"] 125 | } 126 | 127 | # Disable a bunch of random features we don't need 128 | DisablePocket: true 129 | DisableFirefoxStudies: true 130 | DisableFirefoxAccounts: true 131 | DisableFeedbackCommands: true 132 | NewTabPage: false 133 | NoDefaultBookmarks: true 134 | 135 | # Disable a bunch of messages 136 | UserMessaging: 137 | WhatsNew: false 138 | ExtensionRecommendations: false 139 | FeatureRecommendations: false 140 | UrlbarInterventions: false 141 | SkipOnboard: true 142 | MoreFromMozilla: false 143 | Locked: true 144 | 145 | # A list of sites to configure in the nginx proxy. Only these will pass through, all others will fail 146 | reverseproxy_sites: 147 | contest: 148 | backend_host: "1.2.3.4" 149 | scheme: 'http' 150 | port: "12345" # strings not ints 151 | paths: 152 | - {path: "/"} 153 | #kattis.com: 154 | # paths: 155 | # - {path: "/"} 156 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # ICPC Contest Image Tools 2 | 3 | This repository contains the tools necessary to build the ICPC Southeast Regional contestant image. The contestant image is a linux installation optimized for booting off a flash drive that is used by all the teams in our region. 4 | 5 | ## Key Features 6 | This image has been tuned and tweaked over the years, but it currently supports the following: 7 | 8 | * A wide array of programming languages: c, c++, java, haskell, pascal, python2/3, scala, fortran, ADA, c#, f#, D, lua, go, ruby, erlang, groovy, nim, clojure, prolog, objective-c 9 | * Multiple IDEs and developer tools: Eclipse(with PyDev/CDT), Monodevelop, Code::Blocks, gvim, emacs, gedit, Visual Studio Code, Geany, IntelliJ 10 | * Local web server with copies of language documentation for: STL, Scala, Java, Python2/3, Pascal, Haskell 11 | * Automatically populate the linux disk cache on boot to speed up response time for certain programs 12 | * Automatic login of teams to DOMjudge without giving teams access to their credentials 13 | * Advanced firewall to restrict team access to the network 14 | * Fat32 partition for teams to store files that allows for easy access after the contest 15 | * Supports 32 and 64 bit machines 16 | * Simple management/set up for admins 17 | * Custom home directory content(for configuring firefox, desktop shortcuts, etc) 18 | * Fully customizable, entirely automated process for building consistent images 19 | * Lightweight XFCE window manager 20 | 21 | ## Usage Requirements 22 | * 64bit hardware 23 | * USB boot capable(BIOS + UEFI supported) 24 | * 1gb of ram(2+ recommended) 25 | * 32gb flash drive(USB3.0 strongly recommended) 26 | 27 | ## Build Requirements 28 | * Linux host system 29 | * qemu, uml-utlities 30 | * Approx 30GB disk space free 31 | * Ansible 32 | 33 | ## Building the Image 34 | Building the image is a very simple process, and takes between 10-30minutes 35 | depending on connection speed and various other factors. 36 | 37 | 1. Clone this repository: 38 | ```bash 39 | git clone http://github.com/icpc-env/icpc-environment.git icpcenv 40 | cd icpcenv 41 | ``` 42 | 1. Make sure dependencies are met 43 | * Install required packages 44 | 45 | ```bash 46 | sudo apt-get install qemu-system-x86 genisoimage bsdtar ansible 47 | ``` 48 | * Download the 64 bit version of Ubuntu 20.04 Server: 49 | ```bash 50 | wget https://releases.ubuntu.com/20.04/ubuntu-20.04.5-live-server-amd64.iso 51 | ``` 52 | * Download the 64 bit version of eclipse into the `files/` directory: 53 | ```bash 54 | cd files && wget https://ftp.osuosl.org/pub/eclipse/technology/epp/downloads/release/2022-09/R/eclipse-java-2022-09-R-linux-gtk-x86_64.tar.gz 55 | ``` 56 | * Download kotlin zip to the `files` directory 57 | ```bash 58 | cd files && wget https://github.com/JetBrains/kotlin/releases/download/v1.7.10/kotlin-compiler-1.7.10.zip 59 | ``` 60 | 1. Run `secrets/gen-secrets.sh` to create some ssh keys/other secret data. Follow this with `./fetch-secrets.sh` to put them in the right place for ansible. 61 | 1. Copy `group_vars/all.dist` to `group_vars/all` and edit it to your liking. Specifically 62 | set the icpcadmin password, and firewall expiration properly. 63 | 1. Run the `create_baseimg.sh` script to create an unattended installation disk for ubuntu, 64 | perform the installation, and leave the base image ready for processing. During this 65 | step you can specify how large you want the image to be(Default 28500M to fit on most 66 | 32G flash drives). 67 | ```bash 68 | # This step takes around 3-5minutes depending on system/internet speed. 69 | ./create_baseimg.sh # optionally add '-s 28500M', or --no-usb 70 | ``` 71 | 1. Build the actual contestant image. This step takes the base image, boots it up, 72 | runs ansible to configure everything, performs a few final cleanup steps, and finally 73 | powers it off. Take a walk, this step takes some time(10-30minutes) 74 | ```bash 75 | ./build-final.sh 76 | ``` 77 | 1. Take the newly minted image and copy it to a usb drive (or hard drive) (as root) 78 | ``` 79 | # WARNING: Make sure to replace /dev/sdx with your actual device 80 | sudo dd if=output/2020-09-01_image-amd64.img of=/dev/sdx bs=1M status=progress oflag=direct conv=sparse 81 | ``` 82 | 83 | ## Customization of the Image 84 | One of our goals with this image is for it to be easily customized. To achieve this 85 | the image is configured using Ansible. Ansible is kicked off with the `main.yml` 86 | file, which mostly just includes things in the `playbooks/` subdirectory. For more 87 | details please refer to `playbooks/readme.yml`. Support files for ansible are 88 | found in the `files/` subdirectory. 89 | 90 | Some of the ansible plays depend on variables that you can set in the file 91 | `group_vars/all`. Please refer to `group_vars/all.dist` for an example of what 92 | this file should contain. That's where you'll want to go to edit the contest 93 | admin password and configure what urls contestants are allowed to access. 94 | 95 | If you want to customize the partition layout, you'll need to edit the 96 | `configs/2004_autoinnstall.yaml` file. By default you'll get a 192MB Fat32 partition 97 | and the rest of the space will be dedicated to the image itself. 14700M works well 98 | as a default size and fits easily on most 16G flash drives you'll encounter. You can 99 | also run `create_baseimage.sh` with `--no-usb` to skip getting the 192MB Fat32 partition 100 | if you don't intend to use these on usb drives the contestants get to keep. 101 | 102 | ### Testing customizations 103 | There is a script available to help with development so you don't have to build 104 | the full image, wait for it to copy to a usb drive, and then boot. 105 | 106 | Follow steps the above until you get to running the `build-final.sh` script; 107 | instead run `./runvm.sh` instead. This will start a VM off the base image, then 108 | give you a menu allowing you to run ansible, ssh in, and a few other utility 109 | functions. 110 | 111 | Once you have ansible performing all the tasks you need, halt the vm, then 112 | continue with the `build-final.sh` script. You should never use an image created 113 | by the `runvm.sh` script, always build images using `build-final.sh` 114 | -------------------------------------------------------------------------------- /files/management-server/wg-discover: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | import subprocess 3 | import time 4 | import requests 5 | import re 6 | import json 7 | import sys 8 | from prometheus_client.parser import text_string_to_metric_families 9 | # sudo apt-get install python3-prometheus-client 10 | from collections import defaultdict 11 | 12 | cmd='sudo wg show contest dump | tail -n +2|awk \'{gsub(/\\/128/, "", $4);print $4 " " $5}\'' 13 | out = subprocess.run(cmd, shell=True, check=True, capture_output=True) 14 | # out is 'IP last_handshake' 15 | 16 | from pprint import pprint 17 | 18 | now = time.time() 19 | 20 | def get_info(ip): 21 | resp = None 22 | try: 23 | resp = requests.get(f'http://[{ip}]:9100/metrics', timeout=5) 24 | except Exception as e: 25 | print(e, file=sys.stderr) 26 | return None 27 | 28 | # content = resp.content.decode() 29 | # cachefile = open(f"/home/ubuntu/promcache/{ip}.metrics", "w") 30 | # cachefile.write(content) 31 | # cachefile.close() 32 | 33 | machine_info = {} 34 | 35 | for f in text_string_to_metric_families(resp.text): 36 | if f.name == 'icpc_workstation_info': 37 | for s in f.samples: 38 | site = s.labels['site'] 39 | team = s.labels['team'] 40 | if len(team) == 0: 41 | team = None 42 | if len(site) == 0: 43 | site = None 44 | machine_info['team'] = team 45 | machine_info['site'] = site 46 | machine_info['name'] = s.labels.get('name', None) 47 | machine_info['affiliation'] = s.labels.get('affiliation', None) 48 | elif f.name == 'node_memory_MemTotal_bytes': 49 | for s in f.samples: 50 | machine_info['memory'] = s.value / (1024*1024) # in megabytes 51 | elif f.name == 'node_cpu_frequency_max_hertz': 52 | cores = {} 53 | for s in f.samples: 54 | cores[s.labels.get('cpu')] = s.value / (1000 * 1000 * 1000) # Gigahertz 55 | machine_info['cpu_cores'] = len(cores) 56 | machine_info['cpu_max'] = max(cores.values()) 57 | # pprint(machine_info) 58 | # return (machine_info['team'],machine_info['site']) 59 | return machine_info 60 | 61 | num_hosts=defaultdict(int) 62 | 63 | targets = [] 64 | hostlines = [] 65 | ansible = defaultdict(list) 66 | machines = [] 67 | num_unknown = 0 68 | for line in filter(lambda f: len(f) > 0, out.stdout.decode().split('\n')): 69 | ip, handshake = line.split(' ') 70 | handshake = int(handshake) 71 | if handshake > now - 300: #alive in the last 5 minutes 72 | print(f'{ip} is alive', file=sys.stderr) 73 | m = get_info(ip) 74 | if m is None: # skip offline/broken hosts 75 | continue 76 | machines.append(m) 77 | site = m['site'] 78 | team = m['team'] 79 | if site is not None and team is not None: 80 | num_hosts[site] = num_hosts[site] + 1 81 | if f't{team}' in ansible[site]: # machine already exists (i.e. this is a duplicate) 82 | suffix = 0 83 | while f't{team}_{suffix}' in ansible[site]: 84 | suffix = suffix + 1 85 | team = f'{team}_{suffix}' 86 | targets.append({'targets': [f'[{ip}]:9100'], 'labels': {'team': team, 'site': site, 'instance': f'team{team}'}}) 87 | print(f' Adding t{team}.{site}.icpcnet.internal to hosts file') 88 | hostlines.append( f'{ip} t{team}.{site}.icpcnet.internal t{team}.icpcnet.internal t{team}'.lower()) 89 | ansible[site].append(f't{team}') 90 | else: 91 | num_unknown += 1 92 | print(f' missing team/site:\n Team: {team} Site: {site}') 93 | hostlines.append(f'{ip} u{num_unknown}.uninitialized.icpcnet.internal u{num_unknown}.icpcnet.internal u{num_unknown}') 94 | ansible['uninitialized'].append(f'u{num_unknown}') 95 | 96 | 97 | with open("icpcnet_hosts", "w") as f: 98 | f.write("\n".join(hostlines) + '\n') # Put a trailing newline on it 99 | 100 | with open("icpcnet_prometheus_targets.json", "w") as f: 101 | f.write(json.dumps(targets) + '\n') 102 | 103 | with open('icpcnet_ansible', 'w') as f: 104 | for _,hosts in ansible.items(): 105 | for h in hosts: 106 | f.write(f'{h}\n') 107 | f.write('\n') 108 | for site,hosts in ansible.items(): 109 | f.write(f'[{site}]\n') 110 | for h in hosts: 111 | f.write(f'{h}\n') 112 | 113 | with open('/srv/contestweb/index.html', 'w') as f: 114 | f.write(''' 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 |
126 |
127 |
128 |

Contest Management

129 | 133 |
134 |
135 |

Online Teams

136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | ''') 145 | for m in sorted(machines, key=(lambda m: m.get('team') if m.get('team') is not None else 'zz')): 146 | f.write(f""" 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | """) 155 | f.write(''' 156 | 157 | 158 | 159 | 160 | ''') 161 | 162 | print(f"Found {len(machines)} hosts") 163 | print(f'{num_hosts}') 164 | -------------------------------------------------------------------------------- /files/scripts/self_test: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # print out the image version 4 | RED=$(tput setaf 1) 5 | GREEN=$(tput setaf 2) 6 | BLUE=$(tput bold; tput setaf 4) 7 | ORANGE=$(tput setaf 3) 8 | RESET=$(tput sgr0) 9 | 10 | function print_ok() { 11 | echo "${GREEN}Pass${RESET}" 12 | } 13 | function print_fail() { 14 | echo "${RED}Fail${RESET}" 15 | } 16 | function print_check() { 17 | printf "%-60s" "$1" 18 | } 19 | function language_supported() { 20 | if grep -q "^$1\$" /icpc/supported_languages ; then 21 | return 0 22 | fi 23 | return 1 24 | } 25 | function print_version() { 26 | language_supported "$1" || return 27 | print_check "${3:-$1}" 28 | shift 29 | VERSION=$($@ 2>&1| grep -Eo '[0-9]+\.[0-9]+(\.[0-9]+)?([_-p][0-9]+)?' |awk '{print length, $0}'|sort -rn |cut -d " " -f2- | head -n1) 30 | UNKNOWN="${RED}Unknown${RESET}" 31 | VERSION="${VERSION:-$UNKNOWN}" 32 | echo $VERSION 33 | } 34 | function print_first_version() { 35 | language_supported "$1" || return 36 | print_check "${3:-$1}" 37 | shift 38 | VERSION=$($@ 2>&1| grep -Eo '[0-9]+\.[0-9]+(\.[0-9]+)?([_-p][0-9]+)?' | head -n1) 39 | UNKNOWN="${RED}Unknown${RESET}" 40 | VERSION="${VERSION:-$UNKNOWN}" 41 | echo $VERSION 42 | } 43 | function check_return() { 44 | if [ $1 -eq 0 ]; then 45 | print_ok 46 | else 47 | print_fail 48 | fi 49 | } 50 | function check_service() { 51 | systemctl is-active $1 >/dev/null 2>&1 52 | check_return $? 53 | } 54 | 55 | echo "${BLUE}System Configuration${RESET}" 56 | print_check "Contest ID" 57 | CONTESTID="$(cat /icpc/CONTEST 2>/dev/null)" 58 | NOTEAM="${RED}Not Set${RESET}" 59 | CONTESTID="${CONTESTID:-$NOTEAM}" 60 | echo $CONTESTID 61 | 62 | print_check "Team ID" 63 | TEAMNAME="$(cat /icpc/TEAM 2>/dev/null)" 64 | NOTEAM="${RED}Not Set${RESET}" 65 | TEAMNAME="${TEAMNAME:-$NOTEAM}" 66 | echo $TEAMNAME 67 | 68 | print_check "Site ID" 69 | SITENAME="$(cat /icpc/SITE 2>/dev/null)" 70 | NOSITE="${RED}Not Set${RESET}" 71 | SITENAME="${SITENAME:-$NOSITE}" 72 | echo $SITENAME 73 | 74 | # only check domjudge autologin if there are autologin urls defined 75 | if grep -q 'acl autologin url_regex' /etc/squid/squid.conf ; then 76 | print_check "DOMjudge Autologin Configured" 77 | cat /etc/squid/autologin.conf 2>/dev/null | grep "X-DOMjudge" > /dev/null 78 | if [ $? -eq 0 ]; then 79 | echo "${GREEN}Yes${RESET}" 80 | print_check " Team Login" 81 | DJTEAM=$(cat /etc/squid/autologin.conf | awk '/X-DOMjudge-Login/{print $3}' | tr -d '"') 82 | echo $DJTEAM 83 | else 84 | echo "${ORANGE}No${RESET}" 85 | fi 86 | fi 87 | 88 | echo 89 | echo "${BLUE}Printing Configuration${RESET}" 90 | print_check "Making sure cups is running" 91 | check_service 'cups' 92 | PRINTERS=$(lpstat -v 2>/dev/null| grep -v ContestPrinter | sed -e 's|socket://||' -e 's|:||' | awk '{printf " %-56s%s\n", $3, $4}') 93 | NUMPRINTERS=$(echo -n "$PRINTERS" | grep -c Printer) 94 | print_check "Checking there are printers present" 95 | if [ "$NUMPRINTERS" -eq "0" ]; then 96 | print_fail 97 | else 98 | print_ok 99 | fi 100 | echo "Configured Printers:" 101 | echo "$PRINTERS" 102 | 103 | echo 104 | echo "${BLUE}Connectivity Test${RESET}" 105 | print_check "Checking if the firewall is enabled" 106 | ufw status 2>&1 | grep 'Status: active' >/dev/null 107 | check_return $? 108 | 109 | 110 | print_check "Making sure the squid proxy is running" 111 | check_service 'squid' 112 | 113 | print_check "Testing access to google is blocked with a proxy error" 114 | curl -s --proxy http://127.0.0.1:3128 http://google.com 2>/dev/null | grep "

Access Denied

" > /dev/null 115 | check_return $? 116 | 117 | while IFS="" read -r url ; do 118 | [ -z "$url" ] && continue 119 | print_check "Testing access to $url is successful with proxy" 120 | # wget -e use_proxy=yes -T 5 -t 1 -e http_proxy=127.0.0.1:3128 -e https_proxy=127.0.0.1:3128 -O- --no-check-certificate $url 2>/dev/null | grep -i "html" > /dev/null 121 | curl --proxy http://127.0.0.1:3128 --connect-timeout 5 --max-time 10 --silent -o /dev/null --fail $url 122 | check_return $? 123 | break # only check one url from the list 124 | done < /icpc/config_homepage 125 | 126 | print_check "Testing direct internet access is disabled" 127 | su - contestant -c 'curl --noproxy "*" --connect-timeout 5 --max-time 10 --silent --fail -o /dev/null http://google.com' 2>/dev/null 128 | if [ $? -eq 0 ]; then 129 | print_fail 130 | else 131 | print_ok 132 | fi 133 | 134 | if [ -f /usr/local/bin/wg_setup ]; then 135 | print_check "Checking if the management VPN is alive" 136 | ping -c1 contestmanager.icpcnet.internal >/dev/null 2>&1 137 | check_return $? 138 | fi 139 | 140 | echo 141 | echo "${BLUE}Compiler Versions${RESET}" 142 | ERLANG_VERSION_OUT=$(erl -eval '{ok, Version} = file:read_file(filename:join([code:root_dir(), "releases", erlang:system_info(otp_release), "OTP_VERSION"])), io:fwrite(Version), halt().' -noshell 2>/dev/null) 143 | print_version "ada" "gnat --version" 144 | print_version "c" "gcc --version" 145 | print_version "cpp" "g++ --version" "c++" 146 | print_version "c#" "mono --version" 147 | print_version "clojure" "clojure -e (clojure-version)" 148 | print_version "dart" "dart --version" 149 | print_version "D" "gdc --version" 150 | print_version "erlang" "echo $ERLANG_VERSION_OUT" 151 | print_version "f#" "fsharpc" 152 | print_version "fortran" "gfortran --version" 153 | print_version "go" "go version" 154 | print_version "gccgo" "gccgo --version" 155 | print_version "groovy" "groovy -version" 156 | print_version "haskell" "ghc --version" 157 | print_version "java" "java -version" 158 | print_version "lua" "lua -v" 159 | print_version "js" "nodejs --version" 160 | print_first_version "kotlin" "kotlin -version" 161 | print_version "nim" "nim --version" 162 | print_version "ocaml" "ocaml -version" 163 | print_version "pascal" "fpc -version" 164 | print_version "prolog" "swipl --version" 165 | print_version "python2" "python --version" 166 | print_version "pypy" "pypy --version" 167 | print_version "python3" "python3 --version" 168 | print_first_version "python3" "pypy3 --version" "pypy3" 169 | print_version "ruby" "ruby --version" 170 | print_version "rust" "rustc --version" 171 | print_version "scala" "scala -version" 172 | 173 | echo 174 | echo "${BLUE}Image Details${RESET}" 175 | VERSIONINFO=$(cat /icpc/version 2>/dev/null) 176 | VERSIONDEFAULT="${RED}No version info found!${DEFAULT}" 177 | echo -e "${VERSIONINFO:-$VERSIONDEFAULT}" 178 | -------------------------------------------------------------------------------- /playbooks/icpc.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: create icpc directory 3 | file: path=/icpc state=directory 4 | 5 | - name: copy icpc scripts 6 | copy: src=files/scripts dest=/icpc mode=0755 7 | 8 | - name: add a supported_languages file 9 | copy: 10 | dest: /icpc/supported_languages 11 | content: | 12 | {% for l in languages %} 13 | {{ l }} 14 | {% endfor %} 15 | 16 | - name: write out the config url 17 | copy: 18 | dest: /icpc/config_url_base 19 | content: "{{ config_url }}" 20 | 21 | - name: write out the contest id (if specified) 22 | copy: 23 | dest: /icpc/CONTEST 24 | content: "{{ contest_id }}" 25 | when: contest_id|default('')|length > 0 26 | 27 | - name: create printfile symlink(to match world finals image) 28 | file: src=/icpc/scripts/pcpr dest=/icpc/scripts/printfile state=link 29 | 30 | - name: set default papersize to letter 31 | copy: dest=/etc/papersize content="letter\n" mode=0644 32 | 33 | - name: setup PATH variable 34 | copy: 35 | dest: /etc/profile.d/icpc_path.sh 36 | content: | 37 | export PATH="/icpc/scripts:$PATH" 38 | 39 | - name: copy icpc default wallpaper 40 | copy: dest=/icpc/wallpaper.png src=files/wallpaper.png 41 | 42 | - name: set timezone 43 | community.general.timezone: 44 | name: "{{ icpc_timezone }}" 45 | 46 | # ICPC tools we need 47 | - name: install ICPC tools we need 48 | apt: 49 | state: present 50 | pkg: 51 | - ufw 52 | - imagemagick 53 | - git 54 | # - ntp # we use systemd-timesyncd instead (and installing ntp will break that) 55 | - cups 56 | - cups-bsd 57 | - enscript 58 | - python3-typing-extensions # needed for firstboot script 59 | 60 | # experimental programs to support showing a banner image to all contestants 61 | - feh # to display fullscreen images 62 | - wmctrl # can set always on top, plus check if it's still the active window and restart it if not (e.g. ctrl+alt+left/right swap desktops) 63 | - xdotool # to detect if it's still on top/other things? 64 | - python3-xlib # maybe we'll write our own xorg fullscreen thing to replace the above 3 things... 65 | 66 | - name: stop/disable cups-browsed from autodiscovering printers 67 | service: name=cups-browsed state=stopped enabled=no 68 | 69 | - name: copy local git repos to server 70 | synchronize: 71 | use_ssh_args: yes 72 | src: home_dirs/{{item}}/.git 73 | dest: /tmp/{{item}}.git 74 | with_items: 75 | - contestant 76 | - admin 77 | 78 | - name: remove existing skel directory 79 | file: path=/etc/skel state=absent 80 | - name: setup user skeleton directory 81 | git: dest=/etc/skel repo=file:///tmp/contestant.git 82 | 83 | 84 | - name: create icpcadmin group 85 | group: name='icpcadmin' state='present' 86 | - name: create icpcadmin user 87 | user: name='icpcadmin' comment="ICPC Local Admin" group='icpcadmin' groups='sudo,adm,lpadmin' password='{{ icpcadmin_pass | password_hash('sha512') }}' shell='/bin/bash' 88 | - name: setup icpcadmin home directory 89 | git: dest=/home/icpcadmin repo=file:///tmp/admin.git 90 | become: yes 91 | become_user: icpcadmin 92 | 93 | - name: create contestant group 94 | group: name='contestant' state='present' 95 | - name: create contestant user (with empty/blank password) 96 | user: name='contestant' group='contestant' groups='lpadmin' password='$1$salty$lOyh/41oDtq.J4v0Lltp4.' shell='/bin/bash' 97 | notify: 98 | - clear user password 99 | 100 | # This will get cleared when the user is re-created 101 | - name: create gitconfig for contestant(to make updating the home directory easier) 102 | copy: 103 | dest: /home/contestant/.gitconfig 104 | owner: contestant 105 | group: contestant 106 | content: | 107 | [user] 108 | email = contestant@icpcenv 109 | name = icpcenv 110 | 111 | - name: copy ssh ca 112 | copy: 113 | src: files/secrets/server_ca.pub 114 | dest: /etc/ssh/ca.pub 115 | mode: 0644 116 | 117 | - name: configure ssh ca to be enabled for allowing users to log in 118 | copy: 119 | content: 120 | TrustedUserCAKeys /etc/ssh/ca.pub 121 | dest: /etc/ssh/sshd_config.d/trusted-user-ca.conf 122 | mode: 0644 123 | 124 | - name: configure ssh ca to be trusted for host verification 125 | shell: echo "@cert-authority * $(cat /etc/ssh/ca.pub)" >>/etc/ssh/ssh_known_hosts 126 | 127 | - name: Install a set of host keys that are signed by the ssh CA 128 | copy: 129 | src: files/secrets/contestant.icpcnet.internal_{{ item }} 130 | dest: /etc/ssh/ssh_{{ item }} 131 | with_items: 132 | - host_ed25519_key 133 | - host_ed25519_key.pub 134 | - host_ed25519_key-cert.pub 135 | 136 | - name: sshd host certificate 137 | copy: 138 | content: | 139 | HostKey /etc/ssh/ssh_host_ed25519_key 140 | HostCertificate /etc/ssh/ssh_host_ed25519_key-cert.pub 141 | dest: /etc/ssh/sshd_config.d/ssh_host_cert.conf 142 | mode: 0644 143 | notify: restart ssh 144 | 145 | 146 | - name: create gitconfig for icpcadmin(to make updating the home directory easier) 147 | copy: 148 | dest: /home/icpcadmin/.gitconfig 149 | owner: icpcadmin 150 | group: icpcadmin 151 | content: | 152 | [user] 153 | email = icpcadmin@icpcenv 154 | name = icpcenv 155 | 156 | - name: disable proxy for icpcadmin 157 | copy: src=files/pam_environment dest=/home/icpcadmin/.pam_environment 158 | 159 | - name: use polkit to disable mounting anything 160 | copy: src=files/99-deny-polkit-mount.pkla dest=/etc/polkit-1/localauthority/50-local.d/disable-mount.pkla mode=0644 owner=root group=root 161 | 162 | - name: create team reset group 163 | group: name='teamreset' state='present' 164 | - name: create team reset user 165 | user: name='teamreset' comment="Clear Team Account" group='teamreset' groups='sudo,adm,lpadmin' createhome=no password='{{icpcadmin_pass | password_hash('sha512') }}' shell='/bin/bash' 166 | - name: make home folder/autostart path 167 | file: state=directory path=/home/teamreset/.config/autostart owner=teamreset group=teamreset 168 | - name: create autostart file 169 | copy: src=files/teamreset.desktop dest=/home/teamreset/.config/autostart/teamreset.desktop 170 | 171 | - name: setup sudo for the admin 172 | copy: 173 | dest: /etc/sudoers.d/icpcadmins 174 | mode: 0440 175 | content: | 176 | # icpcadmin can run any command without a password 177 | icpcadmin ALL=NOPASSWD: ALL 178 | teamreset ALL=NOPASSWD: ALL 179 | 180 | - name: set up firstboot configuration 181 | copy: src=files/firstboot.service dest=/etc/systemd/system/firstboot.service 182 | 183 | - name: enable firstboot service 184 | service: name=firstboot enabled=yes 185 | 186 | - name: check for ICPC partition 187 | shell: blkid -L "ICPC" 188 | register: icpc_partition 189 | ignore_errors: true 190 | failed_when: false 191 | 192 | - name: if there is an icpc partition 193 | when: icpc_partition.rc == 0 194 | block: 195 | - name: create mountpoint 196 | file: path=/mnt/usbdrive state=directory 197 | - name: mount fat32 partition 198 | mount: > 199 | name=/mnt/usbdrive 200 | src='LABEL=ICPC' 201 | fstype=vfat 202 | opts="defaults,uid=contestant,gid=contestant" 203 | state=present 204 | 205 | - name: write the homepage to a file so we can make sure access works in the self_test 206 | copy: 207 | dest: /icpc/config_homepage 208 | content: | 209 | {{ firefox_policies.Homepage.URL }} 210 | -------------------------------------------------------------------------------- /files/management-server/do-screenshots.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | import os 3 | import os.path 4 | import sys 5 | import re 6 | import subprocess 7 | from datetime import datetime 8 | import multiprocessing as mp 9 | from collections import defaultdict 10 | import pathlib 11 | from pprint import pprint 12 | 13 | 14 | home = os.environ['HOME'] 15 | SCREENSHOT_DIR = f'/srv/contestweb/screens' 16 | TIMESTAMP = datetime.now().strftime("%Y%m%d-%H_%M_%S") 17 | 18 | SITELIMIT = sys.argv[1] if len(sys.argv) > 1 else None 19 | 20 | 21 | class Host: 22 | def __init__(self, host, site): 23 | self.host = host 24 | self.site = site 25 | self.filename = f"{TIMESTAMP}.png" 26 | self.screenshot_dir = f"{SCREENSHOT_DIR}/{self.site}/{self.host}" 27 | self.thumbnail_dir = f"{self.screenshot_dir}/thumbs" 28 | 29 | # actual files 30 | self.screenshot = None 31 | self.thumbnail = None 32 | 33 | def screenshot_url(self): 34 | return self.screenshot[len(f"{SCREENSHOT_DIR}/"):] 35 | def thumbnail_url(self): 36 | return self.thumbnail[len(f"{SCREENSHOT_DIR}/"):] 37 | 38 | def take_screenshot(self): 39 | pathlib.Path(self.screenshot_dir).mkdir(parents=True, exist_ok=True) 40 | 41 | cmd = f'timeout 10 ssh {self.host} sudo -u contestant env DISPLAY=:0 import -window root png:- > {self.screenshot_dir}/{self.filename}' 42 | try: 43 | out = subprocess.run(cmd, shell=True, check=True, capture_output=True) 44 | print(f'Screenshot taken of {self.host}') 45 | self.screenshot = f"{self.screenshot_dir}/{self.filename}" 46 | except subprocess.CalledProcessError as e: 47 | print(f'Failed to screenshot {self.host}') 48 | print(e) 49 | print('============================') 50 | os.remove(f'{self.screenshot_dir}/{self.filename}') 51 | 52 | def create_thumbnail(self): 53 | print(f"checking thumbnail for {self.host}") 54 | # Nothing to do if there's no screenshot 55 | if self.screenshot is None: 56 | return 57 | 58 | # Make sure the directory exists 59 | pathlib.Path(self.thumbnail_dir).mkdir(parents=True, exist_ok=True) 60 | 61 | # otherwise, make a thumbnail 62 | print(f'Thumbnailing {self.host}') 63 | cmd = f"cd {self.screenshot_dir} && mogrify -format png -path thumbs -thumbnail 320x {self.filename}" 64 | out = subprocess.run(cmd, shell=True, capture_output=True) 65 | if out.returncode != 0: 66 | print(out) 67 | self.thumbnail = f"{self.thumbnail_dir}/{self.filename}" 68 | 69 | def screenshot_wrapper(h): 70 | h.take_screenshot() 71 | return h 72 | def thumbnail_wrapper(h): 73 | h.create_thumbnail() 74 | return h 75 | 76 | def main(): 77 | now = datetime.now() 78 | 79 | with open(f'{home}/icpcnet_hosts', 'r') as f: 80 | hostlines = f.readlines() 81 | hostlines = [line.strip() for line in hostlines 82 | if not line.startswith('#') and line.strip() != ''] 83 | hosts = [] 84 | for line in hostlines: 85 | hostnames = line.split('#')[0].split()[1:] 86 | hosts.extend(hostnames) 87 | 88 | # grab full hostnames, so we have site information 89 | r = re.compile(r"[^.]+\.[^.]+\.icpcnet\.internal") 90 | hosts = list(filter(r.match, hosts)) 91 | hosts = list(map(lambda h: Host(*h.split('.')[:2]), hosts)) 92 | 93 | hosts = list(filter(lambda h: SITELIMIT is None or SITELIMIT == h.site, hosts)) 94 | 95 | # fetch 24 screenshots at a time 96 | pool = mp.Pool(processes=24) 97 | hosts = pool.map(screenshot_wrapper, hosts) 98 | pool = mp.Pool(processes=4) 99 | hosts = pool.map(thumbnail_wrapper, hosts) 100 | 101 | site_hosts = defaultdict(list) 102 | for h in hosts: 103 | site_hosts[h.site].append(h) 104 | 105 | # site content generation 106 | site_html = defaultdict(str) 107 | for site,hosts in site_hosts.items(): 108 | for h in sorted(hosts, key=lambda h: h.host): 109 | if h.screenshot is None: 110 | site_html[site] += f'
{h.host}
\n' 111 | else: 112 | site_html[site] += f'
{h.host}
\n' 113 | site_html[site] += f'' 114 | 115 | header = f''' 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | Screenshots {TIMESTAMP}! 126 | 129 | 130 | 131 |
132 |
133 |

{TIMESTAMP}

134 |
135 | ''' 136 | 137 | footer = ''' 138 |
139 | 140 | 141 | 142 | 143 | 144 | 151 | 152 | 153 | 157 | 158 | 159 | ''' 160 | 161 | # Only write the index.html if there's not a sitelimit in place 162 | if SITELIMIT is None: 163 | with open(f'{SCREENSHOT_DIR}/index.html', 'w') as html: 164 | html.write(header) 165 | html.write(f'
    ') 166 | for site,html_content in site_html.items(): 167 | html.write(f'
  • {site}
  • ') 168 | html.write(f'
') 169 | 170 | for site,html_content in site_html.items(): 171 | html.write(f'
') 172 | html.write(html_content) 173 | html.write(footer) 174 | 175 | # make individual site pages 176 | for site,html_content in site_html.items(): 177 | with open(f'{SCREENSHOT_DIR}/{site}.html', 'w') as html: 178 | html.write(header) 179 | html.write(f'

{site}

') 180 | html.write(html_content) 181 | html.write(footer) 182 | 183 | if __name__ == '__main__': 184 | main() 185 | -------------------------------------------------------------------------------- /files/scripts/whiptail.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # 3 | # __init__.py 4 | """ 5 | Use whiptail to display dialog boxes from Python scripts. 6 | """ 7 | # Copyright (c) 2020-2021 Dominic Davis-Foster 8 | # Copyright (c) 2013 Marwan Alsabbagh and contributors. 9 | # All rights reserved. 10 | # Licensed under the BSD License. See LICENSE file for details. 11 | # 12 | # Docstrings based on the whiptail manpage 13 | # https://manpages.debian.org/buster/whiptail/whiptail.1.en.html 14 | # Written by 15 | # Savio Lam (lam836@cs.cuhk.hk) - version 0.3 16 | # Stuart Herbert (S.Herbert@sheffield.ac.uk) - patch for version 0.4 17 | # Enrique Zanardi (ezanard@debian.org) 18 | # Alastair McKinstry (mckinstry@debian.org) 19 | # 20 | 21 | # stdlib 22 | import itertools 23 | import os 24 | import pathlib 25 | import shlex 26 | import sys 27 | from collections import namedtuple 28 | from shutil import get_terminal_size 29 | from subprocess import PIPE, Popen 30 | from typing import AnyStr, Iterable, List, Optional, Sequence, Tuple, TypeVar, Union, cast 31 | 32 | # 3rd party 33 | # from domdf_python_tools.typing import PathLike # EDIT BY UBERGEEK42 34 | PathLike = Union[str, pathlib.Path, os.PathLike] # EDIT BY UBERGEEK42 35 | from typing_extensions import Literal 36 | 37 | __author__: str = "Dominic Davis-Foster" 38 | __copyright__: str = "2020 Dominic Davis-Foster" 39 | __license__: str = "BSD" 40 | __version__: str = "0.4.0" 41 | __email__: str = "dominic@davis-foster.co.uk" 42 | 43 | __all__ = ["Response", "Whiptail"] 44 | 45 | # TODO: 46 | # --default-item string 47 | # Set the default item in a menu box. Normally the first item in the box is the default. 48 | # --fb, --fullbuttons 49 | # Use full buttons. (By default, whiptail uses compact buttons). 50 | # --nocancel 51 | # The dialog box won't have a Cancel button. 52 | # --yes-button text 53 | # Set the text of the Yes button. 54 | # --no-button text 55 | # Set the text of the No button. 56 | # --ok-button text 57 | # Set the text of the Ok button. 58 | # --cancel-button text 59 | # Set the text of the Cancel button. 60 | # --noitem 61 | # The menu, checklist and radiolist widgets will display tags only, not the item strings. The menu widget still needs some items specified, but checklist and radiolist expect only tag and status. 62 | # --notags 63 | # Don't display tags in the menu, checklist and radiolist widgets. 64 | # --infobox text height width 65 | # An info box is basically a message box. However, in this case, whiptail will exit immediately 66 | # after displaying the message to the user. The screen is not cleared when whiptail exits, 67 | # so that the message will remain on the screen until the calling shell script clears it later. 68 | # This is useful when you want to inform the user that some operations are carrying on that may 69 | # require some time to finish. 70 | # --gauge text height width percent 71 | # A gauge box displays a meter along the bottom of the box. The meter indicates a percentage. 72 | # New percentages are read from standard input, one integer per line. The meter is updated to 73 | # reflect each new percentage. If stdin is XXX, the first following line is a percentage and 74 | # subsequent lines up to another XXX are used for a new prompt. The gauge exits when EOF is 75 | # reached on stdin. 76 | 77 | 78 | class Response(namedtuple("__BaseResponse", "returncode value")): 79 | """ 80 | Namedtuple to store the returncode and value returned by a whiptail dialog. 81 | 82 | :param returncode: The returncode. 83 | :param value: The value returned from the dialog. 84 | 85 | Return values are as follows: 86 | 87 | * ``0``: The ``Yes`` or ``OK`` button was pressed. 88 | * ``1``: The ``No`` or ``Cancel`` button was pressed. 89 | * ``255``: The user pressed the ``ESC`` key, or an error occurred. 90 | """ 91 | 92 | returncode: int 93 | value: str 94 | 95 | __slots__ = () 96 | 97 | def __new__(cls, returncode: int, value: AnyStr): 98 | """ 99 | Create a new instance of :class:`~.Response`. 100 | 101 | :param returncode: The returncode. 102 | :param value: The value returned from the dialog. 103 | """ 104 | 105 | if isinstance(value, bytes): 106 | val = value.decode("UTF-8") 107 | else: 108 | val = value 109 | return super().__new__(cls, returncode, val) 110 | 111 | 112 | _T = TypeVar("_T") 113 | 114 | 115 | def _flatten(data: Iterable[Iterable[_T]]) -> List[_T]: 116 | return list(itertools.chain.from_iterable(data)) 117 | 118 | 119 | class Whiptail: 120 | """ 121 | Display dialog boxes in the terminal from Python scripts. 122 | 123 | :param title: The text to show at the top of the dialog. 124 | :param backtitle: The text to show on the top left of the background. 125 | :param height: The height of the dialog. Default is 2-5 characters shorter than the terminal window 126 | :no-default height: 127 | :param width: The height of the dialog. Default is approx. 10 characters narrower than the terminal window 128 | :no-default width: 129 | :param auto_exit: Whether to call :func:`sys.exit` if the user selects cancel in a dialog. 130 | """ 131 | 132 | def __init__( 133 | self, 134 | title: str = '', 135 | backtitle: str = '', 136 | height: Optional[int] = None, 137 | width: Optional[int] = None, 138 | auto_exit: bool = False, 139 | ): 140 | 141 | self.title: str = str(title) 142 | self.backtitle: str = str(backtitle) 143 | self.height: Optional[int] = height 144 | self.width: Optional[int] = width 145 | self.auto_exit: bool = auto_exit 146 | 147 | def run( 148 | self, 149 | control: str, 150 | msg: str, 151 | extra_args: Sequence[str] = (), 152 | extra_values: Sequence[str] = (), 153 | exit_on: Sequence[int] = (1, 255) 154 | ) -> Response: 155 | """ 156 | Display a control. 157 | 158 | :param control: The name of the control to run. One of ``'yesno'``, ``'msgbox'``, ``'infobox'``, 159 | ``'inputbox'``, ``'passwordbox'``, ``'textbox'``, ``'menu'``, ``'checklist'``, 160 | ``'radiolist'`` or ``'gauge'`` 161 | :param msg: The message to display in the dialog box 162 | :param extra_args: A sequence of extra arguments to pass to the control 163 | :param extra_values: A sequence of extra values to pass to the control 164 | :param exit_on: A sequence of return codes that will cause program execution to stop if 165 | :attr:`Whiptail.auto_exit` is :py:obj:`True` 166 | 167 | :return: The response returned by whiptail 168 | """ 169 | 170 | width: Optional[int] = self.width 171 | height: Optional[int] = self.height 172 | 173 | if height is None or width is None: 174 | w, h = get_terminal_size() 175 | 176 | if width is None: 177 | width = w - 10 178 | width = width - (width % 10) 179 | 180 | if height is None: 181 | height = h - 2 182 | height = height - (height % 5) 183 | 184 | cmd = [ 185 | "whiptail", 186 | "--title", 187 | self.title, 188 | "--backtitle", 189 | self.backtitle 190 | ] 191 | 192 | if any(extra_args): 193 | cmd.extend(list(extra_args)) 194 | 195 | cmd.extend([ 196 | *list(extra_args), 197 | f"--{control}", 198 | "--", 199 | str(msg), 200 | str(height), 201 | str(width), 202 | ]) 203 | 204 | if any(extra_values): 205 | cmd.extend(list(extra_values)) 206 | 207 | p = Popen(cmd, stderr=PIPE) 208 | out, err = p.communicate() 209 | 210 | if self.auto_exit and p.returncode in exit_on: 211 | print("User cancelled operation.") 212 | sys.exit(p.returncode) 213 | 214 | return Response(p.returncode, err) 215 | 216 | def inputbox(self, msg: str, default: str = '', password: bool = False) -> Tuple[str, int]: 217 | """ 218 | An input box is useful when you want to ask questions that require the user to input a string as the answer. 219 | If ``default`` is supplied it is used to initialize the input string. 220 | When inputting the string, the ``BACKSPACE`` key can be used to correct typing errors. If the input string 221 | is longer than the width of the dialog box, the input field will be scrolled. 222 | 223 | If ``password`` is :py:obj:`True`, the text the user enters is not displayed. 224 | This is useful when prompting for passwords or other sensitive information. 225 | Be aware that if anything is passed in "init", it will be visible in the system's 226 | process table to casual snoopers. Also, it is very confusing to the user to provide 227 | them with a default password they cannot see. For these reasons, using "init" is highly discouraged. 228 | 229 | :param msg: The message to display in the dialog box 230 | :param default: A default value for the text 231 | :param password: Whether the text being entered is a password, and should be replaced by ``*``. Default :py:obj:`False` 232 | 233 | :return: The value entered by the user, and the return code 234 | """ 235 | 236 | control = "passwordbox" if password else "inputbox" 237 | returncode, val = self.run(control, msg, extra_values=[default]) 238 | return val, returncode 239 | 240 | def yesno(self, msg: str, default: str = "yes") -> bool: # todo: Literal 241 | r""" 242 | Display a yes/no dialog box. 243 | 244 | The string specified by ``msg`` is displayed inside the dialog box. 245 | If this string is too long to be fit in one line, it will be automatically 246 | divided into multiple lines at appropriate places. 247 | The text string may also contain the newline character ``\n`` to control line breaking explicitly. 248 | 249 | This dialog box is useful for asking questions that require the user to answer either yes or no. 250 | The dialog box has a ``Yes`` button and a ``No`` button, in which the user can switch between 251 | by pressing the ``TAB`` key. 252 | 253 | :param msg: The message to display in the dialog box 254 | 255 | :param default: The default button to select, either ``'yes'`` or ``'no'``. 256 | 257 | :return: :py:obj:`True` if the user selected ``yes``. :py:obj:`False` otherwise. 258 | """ 259 | 260 | if default.lower() == "no": 261 | extra = ["--defaultno"] 262 | else: 263 | extra = [] 264 | 265 | return not bool(self.run("yesno", msg, extra_args=extra, exit_on=[255]).returncode) 266 | 267 | def msgbox(self, msg: str) -> int: 268 | """ 269 | A message box is very similar to a yes/no box. 270 | 271 | The only difference between a message box and a yes/no box is that 272 | a message box has only a single ``OK`` button. 273 | 274 | You can use this dialog box to display any message you like. 275 | After reading the message the user can press the ENTER key so that whiptail will 276 | exit and the calling script can continue its operation. 277 | 278 | :param msg: The message to display in the dialog box 279 | """ 280 | 281 | return self.run("msgbox", msg).returncode 282 | 283 | def textbox(self, path: PathLike) -> int: 284 | """ 285 | A text box lets you display the contents of a text file in a dialog box. 286 | It is like a simple text file viewer. The user can move through the file by using 287 | the ``UP``/``DOWN``, ``PGUP``/``PGDN`` and ``HOME``/``END`` keys available on most keyboards. 288 | If the lines are too long to be displayed in the box, the ``LEFT``/``RIGHT`` keys can be used 289 | to scroll the text region horizontally. For more convenience, forward and backward searching 290 | functions are also provided. 291 | 292 | :param path: The file to display the contents of 293 | 294 | :return: The return code 295 | """ 296 | 297 | if not isinstance(path, pathlib.Path): 298 | path = pathlib.Path(path) 299 | 300 | return self.run("textbox", os.fspath(path), extra_args=["--scrolltext"]).returncode 301 | 302 | def calc_height(self, msg: str) -> List[str]: 303 | """ 304 | Calculate the height of the dialog box based on the message. 305 | 306 | :param msg: The message to display in the dialog box 307 | """ 308 | 309 | height_offset = 9 if msg else 7 310 | 311 | if self.height is None: 312 | width, height = get_terminal_size() 313 | height = height - 2 314 | height = height - (height % 5) 315 | else: 316 | height = self.height 317 | 318 | return [str(height - height_offset)] 319 | 320 | def menu( 321 | self, 322 | msg: str = '', 323 | items: Union[Sequence[str], Sequence[Iterable[str]]] = (), 324 | prefix: str = " - ", 325 | ) -> Tuple[str, int]: 326 | """ 327 | As its name suggests, a menu box is a dialog box that can be used to present a 328 | list of choices in the form of a menu for the user to choose. 329 | 330 | Each menu entry consists of a tag string and an item string. 331 | The tag gives the entry a name to distinguish it from the other entries in the menu. 332 | The item is a short description of the option that the entry represents. 333 | The user can move between the menu entries by pressing the ``UP``/``DOWN`` keys, 334 | the first letter of the tag as a hot-key. There are menu-height entries displayed 335 | in the menu at one time, but the menu will be scrolled if there are more entries than that. 336 | 337 | :param msg: The message to display in the dialog box. 338 | :param items: A sequence of items to display in the menu. 339 | :param prefix: 340 | 341 | :return: The tag of the selected menu item, and the return code. 342 | """ # noqa: D400 343 | 344 | if isinstance(items[0], str): 345 | items = cast(Sequence[str], items) 346 | parsed_items = [(i, '') for i in items] 347 | else: 348 | items = cast(Sequence[Iterable[str]], items) 349 | parsed_items = [(k, prefix + v) for k, v in items] 350 | 351 | extra = self.calc_height(msg) + _flatten(parsed_items) 352 | returncode, val = self.run("menu", msg, extra_values=extra) 353 | return val, returncode 354 | 355 | def showlist( 356 | self, 357 | control: "Literal['checklist', 'radiolist']", 358 | msg: str, 359 | items: Union[Sequence[str], Sequence[Iterable[str]]], 360 | prefix: str, 361 | ) -> Tuple[List[str], int]: 362 | """ 363 | Helper function to display radio- and check-lists. 364 | 365 | :param control: The name of the control to run. Either ``'checklist'`` or ``'radiolist'``. 366 | :param msg: The message to display in the dialog box/ 367 | :param items: A sequence of items to display in the list/ 368 | :param prefix: 369 | 370 | :return: A list of the tags strings that were selected, and the return code/ 371 | """ 372 | 373 | if isinstance(items[0], str): 374 | items = cast(Sequence[str], items) 375 | parsed_items = [(i, '', "OFF") for i in items] 376 | else: 377 | items = cast(Sequence[Iterable[str]], items) 378 | parsed_items = [(k, prefix + v, s) for k, v, s in items] 379 | 380 | extra = self.calc_height(msg) + _flatten(parsed_items) 381 | returncode, val = self.run(control, msg, extra_values=extra) 382 | return shlex.split(val), returncode 383 | 384 | def radiolist( 385 | self, 386 | msg: str = '', 387 | items: Union[Sequence[str], Sequence[Iterable[str]]] = (), 388 | prefix: str = " - " 389 | ) -> Tuple[List[str], int]: 390 | """ 391 | A radiolist box is similar to a menu box. 392 | 393 | The only difference is that you can indicate which entry is currently selected, 394 | by setting its status to on. 395 | 396 | :param msg: The message to display in the dialog box. 397 | :param items: A sequence of items to display in the radiolist. 398 | :param prefix: 399 | 400 | :return: A list of the tags strings that were selected, and the return code. 401 | """ 402 | 403 | return self.showlist("radiolist", msg, items, prefix) 404 | 405 | def checklist( 406 | self, 407 | msg: str = '', 408 | items: Union[Sequence[str], Sequence[Iterable[str]]] = (), 409 | prefix: str = " - " 410 | ) -> Tuple[List[str], int]: 411 | """ 412 | A checklist box is similar to a menu box in that there are multiple entries presented in the form of a menu. 413 | 414 | You can select and deselect items using the SPACE key. 415 | The initial on/off state of each entry is specified by status. 416 | 417 | :param msg: The message to display in the dialog box 418 | :param items: A sequence of items to display in the checklist 419 | :param prefix: 420 | 421 | :return: A list of the tag strings of those entries that are turned on, and the return code 422 | """ 423 | 424 | return self.showlist("checklist", msg, items, prefix) 425 | -------------------------------------------------------------------------------- /management-server.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Management Server 3 | hosts: all 4 | become: true 5 | gather_facts: true 6 | vars: 7 | ansible_python_interpreter: /usr/bin/python3 8 | tasks: 9 | - name: be sure apt cache is updated 10 | apt: update_cache=yes upgrade=dist 11 | 12 | - name: give the host a good name 13 | hostname: 14 | name: contestmanager.icpcnet.internal 15 | 16 | - name: put the hostname in /etc/hosts 17 | lineinfile: 18 | insertafter: '^127.0.0.1' 19 | line: 127.0.1.1 contestmanager.icpcnet.internal contestmanager 20 | path: /etc/hosts 21 | 22 | - name: install misc tools 23 | apt: 24 | state: present 25 | pkg: 26 | - wireguard-tools 27 | - vim 28 | # performance tools 29 | - htop 30 | - dstat 31 | - iotop 32 | - iftop 33 | - sysstat 34 | - dstat 35 | # misc admin tools 36 | - curl 37 | - ncdu 38 | - jq 39 | - git 40 | - pssh 41 | # needed for provisioning (to let ansible become unprivleged users) 42 | - acl 43 | # Monitoring related things 44 | - python3-prometheus-client 45 | - python3-passlib # So we can use htpasswd in ansible 46 | - imagemagick # for do-screenshots 47 | - ansible # to let people run ad-hoc commands via ansible 48 | 49 | - name: create a git user (using git-shell) 50 | ansible.builtin.user: 51 | name: git 52 | shell: /usr/bin/git-shell 53 | 54 | - name: Set authorized key for git repo 55 | ansible.posix.authorized_key: 56 | user: git 57 | state: present 58 | key: "{{ lookup('file', 'secrets/server_ca.pub') }}" 59 | key_options: restrict,cert-authority 60 | 61 | # TODO: initialize lastminute git repo 62 | - name: create git repo for lastminute script 63 | shell: | 64 | git init --bare /home/git/ansible 65 | args: 66 | creates: /home/git/ansible 67 | become_user: git 68 | 69 | - name: copy private key 70 | copy: 71 | src: files/secrets/{{ item }} 72 | dest: /home/{{ ansible_user }}/.ssh/{{ item }} 73 | mode: 0400 74 | owner: "{{ ansible_user }}" 75 | group: "{{ ansible_user }}" 76 | with_items: 77 | - icpcadmin@contestmanager 78 | - icpcadmin@contestmanager-cert.pub 79 | - icpcadmin@contestmanager.pub 80 | # Used for git clone/editing 81 | - jumpy@icpc 82 | - jumpy@icpc.pub 83 | - jumpy@icpc-cert.pub 84 | 85 | - name: update ssh config to use private key by default 86 | copy: 87 | content: | 88 | Match user git host contestmanager.icpcnet.internal 89 | IdentityFile ~/.ssh/jumpy@icpc 90 | User git 91 | Host *.icpcnet.internal 92 | Host * 93 | IdentityFile ~/.ssh/icpcadmin@contestmanager 94 | User icpcadmin 95 | # This does some magic to set PS1 to include the /icpc/TEAMID. To get the value for the printf bit, get PS1 set to what you want 96 | # then run: `declare -p PS1`, then replace the double quotes with single quotes so it does the cat /icpc/TEAMID every time. 97 | Host t* 98 | # Super neat hack to only run RemoteCommand if you don't pass any command to ssh 99 | Match exec "ps -o args= $PPID | grep -v ' .* '" 100 | RequestTTY yes 101 | RemoteCommand exec bash --rcfile <(cat /etc/bash.bashrc ~/.bashrc 2> /dev/null; printf "%%s\n" "declare -- PS1='\\[\\033[01;32m\\]\\u@\\h\\[\\033[00m\\]\\[\\033[38;5;3m\\](t\$(cat /icpc/TEAMID 2>/dev/null))\\[\\033[00m\\]:\\[\\033[01;34m\\]\\w\\[\\033[00m\\]\\\$ '") 102 | dest: /home/{{ ansible_user }}/.ssh/config 103 | 104 | - name: sshd config to listen on 443 105 | copy: 106 | content: | 107 | Port 22 108 | Port 443 109 | dest: /etc/ssh/sshd_config.d/ssh_port_443.conf 110 | mode: 0644 111 | notify: restart ssh 112 | 113 | - name: use different set of host keys 114 | copy: 115 | src: files/secrets/contestmanager.icpcnet.internal_{{ item }} 116 | dest: /etc/ssh/ssh_{{ item }} 117 | with_items: 118 | - host_ed25519_key 119 | - host_ed25519_key.pub 120 | - host_ed25519_key-cert.pub 121 | notify: restart ssh 122 | 123 | - name: sshd host certificate 124 | copy: 125 | content: | 126 | HostKey /etc/ssh/ssh_host_ed25519_key 127 | HostCertificate /etc/ssh/ssh_host_ed25519_key-cert.pub 128 | dest: /etc/ssh/sshd_config.d/ssh_host_cert.conf 129 | mode: 0644 130 | notify: restart ssh 131 | 132 | - name: configure ssh ca.pub (so we can verify other hosts easily) 133 | copy: 134 | src: files/secrets/server_ca.pub 135 | dest: /etc/ssh/ca.pub 136 | mode: 0644 137 | - name: configure ssh ca to be trusted for host verification 138 | shell: echo "@cert-authority * $(cat /etc/ssh/ca.pub)" >>/etc/ssh/ssh_known_hosts 139 | 140 | - name: Flush handlers so ssh is restarted before we try to do git operations 141 | meta: flush_handlers 142 | 143 | - name: create sample lastminute.yml (and commit/push it) if there isn't one 144 | shell: | 145 | git config --global user.name 'ICPC Admin' 146 | git config --global user.email 'icpcadmin@{{ansible_hostname}}' 147 | git clone git@contestmanager.icpcnet.internal:~/ansible /home/{{ansible_user}}/ansible-lastminute 148 | cd /home/{{ansible_user}}/ansible-lastminute 149 | 150 | # bail if there are commits/items already here 151 | git log -n1 >/dev/null 2>/dev/null && exit 152 | 153 | cat < local.yml 154 | - name: Lastminute Setup 155 | hosts: icpc 156 | become: true 157 | gather_facts: true 158 | tasks: 159 | - shell: 'echo "Ansible-Pull on \$(date +"%Y-%m-%d %H:%M:%S")\nRevision: \$(git rev-list --full-history --all --abbrev-commit | head -1)\n"' 160 | register: git_revision 161 | - name: copy version info 162 | copy: content="{{ '{{' }}git_revision.stdout{{ '}}' }}\n" dest=/icpc/update-version 163 | - name: remove ansible trigger file 164 | file: path=/icpc/trigger-ansible state=absent 165 | EOF 166 | git add local.yml 167 | git commit -m "Initial last minute ansible script" 168 | git push -u origin master 169 | args: 170 | creates: /home/{{ ansible_user }}/ansible-lastminute 171 | become: no 172 | 173 | - name: install dsnet 174 | get_url: 175 | url: https://github.com/naggie/dsnet/releases/latest/download/dsnet-linux-amd64 176 | dest: /usr/local/bin/dsnet 177 | mode: 0755 178 | 179 | - name: give dsnet cap_net_admin (so regular users can interact with it) 180 | community.general.capabilities: 181 | path: /usr/local/bin/dsnet 182 | capability: cap_net_admin+eip 183 | state: present 184 | 185 | - name: set up reverse ssh tunnel account (jumpy) 186 | ansible.builtin.user: 187 | name: jumpy 188 | shell: /bin/bash 189 | 190 | - name: Set authorized key for reverse ssh tunnel account (jumpy) 191 | ansible.posix.authorized_key: 192 | user: jumpy 193 | state: present 194 | key: "{{ lookup('file', 'secrets/server_ca.pub') }}" 195 | key_options: command="echo 'This account can only be used for opening a reverse tunnel.'",no-agent-forwarding,no-X11-forwarding,cert-authority 196 | exclusive: yes 197 | 198 | - name: copy our wireguard registration script 199 | copy: 200 | src: files/management-server/register_wireguard_client 201 | dest: /usr/local/bin/register_wireguard_client 202 | mode: 0755 203 | 204 | - name: set up ssh wireguard registration account 205 | ansible.builtin.user: 206 | name: wg_client 207 | shell: /bin/bash 208 | - name: Set authorized key for wireguard registration account (wg_client) 209 | ansible.posix.authorized_key: 210 | user: wg_client 211 | state: present 212 | key: "{{ lookup('file', 'secrets/server_ca.pub') }}" 213 | key_options: command="/usr/local/bin/register_wireguard_client",no-port-forwarding,no-agent-forwarding,no-X11-forwarding,cert-authority 214 | exclusive: yes 215 | 216 | - name: configure dsnet 217 | template: 218 | src: files/management-server/dsnetconfig.json.j2 219 | dest: /etc/dsnetconfig.json 220 | owner: root 221 | group: wg_client 222 | mode: 0660 223 | force: no # don't overwrite the file if it exists 224 | 225 | - name: set up dsnet service configuration 226 | copy: src=files/management-server/dsnet.service dest=/etc/systemd/system/dsnet.service 227 | 228 | - name: enable dsnet wireguard service 229 | service: name=dsnet enabled=yes state=started 230 | 231 | 232 | 233 | 234 | 235 | # coredns to do dns serving magic 236 | # https://github.com/coredns/coredns/releases/download/v1.10.1/coredns_1.10.1_linux_amd64.tgz 237 | - name: Create the coredns group 238 | group: 239 | name: coredns 240 | state: present 241 | system: true 242 | - name: Create the coredns user 243 | user: 244 | name: coredns 245 | groups: coredns 246 | append: true 247 | shell: /usr/sbin/nologin 248 | system: true 249 | createhome: false 250 | home: / 251 | - name: create coredns configuration directories 252 | file: 253 | path: /etc/coredns 254 | state: directory 255 | owner: root 256 | group: root 257 | mode: 0755 258 | - name: install coredns config file 259 | copy: 260 | dest: /etc/coredns/Corefile 261 | content: | 262 | # We handle this by probing our wireguard network for peers 263 | icpcnet.internal. { 264 | hosts /home/{{ansible_user}}/icpcnet_hosts 265 | log 266 | } 267 | . { 268 | forward . 8.8.8.8 # Forward everything else upstream 269 | log 270 | errors 271 | cache 272 | } 273 | - name: install coredns 274 | unarchive: 275 | src: https://github.com/coredns/coredns/releases/download/v1.10.1/coredns_1.10.1_linux_amd64.tgz 276 | dest: /usr/local/bin 277 | remote_src: true 278 | - name: install coredns service 279 | template: 280 | src: files/management-server/coredns.service.j2 281 | dest: /etc/systemd/system/coredns.service 282 | mode: 0644 283 | owner: root 284 | group: root 285 | notify: restart coredns 286 | - name: disable systemd-resolved 287 | systemd: 288 | name: systemd-resolved 289 | enabled: false 290 | state: stopped 291 | - name: replace resolv.conf so we use coredns instead 292 | copy: 293 | dest: /etc/resolv.conf 294 | follow: false 295 | content: | 296 | nameserver ::1 297 | nameserver 127.0.0.1 298 | options trust-ad 299 | - name: enable coredns 300 | systemd: 301 | daemon_reload: true 302 | name: coredns 303 | enabled: true 304 | state: started 305 | 306 | - name: install grafana 307 | block: 308 | - name: key for grafana apt repo 309 | apt_key: url=https://apt.grafana.com/gpg.key state=present 310 | - name: apt repo for grafana 311 | apt_repository: repo="deb https://apt.grafana.com stable main" update_cache=yes 312 | - name: install grafana 313 | apt: 314 | state: present 315 | pkg: 316 | - grafana 317 | - prometheus 318 | - prometheus-node-exporter 319 | - nginx 320 | - name: configure grafana 321 | copy: 322 | content: | 323 | GRAFANA_USER=grafana 324 | GRAFANA_GROUP=grafana 325 | GRAFANA_HOME=/usr/share/grafana 326 | LOG_DIR=/var/log/grafana 327 | DATA_DIR=/var/lib/grafana 328 | MAX_OPEN_FILES=10000 329 | CONF_DIR=/etc/grafana 330 | CONF_FILE=/etc/grafana/grafana.ini 331 | RESTART_ON_UPGRADE=true 332 | PLUGINS_DIR=/var/lib/grafana/plugins 333 | PROVISIONING_CFG_DIR=/etc/grafana/provisioning 334 | # Only used on systemd systems 335 | PID_FILE_DIR=/var/run/grafana 336 | GF_SECURITY_ADMIN_PASSWORD={{management_server_grafana_password}} 337 | # Run grafana from /grafana 338 | GF_SERVER_DOMAIN={{wg_vpn_server_external_hostname}} 339 | GF_SERVER_ROOT_URL=%(protocol)s://%(domain)s:%(http_port)s/grafana/ 340 | GF_SERVER_SERVE_FROM_SUB_PATH=true 341 | dest: /etc/default/grafana-server 342 | - name: set up grafana datasources 343 | copy: 344 | dest: /etc/grafana/provisioning/datasources/default.yml 345 | content: | 346 | apiVersion: 1 347 | deleteDatasources: 348 | - name: Prometheus 349 | orgId: 1 350 | datasources: 351 | - name: Prometheus 352 | type: prometheus 353 | access: proxy 354 | url: http://localhost:9090 355 | isDefault: true 356 | version: 1 357 | editable: false 358 | # - name: set up grafana dashboards 359 | # copy: 360 | # dest: /etc/grafana/provisioning/dashboards/default.yml 361 | # content: | 362 | # - name: 'default' # name of this dashboard configuration (not dashboard itself) 363 | # org_id: 1 # id of the org to hold the dashboard 364 | # folder: '' # name of the folder to put the dashboard (http://docs.grafana.org/v5.0/reference/dashboard_folders/) 365 | # type: 'file' # type of dashboard description (json files) 366 | # options: 367 | # folder: '/etc/grafana/dashboards' # where dashboards ar 368 | # - name: create dashboard directory 369 | # file: path=/etc/grafana/dashboards state=directory 370 | # - name: copy grafana dashboards 371 | # copy: 372 | # src: files/grafana/dashboards/ 373 | # dest: /etc/grafana/dashboards/ 374 | 375 | - name: configure prometheus scrape config 376 | copy: 377 | dest: /etc/prometheus/prometheus.yml 378 | content: | 379 | --- 380 | global: 381 | scrape_interval: 15s # By default, scrape targets every 15 seconds. 382 | evaluation_interval: 15s # By default, scrape targets every 15 seconds. 383 | scrape_configs: 384 | - job_name: 'self' 385 | static_configs: 386 | - targets: ['localhost:9100'] 387 | - job_name: 'contestants' 388 | file_sd_configs: 389 | - files: 390 | - '/home/{{ansible_user}}/icpcnet_prometheus_targets.json' 391 | 392 | - name: copy default nginx config 393 | copy: src=files/management-server/nginx.conf dest=/etc/nginx/nginx.conf 394 | notify: restart nginx 395 | - name: disable default nginx site 396 | file: state=absent path=/etc/nginx/sites-enabled/default 397 | notify: restart nginx 398 | - name: htpasswd for nginx 399 | htpasswd: 400 | path: /etc/nginx/contestadmin_users.htpasswd 401 | name: admin 402 | password: "{{management_server_grafana_password}}" 403 | owner: root 404 | group: www-data 405 | mode: 0640 406 | - name: create web directory 407 | file: 408 | state: directory 409 | dest: /srv/contestweb 410 | owner: "{{ ansible_user }}" 411 | group: www-data 412 | mode: 0755 413 | - name: Start/enable our services 414 | service: name={{ item }} state=started enabled=yes 415 | with_items: 416 | - grafana-server.service 417 | - prometheus.service 418 | - nginx.service 419 | 420 | - name: install our wg-discover tool 421 | copy: 422 | src: files/management-server/wg-discover 423 | dest: /usr/local/bin/discover-clients 424 | mode: 0755 425 | - name: run wg-discover tool every minute 426 | block: 427 | - name: sudo rule so we can run wg show 428 | community.general.sudoers: 429 | name: passwordless_wg_show 430 | user: "{{ ansible_user }}" 431 | commands: "/usr/bin/wg show contest dump" 432 | nopassword: true 433 | - name: systemd unit 434 | copy: 435 | dest: /etc/systemd/system/discover-clients.service 436 | content: | 437 | [Unit] 438 | Description=Discovers clients on the wireguard interface 439 | Wants=discover-clients.timer 440 | [Service] 441 | Type=oneshot 442 | User={{ ansible_user }} 443 | WorkingDirectory=/home/{{ ansible_user }} 444 | ExecStart=/usr/local/bin/discover-clients 445 | [Install] 446 | WantedBy=multi-user.target 447 | - name: systemd timer 448 | copy: 449 | dest: /etc/systemd/system/discover-clients.timer 450 | content: | 451 | [Unit] 452 | Description=Periodically triggers client discovery 453 | Requires=discover-clients.service 454 | [Timer] 455 | Unit=discover-clients.service 456 | OnCalendar=*-*-* *:*:00 457 | [Install] 458 | WantedBy=timers.target 459 | - name: enable/start the timer 460 | systemd: 461 | name: discover-clients.timer 462 | daemon_reload: true 463 | state: started 464 | enabled: true 465 | 466 | - name: screenshot wizardry 467 | copy: 468 | src: files/management-server/do-screenshots.py 469 | dest: /usr/local/bin/do-screenshots 470 | mode: 0755 471 | - name: make sure screens dir exists in web folder 472 | file: dest=/srv/contestweb/screens state=directory owner={{ ansible_user }} group=www-data mode=0755 473 | 474 | - name: make PS1 a bit more useful by including the contest id 475 | lineinfile: 476 | dest: /home/{{ansible_user}}/.bashrc 477 | line: PS1='\[\033[01;32m\]\u@\h\[\033[00m\]\[\033[38;5;5m\]({{ contest_id }})\[\033[00m\] :\[\033[01;34m\]\w\[\033[00m\]\$ ' 478 | 479 | # Copy some build information to the image 480 | - shell: 'echo "Built on $(date +%Y-%m-%d)\nRevision: $(git rev-list --full-history --all --abbrev-commit | head -1)"\n' 481 | become: false 482 | register: git_revision 483 | delegate_to: 127.0.0.1 484 | - name: copy version info 485 | copy: content="{{git_revision.stdout}}" dest=/applied-version 486 | 487 | handlers: 488 | - name: restart ssh 489 | service: name=ssh state=restarted 490 | - name: restart coredns 491 | service: name=coredns state=restarted 492 | - name: restart nginx 493 | service: name=nginx state=restarted 494 | -------------------------------------------------------------------------------- /files/scripts/icpc_setup: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | from whiptail import Whiptail 3 | from pathlib import Path 4 | import urllib.request 5 | import csv 6 | from subprocess import PIPE, Popen 7 | import re 8 | import time 9 | import sys 10 | import signal 11 | import os 12 | from urllib.parse import urlparse, quote 13 | 14 | # Trap SIGHUP because systemd sends it and we don't want it to interrupt us 15 | signal.signal(signal.SIGHUP, signal.SIG_IGN) 16 | 17 | 18 | 19 | def make_safe_url(url: str) -> str: 20 | """ 21 | Returns a parsed and quoted url 22 | """ 23 | _url = urlparse(url) 24 | url = _url.scheme + "://" + _url.netloc + quote(_url.path) + "?" + quote(_url.query) 25 | return url 26 | 27 | def read_file(path, default): 28 | try: 29 | contents = Path(path).read_text().strip() 30 | if len(contents) == 0: 31 | return default 32 | return contents 33 | except Exception as e: 34 | # TODO log/handle specific errors more properly here 35 | pass 36 | return default 37 | 38 | 39 | def fetch_url(url): 40 | try: 41 | with urllib.request.urlopen(make_safe_url(url)) as response: 42 | return response.read().decode("UTF-8").strip() 43 | except urllib.error.URLError as e: 44 | w = Whiptail(title="URL Fetch Error") 45 | w.msgbox(f"Exception fetching {url}:\n\n{e}") 46 | except Exception as e: 47 | w = Whiptail(title="URL Fetch Error") 48 | w.msgbox(f"Exception fetching {url}:\n\n{e}") 49 | return '' 50 | 51 | 52 | def run_cmd(cmd, ignore_errors=False): 53 | p = Popen(cmd, stdout=PIPE, stderr=PIPE) 54 | out, err = p.communicate() 55 | 56 | if p.returncode != 0 and not ignore_errors: 57 | w = Whiptail("Command Error") 58 | w.msgbox( 59 | f"""Command: {cmd} 60 | Code: {p.returncode} 61 | Out: 62 | {out} 63 | 64 | Err: 65 | {err} 66 | """ 67 | ) 68 | 69 | return (p.returncode, out, err) 70 | 71 | 72 | def load_printers(): 73 | printers = [] 74 | (ret, out, err) = run_cmd(["lpstat", "-v"], ignore_errors=True) 75 | lpstat_re = re.compile(r"^device for (?P[^:]+): (?P.+)$") 76 | for l in out.decode("UTF-8").splitlines(): 77 | m = lpstat_re.match(l) 78 | # Only return printers named 'Printer' (because those are the ones we're managing) 79 | if not m.group("name").startswith("Printer"): 80 | continue 81 | printers.append({"uri": m.group("uri"), "name": m.group("name")}) 82 | return printers 83 | 84 | 85 | def wifi_interfaces(): 86 | # WIRELESS_INTERFACES=$(iw dev | awk -F' ' '/Interface/{print $2}') 87 | if_re = re.compile(r".+Interface (?P.+)$") 88 | (ret, out, err) = run_cmd(["iw", "dev"]) 89 | 90 | interfaces = [] 91 | for l in filter(lambda x: "Interface" in x, out.decode("UTF-8").splitlines()): 92 | m = if_re.match(l) 93 | interfaces.append(m.group("name")) 94 | 95 | return interfaces 96 | 97 | 98 | def wifi_scan(interface): 99 | # iw $WIFI_INTERFACE scan | awk -F': ' '/SSID:/{print $2}' 100 | 101 | # Make sure the interface is up, otherwise scan won't work 102 | (ret, out, err) = run_cmd(["ip", "link", "set", "dev", interface, "up"]) 103 | 104 | # Scan for networks 105 | (ret, out, err) = run_cmd(["iw", interface, "scan"]) 106 | ssid_re = re.compile(r"^SSID: (?P.+)$") 107 | 108 | ssids = [] 109 | for l in filter(lambda x: "SSID:" in x, out.decode("UTF-8").splitlines()): 110 | m = ssid_re.match(l.strip()) 111 | if m is not None: 112 | ssids.append(m.group("ssid")) 113 | 114 | return ssids 115 | 116 | 117 | def check_network(): 118 | try: 119 | urllib.request.urlopen("http://configs.cloudcontest.org/online_check", timeout=1) 120 | return True 121 | except Exception as e: 122 | print(f"network broken: {e}") 123 | time.sleep(2) 124 | return False 125 | 126 | 127 | config_url = read_file("/icpc/config_url_base", "http://configs.cloudcontest.org") 128 | configuration = {} 129 | PAPERSIZE = "letter" # or 'a4' 130 | 131 | 132 | def main(): 133 | 134 | # This script must be run as root 135 | if os.geteuid() != 0: 136 | print("Error: This script must be run as root!") 137 | sys.exit(1) 138 | 139 | # Do some _really_ basic cli argument handling 140 | force = False 141 | if len(sys.argv) > 1 and (sys.argv[1] == "-f" or sys.argv[1] == "--force"): 142 | force = True 143 | 144 | # Exit if the configuration has already been run and the force argument was not specified 145 | if Path("/icpc/setup-complete").exists() and not force: 146 | print("Already configured. Run with -f or --force to reconfigure.") 147 | sys.exit(0) 148 | 149 | # Figure out what contest this is, because it informs some other choices 150 | configuration["contest"] = read_file("/icpc/CONTEST", None) 151 | 152 | # Initialize our dialog (with network status in the backtitle) 153 | print("Waiting up to 30s for network...") 154 | network_status = False 155 | for i in range(0, 30): 156 | if check_network(): 157 | network_status = True 158 | break 159 | print(".", end="") 160 | time.sleep(1) 161 | w = Whiptail(title="ICPC Setup", backtitle=f"Network status: {'online' if network_status else 'offline'}") 162 | 163 | # Print a welcome message! (and record whether they pressed esc or ok) 164 | welcome_ok = welcome(w) 165 | 166 | # prompt to configure Wifi if the network isn't functional and the computer has a wireless interface 167 | if not check_network() and len(wifi_interfaces()) > 0: 168 | configure_wifi(w) 169 | 170 | # Refresh the backtitle with network status now that wifi is configured (hopefully) 171 | network_status = check_network() 172 | w.backtitle = f"Network status: {'online' if network_status else 'offline'}" 173 | 174 | # they pressed escape on the welcome screen (instead of ok), load some demo data and then exit 175 | # if not welcome_ok: 176 | # configuration['contest'] = 'demo' 177 | # configuration['site'] = 'demo site' 178 | # configuration['team'] = { 179 | # "id": '000', 180 | # "name": 'demo team', 181 | # "affiliation": 'fake affiliation', 182 | # "extra": 'team extra - like division information', 183 | # } 184 | # configuration['printers'] = [] 185 | # configuration["print_test_page"] = False 186 | # cfg_printing = False 187 | # else: 188 | 189 | # Choose the contest this image is participating in 190 | configure_contest(w) 191 | 192 | # Choose a contest site (this will decide what teams/printers are available later) 193 | configure_site(w) 194 | 195 | # Choose a team that will use this computer 196 | configure_team(w) 197 | 198 | # configure_autologin(w) # TODO 199 | 200 | # Figure out if they want to configure printers (and what that looks like) 201 | cfg_printing = configure_printing(w) 202 | 203 | # show a summary/confirm they want to apply/move ont 204 | if not confirm_summary(w): 205 | # re-invoke ourselves to start over 206 | os.execv(sys.argv[0], sys.argv) 207 | 208 | # Apply the configuration things previously specified 209 | apply_configuration() 210 | apply_wallpaper() 211 | if cfg_printing: 212 | ret = apply_printers() 213 | if not ret: 214 | w.msgbox("Printing configuration failed. Re-run this script to correct any printer configuration errors") 215 | 216 | # Clear the screen 217 | print(chr(27) + "[H" + chr(27) + "[J") 218 | 219 | # Run the self-test script 220 | p = Popen("su - -c /icpc/scripts/self_test | tee /icpc/self_test_report", shell=True, stderr=PIPE) 221 | out, err = p.communicate() 222 | code = p.returncode 223 | 224 | if configuration["print_test_page"]: 225 | (ret, out, err) = run_cmd(["su", "-", "contestant", "-c", "/icpc/scripts/printfile /icpc/self_test_report"]) 226 | print("Test page sent to printer...") 227 | 228 | # Remove the self-test report 229 | Path("/icpc/self_test_report").unlink() 230 | 231 | input("Press [enter] to continue") 232 | Path("/icpc/setup-complete").write_text(configuration["site"] + "\n") 233 | 234 | 235 | def confirm_summary(w: Whiptail): 236 | w.title = "ICPC Setup Summary" 237 | return w.yesno( 238 | msg=f""" 239 | Contest: {configuration['contest']} 240 | Site: {configuration['site']} 241 | Team: {configuration['team']['id']} - {configuration['team']['name']} ({configuration['team']['affiliation']}) 242 | Printers: 243 | {configuration['printers']} 244 | 245 | Is this correct? 246 | """ 247 | ) 248 | 249 | 250 | def apply_printers(): 251 | failed = False 252 | existing_printers = load_printers() 253 | if configuration["delete_printers"]: 254 | for p in existing_printers: 255 | (ret, out, err) = run_cmd(["lpadmin", "-x", p["name"]]) 256 | # TOOO handle errors 257 | (ret, out, err) = run_cmd(["lpadmin", "-x", "ContestPrinter"]) # this is a printer class 258 | if ret != 0: 259 | failed = True 260 | existing_printers = [] 261 | 262 | for i, p in enumerate(configuration["printers"]): 263 | uri = p["uri"] 264 | driver = p["driver"] 265 | 266 | # A few driver overrides, but everything should us the everywhere driver these days 267 | if driver == "ps": 268 | driver = "drv:///sample.drv/generic.ppd" 269 | if driver == "pcl": 270 | driver = "drv:///sample.drv/generpcl.ppd" 271 | 272 | name = f"Printer{len(existing_printers) + i}" 273 | 274 | # TODO: acls on printing (default deny? then only allow the contestant to print to what we configure?) 275 | (ret, out, err) = run_cmd( 276 | [ 277 | "lpadmin", 278 | "-p", 279 | name, 280 | "-v", 281 | p["uri"], 282 | "-E", # enable + set to accept jobs 283 | "-m", 284 | driver, 285 | "-L", 286 | p["note"], # location field, but we put our "note" here 287 | "-o", 288 | f"media=#{PAPERSIZE}", # Set paper size (a4 or letter) 289 | "-o", 290 | "printer-is-shared=false", # don't set it as shared 291 | ] 292 | ) 293 | if ret != 0: 294 | failed = True 295 | continue 296 | 297 | # Add the printer to the ContestPrinter class 298 | (ret, out, err) = run_cmd(["lpadmin", "-p", name, "-c", "ContestPrinter"]) 299 | if ret != 0: 300 | failed = True 301 | continue 302 | 303 | # really make sure the printer is enabled/accepting jobs 304 | (ret, out, err) = run_cmd(["cupsenable", name]) 305 | if ret != 0: 306 | failed = True 307 | continue 308 | 309 | (ret, out, err) = run_cmd(["cupsaccept", name]) 310 | if ret != 0: 311 | failed = True 312 | continue 313 | 314 | # Check if we have any printers present, if so make sure we enable the class and set it as default 315 | # also ask about printing a test page 316 | existing_printers = load_printers() 317 | if len(existing_printers) > 0: 318 | # Set as default 319 | (ret, out, err) = run_cmd(["lpadmin", "-d", "ContestPrinter"]) 320 | if ret != 0: 321 | return ( 322 | False # return here because setting failed and breaking out of the if statement we're in is difficult 323 | ) 324 | 325 | # Set it as enabled 326 | (ret, out, err) = run_cmd(["cupsenable", "ContestPrinter"]) 327 | if ret != 0: 328 | return False 329 | 330 | # Set it as accepting jobs 331 | (ret, out, err) = run_cmd(["cupsaccept", "ContestPrinter"]) 332 | if ret != 0: 333 | return False 334 | 335 | return not failed 336 | 337 | 338 | def apply_configuration(): 339 | # Write the configuration out to files 340 | Path("/icpc/CONTEST").write_text(configuration["contest"] + "\n") 341 | Path("/icpc/SITE").write_text(configuration["site"] + "\n") 342 | Path("/icpc/TEAMID").write_text(configuration["team"]["id"] + "\n") 343 | Path("/icpc/TEAMNAME").write_text(configuration["team"]["name"] + "\n") 344 | Path("/icpc/TEAMAFFILIATION").write_text(configuration["team"]["affiliation"] + "\n") 345 | Path("/icpc/TEAM").write_text( 346 | f"{configuration['team']['id']} - {configuration['team']['name']} ({configuration['team']['affiliation']})\n" 347 | ) 348 | 349 | # Update the hostname to the team's id 350 | new_hostname = "icpc-team-" + configuration["team"]["id"] 351 | Path("/etc/hostname").write_text(new_hostname + "\n") 352 | Path("/proc/sys/kernel/hostname").write_text(new_hostname + "\n") 353 | 354 | # Update the hosts file 355 | host_lines = Path("/etc/hosts").read_text().splitlines() 356 | for i, line in enumerate(host_lines): 357 | if line.startswith("127.0.1.1 "): 358 | host_lines[i] = f"127.0.1.1 {new_hostname}.icpcnet.internal {new_hostname}" 359 | Path("/etc/hosts").write_text("\n".join(host_lines) + "\n") 360 | 361 | 362 | 363 | def apply_wallpaper(): 364 | cmd = [ 365 | "convert", 366 | "/icpc/wallpaper.png", 367 | "-gravity", 368 | "center", 369 | "-pointsize", 370 | "50", 371 | "-family", "Ubuntu", 372 | "-stroke", 373 | "#000C", 374 | "-strokewidth", 375 | "2", 376 | "-annotate", 377 | "0", 378 | configuration["team"]["name"], 379 | "-stroke", 380 | "none", 381 | "-fill", 382 | "white", 383 | "-annotate", 384 | "0", 385 | configuration["team"]["name"], 386 | "-pointsize", 387 | "30", 388 | "-stroke", 389 | "#000C", 390 | "-strokewidth", 391 | "2", 392 | "-annotate", 393 | "+0+60", 394 | configuration["team"]["affiliation"], 395 | "-stroke", 396 | "none", 397 | "-fill", 398 | "white", 399 | "-annotate", 400 | "+0+60", 401 | configuration["team"]["affiliation"], 402 | "/icpc/teamWallpaper.png", 403 | ] 404 | (code, out, err) = run_cmd(cmd) 405 | if code != 0: 406 | w = Whiptail(title="Wallpaper Generation Error") 407 | w.msgbox( 408 | f"Error applying wallpaper:\ncommand:{' '.join(cmd)}\n\nReturn Code: {code}\n\nout: {out}\n\nerr: {err}" 409 | ) 410 | # convert /icpc/wallpaper.png \ 411 | # -gravity center -pointsize 50 \ 412 | # -stroke '#000C' -strokewidth 2 -annotate 0 "$(cat /icpc/TEAMNAME)" \ 413 | # -stroke none -fill white -annotate 0 "$(cat /icpc/TEAMNAME)" \ 414 | # -pointsize 30 \ 415 | # -stroke '#000C' -strokewidth 2 -annotate +0+60 "$(cat /icpc/TEAMAFFILIATION)" \ 416 | # -stroke none -fill white -annotate +0+60 "$(cat /icpc/TEAMAFFILIATION)" \ 417 | # /icpc/teamWallpaper.png 418 | 419 | 420 | def welcome(w: Whiptail): 421 | w.title = "ICPC Setup - Welcome" 422 | code = w.msgbox( 423 | "Please answer the following questions to finalize the configuration of your environment.\n\nYou can re-run this script later by executing '/icpc/scripts/icpc_setup -f' as root." 424 | ) 425 | 426 | return code == 0 427 | 428 | 429 | def apply_wifi(interface, ssid, password): 430 | with open("/etc/netplan/50-wifi-config.yaml", "w+") as f: 431 | f.write( 432 | f""" 433 | # Created by firstboot script 434 | network: 435 | version: 2 436 | renderer: networkd 437 | wifis: 438 | {interface}: 439 | access-points: 440 | "{ssid}": 441 | password: "{password}" 442 | dhcp4: true 443 | dhcp-identifier: mac 444 | """ 445 | ) 446 | 447 | print("Connecting to wifi...") 448 | (ret, out, err) = run_cmd("netplan", "apply") 449 | has_net = False 450 | for i in range(0, 60, 5): 451 | if check_network(): 452 | has_net = True 453 | break 454 | time.sleep(5) 455 | 456 | if has_net: 457 | print("Connected successfully!") 458 | else: 459 | print("Error connecting") 460 | 461 | 462 | def configure_wifi(w: Whiptail): 463 | interfaces = wifi_interfaces() 464 | 465 | w.title = "ICPC Setup - WiFi Setup" 466 | 467 | # TODO: error handling 468 | (choice, code) = w.menu( 469 | msg="No network was detected. Choose your wireless interface to configure wifi", items=interfaces 470 | ) 471 | interface = choice 472 | print(f"Scanning for networks on: {interface}") 473 | ssids = wifi_scan(interface) 474 | 475 | # TODO: error handling 476 | (choice, code) = w.menu(msg="Select a network to connect to.\nIt must support WPA 2 authentication.", items=ssids) 477 | ssid = choice 478 | 479 | (password, code) = w.inputbox(msg=f"Enter the network password for: {ssid}", password=True) 480 | 481 | apply_wifi(interface, ssid, password) 482 | 483 | 484 | def configure_contest(w: Whiptail): 485 | configuration["contest"] = read_file("/icpc/CONTEST", None) 486 | 487 | # Skip contest configuration if it's already specified 488 | if configuration['contest'] is not None: 489 | configuration['url'] = config_url + '/' + configuration['contest'] 490 | return 491 | 492 | w.title = "ICPC Setup - Contest Selection" 493 | while True: 494 | content = fetch_url(config_url + "/contests") 495 | contests = list(filter(lambda l: len(l.strip()) > 0, content.splitlines())) 496 | contests.append('Enter Manually') 497 | 498 | from pprint import pprint 499 | pprint(contests) 500 | 501 | contest, _code = w.menu(msg="Choose the contest this machine belongs to:", items=contests) 502 | 503 | if contest == 'Enter Manually': 504 | contest, _code = w.inputbox(msg="Enter your contest id:") 505 | 506 | confirm = w.yesno(msg=f"You chose the following contest. Be absolutely sure this is correct as this wizard will not ask again:\n\n{contest}\n\nIs this correct?", default='yes') 507 | if confirm: 508 | break 509 | 510 | # Specify the config url/contest in the configuration hash 511 | configuration['contest'] = contest 512 | configuration['url'] = config_url + '/' + contest 513 | 514 | 515 | 516 | def configure_site(w: Whiptail): 517 | w.title = "ICPC Setup - Site Configuration" 518 | configuration["site"] = read_file("/icpc/SITE", None) 519 | 520 | while True: 521 | content = fetch_url(configuration['url'] + "/sites.csv") 522 | sites = list(csv.DictReader(content.splitlines(), fieldnames=["id", "name"])) 523 | 524 | menu = [(s["id"], s["name"]) for s in sites] 525 | if configuration['site'] is not None: 526 | menu.insert(0, ("keep", f"Keep existing site({configuration['site']})")) 527 | menu.append(('manual', 'Enter site name manually')) 528 | 529 | (site, code) = w.menu("Choose your contest site from the list", items=menu) 530 | if code != 0: 531 | print("Screen aborted, asking again.") 532 | continue 533 | 534 | if site == 'manual': 535 | site, code = w.inputbox(msg="Enter your contest site:") 536 | 537 | if site == 'keep': 538 | site = configuration['site'] 539 | 540 | if code == 0: 541 | break 542 | 543 | configuration["site"] = site 544 | 545 | 546 | def configure_team(w: Whiptail): 547 | w.title = "ICPC Setup - Team Configuration" 548 | configuration["team"] = { 549 | "id": read_file("/icpc/TEAMID", "000"), 550 | "name": read_file("/icpc/TEAMNAME", "default team"), 551 | "affiliation": read_file("/icpc/TEAMAFFILIATION", "default university"), 552 | "extra": read_file("/icpc/TEAMEXTRA", "extra data"), 553 | } 554 | 555 | content = fetch_url(configuration['url'] + f"/teams/{configuration['site']}_teams.csv") 556 | teams = list(csv.DictReader(content.splitlines(), fieldnames=["id", "name", "affiliation", "extra"])) 557 | # strip whitespace around any csv entries 558 | teams = list(map(lambda t: {k: v.strip() for k, v in t.items()}, teams)) 559 | 560 | menu = [ 561 | ( 562 | t["id"], 563 | f"{t['name'] : <30} {t['affiliation'] : <40}{' '+t['extra'] if len(t['extra']) > 0 else ''}", 564 | ) 565 | for t in teams 566 | ] 567 | menu.append(('manual', 'Enter team name manually')) 568 | 569 | if configuration["team"]["id"] != "000": 570 | menu.insert( 571 | 0, 572 | ( 573 | configuration["team"]["id"], 574 | f"Keep existing team selection ({configuration['team']['id']} {configuration['team']['name']} {configuration['team']['affiliation']})", 575 | ), 576 | ) 577 | 578 | (teamid, code) = w.menu(msg="Choose a team", items=menu) 579 | if code != 0: 580 | print('Skipping team selection') 581 | return 582 | 583 | if teamid == 'manual': 584 | id, code = w.inputbox('Enter team id (e.g. 123)') 585 | name, code = w.inputbox('Enter team name (e.g. "null pointer")') 586 | affiliation, code = w.inputbox('Enter team affiliation (e.g. "University of ICPC")') 587 | 588 | # Fake the data so our manually entered data is present 589 | teams = [{ 590 | 'id': id, 591 | 'name': name, 592 | 'affiliation': affiliation, 593 | 'extra': '', 594 | }] 595 | teamid = id 596 | 597 | 598 | # Update configuration only if they picked a different teamid 599 | if teamid != configuration["team"]["id"]: 600 | configuration["team"] = [t for t in teams if t["id"] == teamid][0] 601 | else: 602 | # make sure the site matches the team since they picked 'keep same' 603 | configuration["site"] = read_file("/icpc/SITE", "other") 604 | 605 | 606 | def configure_autologin(w: Whiptail): 607 | w.msgbox("TODO autologin CONFIGURATION") 608 | 609 | 610 | def configure_printing(w: Whiptail): 611 | w.title = "ICPC Setup - Printing Configuration" 612 | 613 | configuration["printers"] = [] 614 | configuration["print_test_page"] = False 615 | existing_printers = load_printers() 616 | 617 | skip = not w.yesno(msg="Configure printers?", default="yes") 618 | if skip: 619 | return False 620 | 621 | configuration["delete_printers"] = False 622 | if len(existing_printers) > 0: 623 | configuration["delete_printers"] = w.yesno(msg="Delete existing printers?", default="no") 624 | 625 | content = fetch_url(configuration['url'] + f"/printers/{configuration['site']}_printers.csv") 626 | printer_groups = list(csv.DictReader(content.splitlines(), fieldnames=["id", "name"])) 627 | menu = [(g["id"], f"{g['name']}") for g in printer_groups] 628 | menu.append(("manual", "Enter printer ip manually")) 629 | menu.append(("skip", "Skip configuring printers")) 630 | 631 | (printer_group, code) = w.menu(msg="Select printers to install", items=menu) 632 | if code != 0: 633 | raise Exception("exited whiptail...") 634 | 635 | if printer_group == "manual": # Choosing to manually enter printers 636 | while True: 637 | (printer, code) = w.inputbox( 638 | msg="Please enter your printer ip address/hostname.\nNote: Your printer must support IPP Everywhere.\n\nLeave this blank to finish adding printers.", 639 | ) 640 | if code != 0 or len(printer) == 0: # they exited abnormally/entered nothing 641 | break 642 | # Force it to be a uri if they entered a plain ip address/hostname 643 | if "://" not in printer: 644 | printer = f"ipp://{printer}" 645 | configuration["printers"].append({"uri": printer, "driver": "everywhere", "note": "Entered Manually"}) 646 | elif printer_group != "skip": 647 | content = fetch_url(configuration['url'] + f"/printers/{configuration['site']}/{printer_group}.csv") 648 | printers = list( 649 | csv.DictReader( 650 | content.splitlines(), fieldnames=["uri", "driver", "note"], restval="Automatically Provisioned" 651 | ) 652 | ) 653 | configuration["printers"] = printers 654 | 655 | configuration["print_test_page"] = w.yesno(msg="Would you like to send a test page to the printer?", default="no") 656 | 657 | return True 658 | 659 | 660 | if __name__ == "__main__": 661 | main() 662 | --------------------------------------------------------------------------------
idsitenameaffiliationspecs
{m['team']}{m['site']}{m['name']}{m['affiliation']}{m.get('cpu_cores',0)}@{m.get('cpu_max',0):.2f}GHz
{m.get('memory',0) / 1024.0 :.2f}GiB Ram