Web server (remote use)¶
The web site lets the students work on the labs from a browser, without the Qt interface and without a remote desktop: the projects run on a pool of Docker hosts, every tab of the graphical interface (Schema, Informations, Machines, Networks, Questions, Evaluation, Terminals, Apply Configuration, Log) is rendered in the browser and the terminals go through WebSockets. The users log in with an LDAP directory (the portal binds to it, or an Apache reverse proxy in front authenticates them), or everybody is anonymous.
Nothing of a lab ever reaches the browser but what a student sees today: the content of
info.json (resolved to the user’s language, the instructor texts stripped), the outputs of the
sre --user commands (the schema, the grades of a self-evaluation, the console of a machine).
The lab files, data.json, the hidden machines and the archives stay on the pool hosts.
The instructors get, on top of what a student sees (Instructors below):
a Watch page, the web counterpart of
sre watch: every running project of the pool, one row per project with its owner, its lab, the last time the student did something on it, the grade and mark of its last evaluation, its age and its errors, the number of self-evaluations the student has run, refreshed every few seconds;an Archives page: every evaluation of the pool hosts, the closed projects included, kept in a database (the results, never the answers), searched by student, group, lab, host and period, with the detail of an evaluation and the summary matrix of a lab;
the projects of the students, open to them to help: every tab of the project, and the student’s terminals joined when the Terminals tab opens, so that the instructor and the student see the same shell and both type in it (a private terminal, unseen by the student, is offered too);
presentations: a project, the instructor’s own or a student’s, duplicated read-only on the browsers of the whole class or of chosen students and groups to demonstrate something. What the instructor does on their own page (changing tabs, typing in a terminal, selecting a question, changing the drawing of the schema, …) is shown at once on the students’ browsers;
the focus on one student: while helping a student on their project, the instructor brings the student’s browser to that project and keeps the two pages on the same tab, the same terminal and the same question, whichever of the two changes it;
the web conference: an instructor opens a BigBlueButton room for chosen students or groups (or the whole class) and talks to them, camera and screen if they wish, in a window over the pages of the site while everybody goes on working (Web conference below).
Architecture¶
browser ──HTTPS──► sre-web (the portal) ──HTTP + token──► sre-agent (one per pool host) ──► sre --user …
sre-web, the portal, is the web site the browsers talk to. It authenticates the users, keeps a signed session cookie, knows the pool of hosts and relays every request about a project to the agent of the host running it. It runs as an unprivileged user, needs neither Docker, Kathara nor the labs, and may run anywhere on the network (the Debian packagesysreseval-webinstalls alone).sre-agentruns on every pool host, as root, next tosre. It drives thesreCLI in user mode on behalf of the web users exactly assre-wrapper+sudodo for the students of a workstation (sre --user <cmd>withSUDO_USER=<login>), owns the terminals (a pseudo-terminal persre connect), writes the answers file with the user’s identity (atomically and under the advisory lock on theanswers/directory the Qt GUI takes too, the owner of a file the Qt GUI of the same student created kept, so that the GUI goes on writing it; what that GUI writes is pushed to the open pages, which show the same answers), merges the cheat answers of the states, tails the operations log and schedules what the Qt GUI used to schedule: the periodic evaluations of a lab (eval_interval_without_exam_mode), the evaluation before a project is closed (eval_before_exit), and an idle reaper closing the projects nobody touched for the configured time. For an exam project of the web site (Exams below) it applies the rules of the Qt GUI’s exam mode to that project alone: the evaluations at the period of the exam, the final one at its end, the owner cut off then.sre-web-toolis the command line next to the portal:sre-web-tool …lists and changes the pool of the running portal through its administration API (The pool below).The pool hosts share the lab tree as today (NFS, rsync or the Debian package) and, if the instructor wants
sre watch/sre sheet/sre outlineacross the pool, anarchive_dirsentry on a shared directory. Thehostnameof the archives is the pool host. The Watch page of the instructors needs none of that: each agent reads the archives of its own host (params.archive_dirs) on demand.Placement: a new project goes to the host where its user has a local login session (the workstation they sit at,
follow_local_sessions), else to the reachable host with the lowestprojects / max_projects(then the lowest load); a host in exam mode is never used, a host withinstructors_only = truenever for a student (an instructor may also name the host on the Open page); the whole rule is under Placement in A pool of workstations. A classroom workstation holds about thirty projects (210 containers in 3.4 GB).The lab list of the Open page comes from one host, the one a new project of the user would go to (every host of a pool serves the same lab tree; the next candidate is asked when it fails), and is kept
labs_cache_ttlseconds (60) per role, since the reserved hosts are the instructors’ only: opening the page costs onesre listper minute whatever the size of the pool.
The agent enforces what the sre CLI does not check: the exact running lab name (the CLI
resolves substrings) and its owner (a user only reaches their own projects, an instructor every
project; the CLI’s own check, params.check_project_owner, lets a --user caller reach their
own projects and root’s only, which is why an instructor is served by privileged calls; the
wipe, stop-all, exec and host-wide exam commands have no endpoint: an exam of the web site
is a POST /v1/projects with an exam object, adjusted with PUT / DELETE …/exam),
allow_connection of the
devices, allow_self_grade, the
user-allowed states, the export_schema_pdf flag of the lab (the Export Schema to PDF
button). Save, restore and the Kathara export are not offered on the web. An instructor acting on
somebody else’s project is served by a privileged sre (no --user, their own login:
sre eval without cooldown nor counter, sre connect to every listed device, sre state),
never by a command run as the student; the owner, instructor or not, follows the student path.
Installation¶
From a checkout¶
make venv installs the Python dependencies (aiohttp, jinja2, aiohttp-jinja2, ldap3, and
pymysql for the optional database of the connection log);
make xterm and make novnc (run by make install) download the terminal library and the
VNC viewer of the browser into web/static/vendor/ (checksum-pinned, not committed). scripts/install.sh offers to install the
systemd units sre-agent.service and sre-web.service (disabled) and the example configuration
files agent.toml and web.toml in /etc/sreweb/ (params.agent_config_file /
params.web_config_file; --config names another file).
On every pool host: write /etc/sreweb/agent.toml (root, mode 0600) with a long random token
(the example file listens on every interface, 0.0.0.0:8451: allowed_clients and the mutual
TLS below keep the port to the portal), then systemctl enable --now sre-agent.
The unit starts the agent only when that file exists and /etc/sreweb/DO_NOT_START_AGENT
does not (ConditionPathExists=: one image for a whole classroom may ship the unit enabled, the
hosts without a configuration file, or with the marker, skip it without any failure; checked at
boot and on systemctl start / restart), and it does not restart the agent after a
configuration error (exit status 78, EX_CONFIG: a bad agent.toml, a missing certificate;
the message is in the journal, fix the file then systemctl restart sre-agent); five failed
starts within five minutes leave it stopped until the next boot or systemctl reset-failed.
For the Display tab (below), install tigervnc-standalone-server and openbox, create the
sreweb-x11 system user (scripts/install.sh offers it) and set [display] enabled = true.
For a whole classroom, scripts/make-web-hosts.sh (see A whole site at once below) writes
the tokens, the certificates and the [[hosts]] entries from a list of machines.
On the portal host: create the sre-web system user (adduser --system --group --no-create-home --shell /usr/sbin/nologin sre-web), write /etc/sreweb/web.toml (root:sre-web, mode 0640:
it holds the cookie secret and the agent tokens; the files it imports likewise), then
systemctl enable --now sre-web. Put a
reverse proxy doing TLS in front (scripts/etc/sre-web-nginx.conf is an nginx example,
sre-web-apache.conf an Apache one that also authenticates the users: the proxy mode below), or
give the portal its own certificate (tls_cert / tls_key).
Debian packages¶
sysreseval-agent on every pool host (it depends on sysreseval and sysreseval-web, whose
code it shares), sysreseval-web on the portal host (it depends on nothing of SysResEval). The
units are sysreseval-agent.service and sysreseval-web.service, disabled by default (the
agent’s with the conditions and restart rules above, on /etc/sysreseval/agent.toml and
/etc/sysreseval/DO_NOT_START_AGENT); the
configuration files are /etc/sysreseval/agent.toml and /etc/sysreseval/web.toml (examples in
/usr/share/doc/sysreseval-*/examples/); the postinst of sysreseval-web creates the sre-web
user and protects web.toml and web.d/*.toml when they exist.
The postinst of sysreseval-agent creates the sreweb-x11 user of the virtual X desktops;
tigervnc-standalone-server and openbox are its Recommends (the Display tab is off until
[display] enabled = true).
Configuration¶
agent.toml¶
Key |
Default |
Meaning |
|---|---|---|
|
|
the address the portal reaches: |
|
required |
the shared secret of this host, repeated in the portal’s |
|
the installation’s |
the command run for the users |
|
|
seconds between two scans of the projects directory (the Qt GUI polls every second too) |
|
|
default idle timeout of a project (the portal gives its own per user); |
|
|
caps on the open terminals (every Connect opens a new terminal on the machine); at a cap, the idle terminal that has waited longest gives way |
|
|
how long a terminal whose last browser left (the page closed or reloaded, another project opened, the network cut) is kept for the browser to come back to it; |
|
|
concurrent |
|
|
seconds the output of |
|
|
the idle clocks of the projects, kept across restarts |
|
none |
the certificate and key of this agent (TLS to the portal) |
|
none |
the CA a client certificate must be signed by: only the portal gets past the handshake (mutual TLS) |
|
|
the addresses or networks the agent answers, e.g. |
|
|
when the host reports itself occupied (no new project, never powered off): |
|
|
what the agent does with the projects it manages when its unit stops: |
|
|
the portal may power the host off ( |
|
none |
how the agent restarts itself on |
|
|
seconds, per |
|
|
one virtual X desktop per project whose lab has an |
|
|
the Docker images of the labs pulled by the agent ( |
Durations are written as 90, "90s", "30m", "2h" or "1d". An unknown key is refused.
import (a path or a list of paths relative to agent.toml: a directory, a glob or a file)
merges other files into this one, as for web.toml below.
web.toml¶
Key |
Default |
Meaning |
|---|---|---|
|
|
behind a reverse proxy: the loopback |
|
none |
the URL the users type (the |
|
required |
signs the session cookies (32+ random characters) |
|
|
proxy mode: the addresses or networks of the reverse proxy, the only peers whose identity headers are trusted (the TCP peer address, never |
|
|
the languages offered at login, the first one by default |
|
none |
own TLS instead of a proxy |
|
|
|
|
|
the logins of the administrators: instructors who may read the connection log on the History page (Administrators below); a login that is not an instructor gets nothing. By group: |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
the connection log (below): |
|
|
none |
the same events in a MariaDB / MySQL table (below): |
|
the archives of the evaluations of the pool hosts in a second table of that database (The archives of the evaluations below; on iff |
|
|
the user groups of the Groups page (User groups below): |
|
|
the exams of the Exams page (Exams below; on iff |
|
|
none |
the institution shown next to the SRE logo in the header of every page, the login page included: |
|
none |
the BigBlueButton server behind the Webconf button of the instructors (Web conference below): |
|
required |
one per pool host: |
|
none |
other files merged into this one before it is checked: a path or a list of paths, relative to the directory of |
import = ["web.d"] splits the configuration into /etc/sysreseval/web.toml and the *.toml
files of /etc/sysreseval/web.d/, read by name after it: a file per pool host, the secrets
apart. Nothing is silently overridden: a key given twice is an error naming both files, and an
error found after the merge names web.toml. The imported files hold the same secrets as
web.toml and need the same owner and mode (root:sre-web, 0640).
The full name and the e-mail of an LDAP account (attr_fullname, attr_mail) go into the
answers file of the projects like the GECOS fields do on a workstation, so sre outline and
sre sheet show them; an anonymous user is identified by the pseudonym and the display name.
The student group (attr_group in LDAP mode, header_group or ldap_roles / ldap_groups +
attr_group in proxy mode; departmentNumber typically, its first value, capped at 64
characters) goes there too under the key group (hence into the archives): the Watch page shows
it and filters on it. With a directory the portal can read (attr_group in LDAP mode,
ldap_roles or ldap_groups in proxy mode), the Watch page also looks up the group of the
owners the agents know none for, first of all the projects started on the Qt GUI, whose answers
file never carries one, and shows for the projects of the Qt GUI the full name of the directory
(attr_fullname) rather than the GECOS name of the account their answers file carries (lower
case on some sites): one cached lookup per login.
The agent writes that metadata as soon as a project is created, before any answer, and again
when the owner opens the project page and the file lacks it or holds other values (so a
project created by an older agent, or on the Qt GUI, gets its name and group then); a request
whose identity carries no full name or no group (a header the proxy did not set) never erases
the stored values.
Authentication by the reverse proxy¶
With [auth] mode = "proxy" the portal has no login page: the reverse proxy in front
authenticates the users itself and names them in request headers, which the portal reads on
every request (pages, API calls, the NDJSON streams of the starts, the WebSockets of the
terminals). Nothing is stored in a cookie: the proxy’s session is the session, and a request
that does not come from a peer of trusted_proxies has no identity whatever its headers.
scripts/etc/sre-web-apache.conf is the Apache 2.4 example (mod_authnz_ldap + mod_proxy):
<Location "/">
AuthType Basic
AuthName "SRE"
AuthBasicProvider ldap
AuthLDAPURL "ldaps://ldap.example.org/ou=people,dc=example,dc=org?uid,cn,mail,memberOf,gidNumber,departmentNumber?sub?(objectClass=inetOrgPerson)"
AuthLDAPRemoteUserAttribute uid
Require valid-user
RequestHeader set X-Remote-User "expr=%{REMOTE_USER}"
RequestHeader unset X-Remote-Name
RequestHeader set X-Remote-Name "%{AUTHENTICATE_CN}e" env=AUTHENTICATE_CN
RequestHeader unset X-Remote-Email
RequestHeader set X-Remote-Email "%{AUTHENTICATE_MAIL}e" env=AUTHENTICATE_MAIL
RequestHeader unset X-Remote-Groups
RequestHeader set X-Remote-Groups "%{AUTHENTICATE_MEMBEROF}e" env=AUTHENTICATE_MEMBEROF
RequestHeader unset X-Remote-Gid
RequestHeader set X-Remote-Gid "%{AUTHENTICATE_GIDNUMBER}e" env=AUTHENTICATE_GIDNUMBER
RequestHeader unset X-Remote-Group
RequestHeader set X-Remote-Group "%{AUTHENTICATE_DEPARTMENTNUMBER}e" env=AUTHENTICATE_DEPARTMENTNUMBER
RequestHeader unset Authorization
</Location>
# Logout: a location that knows no user, so the browser forgets the credentials it cached
ProxyPass "/apache-logout" !
<Location "/apache-logout">
AuthBasicProvider file
AuthUserFile /dev/null
Require valid-user
ErrorDocument 401 "<!DOCTYPE html>... You are logged out. <a href='/'>Log in again</a> ..."
</Location>
ProxyPreserveHost On
ProxyPass "/" "http://127.0.0.1:8450/" upgrade=websocket timeout=3600
ProxyPassReverse "/" "http://127.0.0.1:8450/"
AuthLDAPRemoteUserAttribute is not optional: it makes REMOTE_USER, hence the login header,
the value the directory stores in the login attribute. Without it Apache passes what the user
typed, and since the directory matches Bob for bob (uid is caseIgnoreMatch) one account
would become two users of the portal (separate projects, out of the exam audiences and custom
groups naming bob, split Watch and Archives rows). The attribute must be the one the agents and
the exam audiences know the users by (uid for the Unix login of the workstations, see A pool of
workstations). In LDAP mode the portal does this itself: the identity carries the stored value
of the login attribute (attr_login, else the attribute of the search_filter or of the
bind_dn_template), whatever case the user typed.
The rules that keep this safe:
The portal listens on the loopback (or on an address only the proxy reaches) and
trusted_proxiesholds the proxy’s address as the portal sees it (127.0.0.1and::1for a proxy on the same host); the portal takes the TCP peer address, neverX-Forwarded-For(the connection log alone reads that header, from these peers only, for the client address).The proxy sets the four headers on every request, replacing whatever the browser sent (
RequestHeader setdoes; never pass a client header through), after the authentication (notearly). An optional header is removed when the attribute is missing, ormod_headerswrites the literal(null), which the portal treats as empty.The attributes exported by
mod_authnz_ldap(AUTHENTICATE_CN…) are those listed in theAuthLDAPURL; a multi-valued one is joined with;(hencegroups_separator = ";"). The groups header may carry any attribute,memberOfDNs oreduPersonAffiliationvalues:instructor_groupslists the values giving the instructor role;instructor_header/instructor_valuestest a second attribute the same way (gidNumberabove, the primary group);header_groupnames the header carrying the student group (departmentNumberabove). A group membership the proxy cannot export (aposixGrouplisting itsmemberUid, without the memberof overlay) is tested by the portal itself withldap_roles = trueand the[auth.ldap]settings (see Who is an instructor). A long list of groups can exceed Apache’sLimitRequestFieldSize(8190 bytes): raise it or export fewer attributes.public_urlmust be thehttps://URL the users type (theSecureflag of the language cookie, theOrigincheck of the WebSockets together withProxyPreserveHost On).An HTTP Basic authentication has no session to end, but the browser forgets the credentials it cached as soon as the server refuses them: the
/apache-logoutlocation of the example knows no user (mod_authn_fileon/dev/null, the realm inherited from the main location), so every request gets a 401 and the browser shows its password dialog once more; cancelling it displays the “logged out” page of theErrorDocument(a password typed there is refused and the dialog comes back).logout_url = "/apache-logout"makes the Logout button appear; the button is hidden without it. Amod_auth_formlogin (sketched at the end of the example) has a real logout handler at the same URL.sre-web-tool …talks to the loopback port directly, never through the proxy (the Basic authentication would refuse its bearer token).
The connection log¶
[access_log] of web.toml makes the portal keep a log of its use: who connected from where
and for how long, which projects were opened, started, evaluated, closed. One key=value line
per event (a value with spaces or quotes is JSON-quoted, so a user agent or a display name
cannot forge a line), in the file named by file (rotated at midnight, keep days of
access.log.YYYY-MM-DD files kept; keep = 0 leaves the rotation to logrotate, the file is
reopened when it moves) and, with journal = true (the default, and the whole log when no
file is set), in the portal’s own log on stderr: the journal under systemd, docker logs in a
container. The unit creates /var/log/sre-web (LogsDirectory=); a container mounts a
directory for the file or writes it under /var/lib/sre-web. The lines of the file start with
a timestamp (2026-10-26T09:12:03+0100):
event=login login=alice addr=10.0.1.7 mode=ldap role=student anon=0 name="Alice Test" group=INFO1
event=session_open login=alice addr=10.0.1.7 role=student anon=0 name="Alice Test" group=INFO1 ua="Mozilla/5.0 …"
event=start login=alice addr=10.0.1.7 lab=sre/dhcp.py instructor=0 debug=0 host=a01 placement=load result=created rln=1761466323@@@dhcp@@@alice status=200 duration=12.4
event=project_open login=alice addr=10.0.1.7 host=a01 rln=1761466323@@@dhcp@@@alice
event=terminal_open login=alice addr=10.0.1.7 host=a01 rln=1761466323@@@dhcp@@@alice device=m1 join=- private=0
event=terminal_close login=alice addr=10.0.1.7 host=a01 rln=1761466323@@@dhcp@@@alice device=m1 join=- private=0 duration=612.3 code=1000
event=eval login=alice addr=10.0.1.7 host=a01 rln=1761466323@@@dhcp@@@alice status=200 duration=8.1
event=project_close login=alice addr=10.0.1.7 host=a01 rln=1761466323@@@dhcp@@@alice duration=1803.6 code=1001
event=stop login=alice addr=10.0.1.7 host=a01 rln=1761466323@@@dhcp@@@alice status=200 duration=3.0
event=logout login=alice addr=10.0.1.7 status=200 duration=0.0
event=session_close login=alice addr=10.0.1.7 role=student name="Alice Test" group=INFO1 reason=logout since=2026-10-26T09:11:50+0100 duration=1850 requests=14 projects=1761466323@@@dhcp@@@alice
A session is the requests of one login from one client address (addr): it opens at the
first request (session_open: role, pseudonym or not, name, student group, user agent) and
closes (session_close) at a logout, after session_timeout without a request (reason=idle;
the duration then ends at the last request, not at the expiry) or when the portal stops
(shutdown); a session whose project page, terminal or desktop is still open never expires.
The closing line repeats the role, the name and the group (the student group of the directory
or of the proxy, attr_group / header_group, - without one; the last non-empty value seen
during the session, since a directory lookup may fail on the first request) and gives the
start, the duration in seconds, the number of requests (the polls of the pages and the
keep-alive of a project are not counted) and the projects used: the Sessions view of the
History page (Administrators below) is built from that line alone. In proxy mode there is no login: the first request of a user opens the
session and the request following a logout opens another one. The other events:
login(mode:anonymousorldap) andlogin_failed(reason:password,rate_limited,directorywhen the directory does not answer,proxy_mode);start: the lab, the host chosen andplacement, why that host (load: the least loaded,session: the user is logged in at its keyboard,named: the instructor named it), whether the instructor mode or the debug mode was asked (instructor,debug),result(created,flavor_form,flavor_error,error) andrln, the running lab name of the new project;project_open/project_close: the project page, open as long as its events WebSocket is (durationin seconds,code: the WebSocket close code;view=instructorfor the instructor view);terminal_open/terminal_close(device,join: the session joined,private) anddisplay_open/display_closelikewise; a socket refused before it opens (401, 403, 404) gives a close line withstatusinstead ofcode;eval,state(state),stop,display_start: the HTTP status and the duration;denied: a call answered 401, 403 or 429 (method,path),login=-when nobody was logged in;admin: a call ofsre-web-toolchanging the pool, or an action of the Pool page of an instructor (theirlogin);history: an administrator read the log on the History page (Administrators below; the filters asked come aslogin_filter,addr_filter,event_filter,host_filter,rln_filter,lab_filter,group_filter(the key of the group,kind:name),since_filter,until_filter,before_filter;view=sessionsfor the Sessions view,view=statisticsfor the Statistics view with the period resolved insince_filter/until_filter);the presentations (Instructors below):
present_start(the instructor’slogin,host,rln,audience: the number of logins and groups chosen),present_audience(the audience changed),present_stop, andpresent_open(a student opened the presented page: theirlogin, thepresenter); the project calls a spectator makes keep their own events, their close lines carryingspectator=1. The presence socket every page keeps open, the view the presenter publishes and the pointer frames of their presentation socket are not logged;the focus on a student (Instructors below):
focus_start(the instructor’slogin,host,rln, thestudent) andfocus_stop(theinstructorof the focus, thestudent); the view the two pages share and their sockets are not logged;the web conferences (Web conference below):
webconf_start(the instructor’slogin,webconf= the id of the room, itsname,audience= the number of logins and groups),webconf_audience(the audience changed, theownerof the room),webconf_attend(an instructor joining a room),webconf_stop, andwebconf_join(a join URL served to a participant: theirlogin,webconf,role, status 302); the listing of the rooms is a keepalive, the page the client comes back to after Leave meeting is not logged;the private conversations (Private conversations below):
webconf_invite(the instructor’slogin, thestudentasked, thehostandrlnof the project they asked from, theinvitation; status 409 when the student is not connected or busy),webconf_accept(the student’slogin, theinvitation, theinstructor,webconf= the room made; 410 when the invitation was over, 502 / 503 as the server answered),webconf_decline,webconf_cancel(the invitation taken back by an instructor, thestudent) andwebconf_leave(one of the two ending the conversation:webconf,owner); an invitation nobody answers expires without a line.
The client address is the last entry of X-Forwarded-For when the TCP peer is one of
trusted_proxies (the Apache and nginx examples set that header), the peer address otherwise:
set trusted_proxies in every mode to log the real addresses behind a proxy. The login rate
limit ([limits] login_per_minute) counts per logged address too. The log holds personal data
(names, addresses, what was done when): keep is its retention, and the file is readable by
sre-web and root only (/var/log/sre-web is mode 0750).
In a database. [access_log.database] sends the same events to a MariaDB / MySQL table
through PyMySQL (python3-pymysql, suggested by the Debian package rather than installed with it since
the feature is off by default, or pip install pymysql in the venv; the file and the journal stay
what they are), where the History page of the administrators (Administrators
below) reads them:
[access_log.database]
host = "db.example.org" # or unix_socket = "/run/mysqld/mysqld.sock"
user = "sre_web"
name = "sre" # the database; password = "…" in an imported secrets file
keep = 400 # days of rows kept (purged daily); 0 = forever
One row per line: the time (ts, UTC, millisecond precision), the event, the login (empty when
nobody was logged in), the client address, the host, the project (rln), the lab, the status
and the duration in columns, the other fields of the line as JSON in details (SELECT login, COUNT(*) FROM access_log WHERE event = 'start' GROUP BY login works as expected). The table
(table, access_log by default) is created when it is missing, so the account needs CREATE
for the first start; or create it by hand and grant SELECT, INSERT, DELETE only (DELETE for
the purge of keep):
CREATE DATABASE sre CHARACTER SET utf8mb4;
CREATE USER 'sre_web'@'%' IDENTIFIED BY '…';
GRANT SELECT, INSERT, DELETE ON sre.* TO 'sre_web'@'%';
CREATE TABLE sre.access_log (
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
ts DATETIME(3) NOT NULL,
event VARCHAR(32) NOT NULL,
login VARCHAR(64) NOT NULL DEFAULT '',
addr VARCHAR(64) NOT NULL DEFAULT '',
host VARCHAR(64) NOT NULL DEFAULT '',
rln VARCHAR(255) NOT NULL DEFAULT '',
lab VARCHAR(255) NOT NULL DEFAULT '',
status VARCHAR(16) NOT NULL DEFAULT '',
duration DOUBLE NULL,
details TEXT NOT NULL,
PRIMARY KEY (id), KEY ts (ts), KEY login_ts (login, ts), KEY addr_ts (addr, ts), KEY event_ts (event, ts)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
The rows are written by a thread of the portal, in batches, and a request never waits for the
server: while it does not answer, the rows wait in memory (queue of them at most, the oldest
dropped beyond; one warning in the portal’s log, one line when the writing resumes; the History
page says it too) and the portal goes on; a batch the server refuses is written row by row and
a refused row is dropped with a warning; the rows still waiting when the portal stops get one
last attempt. The rows older than keep days are deleted once a day (DELETE … LIMIT: a
replicated server needs a row-based binlog). Every consultation of the History page is a
history line, in the file and in the table alike. The table holds the same personal data as
the file: keep is its retention, and the account of the portal should be the only one reading
it. In the production container the driver must be in the image (or the venv) and the server
reachable from it (a local MariaDB over the host network, or its socket bind-mounted).
Retention. The portal keeps keep days of each store while it runs: the rotated files
beyond keep go at the next rotation, the rows older than keep days once a day.
scripts/web-purge-log.py applies a retention of its own, for a cron job or a timer of the host
(run with the Python of the portal, PyMySQL included; it reads web.toml): --days (365 by
default), --dry-run, --optimize (the table rebuilt after the deletion, so that the deleted
rows no longer occupy its file; a short lock), --files-only / --database-only,
--archives-days N (the archives of the evaluations too, below; left alone otherwise); the
current file is never touched, a rotated file goes by its modification time. For instance in
/etc/cron.d/sre-web:
15 4 * * * root /opt/sre/venv/bin/python3 /opt/sre/scripts/web-purge-log.py --days 365
The archives of the evaluations¶
Every sre eval writes an archive on the host that ran it (params.archive_dirs,
/var/lib/sre/archives: the student’s self-evaluations, the periodic evaluations of the web
agent, an instructor’s, the final one of a project closed with eval_before_exit, those of the
Qt GUI), and the Watch page reads the newest archive of each running project. With
[access_log.database] configured, the portal also collects every archive of every pool host
into a second table of that database ([archives] of web.toml: table, archives by
default; keep, 0 = the rows are kept forever; poll_interval, 30s), and the Archives
page of the instructors (Instructors below) reads it: the results of a project survive its
closing, the history of its evaluations is kept, and the results of a student or of a class group
can be searched across the labs and the hosts.
The portal pulls, as it does for everything else: every poll_interval the collector asks each
reachable agent for the archives of its host it has not taken yet (GET /v1/archives?after=<file name>&limit=50, the token alone: the archives whose name is greater than the last one taken, in
the order of the names, which start with the evaluation time; up to ten calls per host and
round, so that a host with a backlog of thousands of archives, the first run on a site that
already evaluated for months, is imported at about a thousand archives per minute), and queues
one row per archive. The agent decodes the archives and sends what the page needs: the project,
the lab, the evaluation time, the student’s login, full name, group, e-mail, the host that
evaluated, the exam flag and fields, the exo-eval and self-eval totals and marks, the errors and
warnings, and every element of the grade list with its title and description as the lab wrote
them (in every language of the lab: the browser shows the viewer’s), its grade, maximum, letter,
part and bonus flag. The answers of the students are not sent and not stored. The group and
the full name an archive lacks (the projects of the Qt GUI) are completed from the directory
when the portal reads one (attr_group, ldap_roles / ldap_groups), as on the Watch page.
The reports an instructor downloads from the page (the spreadsheet of sre sheet, the PDF
reports of sre outline or their summary spreadsheet alone: Instructors below) are built from
these rows, not from the archive
files of the hosts, which may be asleep or reinstalled: a row holds everything the two commands
read from an archive (the student, the time, the totals, the grade elements with their titles and
parts, the number of errors; never the tests nor the answers). The portal turns the rows back
into the records the agents had sent and hands them to one agent of the pool, chosen as the
lab list is (an enabled, reachable host, up and little loaded first; the next one when it does
not answer), which runs sre report (an internal command: the records on stdin, the file on
stdout, one report at a time per host, [timeouts] report of agent.toml, 600 seconds) and
answers the download the portal streams to the browser. The files are therefore those sre sheet / sre outline would give on the same archives, in the language of the instructor, but
for one thing: a student is a login whatever the host of the pool the project ran on, where the
two commands tell a login on two hosts apart (Reports, under Instructors below); the
elements of an evaluation whose details were too big for the table (over 1 MB) are missing from
them. The PDF reports and their summary read the grade elements of one evaluation per student
only, the best one (the highest total, the earliest of them): when every evaluation of the
selected projects is asked, the portal sends the others as light records (the student, the
time, the totals, the error counts: about 300 bytes instead of several kilobytes), read without
their details from the database, so that such a request may cover 150 000 evaluations (hundreds
of projects evaluated every minute for hours) where the spreadsheet of sre sheet, which shows
the elements of every evaluation, stops at 5000. Every download is a report line of the
connection log (the kind, the rows ticked, the options, the evaluations and projects sent, full
= the evaluations sent with their details, the host that built the file).
One cursor per host, the greatest archive name taken, lives in the portal’s memory and is read
from the table itself at the first round after a start (SELECT host, MAX(archive) … GROUP BY host): nothing is collected while the database does not answer, and a restart never imports a
host again. The name of an archive is unique in the table (UNIQUE (archive), INSERT IGNORE),
so a host whose archive directory is shared with another one, or rows still queued when the
portal stopped, only cost a harmless re-insert. An archive rewritten in
place (sre re-eval -p '' -d <archive dir>) or named below the cursor (a host whose clock was
set back) is not taken again, as sre watch would not see it either: the results of a
re-evaluation reach the table through sre re-eval --update-web (Re-evaluated archives below).
An agent not yet updated answers 404 and is skipped with one warning until it is.
The table is created like access_log when it is missing (the CREATE privilege, once); or
create it by hand and grant SELECT, INSERT, DELETE on it (the same grant as the log’s: the
account needs no UPDATE):
CREATE TABLE sre.archives (
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
ts DATETIME(3) NOT NULL, -- the evaluation time, UTC
host VARCHAR(64) NOT NULL DEFAULT '', -- the pool host (the name of [[hosts]])
archive VARCHAR(300) NOT NULL, -- the file name: unique
rln VARCHAR(255) NOT NULL DEFAULT '', -- the running lab name
lab VARCHAR(255) NOT NULL DEFAULT '', -- sre/dhcp.py: the path the student started
canonical_lab VARCHAR(255) NOT NULL DEFAULT '', -- the file really run, symlinks resolved (sre/dhcp.py for s3/r306/dhcp.py -> ../../sre/dhcp.py)
login VARCHAR(64) NOT NULL DEFAULT '',
fullname VARCHAR(255) NOT NULL DEFAULT '',
grp VARCHAR(64) NOT NULL DEFAULT '', -- the student group
hostname VARCHAR(64) NOT NULL DEFAULT '', -- the host the archive names
grade DOUBLE NOT NULL DEFAULT 0, -- exo-eval scope
max_grade DOUBLE NOT NULL DEFAULT 0,
mark DOUBLE NULL,
maximum_mark DOUBLE NULL,
errors INT NOT NULL DEFAULT 0,
warnings INT NOT NULL DEFAULT 0,
exam TINYINT NOT NULL DEFAULT 0,
auto_eval_count INT NULL, -- the self-evaluations of the student so far
details MEDIUMTEXT NOT NULL, -- JSON: elements, parts, errors, self-eval totals, metadata
PRIMARY KEY (id), UNIQUE KEY archive (archive), KEY ts (ts), KEY login_lab_id (login, lab, id),
KEY login_canonical_id (login, canonical_lab, id), KEY rln_id (rln, id), KEY lab_ts (lab, ts),
KEY canonical_lab_ts (canonical_lab, ts), KEY grp_ts (grp, ts), KEY host_archive (host, archive)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
Upgrading a table created before canonical_lab (releases up to 0.15.0): the portal probes
the columns when it connects and, while one is missing, keeps the rows in memory and warns once
(archives database: the table archives lacks a column the rows need …) instead of refusing them;
add the column and its keys from the root console before the portal restarts with the new code,
then give the old rows their own path as the file run, and run that UPDATE once more after the
restart: the rows the old code inserted meanwhile have an empty value (the account of the portal
has neither ALTER nor UPDATE):
ALTER TABLE sre.archives ADD COLUMN canonical_lab VARCHAR(255) NOT NULL DEFAULT '' AFTER lab,
ADD KEY login_canonical_id (login, canonical_lab, id), ADD KEY canonical_lab_ts (canonical_lab, ts);
UPDATE sre.archives SET canonical_lab = lab WHERE canonical_lab = '';
-- to merge the old rows of a symbolic link with their lab, by hand:
-- UPDATE sre.archives SET canonical_lab = 'sre/dns1.py' WHERE lab = 's3/r306/dns1.py';
The new rows carry the file really run as soon as the agents run the new code (an older agent
sends none and the row gets its path). The rows go through the same kind of writer as the log’s (batches, the rows waiting in memory
while the server does not answer, a refused row dropped with a warning, a last attempt at
shutdown) and are purged daily beyond keep days when set; web-purge-log.py --archives-days N
applies a retention from a cron job. Expect about thirty rows per project and hour of
auto-evaluation (eval_interval_without_exam_mode), a few kilobytes each. The table holds
personal data (names, groups, results): the account of the portal should be the only one reading
it, and the page is open to the instructors only. Every consultation of the Archives page is an
archives line of the connection log, with the filters asked.
Re-evaluated archives¶
A grading bug found after the evaluations is fixed in the lab file and the archives are graded
again with sre re-eval, which replays the lab on the test
results each archive holds. The table of the portal was filled when the evaluations ran and does
not follow by itself; sre re-eval --update-web makes it follow:
# as root, on the pool host whose /var/lib/sre/archives holds the archives
sudo sre re-eval -s /opt/sre/lab/s3/r306/dns1.py -p corrected -d /root/regraded \
--update-web /var/lib/sre/archives/*@@@s3@r306@dns1.py@@@*
The command writes the re-graded archives as usual (-p, -d) and, for each of them, queues the
new results on the host: one file per archive in /var/lib/sre/web-updates/ (mode 0700, the
files 0600, owned by sre: they name students and grades), holding what the agent sends for an
archive — never the answers — under the name of the original archive, which is how the portal
knows the evaluation. A later re-evaluation of the same archive replaces the one still waiting.
Nothing else is to be configured: no address of the portal, no token, no privilege of the
database account.
The portal takes them as it takes the archives. The answer of GET /v1/archives says how many
re-evaluations wait on the host (updates); when there are some, the collector reads them in
pages (GET /v1/archives/updates?after=&limit=, the token alone), writes each row again in one
transaction per page, and tells the agent which ones are settled (POST /v1/archives/updates),
which removes them from the host. A row is written with REPLACE under its own id — the
INSERT and DELETE privileges the account already has; no UPDATE — so it keeps its place on
the Archives page and the links to it. Of the stored row it also keeps what only the first
collection knew:
Kept from the stored row |
Taken from the re-evaluation |
|---|---|
|
|
|
|
|
the elements and the parts of the grade list |
the |
the self-eval totals, |
The reports downloaded from the page are built from the rows, so they show the new results at once. The Watch page reads the archive files of the running projects, not the table.
Only the evaluations the table holds are updated; none is added. A re-evaluation whose evaluation is not there yet (collected at the next round, or the database did not answer) stays on the host and is tried again at every round; the agent gives it up, with a line of its log, after a day. Give the archives under their original names: the output of an earlier
re-evaland a renamed archive are not queued.Run it where an agent runs. The queue is read by the agent of the same host. With an archive directory shared by the pool (
archive_dirs), one run on any pool host updates the evaluations of every host: a row is found by the name of its archive, whichever host it came from. On a host withoutagent.tomlthe command warns that nothing will hand the updates over.Applied at least once. The portal acknowledges after its transaction is committed; a round interrupted in between writes the same row again at the next one, which changes nothing. A database or an agent failing here is warned about once in the portal’s log (
archives: <host> (re-evaluations): …) and never stops the collection of the archives.The portal logs
archives: <host>: N evaluation(s) replaced by their re-evaluation; the Archives page’s database status counts them (collector.updated). A re-evaluation of an archive whose row belongs to another project is dropped with a warning.An agent older than this feature never announces the queue and a portal older than it, or one without
[access_log.database], never asks for it: the files wait on the host. Whatever waited for more than a day is dropped when the queue is next read by the agent or written bysre re-eval --update-web, which then warns that the portal never took them.
User groups¶
The Groups page of the instructors (/groups, the link in the header) lists the groups of
users the portal knows, of three kinds, each with its members (login, name, department):
departments: the values of
attr_groupof[auth.ldap](departmentNumber) over every person ofsearch_base, the student group the portal has always carried per user (the Group column of the Watch page), listed as groups;directory groups: the entries of
group_search_basematchinggroups_filter(posixGroupby default), named byattr_group_name(cn), their members the values ofattr_group_members(memberUid): each value is looked up among the people as their login, theiruidor their DN (agroupOfNameswithmemberDNs works withattr_group_members = "member"); a value matching nobody but shaped like a login counts as that login, the DN of a nested group is dropped;custom groups: the groups the instructors create on the page (a name of at most 64 characters without
:nor/, which may not be that of a department or of a directory group, a description), fill (logins typed or picked among the people of the directory, which refuses an unknown login; or every member of another group of any kind at once), empty (one member at a time, or at once every member who also belongs to another group of any kind) and delete. Any instructor may change any custom group; the creator and the dates are shown. A user may belong to several groups.
The directory is read whole, with the service account (or anonymously) of [auth.ldap]: two
paged searches of PAGE = 500 entries (the people base of a site exceeds the default size limit
of a server), kept directory_ttl and read again on demand (the Refresh button, at most once
per refresh_interval: the clicks arriving during a read get that reading, the next ones within
the interval the reading just made, so that a row of clicks never dumps the directory once per
click). The listing
needs uri, search_base and the login attribute of the people: attr_login, or the attribute
of a search_filter of the form (attribute={login}) ((supannAliasLogin={login})); the
departments need attr_group, the directory groups group_search_base. It works in every
authentication mode, proxy mode included. The page says what is not configured, and shows the
error of a directory read that failed (the previous reading stays until the next attempt, one
minute later).
The custom groups live in two tables of the database of [access_log.database] ([groups] table, user_groups by default, and <table>_members), on iff that section is set. The tables
are created when they are missing (the CREATE privilege, once), or create them by hand; the
account needs SELECT, INSERT, UPDATE, DELETE on these two tables (UPDATE: a renamed group or a
changed description; the other tables keep SELECT, INSERT, DELETE):
CREATE TABLE sre.user_groups (
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
name VARCHAR(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL,
description VARCHAR(255) NOT NULL DEFAULT '',
created_by VARCHAR(64) NOT NULL DEFAULT '',
created_at DATETIME(3) NOT NULL,
PRIMARY KEY (id),
UNIQUE KEY name (name)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
CREATE TABLE sre.user_groups_members (
group_id BIGINT UNSIGNED NOT NULL,
login VARCHAR(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL,
added_by VARCHAR(64) NOT NULL DEFAULT '',
added_at DATETIME(3) NOT NULL,
PRIMARY KEY (group_id, login),
KEY login (login)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
GRANT SELECT, INSERT, UPDATE, DELETE ON sre.user_groups TO 'sre_web'@'%';
GRANT SELECT, INSERT, UPDATE, DELETE ON sre.user_groups_members TO 'sre_web'@'%';
The name and login columns are binary-collated: the names and logins are exact and
case-sensitive, like the logins everywhere else (a table created with the default collation
would find INFO1 for info1). The members of a deleted group are deleted with it, in one
transaction; the rows are never purged.
Every group is named by a key kind:name (department:1A, directory:etudiants,
custom:TP lundi; a bare name is a department: the links and audiences made before the kinds
keep working). The groups of every kind are offered by the group select of the Watch page (the
column still shows the department) and by the audience dialog of a presentation: a student is in
the audience when one of their groups is chosen, the department by the group of their identity,
the others by the memberships the portal knows, read again every 30 s and after every change made
on the page (a student added to a presented group is told at once, one removed loses the
presentation). Every reading of the page is a groups line of the connection log, every change a
group_edit line (action = create / update / delete / add / remove, group, and
member (the one member removed), source (the group whose members were added or removed),
count (the logins given to an add, the members a removal by source took out) or new_name).
Exams¶
The Exams page of the instructors (/exams, the link in the header) programs the exams of
the web site. An exam is a title, an audience (logins and/or groups of any kind of the
Groups page: a department, a directory group, a custom group), the labs to do (the exam-only
labs, whose names hold _EXAM_, _OLD_, _DRAFT_ or _TESTS_, are offered here and nowhere
else; each lab may name a preset of its Flavor class, as sre set-exam --labs lab:preset
does), a duration and/or a window (start_after, end_before; no start time = the exam
starts when created, for each student as soon as they log in), two delays before the start — the
lead (the other projects of the students are hidden and a countdown appears) and the
pre-start (their exam projects are started in the background) —, the networks the
students’ browsers must come from (none = anywhere; the address is the client’s, through
trusted_proxies) and the evaluation interval of the agents during the exam. The defaults
of the delays and of the interval are those of [exams].
Unlike the exam mode of the Qt GUI (sre set-exam, exam.json, which locks a whole
workstation and keeps a pool host out of the pool), an exam of the web site binds its students
only, wherever their projects run: the portal computes the phase of each student and enforces
it on every request, the agents apply the exam rules to the exam projects alone. Instructors are
never bound, even when listed in the audience.
What a student sees, in order:
scheduled: nothing changes before the lead window.
waiting (the lead window, 10 min by default): a bar on every page counts down to the start; their other projects are hidden and refused (the Open page too); from the pre-start (5 min before) their Projects page starts the exam projects in the background, one at a time (the start slot and the placement of a normal start; a lab asking a flavor form shows it), and shows them without an Open link.
starting: the start passed (or the exam has no start time), the exam projects are being started; the clock of the student starts when every lab of theirs has a running project.
active: the countdown to their end; the Projects page lists the exam projects alone (a single one is opened by itself at the start); the project page has no Close button, no Evaluation tab nor Apply Configuration tab (as in the Qt GUI); the agent evaluates the project every
eval_intervalof the exam. Each student has an individual clock: their end ismin(started + duration, end_before)plus the minutes an instructor adds to them; an End now ends everybody’s exam, or a selection’s, at once.ended: the end banner (That’s all folks) until the student presses Continue; the agent has cut the terminals and the X desktop, run the final evaluation and refuses every change (the answers included). After Continue the usual pages are back; the exam projects stay hidden from the student (and refused) until they are closed:
close_afterafter the end by the agent (the idle timeout of an exam project counts from its end), or by the instructor (Close projects on the page, or the Watch page; the page stops them in the background, eight at a time, and shows the progress under the title of the exam; a second click while a close runs changes nothing).A student connecting from an address outside the networks of their exam gets the bar in red naming their address, and no project at all (neither the exam’s nor the others) while the exam binds them.
A student in several exams at once follows the one started first. The phases are pushed to the
open pages through the presence socket ({"t": "exam", "id", "phase"}: the pages read
GET /api/exam then) and the countdowns use the portal’s time.
On the page the instructor sees, per exam, every student the portal can name (the logins, the members of the groups it knows, those it has seen), connected or not, with their address, their phase, their clock, their extra minutes and their running exam projects (a link to each: the instructor’s view of the student’s project, the Evaluation tab included), and can, for everybody or for a selection (the checked rows): add or take minutes (for everybody the duration and the end time move together and an End now is lifted; for a selection their extra minutes, cumulative), End now, change the labs (the browsers start the new ones; the projects of a lab dropped stay as plain projects), Close projects, Start now (the start time set to now), Edit (every field; the students keep their clocks and adjustments) and Delete (the projects become plain projects, or are closed with the checkbox). The listing shows the scheduled, the running and the past exams.
The exams live in two tables of the database of [access_log.database] ([exams] table, exams
by default, and <table>_members), on iff that section is set (the page says so otherwise): an
exam survives a restart of the portal, the clocks of the students included, and the agents are
told again what they missed. The tables are created when they are missing (the CREATE
privilege, once), or create them by hand; the account needs SELECT, INSERT, UPDATE, DELETE on
the exams table (an edited exam) and SELECT, INSERT, DELETE on the members one (its rows are
replaced):
CREATE TABLE sre.exams (
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
title VARCHAR(200) NOT NULL,
created_by VARCHAR(64) NOT NULL DEFAULT '',
created_at DATETIME(3) NOT NULL,
updated_at DATETIME(3) NOT NULL,
start_after DATETIME(3) NULL,
end_before DATETIME(3) NULL,
duration INT NULL,
ended_at DATETIME(3) NULL,
details MEDIUMTEXT NOT NULL,
PRIMARY KEY (id),
KEY start_after (start_after),
KEY created_at (created_at)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
CREATE TABLE sre.exams_members (
exam_id BIGINT UNSIGNED NOT NULL,
login VARCHAR(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL,
first_seen DATETIME(3) NULL,
started_at DATETIME(3) NULL,
ended_at DATETIME(3) NULL,
dismissed_at DATETIME(3) NULL,
extra_minutes INT NOT NULL DEFAULT 0,
address VARCHAR(64) NOT NULL DEFAULT '',
details TEXT NOT NULL,
updated_at DATETIME(3) NOT NULL,
PRIMARY KEY (exam_id, login)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
GRANT SELECT, INSERT, UPDATE, DELETE ON sre.exams TO 'sre_web'@'%';
GRANT SELECT, INSERT, DELETE ON sre.exams_members TO 'sre_web'@'%';
duration is in seconds; details holds the audience, the labs, the delays, the networks and
the interval (JSON), the members’ details their labs and their exam projects. The times are
UTC, truncated to the millisecond like the rows of the connection log. The past exams are kept
keep days (0 = forever), their members with them.
The agents: an exam project is started by the portal with an exam object (POST /v1/projects: the id and title of the exam, the start and end of the student’s clock once
known, the evaluation interval) and a privileged sre start run under the student’s login
(LOGNAME), which accepts the exam-only labs and the flavor presets (--set-flavor-name); the
agent keeps the object in the project’s state file and in the rows of its listings (exam),
evaluates the project at the interval of the exam once its clock runs, never reaps it before
the end, refuses the owner’s self-evaluation, states and close during the exam and every change
after the end, and at the end cuts the terminals and the display, runs the final evaluation
(sre --user eval: the exo-eval scope, like sre end-exam) and tells the open pages ({"t": "exam", "ended": true} on the events socket). The portal updates the object with PUT /v1/projects/{rln}/exam when the clock starts or the instructor adjusts the student (every
5 s until the agent has it), and drops it with DELETE …/exam when the exam is deleted or the
lab is no longer the student’s. The answers metadata of an exam project carry the exam fields of
the Qt GUI’s exam mode (exam_mode, exam_started_at, exam_duration,
exam_time_remaining), so the archives carry the exam flag and the Archives page filters on it.
The exam-only labs of the pool are listed by GET /v1/labs?all=1 (instructors), a privileged
sre list --with-titles --include-exam-only.
Connection log: every change of an exam is an exam_edit line (action = create / update /
delete / end / adjust / labs / close, exam, count of students, minutes), a student
dismissing the end banner an exam_dismiss line, and the start lines of the exam projects
carry exam; the polls of the page and of the student’s exam are keepalives.
Limitations: the session records (record_sessions) are not archived by the agents (the lab’s
attribute decides whether records/ exists, sre stop deletes it); the archives of a web exam
hold no exam_json document (the exam flag comes from the answers); anonymous mode has no
stable logins to schedule; a page loaded before the portal was updated has no exam script until
it is reloaded (the server still refuses what it must).
Securing the pool (mutual TLS)¶
The portal and the agents can authenticate each other with certificates of one private CA, on
top of the token: the agent serves TLS and requires a client certificate of that CA, the portal
verifies the agents against the same CA and presents its client certificate.
scripts/web-tls.sh makes the files with openssl:
scripts/web-tls.sh init-ca /root/sre-tls # ca.key, ca.crt (10 years)
scripts/web-tls.sh agent /root/sre-tls a01 IP:10.0.0.11 DNS:a01.example.org # a01.key, a01.crt
scripts/web-tls.sh portal /root/sre-tls # portal.key, portal.crt
On each pool host, agent.toml gets its own certificate and the CA
(tls_cert = ".../a01.crt", tls_key = ".../a01.key", tls_ca = ".../ca.crt", files readable
by root only) and, as a second fence, allowed_clients = ["<address of the portal>"]. On the
portal host, web.toml gets [pool] ca = ".../ca.crt", client_cert = ".../portal.crt",
client_key = ".../portal.key" (readable by sre-web), and every [[hosts]] URL becomes
https://<name or address in the certificate's SANs>:8451. The portal then logs
TLS to the agents: CA …, client certificate … and each host as reachable; an agent without
the right certificate, or a client without one, fails at the handshake, before any request.
A whole site at once (scripts/make-web-hosts.sh)¶
scripts/make-web-hosts.sh writes everything the previous section asks for, for every pool
host of a site, from a list of its machines:
IP_MAC_FILE=ip_mac.txt OUT_DIR=/root/sre-pool REGEXP='^([a-z])[0-9]+$' ROOMS="a h" DISABLED_ROOMS=h \
scripts/make-web-hosts.sh
The list has one mac;ip;name line per machine (e4:54:e8:d1:6c:1c;10.0.128.1;a01: the MAC
address for Wake-on-LAN, the IPv4 address the portal reaches its agent at, the name). A machine gets an
agent when its lower-cased name matches REGEXP; the first group of the expression, when there
is one, is its room, so that ROOMS selects the rooms joining the pool (empty: every qualified
machine) and DISABLED_ROOMS those whose hosts start with enabled = false (polled, no project
placed there until sre-web-tool enable). These five variables are required and have no
default; AGENT_PORT (8451), MAX_PROJECTS (25), POWER (power = true in the entries and
allow_power_off = true on the agents), DOMAIN (a DNS:<name>.<domain> SAN too),
PORTAL_ADDRESS (allowed_clients of the agents), INSTRUCTOR_ROOMS (the rooms whose hosts
get instructors_only = true) and INSTRUCTOR_MACHINES (the names of the hosts that get it on
their own, whatever their room), TLS_DIR_ON_HOSTS (/etc/sreweb/tls,
the paths written in the files) and CA_KEY / CA_CRT (both set: an existing CA copied into
OUT_DIR instead of a new one, so that the hosts of a new OUT_DIR are signed by the CA the
portal already knows; refused when OUT_DIR holds another CA) are optional.
In OUT_DIR (private keys and tokens: outside any repository) it writes, through web-tls.sh,
the CA and the portal’s client certificate once, then per host a key and a certificate (SANs
IP:<ip> and DNS:<name>), a token in <name>.token, and:
hosts.toml: the[[hosts]]entries, one room after the other, with the three[pool]TLS lines ofweb.tomlin its header; a file ofweb.d/(import = ["web.d"]) or pasted at the end ofweb.toml;agent-<name>.toml: the host-specific lines of that host’sagent.toml(listen = "<ip>:8451",token,tls_*, andallowed_clients/allow_power_offwhen asked), to complete with the example file, whose ownlistenandtokenlines are then removed (a key given twice is refused).
Copy <name>.crt, <name>.key and ca.crt to TLS_DIR_ON_HOSTS of each host, and ca.crt,
portal.crt, portal.key to the portal’s. Every run computes hosts.toml and the agent files
from the variables of the moment, so the rooms, the disabled or reserved hosts may change at any
time, and compares them with the previous run: a file whose content did not change is left as it
is, the others are replaced, what changed is printed per host (enabled, instructors_only,
allow_power_off…) with the new hosts and those no longer selected, and the last line says
whether hosts.toml and which agent files are to deploy again; the CA and the portal’s
certificate are made once. The hosts’ keys, certificates and tokens are made anew on every run,
to deploy again, unless the script is run with --keep: the files already there are kept and
only those of the new hosts are made (a kept certificate that no longer names the address of its
host is reported), so a room is added, or a host disabled or reserved, without touching the
hosts already deployed.
A pool of workstations¶
The pool hosts are typically classroom workstations: off when nobody needs them (a nightly cron
shuts them down), woken by Wake-on-LAN, and sometimes used at the keyboard by a student. The
portal gives each host a state from the status of its agent, and sre-web-tool list (or
the Pool page of the instructors) shows it:
State |
Meaning |
New projects |
|---|---|---|
|
the agent does not answer |
no |
|
a wake was sent less than |
no, but a start answers “a host is starting” (503, retried by the Open page) |
|
the agent answers, the host is free |
yes |
|
the agent answers, the host is used locally ( |
only the new projects of the users logged in there ( |
|
the agent answers, |
no |
|
the portal asked the host to power off, or |
no; powered off once it is empty |
|
|
no; the projects already running there go on (listed, usable, evaluated and closed as usual), the host stays polled and is never powered off by the portal — |
Placement. A new project (POST /api/projects) goes to one host, chosen by the portal from
the last status poll of every agent (poll_interval) without asking them again. The candidates
are the hosts up (plus, for a user logged in at the keyboard of an occupied host, that host:
Occupied hosts below), below their max_projects, not reserved for the instructors when the
caller is a student, and whose agent has no restart pending (sre-web-tool restart: the agent
refuses new projects until it is back). Among them: the host of the user’s own session first,
then the lowest projects / max_projects, then the lowest one-minute load, then the name, so
that equal hosts are always settled the same way. The project count of the chosen host is raised
at once and a reservation is kept until the start is over and a status poll sent after its end
has answered: a burst of simultaneous starts is spread over the pool instead of piling onto the
host the last poll showed emptiest, and sre-web-tool list may count a project one poll before
the agent does. Without any candidate the start is refused with 503 and a retry_after the Open
page obeys: “a host is starting” (30 s) when a host the caller may use is waking or
restarting, “no host available for a new project” (60 s) otherwise. An instructor naming a host
on the Open page skips the ranking: the host must exist (404), be open to them (403 for a
student), be available to them (503 “not available”) and have room (503 “full”). The start
line of the connection log says why the host was chosen: placement=load, session or
named.
Waking. A host with a mac gets a magic packet (UDP port 9 to wol_broadcast: the portal
must be on the hosts’ network, or the routers must forward the directed broadcast); a host with a
wake_command gets that command instead (ssh relay wakeonlan {mac}, an IPMI or VM call…).
The portal wakes one sleeping host, in configuration order, whenever the free project slots of
the hosts up (plus the capacity of those waking) drop below spare_projects, and keeps
min_hosts_up hosts awake during active_hours: the floor is re-established in the morning and
not fought at night after the cron. A host still silent after wake_timeout is retried after 1,
2, 4… up to 30 minutes, so a machine that is really down is not hammered. The agent must be an
enabled systemd unit on every host (boot → agent up → the portal sees it).
Occupied hosts. The agent reports occupied when a local login session exists (loginctl,
who as a fallback) or when projects of the Qt GUI run on the host; the portal places nothing
there and never powers it off. The web projects already running there continue. One exception,
follow_local_sessions (true by default): the user logged in at the keyboard of a pool host
gets their new projects on that host, whatever its load, so that a student working at a
classroom workstation uses their own machine through the web too. The agent names the sessions
in its status (session:<login>, the OCCUPIED column of sre-web-tool list; its
occupied_check must include the sessions) and the portal matches them against the login of
the identity, which must therefore be the Unix login of the workstation (the directory uid:
AuthLDAPRemoteUserAttribute uid in the Apache example; a guest-… pseudonym never matches).
The rule overrides the load and the occupation only: when the host is full (max_projects),
in exam mode, disabled, draining or reserved for the instructors, the normal placement applies,
and an instructor naming a host on the Open page is obeyed (they may name the host of their own
session). The start line of the connection log says placement=session.
follow_local_sessions = false restores the strict rule: nothing goes to an occupied host.
Reserved hosts. A host with instructors_only = true never takes a student’s project: an
instructor’s goes there when they name it on the Open page (the Host select lists the
reserved hosts with their state and their projects) or by the automatic placement when it is
the least loaded host they may use. It is outside the elastic pool: its free slots count
neither as spare capacity nor towards min_hosts_up, the portal never wakes it by itself
(sre-web-tool wake, or its own boot; an instructor finds it greyed while it is off) and
powers it off when idle like any host with power = true, whatever the floor and the spare.
list shows (instructors) after its state. A student logged in at the keyboard of a reserved
host is still placed elsewhere: follow_local_sessions never overrides the reservation.
Going down. When the agent’s unit stops because the system halts (the nightly cron, a
poweroff), the agent closes the projects it manages as a Close would (the last evaluation,
then sre stop), concurrently, within TimeoutStopSec; the browsers see “project closed”.
The unit is ordered after remote-fs.target and, on a checkout, requires the mount of
/opt/sre (RequiresMountsFor): it is stopped while sre and the archive directories are
still mounted. Unordered, systemd tries to unmount /opt/sre while the agent is still closing
a project: the unmount fails (device is busy) and is not tried again, the network goes down
with the NFS mount still there, and the machine never powers off (it then answers neither
ping nor Wake-on-LAN until somebody presses its power button). What is
left on the host afterwards (the projects of the Qt GUI, a project not closed in time) is
removed by the unit sre-wipe-at-shutdown.service when it is enabled (sre wipe after the
agent has stopped and before Docker does; see Stopping the projects when the machine halts in
the installation page), so that no container is running when the system
powers off.
With power = true on the host and allow_power_off = true in its agent.toml, the portal
powers off an idle host itself: idle_host_timeout (0 by default: only the cron does it), above
min_hosts_up and as long as the spare capacity stays, or at once with sre-web-tool poweroff.
Pre-loading the images. A project whose Docker images are not on its host yet waits for
their download at its start, and a room of workstations pulling them at the same time takes the
whole bandwidth of the site; the boot unit sre-preload-images.service (see
Installation) only spreads the pulls over a random delay. On the pool, the
agents pull and the portal lets a few hosts at a time do it:
a host asks: its agent starts with
[preload] at_start = trueinagent.toml(the boot of the host, a restart of the agent), orsre-web-tool preload NAME | --all | --failedor the Preload images button of the Pool page ask for it by hand (after new labs or a new image version). The agent then sayspendingin its status and pulls nothing;the portal gives turns: at every poll, as many waiting hosts as
[pool] max_preloads(2) leaves free getPOST /v1/preload/start, the one that waited longest first; the agent then runssre preload-images --progresson its lab directories ([preload] paths) and reports the image it pulls. A request by hand goes through the same queue: the number of hosts downloading never exceedsmax_preloads, whoever asked;every host that asks takes a turn, even with every image already there: its
docker pulls only check the registry, a few seconds, and an image pushed again under the same tag is fetched.
sre-web-tool list shows (preload queued), (preload 2/5) or (preload failed) after the
state, the Pool page the same as flags (the image being pulled, or the error, as tooltip), and
sre-web-tool status the count of each. A preload that fails (failed: an image that does not
exist, the registry unreachable; the error names the images) is not tried again by itself:
sre-web-tool preload --failed asks again. A disabled or occupied host is served like the
others — its local users start labs too —; a host is not given a turn while projects of an exam
of the web site run on it, while it is draining or its agent restarting, and an idle host
waiting for its turn is not powered off by idle_host_timeout. A stop or a restart of the
agent stops the preload in progress (Docker keeps the layers already there): with at_start
the agent started next asks again, a request made by hand is forgotten. Nothing is preloaded
without the portal (the hosts stay pending; the images are then pulled by the first start
that needs them, as without any preload) nor with max_preloads = 0; the agents older than
this feature are left alone. On a host whose agent has at_start, leave
sre-preload-images.service disabled: both would pull.
Nothing asks again on a timer. A host that boots every day checks at each boot; one that stays
up does not notice new labs or a new image version until its agent restarts
(sre-web-tool restart --all after a sync makes every at_start agent ask again) or a preload
is asked by hand, and a failed one stays failed. A cron job on the portal’s host makes the check
periodic, through the same queue (max_preloads hosts at a time):
0 3 * * * sre-web-tool preload --all
Restarting the agents. After an update of the tree the agents run the old Python code until
their unit is restarted: sre-web-tool restart NAME, or restart --all for every reachable host
(whatever its state), tells each agent to restart itself (POST /v1/restart). The agent answers
at once, lets its running sre commands end (a start, an evaluation; no new project is accepted
meanwhile), then runs systemctl reset-failed and systemctl --no-block restart on its own
unit (sre-agent or sysreseval-agent, read from /proc/self/cgroup; restart_command in
agent.toml replaces that for an agent run otherwise). The projects stay
(close_projects_on_stop = "shutdown": a restart is not a shutdown; with always they are
closed), the open terminals and virtual desktops are lost, the project pages reconnect by
themselves. The stop is quick: the agent closes the project pages’ WebSockets itself (aiohttp
would otherwise wait a minute for them) and KillMode=mixed ends what is left. list shows
(restart) after the state until the portal sees the agent’s uptime reset (or gives up after
two minutes with a warning in its journal); the host takes no project meanwhile and keeps its
drain. --wait makes the tool wait for the agents to be back (--timeout, 180 s by default),
so a deployment script can chain it after the sync. A DO_NOT_START_AGENT marker written since
the start is honoured (that agent stops); code that no longer starts leaves the unit failed
after five attempts (journalctl -u sre-agent, then systemctl reset-failed). The portal
itself is restarted by hand (systemctl restart sre-web; Restarting the portal below).
By hand. sre-web-tool … (sbin/sre-web-tool of a checkout, /usr/sbin/sre-web-tool
of the sysreseval-web package; python3 -m sreweb.tool from the sources) talks to the running
portal through its administration API ([admin] token, loopback only by default; --config,
--url, --token override the configuration):
sre-web-tool list # one line per host: state, projects/max, load, terminals, occupied by, uptime
sre-web-tool status # spare capacity, hosts awake vs the floor, active hours, hosts waking, the state file
sre-web-tool add a03 --url https://10.0.0.13:8451 --token … [--mac … | --wake-command …] [--max-projects 25] [--power] [--instructors-only]
sre-web-tool remove a03 [--force] # refused while the host has projects (--force leaves them to the agent's idle reaper)
sre-web-tool disable a03 | enable a03 # disable: no new project on the host, the running ones go on until their users close them
sre-web-tool wake a03 | poweroff a03 | drain a03 # drain: no new project, powered off once the last project is closed
sre-web-tool restart a03 | --all [--wait] [--timeout 180] # the agents restart themselves (new code), the projects stay
sre-web-tool preload a03 | --all | --failed # the hosts pull the Docker images of the labs, max_preloads of them at a time
list shows config or added per host; a config host removed by hand comes back, enabled,
with enable. --json gives the raw rows. The instructors have the same actions on the
Pool page of the portal (/pool, see Instructors below), except add and remove, which
stay with the token.
Restarting the portal. What is changed by hand, with the tool or on the Pool page, is kept in
[admin] state_file (pool.json, written at every change through a temporary file renamed over
it) and merged with the [[hosts]] of web.toml at every start, so that a restart of the
portal leaves the pool as it was:
Kept |
How |
|---|---|
the hosts added and removed by hand |
|
a host enabled or disabled by hand |
|
a drain |
|
a wake in progress, the pause after failed wakes or after a power off, a power off or a restart sent |
|
The file only holds what differs from the configuration, and an edit of the configuration
wins: a flag set by hand holds for as long as the enabled of that host in web.toml is what
it was then. Once the file says the same (the room taken out of DISABLED_ROOMS, for instance)
the entry is dropped at the next start, and a later edit of web.toml is followed again; so is
the entry of a host that is no longer in the pool (one line of the journal each). The start logs
pool state read from …: 0 removed, 0 added, 21 enabled and 1 disabled by hand, 0 draining.
A state file that cannot be written (a read-only directory, a container without its volume) would
lose all of this at the next restart: the portal says so in its journal at the start and at every
failed write (pool state NOT saved in …), sre-web-tool status prints pool state: NOT SAVED in … instead of the path, and the Pool page shows it in red above its table. In a container,
/var/lib/sre-web must be a mounted directory the sre-web user may write; the systemd units
provide it (StateDirectory). A file the portal cannot read (edited as another user, or no
longer a JSON object) is not written over: it is renamed pool.json.unreadable, said in the
journal, and the pool starts from the configuration; edit the file with the portal stopped and
leave it to the sre-web user.
Not kept: the idle clock of idle_host_timeout (it counts again from the start), and what is
listed under Limitations (the presentations, the focuses, the open sockets: the pages reconnect
by themselves, a terminal with its Reconnect button). The exams, the custom groups, the
connection log and the archives are in the database; the sessions are in the browsers’ cookies
(secret).
What the users see¶
Header: the SRE logo and, with
[site]inweb.toml, the institution’s logo and name next to it, on every page.Login: the directory account, or (anonymous mode) a display name and a language; in proxy mode the web server in front has already asked, there is no login page.
Projects: the user’s running projects on the whole pool (Open, Close), “Open a lab” (the lab tree with its titles, in the order the
titles.jsonof each directory sets, by file name, by title in the language of the page or by hand, the sub-directories first, the same tree as the Qt GUI’s; the Flavor form when the lab has one; the progress of the pulls and deploys); with no running project the page shows the SRE logo in large, as the Qt GUI does when no project is open. An instructor’s cards say the mode of each project (Standard / Instructor mode / Debug mode, the words of the Open page), the two non-standard modes also as a coloured suffix after the title. Logout asks what to do with the running projects: close them (each as a Close does: the last evaluation when the lab asks for it, thensre stop; the dialog waits for every answer) or leave them running; with no running project the button logs out at once.A project: the tabs of the Qt GUI. The schema is drawn by the agent (graphviz) with the same options (lines, spacing, order, icons); the questions and the forms are the same; the evaluation table has the same parts, subtotals, bonuses and colours; the terminals are
xterm.jsterminals onsre connect: every Connect button of the Machines and Networks tabs opens a new one on its device in a floating window over the page (like the external xterm of the Qt GUI:gw,gw (2), …; the window stays while the tabs change, is dragged by its title bar, resized by its bottom-right corner and closed by its ×, and takes the font size and the colour scheme of the Terminals tab), the two selects of the Terminals tab open one on any device (the machines open to the students and the managed switches) as a sub-tab or in a window, the ⧉ of a sub-tab moves its terminal into a window and the dock button of a window brings it back as a sub-tab (the same shell goes on), the × of a sub-tab or of a window closes it,terminals_per_devicecaps them; the Log tab appears for debug projects. Export Schema to PDF downloads the drawing of the Schema tab with its current options (never the hidden machines), rendered by the agent as the GUI’s File → Export Schema to PDF does, unless the lab setsexport_schema_pdf = False; the icons needrsvg-convert(librsvg2-bin) on the pool host, graphviz shapes otherwise. The Kathara export is not offered on the web. A terminal is a session shared by its viewers: the live sessions of the project that are not on the page are listed under the bar with a Join button (the same shell for everybody, the last 64 KiB of output replayed to the newcomer, the pty sized to the smallest window), a sub-tab or a window shows how many viewers its session has, and a session ends when the shell exits or when its last viewer closes it with its ×. A viewer that just goes away (the page closed, reloaded or cut, another project opened) leaves the session running forterminals_lingerof the agent (120 minutes by default), listed idle meanwhile, and the page coming back to the project finds the user’s own terminals again, sub-tabs and windows where they were on that browser, with the last 64 KiB of output (a page the browser kept in its back/forward cache, brought back by the Back button, joins its sessions again by itself); after a disconnection, Reconnect joins the same session again.A presentation: when an instructor presents a project to the class (Present below), the page of every chosen student is covered by that project, read-only: every tab is there but nothing can be done on it, and the tab shown, the terminals open (the instructor’s shells, in their exact size, at the largest font that fits; the instructor’s floating windows float on the student’s page too, at the same relative place and size), the question selected, the drawing options of the schema, the instructor texts and the virtual desktop (view-only) follow what the instructor does; so does the position inside the tab: the scroll of the text, of the question (its description and the list of titles), of the tables and of the log (the line at the top of the instructor’s pane is at the top of the student’s, whatever the size of their window or their font size), and the pan and zoom of the schema. The instructor’s mouse pointer is drawn on the student’s page too, an arrow carrying the instructor’s name, and what the instructor selects — in the texts, the question, the tables, the log, the schema, or in a terminal — is highlighted there (in a terminal: selected in the student’s read-only terminal, in lavender); both are placed on the element, the word or the terminal cell the instructor’s mouse is on, never at the same pixel, so that a student with another window size, font size or word wrap sees the pointer on the same word. The Evaluation tab shows the table of the instructor’s page as it is (the letters of their own self-evaluation included), again after each evaluation. The title bar of the presented project is coloured (lavender) and names the instructor presenting it. The student’s own page is untouched underneath, terminals included: when the presentation ends it is back as it was.
Instructors¶
Who is an instructor is decided by the directory, the role being the OR of the tests configured
in web.toml (a test whose key is empty is skipped):
instructor_groups: the user is a member of one of these groups (DNs), through thememberOfattribute of their entry or, withgroup_filter = "(member={dn})", a search of the groups undergroup_search_base(search_basewithout it);instructor_filter: the user’s own entry matches this LDAP filter (a BASE search on their DN),"(gidNumber=2000)"by default — the primary group; a site with the memberof overlay may write"(memberOf=cn=teachers,ou=groups,dc=example,dc=org)";""disables the test;group_search_base+instructor_group_filter: a group found under that base names the user ({login}and{dn}are substituted, filter-escaped), by default aposixGroupof gid 2000 listing the login inmemberUid(the group search that needs no overlay; agroupOfNamessite writes"(&(cn=teachers)(member={dn}))"); settinggroup_search_baseenables the test.
In LDAP mode the portal runs them when the user logs in (the role is kept in the session cookie:
a change applies at the next login). In proxy mode the reverse proxy authenticates and the tests
are run on the request headers (instructor_groups on the groups header, instructor_values on
instructor_header: any attribute mod_authnz_ldap exports) and, with ldap_roles = true, by the
portal itself with the [auth.ldap] settings for the login the proxy gives (the service account
or an anonymous search finds the entry, then the same three tests; the entry’s attr_group is
read at the same time, so with that key set the lookup runs for the users the headers already
made instructors too, who keep the role whatever the directory says), the result being cached
per login for ldap_roles_ttl; a directory failure makes the login a student without group for
a minute (logged once) rather than the site unavailable. ldap_groups = true does the group part
alone: the directory gives the group of a user whose header carries none, the roles stay those
of the headers (the production case: gidNumber exported by Apache, departmentNumber read
by the portal for the owners of the Qt GUI projects on the Watch page).
What an instructor gets:
Watch (the link in the header,
/watch): every running project of every reachable host, one row per project with its host, owner (the login), name (the full name of the answers metadata, or the directory’s for a project of the Qt GUI when the portal reads it, above), the owner’s student group when the site configures one (attr_group/header_group), lab, start time, the last action of its owner (how long ago the student last did something on the project: a key pressed, a click or a turn of the wheel anywhere on their project page, which reports them on its events socket at most every 10 s — a tab change, a question picked, the course read, the virtual desktop —, and what the agent sees by itself, to the second: the input of a terminal, an answer, an evaluation, a state; a mouse that only moves is no action. The owner’s actions only: what an instructor helping on the project types and what the spectators of a presentation do never count, so the column tells whether the student is at work. The agent keeps the time across its restarts; the cell is blank for a project the student uses from the Qt GUI only, and for a host whose agent is older than the column), the age of its last archived evaluation, its grade and mark (exo-eval scope, assre watchshows them), its errors and warnings, the number of self-evaluations the student has run (the agent counts the lines of the project’s.private/auto_eval.log, one persre eval --auto-evalof the student: live, whereas theauto_eval_countof an archive, the AUTO-EVAL column ofsre watchand the Self-evaluations column of the Archives page, is the count before that evaluation), its open terminals and whether the web or the Qt GUI runs it; sortable by any column, filtered by text (host, owner, group, lab) and, with a group source, by the select listing the groups by kind (the departments of the rows, the directory groups and the custom groups of the Groups page, User groups above; every group, one group, or No group: the projects whose owner has no department, those started on the Qt GUI carrying none unless the portal reads the directory, above), the chosen group kept in the URL (?group=kind:name, a bookmark per class group) and remembered by the browser (localStorage): the page comes back with the group chosen last, through the Watch link of the header or in a new tab, until another group or All groups is chosen, a link with?group=winning and becoming the remembered one; refreshed every 5 s; a row whose last evaluation had errors is red, one without an evaluation younger than 15 minutes (or twice the lab’s evaluation interval) is amber. Open goes to the project, Close stops it. The agents read the archives of their own host (params.archive_dirs) on demand, nothing is scanned while nobody watches. As insre watch, the page also shows the details: a click on a row opens a dialog with the last archived evaluation of the project (the owner, the lab and the host in the title, the date of the evaluation, the table of its grade elements with the parts, the subtotals, the bonuses, the total and the mark, then the errors and the warnings of the run; Open the project goes to its page; Esc or Close goes back), and a click on the lab cell of a row opens the summary of that lab for the rows shown (the text filter and the group select apply, so a class group alone when wanted; the lab is the file really run, so the projects started through a course directory’s symbolic link to a lab and through the lab’s own path are one lab): a matrix with one row per project and one column per grade element (numbered, the element as the tooltip of the number, the parts as a header row; a cell is the grade, or the OK / MEH / FAIL letter, coloured like the evaluation table; Total and Mark last; a project never evaluated says so), the rows Max, Average and Success (how many of the evaluated projects got the full grade) below it, then the table of the elements with their maximum, minimum, average and distribution of the grades (1.5(2) 3(7)), a subtotal row per part, as the lab summary ofsre watch. The elements of the projects are matched by their title, whatever the language of each student. A click on the group cell of a row (when it holds a group) opens the same summary for that lab restricted to the whole group: every project of the lab on the pool whose owner has that group, whatever the text filter (the lab cell keeps the rows shown; an empty group cell opens the last evaluation like the other cells). Both dialogs follow the polls: a new evaluation of the project, or of one of the projects of the lab, reloads the content, a project closed is said so. Reading the results of a student this way never counts as their activity (their project’s idle clock is left alone).Archives (the link in the header,
/archives): the evaluations collected into the database (The archives of the evaluations above; without[access_log.database]the page says so), the closed projects included. The Evaluations view lists them newest first, one row per evaluation with its time, the student (login, name, group when a group source is configured), the lab, the pool host, the start of the project, the grade and the mark (exo-eval scope), the errors / warnings counts, how many self-evaluations the student had run and whether it was an exam; a row whose evaluation had errors is red. The form searches by Student (the beginning of a login, or a part of a name), Group (a group of the table, with suggestions), Lab (a part of its name, with suggestions), Host, a From / To window on the evaluation time and the exam flag, and chooses what to Show: the last evaluation of each project (the default: the final result of every project, among the evaluations matching the window, the host and the exam flag), the last evaluation of each student (one row per student and lab) or every evaluation (the progression of a project). Rows and the Older / Newer buttons page through the result; a click on a login, group, lab or host cell filters on that value; the filters and the view are kept in the URL (a search can be bookmarked or shared;?open=<id>opens one evaluation: a link to a result). A click on a row opens the evaluation as the Watch page’s dialog does (the student, the lab and the host in the title, the time of the evaluation and of the start of the project, the table of its grade elements with the parts, the subtotals, the bonuses, the total and the mark, laid out by the browser from the stored elements exactly as the agent lays out the evaluation of a running project, then the errors and the warnings of the run). The Summary view (the second button of the toolbar; a lab is required: without one the view lists the labs of the archives, a click on one loads its summary; the search starts on the last evaluation of each student) is the lab summary of the Watch page for the evaluations found: one block per lab matched (the lab file really run, as on the Watch page: the evaluations of the projects started through a symbolic link to a lab are in the lab’s block, and the lab filter matches the path started as well as the file), with the matrix of the students and the grade elements (Total and Mark last, the rows Max / Average / Success below) and the table of the elements with their statistics; a click on a row of the matrix opens the evaluation. The summary is computed on the 500 newest evaluations found at most (the page says so: narrow the filters). Searching the results of a class group isGroup+Lab+ Summary; the results of one student across the labs,Studentalone. Reports. A checkbox at the left of each row selects evaluations (the header checkbox takes the rows shown; the selection is kept while paging and searching, the bar says how many are selected and how many are not on the page), and the bar under the filters downloads a Spreadsheet (whatsre sheetgives: per lab, the table of every evaluation, the Questions statistics and the Sessions best grades), the PDF reports (whatsre outlinegives: a zip holding one PDF per student and lab,<login>__<lab>.pdf, andoutline.ods, the summary) or the Summary spreadsheet (whatsre outline -ogives: thatoutline.odsalone, one row per student and lab with the best evaluation, its time and the mark) of the selected evaluations. A student is a login here, whatever the host: the portal places each project on any host of the pool, so the instances of a lab a student opened several times are merged into one row, one PDF and one Sessions row even when they ran on different hosts (the two commands, run by hand on the archives of a host, keep one per login and hostname: the hostname is the student’s workstation there, and tells apart the users of a shared account); thehostnameof a merged row, and of the header of its PDF, is the host of the best evaluation, and each history table of the PDF is headed by the start time and the host of its instance. A selected evaluation stands for its whole project: every stored evaluation of the same projects goes into the reports (the history table of a PDF, every row of the spreadsheet, the best evaluation of the summary), assre sheet archives/would do on a host; the Options disclosure unticks that (Every evaluation of the selected projects: the ticked rows alone then) and gives the flags of the two commands: One report / row per project instance (--separate-instances: a student who opened a lab twice gets two reports, or two summary rows, instead of one with two history tables), “Bonus” before the label (--bonus-in-prefix), Evaluation history, Remaining time column (exams) and Group by part (the PDF only: the two spreadsheets ignore them). At most 1000 rows may be ticked; the PDF reports and the summary spreadsheet may cover 150 000 evaluations (300 projects of 300 evaluations give their PDFs in about a minute), the spreadsheet 5000 (the page says so: narrow the selection or untick the option). The file is named after the lab when the selection is of one lab (evaluations_dhcp_20261109-1430.ods,reports_dhcp_20261109-1430.zip,summary_dhcp_20261109-1430.ods), is built by one agent of the pool from the stored rows (The archives of the evaluations above) in the language of the page, and is saved by the browser; the buttons read Generating… meanwhile, a few seconds for a class.Pool (the link in the header,
/pool): every host of the pool, one row per host with its state assre-web-tool listshows it (the seconds and the failed attempts of a wake), its projects and terminals, its load, who occupies it, its uptime, its flags (reserved for instructors, power, drain, power off sent, restart sent, and where the preload of the Docker images is: preload queued, preloading 2/5 with the image as tooltip, preload failed with the error), the agent’s version and its last error, the line ofsre-web-tool statusabove the table (in red, with the reason, when the state file of the pool cannot be written: a restart of the portal would undo what is changed on the page); the state cell is green (up), amber (waking, occupied, exam, draining), red (off: the agent does not answer) or grey (disabled, muted text); sortable by any column, filtered by host name (one or several words: the hosts whose name contains any of them,a01 a02 hfor those two and everyh…host), refreshed every 5 s. The buttons of a row are the commands of the tool: Enable (green) / Disable (red), Wake (light green; a host with amacor awake_command, while it is off), Drain, Power off (asked to confirm; allowed by the agent’sallow_power_off, refused while the host has a project or a local session), each disabled with the reason as its tooltip when it does not apply. A checkbox per row (the one of the header ticks the hosts shown by the filter) selects hosts for the bar above the table: the count of the selected hosts (and how many the filter hides), the same five buttons and Preload images (the bar only: the selected hosts wait for their turn to pull the Docker images of the labs, see Pre-loading the images above), each acting on every selected host its action applies to, one call per host run together (the tooltip says how many, the button is disabled when none; Power off asks once with the names; the failures come in one message, the selection stays). Adding and removing hosts, and restarting the agents (restart, shown as the flag restart sent while it lasts), stay withsre-web-tool. Every action, of a row or of the bar, is anadminline of the connection log with the instructor’s login.Projects stays the instructor’s own projects (Watch holds the others).
A student’s project: the student named in the project bar after the lab title (full name and login, from the answers metadata; the login alone when the file has no name); every tab, the devices all open (the privileged
sre connect), the answers editable (what the instructor types goes into the student’s answers file, whose metadata stays the student’s: the Watch page and the archives still name the student; every save of either page is pushed to the open pages of the project, so the student’s page shows the instructor’s text within a second and the instructor’s page the student’s); the top of the page (the site bar and the project bar) takes the helper colour (teal) on the instructor’s browser only, a reminder that the project is not theirs (the student’s page stays grey; the presentation colour replaces it while the project is presented); the Terminals tab joins the student’s live terminals when it opens (the instructor sees the student’s shell and may type in it, the student sees the same; a closed sub-tab is not joined again), and the selects offer a private terminal (the student does not see it in their list; what the instructor types there stays with them) or a shared one, the Connect buttons a private one in a window; a terminal on a device the student cannot open themself (a closed machine or switch, a hidden machine) is always private, whatever the choice: the shared kind is greyed for it and the agent makes the session private anyway; the Evaluation tab opens on the last archived evaluation of the project (date, errors in the tooltip), shows each new one by itself (the periodic evaluations of the lab, the student’s own, another instructor’s: the agent tells the open pages) and Start evaluation runs a privilegedsre eval(no cooldown, nothing counted on the student’s side, the exo-eval marks); the states apply through the privileged path too. All this is for somebody else’s project: on their own, an instructor is the student (see Instructor mode below).Focus student on project: the toggle button of a student’s project page (off by default) brings the student to what the instructor is looking at. While it is on, every page of the student that does not show the project goes to it (now, and any page they open later: the student stays on the project until the button is turned off), the top of both pages (the site bar and the project bar) takes the focus colour (a deeper teal than the helper colour, which comes back on the instructor’s page when the mode is off), the student’s page names the instructor following them, and the two pages share the tab shown, the terminal last gone to, sub-tab or floating window (a terminal the student may see: their own sessions and the shared ones, joined by both: a terminal one page shows is joined by the other, as a sub-tab, when it has not got it; a private terminal of the instructor is never shown nor named to the student, whose page keeps its sub-tab while the instructor is on one; a page brought to the Terminals tab by the focus opens no terminal of its own, as it does when its user clicks the tab for the first time) and the question selected, in both directions: whichever page changes one of them, the other follows, the last change winning. The student’s page also shows the instructor’s mouse pointer (an arrow with their name) and what the instructor selects, as the pages of a presentation show the presenter’s: on the same paragraph, table cell or tab button, on the same row and column of a terminal both pages have, whatever the sizes of the two windows (nothing is drawn while the student is on another tab). A selected text is highlighted without touching the student’s own selection; a selection in a terminal becomes the selection of the student’s terminal. One way only: the instructor does not see the student’s mouse. Nothing is sent of a private terminal of the instructor, nor of a place in a text the instructor reads with the instructor fragments shown (the Instructor mode button on the Informations, Questions and Apply Configuration tabs: the student’s page has not got the same text). Unlike a presentation, the student keeps every control: they type in the terminals, change tab, open terminals, answer questions (and the instructor may write in the answers too, both pages showing the same text). The button’s tooltip says whether the student’s page follows. A reloaded instructor’s page goes on focusing; closing it for a minute, closing the project, or turning the button off releases the student (their page stays where it is, grey again). Any instructor may end a focus; a student is in one focus at a time (the newest wins, an older one still running comes back when it ends).
Instructor mode on the Open page: the instructor’s own project started with
sre start --instructor-mode(the instructor texts kept ininfo.json, every state listed, the states logged): the Instructor mode button of the project page then shows theinstructor()texts of the Informations and Questions tabs, every state and the Log tab, as in the Qt GUI. A student’s project holds no instructor text (the CLI strips them), so the button does not appear there. The Evaluation tab of the instructor’s own project is the student’s one, as in the Qt GUI: Start evaluation runs the self-evaluation (sre --user eval --auto-eval, the cooldown of the lab, the letters OK / MEH / FAIL when the lab hides the marks withno_mark_on_self_grade) and the table it gives stays; the last archived evaluation is shown on somebody else’s project only (an archive holds the numbers).Debug mode on the Open page: the third of the Project mode radios of the instructors (Standard, Instructor mode, Debug mode). The instructor’s own project is started with
sre start --debug-web-project: the debug project ofsre start --debug-project(see Lab Authoring) with the lab given by its name, so that it keeps the lab name of the catalog and is grouped with the lab’s other projects on the Watch and Archives pages. While Debug mode is chosen the lab tree lists every lab of the host, the exam-only and draft ones included (GET /api/labs?all=1, as the Exams page does; the selection is kept when the lab is still listed). The project page then shows what the Qt GUI shows for a debug project: every machine, the hidden ones in red, each with a Connect button, the Full Schema tab (the hidden machines drawn while the Instructor mode button is on), every state, the Log tab with the states and the evaluations, the evaluation table with both scopes, no self-evaluation cooldown.POST /api/projectstakesinstructor_modeordebug_project(both together is accepted); a student sending either gets 403. Presenting a debug project shows its hidden machines to the spectators, who see the presenter’s page.Groups (the link in the header,
/groups): the user groups of the site by kind (the departments, the groups of the directory, the custom groups), their members, and the editing of the custom groups (User groups above).Exams (the link in the header,
/exams): the exams of the web site, programmed for an audience of logins and/or groups, followed and adjusted while they run (Exams above).The host of a project: the Host select of the Open page (shown when the pool has a host reserved for instructors,
instructors_only = truein[[hosts]]) names one of them, with its state and its projects (greyed when off or full), or leaves the automatic placement: the least loaded host the instructor may use, reserved or shared.POST /api/projectstakes the optionalhost; a student sending one gets 403.Present: the Present button of a project page (the instructor’s own project, or a student’s) opens the list of the users connected to the site (a text filter, the groups of every kind with the number of their members connected: the departments, the directory groups and the custom groups of the Groups page, Select all): the chosen students, one by one and by group (a group takes its members connected now and those connecting later), see the project read-only on their own browsers (above). The top of the page (the site bar, the project bar and the bar under it) then takes the presentation colour (lavender), a reminder that the students are watching, and that bar tells how many students are in the audience and how many are watching; Edit audience changes the list, Stop presenting ends it (so does closing the project) and the bars are grey again. The spectators follow the instructor’s page: the tab, the terminals the instructor opens (joined read-only: the students see the same shell, cannot type, and do not change its size), the question, the schema options, the Instructor mode texts, the Display tab view-only, the scroll of the tab shown and the pan and zoom of the schema (followed as the instructor scrolls, a few times a second), the instructor’s mouse pointer (an arrow with their name, sent on the presentation socket about 25 times a second while it moves over the panes, the tab bar or a terminal; nothing is shown while it is elsewhere) and the text the instructor selects (in the page, highlighted without touching the student’s own selection; in a terminal, selected in the student’s read-only terminal), and the Evaluation tab shows them the table of the instructor’s page as it is, sent through the portal after each evaluation, in the instructor’s language (the archive of the project holds numbers only, while the instructor’s own self-evaluation may be letters). A reloaded page goes on presenting; a page closed for a minute ends the presentation. A student is in one presentation at a time (the newest that includes them), and falls back to the previous one when it ends.
Webconf: the button of the site bar (when
[bbb]ofweb.tomlnames a BigBlueButton server, Web conference above) opens the rooms: start one for chosen students and groups, join a colleague’s as a moderator, change its audience, end it. The students put in a room get the client in a window over their page.Talk privately: on a student’s project, next to Focus student on project: the student is invited to a conversation of the two alone, voice and text (a banner, Join / Not now); once they join, both browsers show a private room instead of the class conference, and come back to it when the conversation ends (Private conversations above).
Administrators¶
An administrator is an instructor who may read the connection log on the History page (the
link in the header, /history). Who is one is decided in web.toml, by login or by group, and
a user who is not an instructor never gets the rights whatever the lists say:
[auth] administrators: a list of logins, in every mode;in LDAP mode,
administrator_groupsof[auth.ldap](DNs, tested likeinstructor_groups: thememberOfattribute, or thegroup_filtersearch) andadministrator_group_filter(a group found undergroup_search_basenaming the user, likeinstructor_group_filter; no default), run at login for the users the instructor tests accepted (the flag is kept in the session cookie, like the role);in proxy mode,
administrator_groupsof[auth.proxy]on the groups header and, withldap_rolesorldap_groups(a directory is configured either way), the two[auth.ldap]tests above, run by the portal for the instructors nothing else made administrators (cached per login like the roles; the flag is lost while the directory is down, like the role ofldap_roles).
The History page needs [access_log.database] (The connection log above; without it the page
says so) and has three views, chosen by the Sessions / Events / Statistics buttons of its
toolbar.
The page opens on the Sessions view: the log as one row per session (The connection log
above: one login from one client address, from its first request to the logout, the idle
timeout or the shutdown): the sessions still open first, in green, from the portal’s memory
(listed even without a database), then the closed ones, newest first, each built from its
session_close line: login, name, student group (the column is shown when a group source is
configured, attr_group or header_group, like the Group column of the Watch page), address,
start, end (the last request: an idle session ends there, not at its expiry), duration, number
of requests, the labs of the projects used (the running lab names as a tooltip) and what closed
it (Logout, Idle, Shutdown; Still open with the number of pages, terminals and desktops
connected). A click on a header sorts the rows by that column, a second click reverses the
order (the default is the end time, newest first, an open session counting as not ended yet,
hence first); the sort applies to the rows shown, the page, not to the whole log: widen the
page with Rows or narrow the filters. Only the login and address
prefixes and the time window apply; the window bounds the time a session was closed
(session_timeout after the last request for an idle one; an open session is listed when it
was active in the window); the other filters are hidden. The show cell of a row opens the
events view on that session: its login and address, from the minute of its start to the minute
after its closing line. The sessions closed before the name, role and group were logged on the
closing line show an empty name and group.
The Events view (?view=events in the URL) lists the events newest first, one row per
event with its time (the local time of the browser), event, login, client address, host,
project, lab, status, duration and the other fields of the line as key=value; a denied or
login_failed line, a status of 400 or more and a failed start are red. The headers sort the
rows shown the same way (the time, newest first, by default). The form filters by
login and address (prefixes: 10.0.1. is a subnet), event (every event name; in proxy mode
without login and login_failed, which never occur there), host (exact), project and lab
(substrings; the lab is only on the start lines) and a time window.
The Statistics view (?view=statistics) says who connected over a period and for how
long. The period is the From / To window of the form: switching to the view with an empty
From fills it with the time 24 hours ago, and an empty To is now (the default period is
the last 24 hours; the login and address prefixes apply, an address prefix being a room). A
session counts when it was active during the period and only its part inside the period is
counted; the time of a user is the union of their sessions (two devices at once are not
counted twice); the sessions still open count up to their last request, the administrator’s
own among them. A user whose role is not instructor is a student (the sessions closed before
the role was logged on the closing line count as students). The page shows the totals
(users, students, instructors, groups, sessions, client addresses, the total connection time,
the average, median and longest connection time of a student, the most users at once and
when, the projects started, the projects used, the evaluations asked and the time spent on the
project pages), the groups of the students (students, sessions, time, average, projects,
evaluations; shown when a group source is configured, like the Group column; a click on a
group lists the users of that group alone, a second click every user again), the users
(name, group, role, sessions, connected time, time on the project pages, first and last
request in the period, addresses, projects, evaluations, requests; sortable by the headers),
the labs (one row per lab, every instance merged: projects, users, students, the time
spent on the project pages and its average per user, the starts, the evaluations) and the
distribution of the students’ connection time (less than 15 minutes, 15 to 30, 30 to 60,
1 to 2 hours, 2 to 4, more). The time on a project page is that of its project_close lines
(the page open as long as its events WebSocket was), clipped to the period and merged per user
and lab; a page still open is not counted yet, and the pages of the spectators of a
presentation never are; the starts are the start lines with result=created, the
evaluations the eval lines answered 200; the requests, projects and evaluations of a session
are those of the whole session. The portal reads the lines of those four events from the
start of the period to two days after its end (a session or a page active in the period but
closed later than that is missed), 200 000 of them at most: beyond, the page says the
statistics are computed on the newest rows only and asks to narrow the period.
The Group select of the form (shown when a source of user groups is configured, User
groups above: the departments, the directory groups and the custom groups, listed by kind as
GET /api/groups gives them) restricts the three views to the members of one group, whatever
its kind: the portal resolves the group into the logins of its current members and asks the
database for their rows alone (one IN list on the indexed login column, so the sessions,
the events or the statistics of a class group are one query; the open sessions are filtered
the same way). A group nobody belongs to as far as the portal knows (a custom group emptied or
deleted since, a department of nobody) gives nothing and the page says so. The department cell
of a session filters on that department. The filter is kept in the URL as group=kind:name (a
bare name is a department, like the Watch page’s) and logged as group_filter; without any
group source the parameter is refused (400).
In the two list views the filters stay in the URL (the sort of the headers does not), so a
filtered view reloads and can be sent to a colleague; Older / Newer page through the rows
(100, 500 or 1000 at a time, by a cursor on the row ids: a page is stable while new events
arrive; each page is sorted on its own) and a click on a login,
address or, in the events view, event, host or project cell filters on that value. Nothing is
polled: Search asks again. Each query is a history line of the log itself, with the filters
asked (view=sessions for the sessions, view=statistics and the period resolved for the
statistics).
In proxy mode (the production site behind Apache) the page shows the same sessions: a session
starts at the first request carrying the proxy’s identity headers, right after the browser’s
password dialog (no login line: Apache authenticates, the portal only sees the result); the
Logout button still closes it on the portal before the browser is sent to logout_url, where
Apache’s 401 makes it forget the Basic credentials; a tab closed without a logout ends it by the
idle timeout like the other modes; the credentials the browser cached make a later return a new
session without any dialog; the client address comes from X-Forwarded-For of the
trusted_proxies peer (set in every mode: the proxy’s address alone would merge the devices of
a user); the authentications Apache refuses never reach the portal (its own log holds them).
The Display tab¶
A lab machine declared x11_host draws its graphical applications (Wireshark, Firefox…) on
the X server of the host: on a workstation, the student’s own desktop. A pool host has no X
server, so the agent runs one virtual desktop per project whose lab has such a machine: a
TigerVNC Xvnc with an openbox window manager, started as the unprivileged sreweb-x11
user right after sre start, shown in the Display tab by noVNC (make novnc). The
containers reach display :N on TCP port 6000+N of the host with the cookie of the project
(the core passes them SRE_DISPLAY and SRE_XAUTH_COOKIE, the .bashrc of the images ≥ 1.31
sets DISPLAY from them); the browser reaches the RFB Unix socket of the server through the
agent and the portal, as a WebSocket of binary frames. The desktop follows the size of the tab
(Resize the desktop; Fit to window scales it instead), the text box of the bar sends text to
the X clipboard and, on the owner’s page only, shows what the applications copy (put into the
browser’s clipboard too; an instructor helping on the project can send text but receives none:
any X client of the containers can set the selection), Reconnect opens the stream again. The
number and the cookie of a project never change: a desktop that died, or that was lost when the
agent restarted (its X server is a child of the agent), is started again at the next use of the
tab, empty. [display] of agent.toml holds the geometry, the range of display numbers and
the commands. When no number of the range is free (every one taken by a project, by the lock
file of another X server in /tmp or by a listener on its TCP port, which any local user can
leave), a project still starts, without a desktop (no Display tab); the agent logs the reason
once.
Session recording (asciinema) applies as on a workstation, since it happens in sre connect.
The answers are saved as the user types (debounced), with the identity of the session.
Security¶
The agent refuses every request without its bearer token and the identity headers the portal adds (
X-SRE-Login,X-SRE-Fullname,X-SRE-Group, …); the browser never talks to it. With mutual TLS (above) only the holder of a client certificate of the CA reaches the token check, the traffic to the agents is encrypted, andallowed_clientsrefuses every other address with 403. Without TLS the token and the terminals cross the network in clear: keep the agents on a trusted management network or behind a tunnel (the agent warns at startup).The session cookie is signed (HMAC-SHA256),
HttpOnly,SameSite=Strict,Securebehind HTTPS; it lives 14 days from the sign-in (a language change sets it again with its original date: a session never slides) and carries the role, group and administrator flag of the sign-in (a change in the directory applies at the next sign-in: Logout, or the expiry); the state-changing calls need theX-Requested-With: sre-webheader and the WebSockets a matchingOrigin; the pages carry aContent-Security-Policy(the origin of the BigBlueButton server added to itsframe-srcwhen[bbb]is set: the only other origin a page may frame).In proxy mode nothing is stored in a cookie: every request carries the identity set by the reverse proxy, accepted from the peers of
trusted_proxiesonly (a request reaching the portal from anywhere else has no identity, headers or not).The directory is reached over TLS (
ldaps://, orstart_tlsonldap://) and the portal verifies the server certificate on every connection: the chain against the system CA store or[auth.ldap] tls_ca, the host of theuriagainst the names of the certificate; a StartTLS the server declines aborts the login, and a plainldap://is refused at startup unlessallow_cleartext = true(the student and instructor passwords, in LDAP mode, and the answers the roles and groups are built from would otherwise be exposed to anyone on the path). Theldap3library validates nothing by itself.One account is one user: in LDAP mode the identity carries the login as the directory stores it (the login attribute of the entry found, else the typed login lower-cased), not what the user typed, since the directory matches
Bobforboband binds either; the role tests naming the login (memberUid, which matches case-exactly) use that form too. In proxy mode the proxy must pass that form (AuthLDAPRemoteUserAttributein the Apache example).Rate limits per user (evaluations, starts; per client address in anonymous mode, where a new pseudonym is free) and per client address (logins); the size of the answers is capped; the reports reading thousands of rows (the Statistics view, the summary of a lab on the Archives page) run two at a time, a third waits ten seconds for a slot then gets a 503 to retry. The sign-in page shows its own messages only (
/login?error=<code>:password,rate_limited,directory), never a text taken from the URL.The
answers/directory of a project is world-writable (the Qt GUI of the student writes there) and the agent, as root, merges the cheat answerssre stateleaves in it (answers/cheat.json): that file is opened without following a symbolic link and read only when it is a regular file with one link, owned by root,sreor the reader, of at mostcheat_file_max_sizebytes (one million); anything else planted under that name is removed (the entry, never what a link points to) and logged once.The connection log (
[access_log]) keeps who did what from where: the sessions with their duration and projects, the logins and their failures, the denied calls, the administration calls. The only header it reads isX-Forwarded-For, from the peers oftrusted_proxies, for the client address; the identity never comes from it. With[access_log.database]the same lines are in a MariaDB / MySQL table, read on the History page by the administrators only ([auth] administratorsand the administrator group tests: instructors, never students; every consultation is itself logged); the account of the portal needsSELECT, INSERT, DELETEon that table and nothing else.A project’s owner is the third field of its running lab name; the portal, the agent and, in user mode, the
sreCLI itself (params.check_project_owner: the local GUI of a pool host lists and reaches the same projects as the CLI lets through) check it. An instructor (see Instructors above) reaches every project, but the agent never runs a command as the student for them: a non-owner’seval,stateandconnectare privilegedsrecalls under the instructor’s own login, the answers a non-owner writes go into the student’s file with the student’s metadata kept (the file never names the instructor), and a private terminal is listed to instructors only; a terminal an instructor opens on a device the student may not open themself (a closed machine or switch, a hidden machine) is private whatever the page asks, since a shared session is joined without the device check. The operations log of an instructor-mode project goes to the subscribers allowed to read it only.What the applications of a project’s desktop copy (the X selection, which any X client of the containers can set) is received by the owner’s browser only, where the Display tab puts it into the clipboard; the page of an instructor helping on the project does not listen to it. Likewise the hyperlinks a program prints in a terminal (OSC 8 sequences) open, after a confirmation naming the target, on the owner’s page only; on an instructor’s or a spectator’s page they are inert, the target shown as a tooltip.
The spectators of a presentation reach the presented project through the portal only, on a short list of read-only calls (the project, its events, the schema, the log, a terminal to join, the display; the evaluation table comes from the presenter’s page through the portal, never from the agent), relayed to the agent under the presenter’s identity (the agent trusts the portal for every identity anyway); everything else is refused, the portal never forwards what a spectator’s browser sends on a terminal socket and the agent attaches a read-only client that cannot type nor resize, and the display socket of a spectator goes through a filter dropping the RFB input and resize messages (an unexpected message closes it). From the browsers, the presentation socket takes the pointer frames of the presenter’s page only (a JSON object of at most 1 KiB locating the mouse, at most 60 a second, every other frame ignored; a spectator’s frames are ignored); the pointer and the selection name nodes of the page and cells of a terminal, never a pixel, and a spectator’s page resolves them on its own layout. The presentations live in the portal’s memory.
The virtual desktops. Each Xvnc listens on TCP port 6000+N on every interface of the pool
host, protected by the MIT cookie of its project only, exactly like the X server of a workstation
configured for the labs (installation.md); an nftables rule such as
nft add rule inet filter input tcp dport 6010-6099 iifname != "docker0" drop keeps the other
hosts out. The RFB socket of a desktop is reachable by root only (the agent); the window manager
runs as sreweb-x11 with no menu, no launcher and no key binding running a program of the host;
the browser reaches a desktop through the owner check of the portal and of the agent.
Limitations¶
After an update of the tree, the portal and the agents must be restarted for the Python code (
sre-web-tool restart --allfor the agents, see Restarting the agents above; the templates are re-read when they change); the scripts and style sheets ofweb/staticare served withCache-Control: no-cache, so a browser revalidates them on every page load (anETaganswers 304 for an unchanged file) and never runs a script older than the page. A page loaded before the update keeps its scripts until it is reloaded.The virtual X desktops die with the agent (a restart of its unit): they are started again, empty, at the next use of the Display tab; the applications shown must be launched again.
The preloads of the Docker images are counted per portal (two portals sharing hosts each give
max_preloadsturns), need the portal (an agent never preloads by itself), are asked at the start of an agent or by hand only (no periodic check: a cron job runningsre-web-tool preload --alldoes it) and are not kept across a restart of an agent: a preload asked by hand and not started yet is forgotten, one in progress is stopped ([preload] at_startasks again). A host that cannot be polled while it preloads stops counting, so one more host may get a turn meanwhile.The
portsof the bridged machines are bound on the pool host and shown with its name.The exam mode of the Qt GUI (
exam.jsonon a host) still excludes that host from the pool: the web site has its own exams (Exams above), per student rather than per host. The save of the terminal sessions across a restart of the agent is a later phase.A terminal is lost when the agent restarts; its sub-tab offers a Reconnect button. A page that left keeps its terminals for
terminals_lingeronly (120 minutes by default); where each one was (a sub-tab, a window) is remembered by the browser, another browser of the same user gets them back as sub-tabs.The replay a joining viewer gets may start inside an escape sequence; the next prompt or redraw of the application settles the display.
The
[limits] eval_intervalof the portal (10 s between two evaluations of a login) also applies to an instructor evaluating several students’ projects in a row.A presentation ends when the portal restarts (nothing is kept on disk); the agents log the spectators under the presenter’s login (the connection log of the portal has the real ones); a spectator’s terminal shows the presenter’s grid at a smaller font when their window is smaller; the presenter’s pointer and selection are shown where the student’s page holds the same element: a student reading the lab in another language sees the pointer on the matching paragraph and no selection there; in a terminal they follow the text (the rows are counted from the cursor, a selection is found again by the text of its first and last rows), so a selection of rows older than the student’s 64 KiB replay is not shown, the student’s terminal does not follow the scrollback position of the presenter’s (a pointer on rows the student’s screen does not show is hidden), and when several users type in the presented terminal its pty takes the smallest window, so a wider presenter’s terminal wraps the long lines elsewhere and the pointer and the selection on them may miss; the pointer is not shown while the presenter’s page has no socket open; a presented project never idles out while it is watched. A focus ends when the portal restarts too; while one is on, the student’s other browser tabs all go to the project.
A web conference is forgotten when the portal restarts (the meeting goes on on the BigBlueButton server until it expires); every page of the site joins the meeting again (a few seconds); two tabs of one user hand the client over to each other, but two pages loading at the same instant may both join.
Development¶
make web-tests runs the offline tests of the web front-end: the agent against a fake sre
(tests/mock_sre.py), the portal against that agent, the templates. python3 -m sreweb.agent --config agent.toml --listen 127.0.0.1:8451 and python3 -m sreweb.portal --config web.toml --listen 127.0.0.1:8450 --web-dir web (from src/, in the venv) run both locally; with
sre_exe pointed at a wrapper of tests/mock_sre.py (SRE_PUB_DIR exported in the wrapper),
the whole site works without Docker. The French catalog is locale/fr/LC_MESSAGES/sreweb.po
(hand-maintained, make web-translations).
The pure JavaScript modules (the lab tree of the Open page, the group keys, the exam form, the
floating terminal windows) are tested under node or bun; on a machine with neither, make tests runs them under bun in a throwaway container of the oven/bun image instead
(SRE_TEST_JS=docker, what the Makefile passes then: the image is pulled once, the tests skip
without a reachable Docker daemon, root or the docker group; SRE_TEST_JS=docker:<image> names
another image, SRE_TEST_JS= leaves them skipped). Installing a runtime (apt install nodejs)
is the lighter alternative.
Stress test¶
scripts/web-stress.py simulates a class of students on a portal through its HTTP / WebSocket
API, as the browser drives it: for each student (logins stress001, stress002…), the Projects
page (GET /api/projects every 5 s, the presence socket), the Open page (GET /api/labs),
POST /api/projects read as NDJSON until the result line (a 503 or 429 is retried after its
retry_after), the project page (the project JSON, the schema, the events socket kept open and
reconnected with the browser’s backoff, POST .../touch every 60 s), a terminal on the first
device open to the student with a few commands typed (the echo of the first one is checked),
then, over --hold seconds, --answers saves and --evals self-evaluations, and finally the
stop. The students begin spread over --ramp seconds. --instructor adds an instructor on the
Watch page (GET /api/watch, the Pool page’s calls, every 5 s), --admin-token samples
GET /api/pool every 5 s (the reachable flag and the project count of each host as the portal
sees them), --probe measures GET /api/me every 2 s. The tool needs Python 3.11 and aiohttp only
(the venv of the installation has them) and runs in proxy authentication mode: the identity
of each student travels in the X-Remote-* headers, so it must run on a host of
trusted_proxies (the portal’s own host, against http://127.0.0.1:8450); the instructor is
made by X-Remote-Gid: 2000 (--instructor-header / --instructor-value otherwise).
The summary on stdout gives, per phase (start, project_get, events_ws, terminal_ws,
eval, stop…), the number of calls, the errors, the latencies (p50 / p95 / max) and the
statuses, then the students completed, the checks failed (own_projects_only: a listing showed
another student’s project; project_listed: the new project was missing from GET /api/projects;
terminal_echo; eval_grades; events_stayed_open; no_unexpected_removed; stopped), the
hosts used and the pool samples (the flips of reachable show the status polls timing out).
--report FILE writes everything as JSON (one entry per student), --events FILE a CSV
timeline (one line per request or socket event). The exit status is 0 when every student
completed with every check true. --keep leaves the projects running; --cleanup stops every
project of the synthetic logins (through the instructor identity) and exits.
Stage 1: a fake pool, no Docker¶
scripts/web-fake-pool.py launches from the checkout N real agents whose sre is a wrapper of
tests/mock_sre.py (the projects are directories, the terminals /bin/sh, a start takes
7 × --start-delay seconds, an evaluation --eval-delay), and a real portal in proxy mode
trusting the loopback, with the production [limits] (--start-interval / --eval-interval
override them), max_projects per host and an admin token; then it runs the command given after
-- with SRE_STRESS_PORTAL, SRE_STRESS_ADMIN_TOKEN and SRE_STRESS_WORKDIR in its environment
(the defaults of --portal and --admin-token) and stops everything. Every MOCK_* variable of
its environment reaches the fake sre (MOCK_NO_MARK=1: the letters of a self-evaluation,
MOCK_EVAL_ERRORS, MOCK_COOLDOWN, see the header of tests/mock_sre.py):
venv/bin/python3 scripts/web-fake-pool.py --hosts 4 --max-projects 25 -- \
venv/bin/python3 scripts/web-stress.py --users 100 --ramp 60 --hold 120 --instructor --probe \
--report report.json --events events.csv
Without a command the pool stays up until Ctrl-C (portal=... admin_token=... workdir=... is
printed first), for runs by hand. The work directory (configs, portal.log, access.log, one
agent.log and sre.log per host) is removed at the end unless --keep-workdir or a daemon died;
the end of the log of a daemon that died is printed with the failure. The agents listen on the
ports after the portal’s (--listen, 18450 by default): keep them below the ephemeral ports of
the system (32768 on Linux), where a port may be held by a client connection, even for a minute
after it closed.
--administrators prof,... makes those logins administrators of the portal ([auth] administrators), so that an instructor among them (X-Remote-Gid: 2000) can exercise the
History page; its Sessions view lists the open sessions without any database.
Stage 2: the production pool¶
On the portal’s host, outside class hours, with the tool copied there (or /opt/sre/scripts/
once synced) and the venv of the installation:
Preflight. The portal must see the tool as a trusted peer: from the host itself when the portal container uses the host network (
docker inspect sre-web --format '{{.HostConfig.NetworkMode}}'), from inside the container otherwise (docker exec). Read the admin token in[admin]ofweb.toml.sre-web-tool statusandlist: the hosts up and the spare slots; with fewer slots than students, the pool wakes hosts during the ramp (wake_timeout, the students refused with 503 “a host is starting” retry after theretry_afterof the answer, 30 s). Checkulimit -nof the portal process (two relayed sockets per student). Note thearchive_dirsof the hosts: every evaluation writes an archive there.Smoke run:
web-stress.py --portal http://127.0.0.1:8450 --users 3 --hold 60 --lab sre/dhcp.py --instructor --admin-token ... --probemust give 3 students completed and no check failed.Full run: the same with
--users 100 --ramp 300 --hold 600 --report ... --events .... Meanwhile:docker stats sre-web(the CPU of one event loop, the memory), the connection log (event=start ... result=,denied, hosts unreachable),sre-web-tool list(states, projects per host), the journal of an agent (journalctl -u sre-agent -f), the probe’s p95 in the summary.Cleanup:
web-stress.py --cleanup ...stops what a stopped run left (a Ctrl-C stops the students’ projects by itself), then remove the archives of the synthetic logins (*@@@stress0*underarchive_dirson the hosts) and checksre-web-tool listis back to 0 projects.
The reverse proxy and the directory are bypassed (the tool speaks to the portal), and
follow_local_sessions never applies (the synthetic logins have no local session).