SRE.params
1import grp 2import os 3import pwd 4import re 5import stat 6from datetime import datetime 7from typing import Literal 8 9from pathlib import Path 10 11################################################################################# 12# 13# Layout and site parameters 14# 15# Plain top-level assignments: scripts/install.sh patches them, `sre update` merges them (the 16# values of the site are kept) and the site parameters file (site_params_file below) may set any 17# of them. A value of None means "the default of the installation layout", computed in the 18# "Derived parameters" block below, after the site parameters file has been read. 19# 20################################################################################# 21 22# "checkout": a git tree (/opt/sre, scripts/install.sh, the venv); "fhs": the Debian package 23# (set by debian/rules on the installed copy: /usr/lib/sysreseval, /usr/share/sysreseval, 24# /etc/sysreseval, /var/lib/sre) 25install_layout = "checkout" 26 27sre_user = "sre" 28sre_uid = 1100 # None: the uid of sre_user 29sre_gid = 1100 # None: the gid of sre_user 30docker_group = "docker" 31docker_gid = 988 # None: the gid of docker_group, else of docker_socket_path, else sre_gid 32 33admin_uids = [] 34admin_gids = [2000] 35 36main_sre_dir = "/opt/sre" 37 38# Override main_sre_dir with the actual install root derived from this file's 39# location, so a single build/checkout works from any install path. Without 40# this, lib_dir stays at the build-time value and `from utils import ...` in 41# srelab.py files fails when sre is installed somewhere other than /opt/sre. 42# The marker file (src/sre.py) sits in a world-readable directory so this 43# probe works whether we're imported by the privileged CLI or by sysreseval 44# running as an unprivileged student (lab/ and lib/ are sre:sre-only). 45_candidate = Path(__file__).resolve().parent.parent.parent 46if (_candidate / "src" / "sre.py").is_file(): 47 main_sre_dir = str(_candidate) 48del _candidate 49sre_pub_dir = "/var/lib/sre" 50sre_user_public_dir = None # None: /home/sre (checkout), sre_pub_dir/home (fhs) 51 52lab_dir = None # None: main_sre_dir/lab (checkout), sre_pub_dir/lab (fhs) 53shipped_lab_dir = None # None: lab_dir (checkout), /usr/share/sysreseval/lab (fhs): the labs of the package 54lib_dir = None # None: main_sre_dir/lib 55graphicdir = None # None: main_sre_dir/graphics (checkout), /usr/share/sysreseval/graphics (fhs) 56fontdir = None # None: graphicdir/fonts (checkout), /usr/share/fonts/truetype/dejavu (fhs) 57translations_dirs = None # None: [<root>/translations, main_sre_dir/translations] (checkout), [/usr/share/sysreseval/translations] (fhs) 58locale_dirs = None # None: [<root>/locale, main_sre_dir/locale] (checkout), [/usr/share/locale] (fhs) 59archive_dirs = None # None: [sre_pub_dir/archives] 60sre_wrapper = None # None: main_sre_dir/bin/sre-wrapper (checkout), /usr/bin/sre-wrapper (fhs) 61sysreseval_exe = None # None: main_sre_dir/bin/sysreseval (checkout), /usr/bin/sysreseval (fhs) 62sre_exe = None # None: main_sre_dir/sbin/sre (checkout), /usr/sbin/sre (fhs) 63docker_socket_path = "/var/run/docker.sock" # absent: no Docker daemon on this host (`sre update` skips the wipe) 64self_update_allowed = None # None: True (checkout), False (fhs: the package is updated with apt); `sre update` 65network_plugin_hint = None # None: how to install the network plugin with the switch types, by layout 66kathara_hint = None # None: how to get a Kathara with the switch modes, by layout 67web_dir = None # None: main_sre_dir/web (checkout), /usr/share/sysreseval/web (fhs): templates and static files of sre-web 68web_config_file = None # None: /etc/sreweb/web.toml (checkout), /etc/sysreseval/web.toml (fhs): configuration of sre-web 69agent_config_file = None # None: /etc/sreweb/agent.toml (checkout), /etc/sysreseval/agent.toml (fhs): configuration of sre-agent 70# Site parameters file: a Python file executed into this module after the parameters above (any 71# of them may be set there) and before the derived ones. It must be owned by root (or by the 72# user running the program) and not writable by the group or others. None: /etc/sysreseval/params.py 73# in the fhs layout (shipped by the Debian package), no file on a checkout. 74site_params_file = None 75 76# debug_mode: 77# - allow environment variables SRE_WRAPPER and SRE_PUB_DIR to overcome the default value (insecure) 78# - allow "sysreseval --debug" 79debug_mode = False 80 81# If True, sysreseval will use the last field in the gecos string and save it in the answers 82# of the user as email (to be used in the pdfs generated with "sre outline") 83email_in_gecos_last_field: bool = True 84 85max_docker_concurrency = 16 86 87display_marks_in_auto_evaluations_by_default = False 88use_numerical_marks_by_default = True 89default_maximum_mark = 20 90 91# Scope bitmask values for GradeElement.scope. A grade element is visible in 92# self-eval (sre eval --auto-eval) if scope & SELF_EVAL_SCOPE, and in non-auto 93# eval / outline / sheet if scope & EXO_EVAL_SCOPE. 94SELF_EVAL_SCOPE = 1 95EXO_EVAL_SCOPE = 2 96BOTH_EVAL_SCOPE = SELF_EVAL_SCOPE | EXO_EVAL_SCOPE # 3 97grade_scopes = (SELF_EVAL_SCOPE, EXO_EVAL_SCOPE, BOTH_EVAL_SCOPE) 98 99default_eval_interval_during_exams = 60 # default duration between two evaluations in exam mode 100default_eval_interval_without_exam_mode = 60 # default interval (seconds) for eval outside exam mode when the lab does not define eval_interval_without_exam_mode (0 for no periodic evals) 101default_save_record_interval_during_exams = 60 # use 0 to disable saving 102 103default_exam_duration = 90 # default exam duration in minutes when "duration" is absent from exam.json 104 105 106default_inactivity_threshold_in_watch_command = 90 # default inactivity (no eval) from a project before triggering an alert in watch 107default_dashboard_refresh_interval_in_watch_command = 1 108 109max_duration_between_exam_pre_start_and_start = 60 # duration in seconds 110 111default_exec_shell = "/bin/bash" 112 113# `sre update`: where the new versions of SRE come from (a GitHub repository) and what the 114# command refreshes in lab/ (the other entries of lab/ are the site's own labs and are kept) 115update_github_repository = "sysreseval/sysreseval" 116update_github_api_url = "https://api.github.com" 117update_tarball_url = "https://codeload.github.com/{repository}/tar.gz/{commit}" 118update_default_branch = "main" # followed by `sre update --main` (the default is the latest release) 119update_http_timeout = 60 # seconds, per request 120update_lab_subdirs = ["sre"] # entries of lab/ owned by the repository, reinstalled by `sre update` 121 122################################################################################# 123# 124# Security settings 125# 126################################################################################# 127 128allow_privileged_machines = True 129disable_volume_mount_on_root_partition = False 130 131# For identifying the users, use the environment variable SUDO_USER instead of USER_USERNAME 132# SUDO_USER is set by sudo itself, thus not spoofable (but in some configurations don't give the current USER variable) 133# USER_USERNAME is set by sre-wrapper from USER (spoofable by the user) 134use_sudo_user_for_username = True 135 136# In user mode (--user: sre-wrapper, hence the GUI, and the student path of the web agent) a 137# caller only sees and drives their own projects and those started by root or the sre user 138# (`sudo sre start ...`); the GUI lists the same projects. False: every local user sees and 139# acts on every project of the host (the behaviour of the versions before 0.14). 140check_project_owner = True 141 142# Should projects be able to execute commands on the host (which might be useful) 143# shell : execution through subprocess.run with shell=True which allow pipes 144# split : execution through shlex.split to forbid pipes 145# False : no execution permitted on the host 146execute_commands_on_host: Literal["shell", "split", False] = "shell" 147 148 149authorized_src_dir = None # None: [lab_dir, shipped_lab_dir, '/home'] 150 151# External terminal emulator used to open machine connections. 152# The command is built as: terminal_cmd_prefix[:-1] + [terminal_title_opt, title] + terminal_cmd_prefix[-1:] + [sre_wrapper, "connect", project, machine] 153# The title is passed as a two-token pair (terminal_title_opt, title), so only 154# terminals that accept a separate "--title TITLE"-style option work here. 155# Uncomment ONE pair below (binary path may differ on your host; check with `command -v`): 156# gnome-terminal: 157# terminal_cmd_prefix = ["/usr/bin/gnome-terminal", "--"] 158# terminal_title_opt = "--title" 159# xfce4-terminal: 160# terminal_cmd_prefix = ["/usr/bin/xfce4-terminal", "-x"] 161# terminal_title_opt = "--title" 162# xterm: 163# terminal_cmd_prefix = ["/usr/bin/xterm", "-e"] 164# terminal_title_opt = "-title" 165# terminator: 166# terminal_cmd_prefix = ["/usr/bin/terminator", "-x"] 167# terminal_title_opt = "-T" 168terminal_cmd_prefix = ["/usr/bin/mate-terminal", "--"] 169terminal_title_opt = "--title" 170 171 172################################################################################# 173# 174# Site parameters file 175# 176################################################################################# 177 178def _load_site_params(path: str, namespace: dict) -> None: 179 """Execute the site parameters file *path* into *namespace* (the globals of this module). 180 The file is refused when it is not owned by root or by the current user, or when its group or 181 others may write it: `sre` runs it as root. A failure stops the program with the file name 182 and the line.""" 183 try: 184 st = os.stat(path) 185 if st.st_uid not in (0, os.geteuid()): 186 raise PermissionError(f"not owned by root (uid {st.st_uid})") 187 if st.st_mode & (stat.S_IWGRP | stat.S_IWOTH): 188 raise PermissionError("writable by the group or others") 189 with open(path, encoding='utf-8') as f: 190 code = compile(f.read(), path, 'exec') 191 except SyntaxError as e: 192 raise SystemExit(f"sre: {path}:{e.lineno}: {e.msg}") from e 193 except OSError as e: 194 raise SystemExit(f"sre: {path}: {e}") from e 195 try: 196 exec(code, namespace) 197 except SystemExit: 198 raise 199 except Exception as e: 200 lineno = e.__traceback__.tb_next.tb_lineno if e.__traceback__ and e.__traceback__.tb_next else '?' 201 raise SystemExit(f"sre: {path}:{lineno}: {type(e).__name__}: {e}") from e 202 203 204if site_params_file is None and install_layout == "fhs": 205 site_params_file = "/etc/sysreseval/params.py" 206if debug_mode: 207 site_params_file = os.environ.get("SRE_SITE_PARAMS", site_params_file) or None 208if site_params_file and os.path.isfile(site_params_file): 209 _load_site_params(site_params_file, globals()) 210 211if debug_mode: 212 sre_wrapper = os.environ.get("SRE_WRAPPER", sre_wrapper) 213 sre_pub_dir = os.environ.get("SRE_PUB_DIR", sre_pub_dir) 214 215 216################################################################################# 217# 218# Derived parameters: the defaults of the installation layout for the parameters left to None 219# above (an `if` block each: not parameters of their own for `sre update`) 220# 221################################################################################# 222 223_fhs = install_layout == "fhs" 224_root = Path(__file__).resolve().parent.parent.parent # the checkout (<root>/src/SRE/params.py) 225if _fhs: 226 main_sre_dir = "/usr/lib/sysreseval" 227if sre_uid is None: 228 try: 229 sre_uid = pwd.getpwnam(sre_user).pw_uid 230 except KeyError: 231 sre_uid = 1100 232if sre_gid is None: 233 try: 234 sre_gid = pwd.getpwnam(sre_user).pw_gid 235 except KeyError: 236 sre_gid = 1100 237if docker_gid is None: 238 try: 239 docker_gid = grp.getgrnam(docker_group).gr_gid 240 except KeyError: 241 try: 242 docker_gid = os.stat(docker_socket_path).st_gid 243 except OSError: 244 docker_gid = sre_gid 245if sre_user_public_dir is None: 246 sre_user_public_dir = sre_pub_dir + "/home" if _fhs else "/home/sre" 247if lab_dir is None: 248 lab_dir = sre_pub_dir + "/lab" if _fhs else main_sre_dir + "/lab" 249if shipped_lab_dir is None: 250 shipped_lab_dir = "/usr/share/sysreseval/lab" if _fhs else lab_dir 251if lib_dir is None: 252 lib_dir = main_sre_dir + "/lib" 253if graphicdir is None: 254 graphicdir = "/usr/share/sysreseval/graphics" if _fhs else main_sre_dir + "/graphics" 255if fontdir is None: 256 fontdir = "/usr/share/fonts/truetype/dejavu" if _fhs else graphicdir + "/fonts" 257if translations_dirs is None: 258 translations_dirs = ["/usr/share/sysreseval/translations"] if _fhs \ 259 else [str(_root / "translations"), main_sre_dir + "/translations"] 260if locale_dirs is None: 261 locale_dirs = ["/usr/share/locale"] if _fhs else [str(_root / "locale"), main_sre_dir + "/locale"] 262if archive_dirs is None: 263 archive_dirs = [sre_pub_dir + '/archives'] 264if sre_wrapper is None: 265 sre_wrapper = "/usr/bin/sre-wrapper" if _fhs else main_sre_dir + "/bin/sre-wrapper" 266if sysreseval_exe is None: 267 sysreseval_exe = "/usr/bin/sysreseval" if _fhs else main_sre_dir + "/bin/sysreseval" 268if sre_exe is None: 269 sre_exe = "/usr/sbin/sre" if _fhs else main_sre_dir + "/sbin/sre" 270if authorized_src_dir is None: 271 authorized_src_dir = list(dict.fromkeys([lab_dir, shipped_lab_dir, '/home'])) 272if self_update_allowed is None: 273 self_update_allowed = not _fhs 274if network_plugin_hint is None: 275 network_plugin_hint = ("Install it with `sysreseval-network-plugin` (see the installation guide)." if _fhs 276 else f"Install it with `make network-plugin` in {main_sre_dir} (see the installation guide).") 277if kathara_hint is None: 278 kathara_hint = "upgrade the python3-kathara package" if _fhs else "install it again with `make venv`" 279if web_dir is None: 280 web_dir = "/usr/share/sysreseval/web" if _fhs else main_sre_dir + "/web" 281if web_config_file is None: 282 web_config_file = "/etc/sysreseval/web.toml" if _fhs else "/etc/sreweb/web.toml" 283if agent_config_file is None: 284 agent_config_file = "/etc/sysreseval/agent.toml" if _fhs else "/etc/sreweb/agent.toml" 285del _fhs, _root 286 287 288################################################################################# 289# 290# Don't modify parameters below 291# 292################################################################################# 293 294sre_version = "0.16.1" 295default_docker_image_version = "1.33" 296sre_docker_namespace = "sysreseval" 297 298def sre_docker_image(image_name: str = "base") -> str: 299 return f"{sre_docker_namespace}/{image_name}:{default_docker_image_version}" 300 301default_docker_image = sre_docker_image() 302 303# `sre update`: state file of the installed version (commit, manifest of the installed files, pristine 304# upstream values of params.py), lock and temporary directory, all in main_sre_dir 305update_state_file_name = ".sre-update.json" 306update_lock_file_name = ".sre-update.lock" 307update_tmp_prefix = ".sre-update-" 308update_params_follow_new = ("sre_version", "default_docker_image_version") # always take the new value 309update_params_backup_suffix = ".bak." # params.py.bak.<YYYYmmdd-HHMMSS>, like scripts/install.sh 310update_system_templates_dir = "scripts/etc" # files installed by hand on every host (sudoers, .desktop...) 311# keys of the state file 312update_state_commit = "commit" 313update_state_ref = "ref" 314update_state_updated_at = "updated_at" 315update_state_previous_commit = "previous_commit" 316update_state_files = "files" 317update_state_params_upstream = "params_upstream" 318 319sre_projects_dir = sre_pub_dir + "/projects" 320 321sre_name_env_variable = "SRE_LAB_NAME" 322sre_xauth_cookie_env_variable = "SRE_XAUTH_COOKIE" 323sre_host_ip_env_variable = "SRE_HOST_IP" 324# the web agent gives `sre start` the number of the virtual X display of the project (digits); 325# the x11_host machines then get SRE_DISPLAY=<sre_host_ip>:<number>, used by the images' .bashrc 326# in place of <sre_host_ip>:0 (the X server of a workstation) 327sre_x11_display_env_variable = "SRE_X11_DISPLAY" 328sre_display_env_variable = "SRE_DISPLAY" 329 330sre_host_ip = "172.17.0.1" 331 332exetests_path = lib_dir + "/exetests.py" 333exetests_machines_path = '/usr/local/sbin/exetests.py' 334exetests_env_name = 'EXETESTS' 335exetests_separator = '@@@' 336default_timeout = 20 337default_state_cmd_timeout = 120 # seconds, for NetScheme.cmd() / host_cmd() (0 = no timeout) 338 339exit_code_flavor_form_needed = 3 340exit_code_flavor_not_allowed = 4 341 342default_show_nat_network = True 343default_host_network_color = "deepskyblue" 344default_host_network_name = "Internet" 345default_host_network_shape = "hexagon" 346default_host_network_exploded = False 347default_host_network_edge_relative_length = 1.0 348 349# Module-level `ipv6` lab option: None = leave Kathara's own enable_ipv6 setting (False by default), 350# True/False = force IPv6 on/off in every machine; Machine(ipv6=...) overrides it per machine. 351default_ipv6 = None 352 353# Switch type of a network (`mode` of a `_network_specs` entry, Kathara collision domain mode): 354# a hub sends every frame to every machine, a switch learns the MAC addresses, a managed switch 355# also has VLANs and a management console. 356network_mode_hub = "hub" 357network_mode_switch = "switch" 358network_mode_managed = "managed" 359network_modes = (network_mode_hub, network_mode_switch, network_mode_managed) 360default_network_mode = network_mode_hub 361min_vlan_id = 1 362max_vlan_id = 4094 363# `@machine` in a command of NetScheme.switch_cmd() / Grade.test_switch() stands for the number of 364# the switch port the machine is plugged into. 365switch_port_reference_prefix = "@" 366switch_cmd_error_code = -2 # exit code of a switch command that could not be run at all 367# Console commands (first word of the line) a student may run on a managed switch. Everything 368# else (plugin/*, port/create, port/remove, port/epclose, debug/*, ...) is refused in user mode. 369switch_user_commands = ( 370 "help", "showinfo", 371 "port/showinfo", "port/print", "port/allprint", "port/setvlan", 372 "vlan/create", "vlan/remove", "vlan/addport", "vlan/delport", "vlan/print", "vlan/allprint", 373 "hash/showinfo", "hash/print", "hash/find", 374 "fstp/showinfo", "fstp/print", "fstp/setfstp", "fstp/setedge", "fstp/bonus", 375) 376switch_console_exit_commands = ("exit", "quit", "logout") 377# An open console checks this often (seconds) that its project still exists, and ends when it does not 378switch_console_watch_interval = 1.0 379 380default_machine_shape = "box" 381default_network_shape = "ellipse" 382 383pdf_schema_file = "schema.pdf" 384pdf_info_file = "informations.pdf" 385 386# Terminal appearance defaults (overridable via ~/.config/sysreseval) 387terminal_font_size = 12 # font size in points 388terminal_color_scheme = "black_on_white" # "white_on_black" or "black_on_white" 389content_font_size = 12 # font size in points for information/questions views 390# default stylesheet of the documents drawn from lab markdown (Informations, Questions, form 391# questions): Qt draws the tables without cell padding, the columns touch each other otherwise 392content_style_sheet = "th, td { padding: 2px 8px; }" 393default_form_field_lines = 4 # visible lines of a @@{name:+}@@ multi-line form field 394system_font_size = 10 # font size in points for menus, titles, labels 395 396# Instructor mode (`sre start --instructor-mode`, `sre set-instructor-mode`): default colours of the 397# texts wrapped in instructor(); a call may override them (instructor(text, color=, background=)) 398instructor_text_color = "#b00020" 399instructor_background_color = "#ffe3e3" 400# Delimiters of an instructor fragment inside a text of info.json (private-use characters): 401# <begin><color>;<background><args end><content><end> 402instructor_begin_mark = "\ue000" 403instructor_args_end_mark = "\ue001" 404instructor_end_mark = "\ue002" 405 406thats_all_folks_svg = graphicdir + "/Thats_all_folks.svg" 407sysreseval_logo_svg = graphicdir + "/sysreseval.svg" 408machine_icon_svg_file = graphicdir + "/machine.svg" 409machine_forbidden_icon_svg_file = graphicdir + "/machine-forbidden.svg" 410switch_icon_svg_file = graphicdir + "/switch.svg" 411cloud_icon_svg_file = graphicdir + "/cloud.svg" 412# the `shape` values of a network (`_network_specs`) drawn as an icon by the GUI schema and the export 413# PDF when icons are on (as the default network shape otherwise) instead of a graphviz node shape 414network_shape_icons = {"cloud": cloud_icon_svg_file} 415 416# TrueType fonts embedded in the PDFs of "sre export" and "sre outline" (downloaded by "make fonts" 417# into fontdir, the fonts-dejavu packages in the fhs layout): {style: file} with the styles '' 418# (regular), 'B', 'I' and 'BI'. A missing style uses the regular file. If the regular file cannot 419# be loaded, the core Helvetica/Courier fonts are used and the text is reduced to Latin-1. 420pdf_font_files = { 421 '': fontdir + "/DejaVuSans.ttf", 422 'B': fontdir + "/DejaVuSans-Bold.ttf", 423 'I': fontdir + "/DejaVuSans-Oblique.ttf", 424 'BI': fontdir + "/DejaVuSans-BoldOblique.ttf", 425} 426pdf_mono_font_files = { 427 '': fontdir + "/DejaVuSansMono.ttf", 428 'B': fontdir + "/DejaVuSansMono-Bold.ttf", 429} 430 431exam_only_affix = ["_EXAM_", "_OLD_", "_DRAFT_", "_TESTS_"] 432 433exam_json_name = "exam.json" 434exam_json_keyword = "exam_json" 435exam_start_after = "start_after" 436exam_end_before = "end_before" 437exam_eval_interval = "eval_interval" 438exam_pre_start_date = "pre_start_date" 439exam_started_at = "started_at" 440exam_ended_at = "ended_at" 441exam_duration = "duration" 442exam_labs = "labs" 443exam_record_sessions = "record_sessions" 444 445 446def parse_lab_entry(entry) -> tuple[str, str | None]: 447 """Parse a labs entry: new [lab, flavor] list or legacy plain string. 448 Returns (lab_cli_arg, flavor_name_or_None).""" 449 if isinstance(entry, list): 450 return entry[0], entry[1] 451 return entry, None # backward compat with old exam.json 452 453 454srelab_py_name = "srelab.py" 455shipped_lab_link_name = "sre" # the shipped labs inside lab_dir: lab/sre (a symlink to shipped_lab_dir in the fhs layout) 456titles_file_name = "titles.json" 457# The options of titles.json: every key starting with titles_reserved_prefix is an option, never an entry 458titles_reserved_prefix = "@" 459titles_sort_key = "@sort" # the order of the entries of the directory in the lab tree 460titles_sort_name = "name" # by file name (the default) 461titles_sort_title = "title" # by title, in the user's language 462titles_sort_manual = "manual" # the order of the entries in titles.json 463titles_sort_modes = (titles_sort_name, titles_sort_title, titles_sort_manual) 464# The keys of the entries of `sre list --with-titles` 465lab_list_name_key = "name" 466lab_list_title_key = "title" 467lab_list_kind_key = "kind" 468lab_list_sort_key = "sort" # the titles_sort_* mode the entry is placed among its siblings by 469lab_list_kind_dir = "dir" 470lab_list_kind_lab = "lab" 471data_json_name = "data.json" 472# Keys/markers used inside data.json by Data0.to_dict()/from_dict() 473data_json_ips6_key = "ips6" # Data0.ips6 container (IPv6Interface values) 474data_json_nets6_key = "nets6" # Data0.nets6 container (IPv6Network values) 475data_json_iface_marker = "__iface__" # standalone IPv4Interface / IPv6Interface field 476data_json_mac_marker = "__mac__" # standalone netaddr.EUI field 477private_dir_name = ".private" 478files_dir_name = "files" 479private_mount_dir_name = "mnt" 480user_public_dir_name = "user_public_dir" 481shared_dir_name = "shared" 482records_dir_name = "records" 483recorder_idle_time_limit = 10 # seconds; passed to asciinema --idle-time-limit to compress idle gaps in .cast files 484recorder_config_dir = sre_pub_dir + "/asciinema" # ASCIINEMA_CONFIG_HOME — avoids asciinema falling back to $HOME/.config (which the sre user cannot write) 485use_asciinema_for_records = True # True: prefer asciinema (fall back to script if missing). False: always record with script(1). 486eval_in_progress_name = "eval_in_progress" 487auto_eval_log_name = "auto_eval.log" 488debug_project_marker_name = "debug_project" 489instructor_mode_marker_name = "instructor_mode" 490# Operations log (GUI "Log" tab): every `sre state` / `sre eval` of a debug project appends what it 491# executed, every `sre state` of an instructor-mode project too; lives in the public project dir, 492# never written for the other projects 493operations_log_name = "operations.log" 494operations_log_max_output_chars = 16384 # per command output written to operations.log 495operations_log_view_max_lines = 100000 # block cap of the GUI Log tab 496auto_eval_count_keyword = "auto_eval_count" 497info_json_name = "info.json" 498 499answer_dir_name = "answers" 500answer_file_name = "answers.json" 501cheat_file_name = "cheat.json" 502cheat_file_max_size = 1_000_000 # bytes: a bigger answers/cheat.json is refused (answers.consume_cheat_file) 503# decompressed bytes of an evaluation archive the web agent reads at most (archive_summary.read_archive: the 504# archive directories may be a site or NFS directory; a zstd bomb there must not exhaust the agent) 505archive_max_size = 64 << 20 506 507hostname_keyword = "hostname" 508login_keyword = "login" 509fullname_keyword = "fullname" 510email_keyword = "email" 511language_keyword = "language" 512group_keyword = "group" # the student group of the owner (web front-end, when configured) 513 514# Languages selectable in the GUI language priority dialog 515available_language_in_interface = ['en', 'fr'] 516language_display_names = {'en': 'English', 'fr': 'Français'} 517 518running_lab_name_keyword = "running_lab_name" 519eval_date_keyword = "eval_date" 520re_eval_date_keyword = "re_eval_date" 521 522sysreseval_answers_updated_at = 'answers_updated_at' 523sysreseval_exam_time_remaining = 'exam_time_remaining' 524sysreseval_exam_started_at = 'exam_started_at' 525sysreseval_exam_duration = 'exam_duration' 526sysreseval_exam_mode = 'exam_mode' 527 528initial_state_name = "initial" 529save_state_name = "save" 530restore_state_name = "restore" 531# States applied by the lab lifecycle itself (start / save / restore), never listed as user states 532lifecycle_state_names = (initial_state_name, save_state_name, restore_state_name) 533 534# --- save / restore of running projects (`sre save`, `sre restore`) --- 535save_file_suffix = ".sre" 536save_file_magic = b"SRESAVE1\n" 537save_format_version = 1 538save_stdio_arg = "-" # `sre restore -` reads the save file from stdin 539save_tmp_dir = sre_pub_dir + "/tmp" # not /tmp (may be a small tmpfs): full-image saves can be large 540save_kathara_name = "kathara.tar" # payload member: the Kathara save archive 541save_user_public_member = "user_public" # payload member: contents of the user public dir (shared/, volumes) 542# keys of the cleartext JSON header line 543save_meta_format_version = "format_version" 544save_meta_sre_version = "sre_version" 545save_meta_running_lab_name = "running_lab_name" 546save_meta_lab_name = "lab_name" 547save_meta_srelab_file = "srelab_file" 548save_meta_username = "username" 549save_meta_saved_at = "saved_at" 550save_meta_debug_project = "debug_project" 551save_meta_full_images = "full_images" 552save_meta_shared_path = "shared_path" 553save_meta_encrypted = "encrypted" 554save_meta_kdf_salt = "kdf_salt" 555save_meta_kdf_iterations = "kdf_iterations" 556save_kdf_iterations = 600_000 557save_cipher_chunk_size = 16 * 1024 * 1024 558 559self_grade_timestamp_dir = sre_pub_dir + "/last_self_grades" 560 561# --- re-evaluations queued for the web portal (`sre re-eval --update-web`, web_updates.py) --- 562# one <original archive name>.json per pending update (the record of archive_summary.archive_record), 563# served by the web agent of the host to the portal, which replaces the row of that archive 564web_updates_dir_name = "web-updates" 565web_updates_dir = sre_pub_dir + "/" + web_updates_dir_name 566web_updates_suffix = ".json" 567web_updates_max_age = 24 * 3600 # seconds: an update the portal never took (its evaluation is not there) is dropped 568web_updates_max_size = 8 << 20 # bytes: a bigger entry is refused 569archive_keyword = "archive" # the file name of an archive in its record 570 571srelab_link_name = "srelab" 572 573graphviz_default_nodesep = 0.8 574graphviz_default_ranksep = 1.5 575graphviz_default_splines = "curved" 576graphviz_default_overlap = "prism" 577 578 579class SRE: 580 args = None 581 module_rvlab = None 582 eval_obj = None 583 username = None 584 585 586def datetime_to_string(dt) -> str: 587 return f"{dt:%Y%m%d%H%M%S}" 588 589 590def string_to_datetime(string) -> datetime: 591 return datetime.strptime(string, "%Y%m%d%H%M%S") 592 593 594def get_lab_name_from_cli_arg(lab_cli_arg: str, is_path: bool) -> str: 595 if is_path: 596 lab_cli_arg = os.path.abspath(lab_cli_arg) 597 return lab_cli_arg.replace("/", "@") 598 599 600def get_running_lab_name(lab_name: str, instance_start_date: datetime, username: str = None) -> str: 601 if username is None: 602 if SRE.username is None: 603 username = "" 604 else: 605 username = SRE.username 606 if exetests_separator in lab_name: 607 raise ValueError(f"lab_name must not contain '{exetests_separator}': {lab_name!r}") 608 if exetests_separator in username: 609 raise ValueError(f"username must not contain '{exetests_separator}': {username!r}") 610 return f"{datetime_to_string(instance_start_date)}@@@{lab_name}@@@{username}" 611 612 613running_lab_name_match_pattern = '^([0-9]+)@@@(.+)@@@(.+)$' 614 615 616def get_lab_name_from_running_lab_name(running_lab_name: str) -> str: 617 match = re.match(running_lab_name_match_pattern, running_lab_name) 618 if match: 619 return match.group(2) 620 else: 621 return 'ERROR-running_lab_name-ILLEGAL-FORMAT' 622 623 624def get_username_from_running_lab_name(running_lab_name: str) -> str: 625 match = re.match(running_lab_name_match_pattern, running_lab_name) 626 if match: 627 return match.group(3) 628 return '' 629 630 631def get_start_date_string_from_running_lab_name(running_lab_name: str) -> str: 632 """Instance start date (YYYYmmddHHMMSS digits) encoded in a running_lab_name, 633 or '' when the name does not follow get_running_lab_name(). The digits 634 sort chronologically, so callers compare/sort the string and only convert 635 it with string_to_datetime() for display.""" 636 match = re.match(running_lab_name_match_pattern, running_lab_name) 637 return match.group(1) if match else '' 638 639 640def get_abbreviated_lab_name_from_running_lab_name(running_lab_name: str) -> str: 641 name = get_lab_name_from_running_lab_name(running_lab_name).replace("@", "/").rpartition("/")[2] 642 return name.removesuffix(".py") 643 644 645def get_current_srelab_file_from_running_lab_name(running_lab_name: str) -> str: 646 lab_name = get_lab_name_from_running_lab_name(running_lab_name) 647 pathstr = lab_name.replace("@", "/") 648 if not pathstr.startswith("/"): 649 abspath = (Path(lab_dir) / pathstr).resolve() 650 else: 651 abspath = Path(pathstr).resolve() 652 if abspath.name.endswith(".py"): 653 return str(abspath) 654 else: 655 return str(abspath / srelab_py_name) 656 657 658def get_srelab_dir(running_lab_name: str): 659 lab_name = get_lab_name_from_running_lab_name(running_lab_name) 660 pathstr = lab_name.replace("@", "/") 661 if not pathstr.startswith("/"): 662 abspath = (Path(lab_dir) / pathstr).resolve() 663 else: 664 abspath = Path(pathstr).resolve() 665 if abspath.name.endswith(".py"): 666 return None 667 else: 668 return str(abspath) 669 670 671def project_has_directory(running_lab_name=None) -> bool: 672 if running_lab_name.endswith(".py"): 673 return False 674 else: 675 return True 676 677 678def get_archive_name(running_lab_name: str, date: datetime) -> str: 679 return f"{datetime_to_string(date)}_{running_lab_name}.zst" 680 681 682# Matches the basename produced by get_archive_name(): group 1 is the eval 683# date (YYYYmmddHHMMSS), group 2 the running_lab_name (which may itself 684# contain '@', '_' and '.'). Use with re.fullmatch. 685archive_name_match_pattern = r'(\d{14})_(.+)\.zst' 686 687 688def get_date_from_archive_name(filename: str) -> datetime | None: 689 """Creation (eval) date encoded in an archive filename, or None when the 690 name does not follow get_archive_name() or the digits are not a date.""" 691 match = re.fullmatch(archive_name_match_pattern, Path(filename).name) 692 if not match: 693 return None 694 try: 695 return string_to_datetime(match.group(1)) 696 except ValueError: 697 return None 698 699 700def private_lab_dir(running_lab_name: str) -> str: 701 return f"{sre_projects_dir}/{running_lab_name}/{private_dir_name}" 702 703 704def auto_eval_log_filename(running_lab_name: str) -> str: 705 return f"{private_lab_dir(running_lab_name)}/{auto_eval_log_name}" 706 707 708def debug_project_marker_filename(running_lab_name: str) -> str: 709 return f"{private_lab_dir(running_lab_name)}/{debug_project_marker_name}" 710 711 712def instructor_mode_marker_filename(running_lab_name: str) -> str: 713 return f"{private_lab_dir(running_lab_name)}/{instructor_mode_marker_name}" 714 715 716def operations_log_filename(running_lab_name: str) -> str: 717 return f"{public_lab_dir(running_lab_name)}/{operations_log_name}" 718 719 720def link_to_user_public_dir(running_lab_name: str) -> str: 721 return f"{sre_projects_dir}/{running_lab_name}/{private_dir_name}/{user_public_dir_name}" 722 723 724def public_lab_dir(running_lab_name: str) -> str: 725 return f"{sre_projects_dir}/{running_lab_name}" 726 727 728def info_filename(running_lab_name: str) -> str: 729 return f"{sre_projects_dir}/{running_lab_name}/{info_json_name}" 730 731 732def data_filename(running_lab_name: str) -> str: 733 return f"{sre_projects_dir}/{running_lab_name}/{private_dir_name}/{data_json_name}" 734 735 736def private_mount_dir(running_lab_name: str) -> str: 737 return f"{sre_projects_dir}/{running_lab_name}/{private_dir_name}/{private_mount_dir_name}" 738 739 740def files_dir(running_lab_name: str) -> str: 741 return f"{sre_projects_dir}/{running_lab_name}/{private_dir_name}/{files_dir_name}" 742 743 744def records_dir(running_lab_name: str) -> str: 745 return f"{sre_projects_dir}/{running_lab_name}/{private_dir_name}/{records_dir_name}" 746 747 748def srelab_link_filename(running_lab_name: str) -> str: 749 return f"{private_lab_dir(running_lab_name)}/{srelab_link_name}" 750 751 752def answers_filename(running_lab_name: str) -> str: 753 return f"{sre_projects_dir}/{running_lab_name}/{answer_dir_name}/{answer_file_name}" 754 755 756def cheat_filename(running_lab_name: str) -> str: 757 return f"{sre_projects_dir}/{running_lab_name}/{answer_dir_name}/{cheat_file_name}" 758 759 760def answers_dir(running_lab_name: str) -> str: 761 return f"{sre_projects_dir}/{running_lab_name}/{answer_dir_name}" 762 763 764def self_grade_timestamp_file(lab_name: str, username: str | None = None) -> str: 765 """The cooldown file of the self-evaluations of a lab: one per lab and owner (``sre eval`` 766 passes the owner of the project), so that the students of a shared host do not block each 767 other; the old per-lab file is used when *username* is not given.""" 768 if username: 769 return f"{self_grade_timestamp_dir}/{lab_name}@@@{username}" 770 return f"{self_grade_timestamp_dir}/{lab_name}"
install_layout =
'checkout'
sre_user =
'sre'
sre_uid =
1100
sre_gid =
1100
docker_group =
'docker'
docker_gid =
988
admin_uids =
[]
admin_gids =
[2000]
main_sre_dir =
$PWD
sre_pub_dir =
'/var/lib/sre'
sre_user_public_dir =
'/home/sre'
lab_dir =
'/Users/em/Orsay/Info/sre/lab'
shipped_lab_dir =
'/Users/em/Orsay/Info/sre/lab'
lib_dir =
'/Users/em/Orsay/Info/sre/lib'
graphicdir =
'/Users/em/Orsay/Info/sre/graphics'
fontdir =
'/Users/em/Orsay/Info/sre/graphics/fonts'
translations_dirs =
['/Users/em/Orsay/Info/sre/translations', '/Users/em/Orsay/Info/sre/translations']
locale_dirs =
['/Users/em/Orsay/Info/sre/locale', '/Users/em/Orsay/Info/sre/locale']
archive_dirs =
['/var/lib/sre/archives']
sre_wrapper =
'/Users/em/Orsay/Info/sre/bin/sre-wrapper'
sysreseval_exe =
'/Users/em/Orsay/Info/sre/bin/sysreseval'
sre_exe =
'/Users/em/Orsay/Info/sre/sbin/sre'
docker_socket_path =
'/var/run/docker.sock'
self_update_allowed =
True
network_plugin_hint =
'Install it with `make network-plugin` in /Users/em/Orsay/Info/sre (see the installation guide).'
kathara_hint =
'install it again with `make venv`'
web_dir =
'/Users/em/Orsay/Info/sre/web'
web_config_file =
'/etc/sreweb/web.toml'
agent_config_file =
'/etc/sreweb/agent.toml'
site_params_file =
None
debug_mode =
False
email_in_gecos_last_field: bool =
True
max_docker_concurrency =
16
display_marks_in_auto_evaluations_by_default =
False
use_numerical_marks_by_default =
True
default_maximum_mark =
20
SELF_EVAL_SCOPE =
1
EXO_EVAL_SCOPE =
2
BOTH_EVAL_SCOPE =
3
grade_scopes =
(1, 2, 3)
default_eval_interval_during_exams =
60
default_eval_interval_without_exam_mode =
60
default_save_record_interval_during_exams =
60
default_exam_duration =
90
default_inactivity_threshold_in_watch_command =
90
default_dashboard_refresh_interval_in_watch_command =
1
max_duration_between_exam_pre_start_and_start =
60
default_exec_shell =
'/bin/bash'
update_github_repository =
'sysreseval/sysreseval'
update_github_api_url =
'https://api.github.com'
update_tarball_url =
'https://codeload.github.com/{repository}/tar.gz/{commit}'
update_default_branch =
'main'
update_http_timeout =
60
update_lab_subdirs =
['sre']
allow_privileged_machines =
True
disable_volume_mount_on_root_partition =
False
use_sudo_user_for_username =
True
check_project_owner =
True
execute_commands_on_host: Literal['shell', 'split', False] =
'shell'
terminal_cmd_prefix =
['/usr/bin/mate-terminal', '--']
terminal_title_opt =
'--title'
sre_version =
'0.16.1'
default_docker_image_version =
'1.33'
sre_docker_namespace =
'sysreseval'
def
sre_docker_image(image_name: str = 'base') -> str:
default_docker_image =
'sysreseval/base:1.33'
update_state_file_name =
'.sre-update.json'
update_lock_file_name =
'.sre-update.lock'
update_tmp_prefix =
'.sre-update-'
update_params_follow_new =
('sre_version', 'default_docker_image_version')
update_params_backup_suffix =
'.bak.'
update_system_templates_dir =
'scripts/etc'
update_state_commit =
'commit'
update_state_ref =
'ref'
update_state_updated_at =
'updated_at'
update_state_previous_commit =
'previous_commit'
update_state_files =
'files'
update_state_params_upstream =
'params_upstream'
sre_projects_dir =
'/var/lib/sre/projects'
sre_name_env_variable =
'SRE_LAB_NAME'
sre_host_ip_env_variable =
'SRE_HOST_IP'
sre_x11_display_env_variable =
'SRE_X11_DISPLAY'
sre_display_env_variable =
'SRE_DISPLAY'
sre_host_ip =
'172.17.0.1'
exetests_path =
'/Users/em/Orsay/Info/sre/lib/exetests.py'
exetests_machines_path =
'/usr/local/sbin/exetests.py'
exetests_env_name =
'EXETESTS'
exetests_separator =
'@@@'
default_timeout =
20
default_state_cmd_timeout =
120
exit_code_flavor_form_needed =
3
exit_code_flavor_not_allowed =
4
default_show_nat_network =
True
default_host_network_color =
'deepskyblue'
default_host_network_name =
'Internet'
default_host_network_shape =
'hexagon'
default_host_network_exploded =
False
default_host_network_edge_relative_length =
1.0
default_ipv6 =
None
network_mode_hub =
'hub'
network_mode_switch =
'switch'
network_mode_managed =
'managed'
network_modes =
('hub', 'switch', 'managed')
default_network_mode =
'hub'
min_vlan_id =
1
max_vlan_id =
4094
switch_port_reference_prefix =
'@'
switch_cmd_error_code =
-2
switch_user_commands =
('help', 'showinfo', 'port/showinfo', 'port/print', 'port/allprint', 'port/setvlan', 'vlan/create', 'vlan/remove', 'vlan/addport', 'vlan/delport', 'vlan/print', 'vlan/allprint', 'hash/showinfo', 'hash/print', 'hash/find', 'fstp/showinfo', 'fstp/print', 'fstp/setfstp', 'fstp/setedge', 'fstp/bonus')
switch_console_exit_commands =
('exit', 'quit', 'logout')
switch_console_watch_interval =
1.0
default_machine_shape =
'box'
default_network_shape =
'ellipse'
pdf_schema_file =
'schema.pdf'
pdf_info_file =
'informations.pdf'
terminal_font_size =
12
terminal_color_scheme =
'black_on_white'
content_font_size =
12
content_style_sheet =
'th, td { padding: 2px 8px; }'
default_form_field_lines =
4
system_font_size =
10
instructor_text_color =
'#b00020'
instructor_background_color =
'#ffe3e3'
instructor_begin_mark =
'\ue000'
instructor_args_end_mark =
'\ue001'
instructor_end_mark =
'\ue002'
thats_all_folks_svg =
'/Users/em/Orsay/Info/sre/graphics/Thats_all_folks.svg'
sysreseval_logo_svg =
'/Users/em/Orsay/Info/sre/graphics/sysreseval.svg'
machine_icon_svg_file =
'/Users/em/Orsay/Info/sre/graphics/machine.svg'
machine_forbidden_icon_svg_file =
'/Users/em/Orsay/Info/sre/graphics/machine-forbidden.svg'
switch_icon_svg_file =
'/Users/em/Orsay/Info/sre/graphics/switch.svg'
cloud_icon_svg_file =
'/Users/em/Orsay/Info/sre/graphics/cloud.svg'
network_shape_icons =
{'cloud': '/Users/em/Orsay/Info/sre/graphics/cloud.svg'}
pdf_font_files =
{'': '/Users/em/Orsay/Info/sre/graphics/fonts/DejaVuSans.ttf', 'B': '/Users/em/Orsay/Info/sre/graphics/fonts/DejaVuSans-Bold.ttf', 'I': '/Users/em/Orsay/Info/sre/graphics/fonts/DejaVuSans-Oblique.ttf', 'BI': '/Users/em/Orsay/Info/sre/graphics/fonts/DejaVuSans-BoldOblique.ttf'}
pdf_mono_font_files =
{'': '/Users/em/Orsay/Info/sre/graphics/fonts/DejaVuSansMono.ttf', 'B': '/Users/em/Orsay/Info/sre/graphics/fonts/DejaVuSansMono-Bold.ttf'}
exam_only_affix =
['_EXAM_', '_OLD_', '_DRAFT_', '_TESTS_']
exam_json_name =
'exam.json'
exam_json_keyword =
'exam_json'
exam_start_after =
'start_after'
exam_end_before =
'end_before'
exam_eval_interval =
'eval_interval'
exam_pre_start_date =
'pre_start_date'
exam_started_at =
'started_at'
exam_ended_at =
'ended_at'
exam_duration =
'duration'
exam_labs =
'labs'
exam_record_sessions =
'record_sessions'
def
parse_lab_entry(entry) -> tuple[str, str | None]:
447def parse_lab_entry(entry) -> tuple[str, str | None]: 448 """Parse a labs entry: new [lab, flavor] list or legacy plain string. 449 Returns (lab_cli_arg, flavor_name_or_None).""" 450 if isinstance(entry, list): 451 return entry[0], entry[1] 452 return entry, None # backward compat with old exam.json
Parse a labs entry: new [lab, flavor] list or legacy plain string. Returns (lab_cli_arg, flavor_name_or_None).
srelab_py_name =
'srelab.py'
shipped_lab_link_name =
'sre'
titles_file_name =
'titles.json'
titles_reserved_prefix =
'@'
titles_sort_key =
'@sort'
titles_sort_name =
'name'
titles_sort_title =
'title'
titles_sort_manual =
'manual'
titles_sort_modes =
('name', 'title', 'manual')
lab_list_name_key =
'name'
lab_list_title_key =
'title'
lab_list_kind_key =
'kind'
lab_list_sort_key =
'sort'
lab_list_kind_dir =
'dir'
lab_list_kind_lab =
'lab'
data_json_name =
'data.json'
data_json_ips6_key =
'ips6'
data_json_nets6_key =
'nets6'
data_json_iface_marker =
'__iface__'
data_json_mac_marker =
'__mac__'
private_dir_name =
'.private'
files_dir_name =
'files'
private_mount_dir_name =
'mnt'
user_public_dir_name =
'user_public_dir'
records_dir_name =
'records'
recorder_idle_time_limit =
10
recorder_config_dir =
'/var/lib/sre/asciinema'
use_asciinema_for_records =
True
eval_in_progress_name =
'eval_in_progress'
auto_eval_log_name =
'auto_eval.log'
debug_project_marker_name =
'debug_project'
instructor_mode_marker_name =
'instructor_mode'
operations_log_name =
'operations.log'
operations_log_max_output_chars =
16384
operations_log_view_max_lines =
100000
auto_eval_count_keyword =
'auto_eval_count'
info_json_name =
'info.json'
answer_dir_name =
'answers'
answer_file_name =
'answers.json'
cheat_file_name =
'cheat.json'
cheat_file_max_size =
1000000
archive_max_size =
67108864
hostname_keyword =
'hostname'
login_keyword =
'login'
fullname_keyword =
'fullname'
email_keyword =
'email'
language_keyword =
'language'
group_keyword =
'group'
available_language_in_interface =
['en', 'fr']
language_display_names =
{'en': 'English', 'fr': 'Français'}
running_lab_name_keyword =
'running_lab_name'
eval_date_keyword =
'eval_date'
re_eval_date_keyword =
're_eval_date'
sysreseval_answers_updated_at =
'answers_updated_at'
sysreseval_exam_time_remaining =
'exam_time_remaining'
sysreseval_exam_started_at =
'exam_started_at'
sysreseval_exam_duration =
'exam_duration'
sysreseval_exam_mode =
'exam_mode'
initial_state_name =
'initial'
save_state_name =
'save'
restore_state_name =
'restore'
lifecycle_state_names =
('initial', 'save', 'restore')
save_file_suffix =
'.sre'
save_file_magic =
b'SRESAVE1\n'
save_format_version =
1
save_stdio_arg =
'-'
save_tmp_dir =
'/var/lib/sre/tmp'
save_kathara_name =
'kathara.tar'
save_user_public_member =
'user_public'
save_meta_format_version =
'format_version'
save_meta_sre_version =
'sre_version'
save_meta_running_lab_name =
'running_lab_name'
save_meta_lab_name =
'lab_name'
save_meta_srelab_file =
'srelab_file'
save_meta_username =
'username'
save_meta_saved_at =
'saved_at'
save_meta_debug_project =
'debug_project'
save_meta_full_images =
'full_images'
save_meta_encrypted =
'encrypted'
save_meta_kdf_salt =
'kdf_salt'
save_meta_kdf_iterations =
'kdf_iterations'
save_kdf_iterations =
600000
save_cipher_chunk_size =
16777216
self_grade_timestamp_dir =
'/var/lib/sre/last_self_grades'
web_updates_dir_name =
'web-updates'
web_updates_dir =
'/var/lib/sre/web-updates'
web_updates_suffix =
'.json'
web_updates_max_age =
86400
web_updates_max_size =
8388608
archive_keyword =
'archive'
srelab_link_name =
'srelab'
graphviz_default_nodesep =
0.8
graphviz_default_ranksep =
1.5
graphviz_default_splines =
'curved'
graphviz_default_overlap =
'prism'
class
SRE:
def
datetime_to_string(dt) -> str:
def
string_to_datetime(string) -> datetime.datetime:
def
get_lab_name_from_cli_arg(lab_cli_arg: str, is_path: bool) -> str:
def
get_running_lab_name( lab_name: str, instance_start_date: datetime.datetime, username: str = None) -> str:
601def get_running_lab_name(lab_name: str, instance_start_date: datetime, username: str = None) -> str: 602 if username is None: 603 if SRE.username is None: 604 username = "" 605 else: 606 username = SRE.username 607 if exetests_separator in lab_name: 608 raise ValueError(f"lab_name must not contain '{exetests_separator}': {lab_name!r}") 609 if exetests_separator in username: 610 raise ValueError(f"username must not contain '{exetests_separator}': {username!r}") 611 return f"{datetime_to_string(instance_start_date)}@@@{lab_name}@@@{username}"
running_lab_name_match_pattern =
'^([0-9]+)@@@(.+)@@@(.+)$'
def
get_lab_name_from_running_lab_name(running_lab_name: str) -> str:
def
get_username_from_running_lab_name(running_lab_name: str) -> str:
def
get_start_date_string_from_running_lab_name(running_lab_name: str) -> str:
632def get_start_date_string_from_running_lab_name(running_lab_name: str) -> str: 633 """Instance start date (YYYYmmddHHMMSS digits) encoded in a running_lab_name, 634 or '' when the name does not follow get_running_lab_name(). The digits 635 sort chronologically, so callers compare/sort the string and only convert 636 it with string_to_datetime() for display.""" 637 match = re.match(running_lab_name_match_pattern, running_lab_name) 638 return match.group(1) if match else ''
Instance start date (YYYYmmddHHMMSS digits) encoded in a running_lab_name, or '' when the name does not follow get_running_lab_name(). The digits sort chronologically, so callers compare/sort the string and only convert it with string_to_datetime() for display.
def
get_abbreviated_lab_name_from_running_lab_name(running_lab_name: str) -> str:
def
get_current_srelab_file_from_running_lab_name(running_lab_name: str) -> str:
646def get_current_srelab_file_from_running_lab_name(running_lab_name: str) -> str: 647 lab_name = get_lab_name_from_running_lab_name(running_lab_name) 648 pathstr = lab_name.replace("@", "/") 649 if not pathstr.startswith("/"): 650 abspath = (Path(lab_dir) / pathstr).resolve() 651 else: 652 abspath = Path(pathstr).resolve() 653 if abspath.name.endswith(".py"): 654 return str(abspath) 655 else: 656 return str(abspath / srelab_py_name)
def
get_srelab_dir(running_lab_name: str):
659def get_srelab_dir(running_lab_name: str): 660 lab_name = get_lab_name_from_running_lab_name(running_lab_name) 661 pathstr = lab_name.replace("@", "/") 662 if not pathstr.startswith("/"): 663 abspath = (Path(lab_dir) / pathstr).resolve() 664 else: 665 abspath = Path(pathstr).resolve() 666 if abspath.name.endswith(".py"): 667 return None 668 else: 669 return str(abspath)
def
project_has_directory(running_lab_name=None) -> bool:
def
get_archive_name(running_lab_name: str, date: datetime.datetime) -> str:
archive_name_match_pattern =
'(\\d{14})_(.+)\\.zst'
def
get_date_from_archive_name(filename: str) -> datetime.datetime | None:
689def get_date_from_archive_name(filename: str) -> datetime | None: 690 """Creation (eval) date encoded in an archive filename, or None when the 691 name does not follow get_archive_name() or the digits are not a date.""" 692 match = re.fullmatch(archive_name_match_pattern, Path(filename).name) 693 if not match: 694 return None 695 try: 696 return string_to_datetime(match.group(1)) 697 except ValueError: 698 return None
Creation (eval) date encoded in an archive filename, or None when the name does not follow get_archive_name() or the digits are not a date.
def
private_lab_dir(running_lab_name: str) -> str:
def
auto_eval_log_filename(running_lab_name: str) -> str:
def
debug_project_marker_filename(running_lab_name: str) -> str:
def
instructor_mode_marker_filename(running_lab_name: str) -> str:
def
operations_log_filename(running_lab_name: str) -> str:
def
link_to_user_public_dir(running_lab_name: str) -> str:
def
public_lab_dir(running_lab_name: str) -> str:
def
info_filename(running_lab_name: str) -> str:
def
data_filename(running_lab_name: str) -> str:
def
private_mount_dir(running_lab_name: str) -> str:
def
files_dir(running_lab_name: str) -> str:
def
records_dir(running_lab_name: str) -> str:
def
srelab_link_filename(running_lab_name: str) -> str:
def
answers_filename(running_lab_name: str) -> str:
def
cheat_filename(running_lab_name: str) -> str:
def
answers_dir(running_lab_name: str) -> str:
def
self_grade_timestamp_file(lab_name: str, username: str | None = None) -> str:
765def self_grade_timestamp_file(lab_name: str, username: str | None = None) -> str: 766 """The cooldown file of the self-evaluations of a lab: one per lab and owner (``sre eval`` 767 passes the owner of the project), so that the students of a shared host do not block each 768 other; the old per-lab file is used when *username* is not given.""" 769 if username: 770 return f"{self_grade_timestamp_dir}/{lab_name}@@@{username}" 771 return f"{self_grade_timestamp_dir}/{lab_name}"
The cooldown file of the self-evaluations of a lab: one per lab and owner (sre eval
passes the owner of the project), so that the students of a shared host do not block each
other; the old per-lab file is used when username is not given.