From 3f0997a32170fd041b94aa7de6815a62dfd42988 Mon Sep 17 00:00:00 2001 From: Ayesh Rodrigo Date: Sun, 4 Oct 2026 10:52:34 +1300 Subject: [PATCH 1/9] Lab tool: server-side setup and cleanup actions, guided exercise labs for chapters 3, 5 and 20 --- .../03-exercise-navigate.md | 18 + .../05-exercise-manage-files.md | 18 + .../07-exercise-links.md | 22 +- .../02-exercise-redirection.md | 22 +- .../ch05-text-and-editors/06-exercise-vim.md | 18 + .../08-exercise-environment.md | 18 + .../ch20-firewalld/02-exercise-web.md | 34 +- .../ch20-firewalld/04-exercise-stores.md | 27 +- .../ch20-firewalld/06-exercise-special.md | 33 +- .../ch20-firewalld/08-exercise-diagnosis.md | 31 +- .../rhel9-sysadmin/lab/sa-environment.yml | 61 ++ .../rhel9-sysadmin/lab/sa-fw-diagnosis.yml | 134 +++++ .../rhel9-sysadmin/lab/sa-fw-special.yml | 163 +++++ .../rhel9-sysadmin/lab/sa-fw-stores.yml | 95 +++ .../programs/rhel9-sysadmin/lab/sa-fw-web.yml | 122 ++++ .../programs/rhel9-sysadmin/lab/sa-links.yml | 88 +++ .../rhel9-sysadmin/lab/sa-manage-files.yml | 124 ++++ .../rhel9-sysadmin/lab/sa-navigate.yml | 52 ++ .../rhel9-sysadmin/lab/sa-redirect.yml | 92 +++ .../programs/rhel9-sysadmin/lab/sa-vim.yml | 44 ++ docs/CATALOG.md | 2 + packages/compiler/src/lab.js | 11 +- .../src/components/content/registry.jsx | 2 +- .../engine/src/components/interactive/Env.jsx | 18 +- packages/lab-tools/grade.py | 4 +- packages/lab-tools/lab | 24 +- packages/lab-tools/prepare.py | 265 ++++++++- packages/schema/catalog/components.json | 10 + packages/schema/generated/validators.js | 2 +- .../schema/schemas/source/lab.schema.json | 560 +++++++++++++++++- scripts/validate-content.mjs | 5 +- 31 files changed, 2000 insertions(+), 119 deletions(-) create mode 100644 content/programs/rhel9-sysadmin/lab/sa-environment.yml create mode 100644 content/programs/rhel9-sysadmin/lab/sa-fw-diagnosis.yml create mode 100644 content/programs/rhel9-sysadmin/lab/sa-fw-special.yml create mode 100644 content/programs/rhel9-sysadmin/lab/sa-fw-stores.yml create mode 100644 content/programs/rhel9-sysadmin/lab/sa-fw-web.yml create mode 100644 content/programs/rhel9-sysadmin/lab/sa-links.yml create mode 100644 content/programs/rhel9-sysadmin/lab/sa-manage-files.yml create mode 100644 content/programs/rhel9-sysadmin/lab/sa-navigate.yml create mode 100644 content/programs/rhel9-sysadmin/lab/sa-redirect.yml create mode 100644 content/programs/rhel9-sysadmin/lab/sa-vim.yml diff --git a/content/programs/rhel9-sysadmin/chapters/ch03-files-and-directories/03-exercise-navigate.md b/content/programs/rhel9-sysadmin/chapters/ch03-files-and-directories/03-exercise-navigate.md index 9d0ff15..bc98cee 100644 --- a/content/programs/rhel9-sysadmin/chapters/ch03-files-and-directories/03-exercise-navigate.md +++ b/content/programs/rhel9-sysadmin/chapters/ch03-files-and-directories/03-exercise-navigate.md @@ -12,9 +12,23 @@ Explore servera's directory tree with `pwd`, `cd` and `ls`, using absolute and r objectives=["ch03.hierarchy","ch03.paths"] id="navigate" title="Find your way around" + exercise="sa-navigate" + ownExercise=true + guided=true + starter=false hosts=["workstation","servera"] outcomes=["Move around with absolute and relative paths, ., .. and ~.","Read long listings and find hidden files.","Recognise a directory you may not enter."] %} + {% task id="task-9812fc5d33ae" title="Start the exercise" %} + On workstation, start the exercise. It removes any practice files from an earlier run from student's home on servera. + +```console +[student@workstation ~]$ lab start sa-navigate +``` + + Later tasks assume that you are logged in to the server they name: `ssh student@servera` from workstation, then `sudo -i` for a root shell. + {% /task %} + {% task id="task-13825aae8cdb" title="Log in to servera and see where you start" %} ```console @@ -106,4 +120,8 @@ total 0 The quotes keep `team notes.txt` as one name, and `ls` shows it quoted for the same reason, so you could copy it into a command as it is. The next lesson shows how to remove files. To put servera back as it was, run `rht-vmctl reset servera` on the Ubuntu host. {% /task %} + + {% task id="task-60a9e9b52693" title="Grade and finish" %} + {% lab-finish exercise="sa-navigate" grade=true servers=true /%} + {% /task %} {% /lab %} diff --git a/content/programs/rhel9-sysadmin/chapters/ch03-files-and-directories/05-exercise-manage-files.md b/content/programs/rhel9-sysadmin/chapters/ch03-files-and-directories/05-exercise-manage-files.md index 07e6186..b790c42 100644 --- a/content/programs/rhel9-sysadmin/chapters/ch03-files-and-directories/05-exercise-manage-files.md +++ b/content/programs/rhel9-sysadmin/chapters/ch03-files-and-directories/05-exercise-manage-files.md @@ -12,9 +12,23 @@ A colleague left a website's files in one heap. On servera, sort them into direc objectives=["ch03.manage","ch03.paths"] id="manage-files" title="Organise a website's files" + exercise="sa-manage-files" + ownExercise=true + guided=true + starter=false hosts=["workstation","servera"] outcomes=["Create directories and empty files.","Move, rename and copy files and whole directories.","Remove files and directories, and see why rmdir is the safe choice."] %} + {% task id="task-d8e2a9960119" title="Start the exercise" %} + On workstation, start the exercise. It removes any site and archive directories from an earlier run from student's home on servera. + +```console +[student@workstation ~]$ lab start sa-manage-files +``` + + Later tasks assume that you are logged in to the server they name: `ssh student@servera` from workstation, then `sudo -i` for a root shell. + {% /task %} + {% task id="task-5bf66e090adf" title="Create the site directory and its three subdirectories" %} ```console @@ -142,4 +156,8 @@ rm: remove directory 'archive'? y Leave `~/site` for now, or run `rht-vmctl reset servera` on the host to put servera back. {% /task %} + + {% task id="task-71399a5f96b8" title="Grade and finish" %} + {% lab-finish exercise="sa-manage-files" grade=true servers=true /%} + {% /task %} {% /lab %} diff --git a/content/programs/rhel9-sysadmin/chapters/ch03-files-and-directories/07-exercise-links.md b/content/programs/rhel9-sysadmin/chapters/ch03-files-and-directories/07-exercise-links.md index 4445c87..3f71945 100644 --- a/content/programs/rhel9-sysadmin/chapters/ch03-files-and-directories/07-exercise-links.md +++ b/content/programs/rhel9-sysadmin/chapters/ch03-files-and-directories/07-exercise-links.md @@ -12,9 +12,23 @@ Many applications keep each release in its own directory and point a link called objectives=["ch03.links"] id="links" title="Switch releases with a link" + exercise="sa-links" + ownExercise=true + guided=true + starter=false hosts=["workstation","servera"] outcomes=["Create and repoint a symbolic link to a directory.","Keep a file's data alive with a hard link.","Recognise a dangling link."] %} + {% task id="task-e87e124c7f4e" title="Start the exercise" %} + On workstation, start the exercise. It removes any release directories and links from an earlier run from student's home on servera. + +```console +[student@workstation ~]$ lab start sa-links +``` + + Later tasks assume that you are logged in to the server they name: `ssh student@servera` from workstation, then `sudo -i` for a root shell. + {% /task %} + {% task id="task-eaf78d3fd850" title="Create two releases" %} On servera, as student, make two release directories, each with its own `app.conf`. `echo "text" > file` writes a line into a file (chapter 5 explains it): @@ -111,11 +125,7 @@ version 2 `current` now dangles: it points to a name that is gone. The hard link kept the data, and its link count is back to 1. {% /task %} - {% task id="task-4a9e86fa7e3c" title="Clean up" %} - -```console -[student@servera ~]$ rm -r release current app.conf.saved -[student@servera ~]$ exit -``` + {% task id="task-d7942db186ed" title="Grade and finish" %} + {% lab-finish exercise="sa-links" grade=true servers=true /%} {% /task %} {% /lab %} diff --git a/content/programs/rhel9-sysadmin/chapters/ch05-text-and-editors/02-exercise-redirection.md b/content/programs/rhel9-sysadmin/chapters/ch05-text-and-editors/02-exercise-redirection.md index e04bd7e..c56c9ff 100644 --- a/content/programs/rhel9-sysadmin/chapters/ch05-text-and-editors/02-exercise-redirection.md +++ b/content/programs/rhel9-sysadmin/chapters/ch05-text-and-editors/02-exercise-redirection.md @@ -12,9 +12,23 @@ On servera, save command output to files, keep errors apart from results, discar objectives=["ch05.redirect"] id="redirection" title="Capture and combine output" + exercise="sa-redirect" + ownExercise=true + guided=true + starter=false hosts=["workstation","servera"] outcomes=["Write and append output to files.","Separate errors from results, and discard them.","Build pipelines, and keep a copy with tee."] %} + {% task id="task-199503c05e6f" title="Start the exercise" %} + On workstation, start the exercise. It removes the result files of an earlier run from student's home on servera. + +```console +[student@workstation ~]$ lab start sa-redirect +``` + + Later tasks assume that you are logged in to the server they name: `ssh student@servera` from workstation, then `sudo -i` for a root shell. + {% /task %} + {% task id="task-79966807d096" title="Save a list of configuration files" %} On servera, as student, list the `.conf` files directly in `/etc` into `conf-files.txt`, then count and peek at it. @@ -111,11 +125,7 @@ devtmpfs 455M 0 455M 0% /dev {% /reveal %} {% /task %} - {% task id="task-fec86414a779" title="Clean up" %} - -```console -[student@servera ~]$ rm conf-files.txt found.txt errors.txt disk-report.txt -[student@servera ~]$ exit -``` + {% task id="task-14c9a44075b1" title="Grade and finish" %} + {% lab-finish exercise="sa-redirect" grade=true servers=true /%} {% /task %} {% /lab %} diff --git a/content/programs/rhel9-sysadmin/chapters/ch05-text-and-editors/06-exercise-vim.md b/content/programs/rhel9-sysadmin/chapters/ch05-text-and-editors/06-exercise-vim.md index 7b2a01a..034d028 100644 --- a/content/programs/rhel9-sysadmin/chapters/ch05-text-and-editors/06-exercise-vim.md +++ b/content/programs/rhel9-sysadmin/chapters/ch05-text-and-editors/06-exercise-vim.md @@ -12,9 +12,23 @@ Work through the first lesson of vim's own tutorial, then use what it taught to objectives=["ch05.vim"] id="vim" title="Create and edit a file with vim" + exercise="sa-vim" + ownExercise=true + guided=true + starter=false hosts=["workstation","servera"] outcomes=["Move between normal, insert and command-line mode.","Copy, paste, delete and undo lines, and replace text.","Save, or quit without saving."] %} + {% task id="task-e3f7ce51483c" title="Start the exercise" %} + On workstation, start the exercise. It removes welcome.txt of an earlier run from student's home on servera. + +```console +[student@workstation ~]$ lab start sa-vim +``` + + Later tasks assume that you are logged in to the server they name: `ssh student@servera` from workstation, then `sudo -i` for a root shell. + {% /task %} + {% task id="task-727284164744" title="Do lesson 1 of vimtutor" %} On servera, start the tutorial: @@ -94,4 +108,8 @@ Contact: admin@lab.example.com ``` {% /reveal %} {% /task %} + + {% task id="task-062ea72bb36e" title="Grade and finish" %} + {% lab-finish exercise="sa-vim" grade=true servers=true /%} + {% /task %} {% /lab %} diff --git a/content/programs/rhel9-sysadmin/chapters/ch05-text-and-editors/08-exercise-environment.md b/content/programs/rhel9-sysadmin/chapters/ch05-text-and-editors/08-exercise-environment.md index c0c136a..0d3026c 100644 --- a/content/programs/rhel9-sysadmin/chapters/ch05-text-and-editors/08-exercise-environment.md +++ b/content/programs/rhel9-sysadmin/chapters/ch05-text-and-editors/08-exercise-environment.md @@ -12,9 +12,23 @@ On servera, use variables and PATH to run your own command, create an alias, and objectives=["ch05.environment","ch05.vim"] id="environment" title="Customise your shell" + exercise="sa-environment" + ownExercise=true + guided=true + starter=false hosts=["workstation","servera"] outcomes=["Use and export variables.","Run your own script through PATH.","Make an alias and environment settings permanent."] %} + {% task id="task-76e145a40cdd" title="Start the exercise" %} + On workstation, start the exercise. It restores student's start-up files on servera to the originals and removes ~/bin. + +```console +[student@workstation ~]$ lab start sa-environment +``` + + Later tasks assume that you are logged in to the server they name: `ssh student@servera` from workstation, then `sudo -i` for a root shell. + {% /task %} + {% task id="task-8ef6dff987b9" title="See exporting at work" %} On servera, set a variable, check that a child shell doesn't see it, export it, and check again. @@ -110,4 +124,8 @@ vim [student@servera ~]$ exit ``` {% /task %} + + {% task id="task-33d39b3f4165" title="Grade and finish" %} + {% lab-finish exercise="sa-environment" grade=true servers=true /%} + {% /task %} {% /lab %} diff --git a/content/programs/rhel9-sysadmin/chapters/ch20-firewalld/02-exercise-web.md b/content/programs/rhel9-sysadmin/chapters/ch20-firewalld/02-exercise-web.md index 6bb50e6..104a2c0 100644 --- a/content/programs/rhel9-sysadmin/chapters/ch20-firewalld/02-exercise-web.md +++ b/content/programs/rhel9-sysadmin/chapters/ch20-firewalld/02-exercise-web.md @@ -12,25 +12,21 @@ Start a web server on servera and find out why serverb cannot reach it. Then ope objectives=["ch20.zones"] id="web" title="Publish a web server through the firewall" + exercise="sa-fw-web" + ownExercise=true + guided=true + starter=false hosts=["workstation","servera","serverb"] outcomes=["Read the active zone and its services.","Allow a service permanently and verify it from another host.","Tell a closed firewall from a missing service."] %} - {% task id="task-d13fed178054" title="Start a web server" %} - On servera as root, install `httpd`, put the line `hello from servera` in `/var/www/html/index.html`, and start and enable the service. Test it locally with `curl`. - - {% reveal title="Show solution" %} + {% task id="task-5c70a2201d6d" title="Start the exercise" %} + On workstation, start the exercise. It installs httpd on servera with the page `hello from servera` and starts it. ```console -[student@workstation ~]$ ssh student@servera -[student@servera ~]$ sudo -i -[root@servera ~]# dnf install -y httpd > /dev/null -[root@servera ~]# echo "hello from servera" > /var/www/html/index.html -[root@servera ~]# systemctl enable --now httpd -Created symlink /etc/systemd/system/multi-user.target.wants/httpd.service → /usr/lib/systemd/system/httpd.service. -[root@servera ~]# curl -s http://localhost/ -hello from servera +[student@workstation ~]$ lab start sa-fw-web ``` - {% /reveal %} + + Later tasks assume that you are logged in to the server they name: `ssh student@servera` from workstation, then `sudo -i` for a root shell. {% /task %} {% task id="task-78d0e2258f6f" title="Try it from serverb" %} @@ -110,15 +106,7 @@ hello from servera {% /reveal %} {% /task %} - {% task id="task-dd89882e76b1" title="Clean up" %} - -```console -[root@servera ~]# firewall-cmd --permanent --remove-service=http; firewall-cmd --permanent --remove-port=8080/tcp; firewall-cmd --reload -success -success -success -[root@servera ~]# rm /etc/httpd/conf.d/extra.conf; systemctl restart httpd -[root@servera ~]# exit -``` + {% task id="task-f31577139d55" title="Grade and finish" %} + {% lab-finish exercise="sa-fw-web" grade=true servers=true /%} {% /task %} {% /lab %} diff --git a/content/programs/rhel9-sysadmin/chapters/ch20-firewalld/04-exercise-stores.md b/content/programs/rhel9-sysadmin/chapters/ch20-firewalld/04-exercise-stores.md index 602d0a3..30582ec 100644 --- a/content/programs/rhel9-sysadmin/chapters/ch20-firewalld/04-exercise-stores.md +++ b/content/programs/rhel9-sysadmin/chapters/ch20-firewalld/04-exercise-stores.md @@ -12,21 +12,21 @@ Watch the two stores part company: a runtime rule that a reload erases, a perman objectives=["ch20.stores"] id="stores" title="Runtime, permanent and temporary rules" + exercise="sa-fw-stores" + ownExercise=true + guided=true + starter=false hosts=["workstation","servera","serverb"] outcomes=["Show that a runtime rule is lost on reload.","Use --timeout for a temporary rule.","Explain what --runtime-to-permanent saves."] %} - {% task id="task-2acb0d9cc583" title="Set up" %} - On servera as root, make sure `httpd` is running with the page from the previous exercise (install it and create `/var/www/html/index.html` if you reset the servers). Check that the firewall allows no `http` at the moment. - - {% reveal title="Show solution" %} + {% task id="task-ed0f6cf56a1f" title="Start the exercise" %} + On workstation, start the exercise. It installs httpd on servera with the page `hello from servera` and starts it. ```console -[root@servera ~]# dnf install -y httpd > /dev/null; systemctl enable --now httpd 2>&1 | tail -1 -[root@servera ~]# echo "hello from servera" > /var/www/html/index.html -[root@servera ~]# firewall-cmd --query-service=http -no +[student@workstation ~]$ lab start sa-fw-stores ``` - {% /reveal %} + + Later tasks assume that you are logged in to the server they name: `ssh student@servera` from workstation, then `sudo -i` for a root shell. {% /task %} {% task id="task-2a976c4335b2" title="A runtime rule" %} @@ -124,12 +124,7 @@ hello from servera {% /reveal %} {% /task %} - {% task id="task-84dbe770d77f" title="Clean up" %} - -```console -[student@servera ~]$ sudo firewall-cmd --permanent --remove-service=http; sudo firewall-cmd --reload -success -success -``` + {% task id="task-b5a458a4e2ba" title="Grade and finish" %} + {% lab-finish exercise="sa-fw-stores" grade=true servers=true /%} {% /task %} {% /lab %} diff --git a/content/programs/rhel9-sysadmin/chapters/ch20-firewalld/06-exercise-special.md b/content/programs/rhel9-sysadmin/chapters/ch20-firewalld/06-exercise-special.md index 3532a3d..15e0846 100644 --- a/content/programs/rhel9-sysadmin/chapters/ch20-firewalld/06-exercise-special.md +++ b/content/programs/rhel9-sysadmin/chapters/ch20-firewalld/06-exercise-special.md @@ -12,25 +12,21 @@ servera runs a web server on ports 80, 8080 and 9000. You will let only serverb objectives=["ch20.special"] id="special" title="One trusted host, a custom port and a forward" + exercise="sa-fw-special" + ownExercise=true + guided=true + starter=false hosts=["workstation","servera","serverb"] outcomes=["Build a source-based zone and test it from two hosts.","Create a custom service and a forward port.","Read rejected packets in the kernel log."] %} - {% task id="task-246bdca80164" title="Prepare the server" %} - On servera as root: install `httpd`, write `hello from servera` as the home page, add `Listen 8080` and `Listen 9000` in `/etc/httpd/conf.d/extra.conf` (one per line), then start and enable the service. Check with `ss -tlnp` that it listens on the three ports. Port 8080 and 9000 are already web ports for SELinux. - - {% reveal title="Show solution" %} + {% task id="task-1fe0885ae68c" title="Start the exercise" %} + On workstation, start the exercise. It installs httpd on servera, serves the page `hello from servera` on ports 80, 8080 and 9000, and starts it. ```console -[root@servera ~]# dnf install -y httpd > /dev/null -[root@servera ~]# echo "hello from servera" > /var/www/html/index.html -[root@servera ~]# printf 'Listen 8080\nListen 9000\n' > /etc/httpd/conf.d/extra.conf -[root@servera ~]# systemctl enable --now httpd 2>&1 | tail -1 -[root@servera ~]# ss -tln | grep -E ':(80|8080|9000) ' -LISTEN 0 511 *:80 *:* -LISTEN 0 511 *:9000 *:* -LISTEN 0 511 *:8080 *:* +[student@workstation ~]$ lab start sa-fw-special ``` - {% /reveal %} + + Later tasks assume that you are logged in to the server they name: `ssh student@servera` from workstation, then `sudo -i` for a root shell. {% /task %} {% task id="task-21c0703eb160" title="A zone for serverb" %} @@ -162,14 +158,7 @@ port 9000 -> 000 {% /reveal %} {% /task %} - {% task id="task-3fdf015029c8" title="Clean up" %} - -```console -[root@servera ~]# firewall-cmd --permanent --delete-zone=lanonly; firewall-cmd --permanent --delete-service=myapp; firewall-cmd --reload -success -success -success -[root@servera ~]# rm /etc/httpd/conf.d/extra.conf; systemctl restart httpd -``` + {% task id="task-8e819d93d3fc" title="Grade and finish" %} + {% lab-finish exercise="sa-fw-special" grade=true servers=true /%} {% /task %} {% /lab %} diff --git a/content/programs/rhel9-sysadmin/chapters/ch20-firewalld/08-exercise-diagnosis.md b/content/programs/rhel9-sysadmin/chapters/ch20-firewalld/08-exercise-diagnosis.md index 5013529..4b68c83 100644 --- a/content/programs/rhel9-sysadmin/chapters/ch20-firewalld/08-exercise-diagnosis.md +++ b/content/programs/rhel9-sysadmin/chapters/ch20-firewalld/08-exercise-diagnosis.md @@ -12,25 +12,21 @@ A web server on servera answers on port 80. You will break it four different way objectives=["ch20.diagnosis"] id="diagnosis" title="Find four faults" + exercise="sa-fw-diagnosis" + ownExercise=true + guided=true + starter=false hosts=["workstation","servera","serverb"] outcomes=["Match each client symptom to a layer.","Use ss, firewall-cmd and the journal to confirm the cause.","Fix each fault at the right layer."] %} - {% task id="task-687b089c6a69" title="A working baseline" %} - On servera: httpd installed and running with the page `hello from servera`, and `http` allowed permanently. Confirm from serverb. - - {% reveal title="Show solution" %} + {% task id="task-13eced3e5d26" title="Start the exercise" %} + On workstation, start the exercise. It installs httpd on servera with the page `hello from servera`, starts it and allows http in the firewall. ```console -[root@servera ~]# dnf install -y httpd > /dev/null -[root@servera ~]# echo "hello from servera" > /var/www/html/index.html -[root@servera ~]# systemctl enable --now httpd 2>&1 | tail -1 -[root@servera ~]# firewall-cmd --permanent --add-service=http; firewall-cmd --reload -success -success -[student@serverb ~]$ curl -sS -m 3 http://servera/ -hello from servera +[student@workstation ~]$ lab start sa-fw-diagnosis ``` - {% /reveal %} + + Later tasks assume that you are logged in to the server they name: `ssh student@servera` from workstation, then `sudo -i` for a root shell. {% /task %} {% task id="task-e270a5c4b302" title="Fault 1: the service is stopped" %} @@ -124,12 +120,7 @@ hello from servera {% /reveal %} {% /task %} - {% task id="task-862e182fbf9a" title="Clean up" %} - -```console -[root@servera ~]# rm /etc/httpd/conf.d/extra.conf; semanage port -d -t http_port_t -p tcp 82 -[root@servera ~]# firewall-cmd --permanent --remove-port=82/tcp; firewall-cmd --permanent --remove-service=http; firewall-cmd --reload -[root@servera ~]# systemctl restart httpd -``` + {% task id="task-da8afc057516" title="Grade and finish" %} + {% lab-finish exercise="sa-fw-diagnosis" grade=true servers=true /%} {% /task %} {% /lab %} diff --git a/content/programs/rhel9-sysadmin/lab/sa-environment.yml b/content/programs/rhel9-sysadmin/lab/sa-environment.yml new file mode 100644 index 0000000..e36997d --- /dev/null +++ b/content/programs/rhel9-sysadmin/lab/sa-environment.yml @@ -0,0 +1,61 @@ +name: sa-environment +title: Variables, PATH and start-up files +page: ch05/exercise-environment +transport: ssh +version: 1 +starter: [] +setup: +- action: directory + hosts: + - servera + path: /home/student/bin + state: absent +- action: restore-skel + hosts: + - servera + user: student +checkpoints: + final: + files: [] + checks: + - id: script + kind: file + 'on': servera.lab.example.com + targets: + - servera.lab.example.com + message: ~/bin/up is an executable script + paths: + - /home/student/bin/up + fileType: regular + contains: + - uptime -p + - id: alias + kind: file + 'on': servera.lab.example.com + targets: + - servera.lab.example.com + message: ~/.bashrc defines the alias now and the history time format + paths: + - /home/student/.bashrc + contains: + - alias now='date +%T' + - HISTTIMEFORMAT + - id: editor + kind: file + 'on': servera.lab.example.com + targets: + - servera.lab.example.com + message: ~/.bash_profile exports EDITOR=vim + paths: + - /home/student/.bash_profile + line: export EDITOR=vim +finish: +- action: directory + hosts: + - servera + path: /home/student/bin + state: absent +- action: restore-skel + hosts: + - servera + user: student diff --git a/content/programs/rhel9-sysadmin/lab/sa-fw-diagnosis.yml b/content/programs/rhel9-sysadmin/lab/sa-fw-diagnosis.yml new file mode 100644 index 0000000..499c78e --- /dev/null +++ b/content/programs/rhel9-sysadmin/lab/sa-fw-diagnosis.yml @@ -0,0 +1,134 @@ +name: sa-fw-diagnosis +title: Find four faults +page: ch20/exercise-diagnosis +transport: ssh +version: 1 +starter: [] +setup: +- action: package + hosts: + - servera + names: + - httpd + state: present +- action: file + hosts: + - servera + path: /var/www/html/index.html + state: present + content: 'hello from servera + + ' +- action: service + hosts: + - servera + names: + - httpd + state: started + enabled: true +- action: firewall + hosts: + - servera + state: present + service: http +checkpoints: + final: + files: [] + checks: + - id: svc + kind: service + 'on': servera.lab.example.com + targets: + - servera.lab.example.com + message: httpd is running + names: + - httpd.service + active: true + - id: port-label + kind: selinux-port + 'on': servera.lab.example.com + targets: + - servera.lab.example.com + message: TCP port 82 is labelled http_port_t + type: http_port_t + proto: tcp + port: 82 + - id: fw-82 + kind: firewall + 'on': servera.lab.example.com + targets: + - servera.lab.example.com + message: 82/tcp is allowed + port: 82/tcp + - id: http + kind: firewall + 'on': servera.lab.example.com + targets: + - servera.lab.example.com + message: http is allowed + service: http + - id: from-80 + kind: http + 'on': serverb.lab.example.com + targets: + - serverb.lab.example.com + message: serverb reaches port 80 + url: http://servera.lab.example.com/ + containsAny: + - hello from servera + - id: from-82 + kind: http + 'on': serverb.lab.example.com + targets: + - serverb.lab.example.com + message: serverb reaches port 82 + url: http://servera.lab.example.com:82/ + containsAny: + - hello from servera +finish: +- action: selinux + hosts: + - servera + kind: port + state: absent + type: http_port_t + proto: tcp + port: 82 +- action: service + hosts: + - servera + names: + - httpd + state: stopped + enabled: false +- action: firewall + hosts: + - servera + state: absent + service: http +- action: firewall + hosts: + - servera + state: absent + port: 8080/tcp +- action: firewall + hosts: + - servera + state: absent + port: 82/tcp +- action: file + hosts: + - servera + path: /etc/httpd/conf.d/extra.conf + state: absent +- action: file + hosts: + - servera + path: /var/www/html/index.html + state: absent +- action: package + hosts: + - servera + names: + - httpd + state: absent diff --git a/content/programs/rhel9-sysadmin/lab/sa-fw-special.yml b/content/programs/rhel9-sysadmin/lab/sa-fw-special.yml new file mode 100644 index 0000000..cba41f2 --- /dev/null +++ b/content/programs/rhel9-sysadmin/lab/sa-fw-special.yml @@ -0,0 +1,163 @@ +name: sa-fw-special +title: One trusted host, a custom port and a forward +page: ch20/exercise-special +transport: ssh +version: 1 +starter: [] +setup: +- action: package + hosts: + - servera + names: + - httpd + state: present +- action: file + hosts: + - servera + path: /var/www/html/index.html + state: present + content: 'hello from servera + + ' +- action: file + hosts: + - servera + path: /etc/httpd/conf.d/extra.conf + state: present + content: 'Listen 8080 + + Listen 9000 + + ' +- action: service + hosts: + - servera + names: + - httpd + state: started + enabled: true +checkpoints: + final: + files: [] + checks: + - id: zone-file + kind: file + 'on': servera.lab.example.com + targets: + - servera.lab.example.com + message: The permanent zone lanonly has a source address + paths: + - /etc/firewalld/zones/lanonly.xml + matches: + - id: zone-http + kind: firewall + 'on': servera.lab.example.com + targets: + - servera.lab.example.com + message: The zone lanonly allows http + zone: lanonly + service: http + - id: zone-myapp + kind: firewall + 'on': servera.lab.example.com + targets: + - servera.lab.example.com + message: The zone lanonly allows the service myapp + zone: lanonly + service: myapp + - id: myapp-file + kind: file + 'on': servera.lab.example.com + targets: + - servera.lab.example.com + message: The service myapp is defined for 9000/tcp + paths: + - /etc/firewalld/services/myapp.xml + matches: port="9000" + - id: forward + kind: firewall + 'on': servera.lab.example.com + targets: + - servera.lab.example.com + message: The zone lanonly forwards 8081 to 8080 + zone: lanonly + forwardPort: port=8081:proto=tcp:toport=8080 + - id: rich + kind: file + 'on': servera.lab.example.com + targets: + - servera.lab.example.com + message: The zone lanonly has a rich rule that rejects serverb on 9000 + paths: + - /etc/firewalld/zones/lanonly.xml + matches: port="9000" + contains: + - reject + - id: log + kind: file + 'on': servera.lab.example.com + targets: + - servera.lab.example.com + message: Logging of rejected packets is off again + paths: + - /etc/firewalld/firewalld.conf + matches: ^LogDenied=off[[:space:]]*$ + - id: public-closed + kind: firewall + 'on': servera.lab.example.com + targets: + - servera.lab.example.com + message: The default zone does not allow http + service: http + allowed: false + - id: from-80 + kind: http + 'on': serverb.lab.example.com + targets: + - serverb.lab.example.com + message: serverb reaches port 80 + url: http://servera.lab.example.com/ + containsAny: + - hello from servera +finish: +- action: firewall + hosts: + - servera + state: absent + zone: lanonly + deleteZone: true +- action: firewall + hosts: + - servera + state: absent + zone: drop + source: 172.25.251.9 +- action: firewall + hosts: + - servera + state: absent + service: myapp + deleteService: true +- action: service + hosts: + - servera + names: + - httpd + state: stopped + enabled: false +- action: file + hosts: + - servera + path: /etc/httpd/conf.d/extra.conf + state: absent +- action: file + hosts: + - servera + path: /var/www/html/index.html + state: absent +- action: package + hosts: + - servera + names: + - httpd + state: absent diff --git a/content/programs/rhel9-sysadmin/lab/sa-fw-stores.yml b/content/programs/rhel9-sysadmin/lab/sa-fw-stores.yml new file mode 100644 index 0000000..0559316 --- /dev/null +++ b/content/programs/rhel9-sysadmin/lab/sa-fw-stores.yml @@ -0,0 +1,95 @@ +name: sa-fw-stores +title: Runtime, permanent and temporary rules +page: ch20/exercise-stores +transport: ssh +version: 1 +starter: [] +setup: +- action: package + hosts: + - servera + names: + - httpd + state: present +- action: file + hosts: + - servera + path: /var/www/html/index.html + state: present + content: 'hello from servera + + ' +- action: service + hosts: + - servera + names: + - httpd + state: started + enabled: true +checkpoints: + final: + files: [] + checks: + - id: http-both + kind: firewall + 'on': servera.lab.example.com + targets: + - servera.lab.example.com + message: http is allowed in the running firewall and in the saved one + service: http + - id: port-closed + kind: firewall + 'on': servera.lab.example.com + targets: + - servera.lab.example.com + message: 8080/tcp is not left open anywhere + port: 8080/tcp + allowed: false + - id: from-80 + kind: http + 'on': serverb.lab.example.com + targets: + - serverb.lab.example.com + message: serverb reaches the page on port 80 + url: http://servera.lab.example.com/ + containsAny: + - hello from servera +finish: +- action: service + hosts: + - servera + names: + - httpd + state: stopped + enabled: false +- action: firewall + hosts: + - servera + state: absent + service: http +- action: firewall + hosts: + - servera + state: absent + port: 8080/tcp +- action: firewall + hosts: + - servera + state: absent + port: 82/tcp +- action: file + hosts: + - servera + path: /etc/httpd/conf.d/extra.conf + state: absent +- action: file + hosts: + - servera + path: /var/www/html/index.html + state: absent +- action: package + hosts: + - servera + names: + - httpd + state: absent diff --git a/content/programs/rhel9-sysadmin/lab/sa-fw-web.yml b/content/programs/rhel9-sysadmin/lab/sa-fw-web.yml new file mode 100644 index 0000000..3655204 --- /dev/null +++ b/content/programs/rhel9-sysadmin/lab/sa-fw-web.yml @@ -0,0 +1,122 @@ +name: sa-fw-web +title: Publish a web server through the firewall +page: ch20/exercise-web +transport: ssh +version: 1 +starter: [] +setup: +- action: package + hosts: + - servera + names: + - httpd + state: present +- action: file + hosts: + - servera + path: /var/www/html/index.html + state: present + content: 'hello from servera + + ' +- action: service + hosts: + - servera + names: + - httpd + state: started + enabled: true +checkpoints: + final: + files: [] + checks: + - id: http-service + kind: firewall + 'on': servera.lab.example.com + targets: + - servera.lab.example.com + message: The service http is allowed, now and permanently + service: http + - id: port-8080 + kind: firewall + 'on': servera.lab.example.com + targets: + - servera.lab.example.com + message: 8080/tcp is allowed, now and permanently + port: 8080/tcp + - id: listen + kind: file + 'on': servera.lab.example.com + targets: + - servera.lab.example.com + message: httpd listens on 8080 + paths: + - /etc/httpd/conf.d/extra.conf + contains: + - Listen 8080 + - id: local-8080 + kind: http + 'on': servera.lab.example.com + targets: + - servera.lab.example.com + message: The page answers on port 8080 + url: http://localhost:8080/ + containsAny: + - hello from servera + - id: from-80 + kind: http + 'on': serverb.lab.example.com + targets: + - serverb.lab.example.com + message: serverb reaches port 80 + url: http://servera.lab.example.com/ + containsAny: + - hello from servera + - id: from-8080 + kind: http + 'on': serverb.lab.example.com + targets: + - serverb.lab.example.com + message: serverb reaches port 8080 + url: http://servera.lab.example.com:8080/ + containsAny: + - hello from servera +finish: +- action: service + hosts: + - servera + names: + - httpd + state: stopped + enabled: false +- action: firewall + hosts: + - servera + state: absent + service: http +- action: firewall + hosts: + - servera + state: absent + port: 8080/tcp +- action: firewall + hosts: + - servera + state: absent + port: 82/tcp +- action: file + hosts: + - servera + path: /etc/httpd/conf.d/extra.conf + state: absent +- action: file + hosts: + - servera + path: /var/www/html/index.html + state: absent +- action: package + hosts: + - servera + names: + - httpd + state: absent diff --git a/content/programs/rhel9-sysadmin/lab/sa-links.yml b/content/programs/rhel9-sysadmin/lab/sa-links.yml new file mode 100644 index 0000000..5abf1ba --- /dev/null +++ b/content/programs/rhel9-sysadmin/lab/sa-links.yml @@ -0,0 +1,88 @@ +name: sa-links +title: Releases with symbolic and hard links +page: ch03/exercise-links +transport: ssh +version: 1 +starter: [] +setup: +- action: directory + hosts: + - servera + path: /home/student/release + state: absent +- action: file + hosts: + - servera + path: /home/student/current + state: absent +- action: file + hosts: + - servera + path: /home/student/app.conf.saved + state: absent +checkpoints: + final: + files: [] + checks: + - id: current + kind: file + 'on': servera.lab.example.com + targets: + - servera.lab.example.com + message: current is a symbolic link to release/v2 + paths: + - /home/student/current + symlinkTo: release/v2 + - id: v1 + kind: file + 'on': servera.lab.example.com + targets: + - servera.lab.example.com + message: release/v1/app.conf still says version 1 + paths: + - /home/student/release/v1/app.conf + line: version 1 + - id: v2-gone + kind: file + 'on': servera.lab.example.com + targets: + - servera.lab.example.com + message: the release/v2 directory was deleted + paths: + - /home/student/release/v2 + exists: false + - id: saved + kind: file + 'on': servera.lab.example.com + targets: + - servera.lab.example.com + message: app.conf.saved kept the data of version 2 + paths: + - /home/student/app.conf.saved + line: version 2 + hardLinks: 1 + - id: no-stray + kind: file + 'on': servera.lab.example.com + targets: + - servera.lab.example.com + message: no stray link v2 is left inside release/v1 + paths: + - /home/student/release/v1/v2 + exists: false +finish: +- action: directory + hosts: + - servera + path: /home/student/release + state: absent +- action: file + hosts: + - servera + path: /home/student/current + state: absent +- action: file + hosts: + - servera + path: /home/student/app.conf.saved + state: absent diff --git a/content/programs/rhel9-sysadmin/lab/sa-manage-files.yml b/content/programs/rhel9-sysadmin/lab/sa-manage-files.yml new file mode 100644 index 0000000..b0d0124 --- /dev/null +++ b/content/programs/rhel9-sysadmin/lab/sa-manage-files.yml @@ -0,0 +1,124 @@ +name: sa-manage-files +title: Build and tidy a small website +page: ch03/exercise-manage-files +transport: ssh +version: 1 +starter: [] +setup: +- action: directory + hosts: + - servera + path: /home/student/site + state: absent +- action: directory + hosts: + - servera + path: /home/student/archive + state: absent +checkpoints: + final: + files: [] + checks: + - id: dirs + kind: file + 'on': servera.lab.example.com + targets: + - servera.lab.example.com + message: site has the directories images, css and pages + paths: + - /home/student/site/images + - /home/student/site/css + - /home/student/site/pages + fileType: directory + - id: images + kind: file + 'on': servera.lab.example.com + targets: + - servera.lab.example.com + message: logo.png and banner.png are in site/images + paths: + - /home/student/site/images/logo.png + fileType: regular + - id: banner + kind: file + 'on': servera.lab.example.com + targets: + - servera.lab.example.com + message: banner.png is in site/images + paths: + - /home/student/site/images/banner.png + fileType: regular + - id: css + kind: file + 'on': servera.lab.example.com + targets: + - servera.lab.example.com + message: style.css is in site/css + paths: + - /home/student/site/css/style.css + fileType: regular + - id: pages + kind: file + 'on': servera.lab.example.com + targets: + - servera.lab.example.com + message: home.html and about.html are in site/pages + paths: + - /home/student/site/pages/home.html + fileType: regular + - id: about + kind: file + 'on': servera.lab.example.com + targets: + - servera.lab.example.com + message: about.html is in site/pages + paths: + - /home/student/site/pages/about.html + fileType: regular + - id: renamed + kind: file + 'on': servera.lab.example.com + targets: + - servera.lab.example.com + message: the contact page was renamed to contact-us.html + paths: + - /home/student/site/pages/contact-us.html + fileType: regular + - id: old-name + kind: file + 'on': servera.lab.example.com + targets: + - servera.lab.example.com + message: pages/contact.html no longer exists + paths: + - /home/student/site/pages/contact.html + exists: false + - id: backup-gone + kind: file + 'on': servera.lab.example.com + targets: + - servera.lab.example.com + message: the pages-backup directory was removed + paths: + - /home/student/site/pages-backup + exists: false + - id: archive-gone + kind: file + 'on': servera.lab.example.com + targets: + - servera.lab.example.com + message: ~/archive was removed + paths: + - /home/student/archive + exists: false +finish: +- action: directory + hosts: + - servera + path: /home/student/site + state: absent +- action: directory + hosts: + - servera + path: /home/student/archive + state: absent diff --git a/content/programs/rhel9-sysadmin/lab/sa-navigate.yml b/content/programs/rhel9-sysadmin/lab/sa-navigate.yml new file mode 100644 index 0000000..e297429 --- /dev/null +++ b/content/programs/rhel9-sysadmin/lab/sa-navigate.yml @@ -0,0 +1,52 @@ +name: sa-navigate +title: Log in and move around the file system +page: ch03/exercise-navigate +transport: ssh +version: 1 +starter: [] +setup: +- action: file + hosts: + - servera + path: /home/student/plan.txt + state: absent +- action: file + hosts: + - servera + path: /home/student/team notes.txt + state: absent +checkpoints: + final: + files: [] + checks: + - id: plan + kind: file + 'on': servera.lab.example.com + targets: + - servera.lab.example.com + message: ~/plan.txt exists and belongs to student + paths: + - /home/student/plan.txt + fileType: regular + owner: student + - id: notes + kind: file + 'on': servera.lab.example.com + targets: + - servera.lab.example.com + message: the file with a space in its name exists (the quotes keep it one name) + paths: + - /home/student/team notes.txt + fileType: regular + owner: student +finish: +- action: file + hosts: + - servera + path: /home/student/plan.txt + state: absent +- action: file + hosts: + - servera + path: /home/student/team notes.txt + state: absent diff --git a/content/programs/rhel9-sysadmin/lab/sa-redirect.yml b/content/programs/rhel9-sysadmin/lab/sa-redirect.yml new file mode 100644 index 0000000..883e6ef --- /dev/null +++ b/content/programs/rhel9-sysadmin/lab/sa-redirect.yml @@ -0,0 +1,92 @@ +name: sa-redirect +title: Redirection and pipes +page: ch05/exercise-redirection +transport: ssh +version: 1 +starter: [] +setup: +- action: file + hosts: + - servera + path: /home/student/conf-files.txt + state: absent +- action: file + hosts: + - servera + path: /home/student/found.txt + state: absent +- action: file + hosts: + - servera + path: /home/student/errors.txt + state: absent +- action: file + hosts: + - servera + path: /home/student/disk-report.txt + state: absent +checkpoints: + final: + files: [] + checks: + - id: list + kind: file + 'on': servera.lab.example.com + targets: + - servera.lab.example.com + message: conf-files.txt holds the list and the footer line + paths: + - /home/student/conf-files.txt + matches: ^/etc/chrony\.conf$ + contains: + - '--- listed on' + - id: found + kind: file + 'on': servera.lab.example.com + targets: + - servera.lab.example.com + message: found.txt holds the results of find + paths: + - /home/student/found.txt + matches: ^/etc/.*\.conf$ + - id: errors + kind: file + 'on': servera.lab.example.com + targets: + - servera.lab.example.com + message: errors.txt holds the permission errors + paths: + - /home/student/errors.txt + contains: + - Permission denied + - id: report + kind: file + 'on': servera.lab.example.com + targets: + - servera.lab.example.com + message: disk-report.txt was saved with tee + paths: + - /home/student/disk-report.txt + contains: + - Filesystem +finish: +- action: file + hosts: + - servera + path: /home/student/conf-files.txt + state: absent +- action: file + hosts: + - servera + path: /home/student/found.txt + state: absent +- action: file + hosts: + - servera + path: /home/student/errors.txt + state: absent +- action: file + hosts: + - servera + path: /home/student/disk-report.txt + state: absent diff --git a/content/programs/rhel9-sysadmin/lab/sa-vim.yml b/content/programs/rhel9-sysadmin/lab/sa-vim.yml new file mode 100644 index 0000000..3bb06c9 --- /dev/null +++ b/content/programs/rhel9-sysadmin/lab/sa-vim.yml @@ -0,0 +1,44 @@ +name: sa-vim +title: Write and change a file with vim +page: ch05/exercise-vim +transport: ssh +version: 1 +starter: [] +setup: +- action: file + hosts: + - servera + path: /home/student/welcome.txt + state: absent +checkpoints: + final: + files: [] + checks: + - id: welcome + kind: file + 'on': servera.lab.example.com + targets: + - servera.lab.example.com + message: welcome.txt has the three lines that remain + paths: + - /home/student/welcome.txt + lines: + - Welcome to servera.lab.example.com. + - 'Contact: admin@lab.example.com' + - 'Contact: oncall@lab.example.com' + - id: no-window + kind: file + 'on': servera.lab.example.com + targets: + - servera.lab.example.com + message: the cancelled maintenance window line is gone + paths: + - /home/student/welcome.txt + lacks: + - Maintenance window +finish: +- action: file + hosts: + - servera + path: /home/student/welcome.txt + state: absent diff --git a/docs/CATALOG.md b/docs/CATALOG.md index b9637df..aefb907 100644 --- a/docs/CATALOG.md +++ b/docs/CATALOG.md @@ -391,6 +391,7 @@ A hands-on exercise with tracked tasks, notes and an optional challenge brief. | `hosts` | string[] | | Labels for the machines the exercise uses, such as 'workstation' or 'Ubuntu host'. | | `outcomes` | string[] | | What the learner will have done. | | `objectives` | string[] | | Objectives the exercise practises. | +| `guided` | boolean (default `false`) | | A guided exercise: its tasks say exactly what to do, so it needs no challenge brief. It still has an exercise definition whose start, grading and finish the lab tool performs. | ## lab-challenge @@ -408,6 +409,7 @@ How to grade and finish an exercise with the lab tools. | --- | --- | --- | --- | | `exercise` | string (exerciseName) | yes | Exercise name. | | `grade` | boolean (default `false`) | | Also show the grading command. | +| `servers` | boolean (default `false`) | | The exercise's cleanup steps undo its changes on the lab servers, so the Finish text says so. | ## lab-network-map diff --git a/packages/compiler/src/lab.js b/packages/compiler/src/lab.js index 7e5e042..d430569 100644 --- a/packages/compiler/src/lab.js +++ b/packages/compiler/src/lab.js @@ -29,6 +29,11 @@ echo " Run: lab update then start the exercise again with --force." exit 1 `; const CONTROL_ONLY = new Set(['git', 'lint']); +// Setup and cleanup actions that run as root on the lab servers; the rest work on the workstation. +const HOST_ACTIONS = new Set([ + 'package', 'service', 'group', 'user', 'directory', 'file', 'remove-lines', 'firewall', 'selinux', 'wipe-disk', 'systemd', 'linger', + 'container-reset', 'run-as', 'restore-skel', +]); /** Every file under `dir` as paths relative to it, sorted. */ function walk(dir) { @@ -87,8 +92,9 @@ export function compileLabs({ labs, pages, references, seen, validator, diagnost if (bytes) files.set(`lab/${name}/${publishedPath(relative)}`, bytes); } const listing = [`# ${def.title}${def.note ? ` (${def.note})` : ''}`]; - if ((def.setup ?? []).length) { - listing.push('# generated: some files are created on your workstation'); + if ((def.setup ?? []).length || (def.finish ?? []).length) { + if ((def.setup ?? []).some((action) => !HOST_ACTIONS.has(action.action))) + listing.push('# generated: some files are created on your workstation'); listing.push('@lab-update-required'); files.set(`lab/${name}/lab-update-required`, UPDATE_REQUIRED); } @@ -140,6 +146,7 @@ export function compileLabs({ labs, pages, references, seen, validator, diagnost lesson: `#/${def.page}`, ...(def.transport === 'ssh' ? { transport: 'ssh' } : {}), ...(setup.length ? { setup } : {}), + ...(def.finish?.length ? { finish: def.finish } : {}), checkpoints: def.checkpoints, }; index.push({ name, page: `${page.number}`, title: def.title, note: def.note }); diff --git a/packages/engine/src/components/content/registry.jsx b/packages/engine/src/components/content/registry.jsx index b459e51..10fd306 100644 --- a/packages/engine/src/components/content/registry.jsx +++ b/packages/engine/src/components/content/registry.jsx @@ -82,7 +82,7 @@ export const tags = { 'lab-notes': () => [LabNotes], 'lab-challenge': () => [LabChallenge], 'lab-setup': () => [HomeSetup], - 'lab-finish': (a) => [Finish, { name: a.exercise, grade: a.grade }], + 'lab-finish': (a) => [Finish, { name: a.exercise, grade: a.grade, servers: a.servers }], 'starter-files': (a) => [StarterFiles, { name: a.exercise }], 'variant-group': () => [Env], diff --git a/packages/engine/src/components/interactive/Env.jsx b/packages/engine/src/components/interactive/Env.jsx index 6067297..9b633a1 100644 --- a/packages/engine/src/components/interactive/Env.jsx +++ b/packages/engine/src/components/interactive/Env.jsx @@ -165,7 +165,23 @@ export function StarterFiles({ name }) { } /** The "Finish" step of an exercise, for both environments. */ -export function Finish({ name, grade = false }) { +export function Finish({ name, grade = false, servers = false }) { + if (singleEnvironment() && servers) + return ( +

+ {grade ? ( + <> + On workstation, check your work with lab grade {name}. Fix anything that fails and grade again; grading only reads. + Then{' '} + + ) : ( + 'On workstation, ' + )} + lab finish {name} undoes this exercise's changes on the servers and moves the exercise folder to{' '} + ~/lab-archive/. It cleans only what this exercise set up; rht-vmctl reset servers on the Ubuntu host + returns the servers to the exact clean baseline. +

+ ); if (singleEnvironment()) return (

diff --git a/packages/lab-tools/grade.py b/packages/lab-tools/grade.py index 5a102cd..21e38d2 100644 --- a/packages/lab-tools/grade.py +++ b/packages/lab-tools/grade.py @@ -76,8 +76,8 @@ def stat_is(path, fmt, expected): def file_conditions(c, path): p = word(path) if c.get('exists', True) is False: - return ['test ! -e ' + p] - out = ['test -s ' + p if c.get('nonEmpty') else 'test -e ' + p] + return ['test ! -e %s && test ! -L %s' % (p, p)] + out = ['test -L ' + p] if 'symlinkTo' in c else ['test -s ' + p if c.get('nonEmpty') else 'test -e ' + p] for literal in c.get('contains', []): out.append('grep -qF -- %s %s' % (word(literal), p)) for literal in c.get('lacks', []): diff --git a/packages/lab-tools/lab b/packages/lab-tools/lab index 8717055..0380aae 100755 --- a/packages/lab-tools/lab +++ b/packages/lab-tools/lab @@ -2,10 +2,10 @@ # lab - starts and finishes exercises on the Kernel Path home lab # (same syntax as a classroom 'lab' command). Runs on workstation as student. # Install or update: see section 1.6 of the guide, or run: lab update -# Version 6 +# Version 7 set -euo pipefail -VERSION=6 +VERSION=7 BASE="${LAB_URL:-https://kernelpath.dev/lab}" LAB_ROOT="${LAB_HOME:-$HOME}" ARCHIVE="$LAB_ROOT/lab-archive" @@ -24,8 +24,10 @@ usage() { cat < "$work/prepare.py" 2>/dev/null && fetch graders.json > "$work/graders.json" 2>/dev/null; then + python3 "$work/prepare.py" "$name" --finish --project "$dir" --catalog "$work/graders.json" --base "$BASE" \ + || warn "the cleanup on the servers did not finish; 'rht-vmctl reset servers' on the Ubuntu host always works" + else + warn "cannot download the cleanup steps; the servers are unchanged" + fi if [[ ! -e $dir ]]; then warn "there is no ~/$name on workstation (already finished?)" elif [[ $opt == --delete ]]; then @@ -92,8 +103,7 @@ cmd_finish() { mkdir -p "$ARCHIVE" && mv "$dir" "$dest" && say "moved ~/$name to ~/lab-archive/$(basename "$dest")" echo " Your work is kept there. 'lab start $name' gives you a fresh copy." fi - echo " One step is left, and only the Ubuntu host can do it: rht-vmctl reset servers" - echo " (lab runs inside the sealed lab network, so it cannot reset the machines itself)" + echo " The servers were cleaned as far as this exercise goes. For the exact clean baseline, run on the Ubuntu host: rht-vmctl reset servers" } cmd_grade() { diff --git a/packages/lab-tools/prepare.py b/packages/lab-tools/prepare.py index 0649a9d..380dbba 100644 --- a/packages/lab-tools/prepare.py +++ b/packages/lab-tools/prepare.py @@ -1,14 +1,17 @@ #!/usr/bin/env python3 -"""Prepare an exercise on the control node, after `lab start` has copied its starter files. +"""Prepare an exercise after `lab start` has copied its starter files, and put it away at `lab finish`. -The exercise catalog (graders.json) lists the setup actions. The catalog supplies values -only: this file owns what each action does. Nothing here runs a command taken from the -catalog, and everything is created inside the project folder or under ~/git-repos. +The exercise catalog (graders.json) lists the setup actions and the cleanup ("finish") actions. +The catalog supplies values only: this file owns what each action does. Nothing here runs a +command taken from the catalog. Control-node actions work inside the project folder or under +~/git-repos. Host actions build a fixed shell script from validated, quoted values and run it as +root on the named lab servers over SSH, the way grading reaches them. """ import argparse import json import os import re +import shlex import shutil import subprocess import sys @@ -194,16 +197,242 @@ def ssh_keypairs(a, ctx): say('created %s/%s.key.pub ... (and their private keys)' % (a['dir'], a['names'][0])) +# --------------------------------------------------------------------------------------- +# Host actions: fixed scripts, built from validated values, run as root on lab servers. + +SAFE_PATH = re.compile(r'^/(?:srv|opt|mnt|tmp|root|home|var/www|var/tmp|usr/local|etc/(?:httpd/conf\.d|systemd|auto\.master\.d|auto\.[a-z0-9_-]+|cron\.d|exports\.d|sudoers\.d|profile\.d|ssh/sshd_config\.d|chrony\.d|yum\.repos\.d|logrotate\.d|rsyslog\.d|security/limits\.d|sysctl\.d|NetworkManager/system-connections|containers|firewalld/(?:services|zones)))(?:/[A-Za-z0-9_.@%+=:, -]+)*$') +PROTECTED = {'/srv', '/opt', '/mnt', '/tmp', '/root', '/home', '/var/www', '/var/tmp', '/usr/local', '/etc/systemd', '/etc/containers'} +NAME = re.compile(r'^[a-z_][a-z0-9_-]{0,31}$') +SSH = ['ssh', '-o', 'BatchMode=yes', '-o', 'ConnectTimeout=10', '-o', 'StrictHostKeyChecking=accept-new', '-o', 'LogLevel=ERROR'] + + +def q(value): + return shlex.quote(str(value)) + + +def safe_path(path, removing=False): + if not SAFE_PATH.match(path) or '..' in path.split('/'): + raise SetupError('this version of lab will not touch ' + path) + if removing and (path.rstrip('/') in PROTECTED or re.match(r'^/home/[^/]+/?$', path)): + raise SetupError('refusing to remove ' + path) + return path + + +def name(value): + if not NAME.match(value): + raise SetupError('invalid name ' + str(value)) + return value + + +def script_package(a): + names = ' '.join(q(n) for n in a['names']) + if a.get('state', 'present') == 'absent': + return 'dnf remove -y -q %s >/dev/null 2>&1' % names + return 'dnf install -y -q %s >/dev/null 2>&1' % names + + +def script_service(a): + ctl = 'systemctl' + pre = '' + if 'user' in a: + user = q(name(a['user'])) + pre = 'runuser -u %s -- env XDG_RUNTIME_DIR=/run/user/$(id -u %s) ' % (user, user) + ctl = 'systemctl --user' + out = [] + for unit in a['names']: + u = q(unit) + state = a.get('state') + enabled = a.get('enabled') + if enabled is True and state == 'started': + out.append('%s%s enable --now %s' % (pre, ctl, u)) + continue + if enabled is False and state == 'stopped': + out.append('%s%s disable --now %s' % (pre, ctl, u)) + continue + if state: + out.append('%s%s %s %s' % (pre, ctl, {'started': 'start', 'stopped': 'stop', 'restarted': 'restart'}[state], u)) + if enabled is not None: + out.append('%s%s %s %s' % (pre, ctl, 'enable' if enabled else 'disable', u)) + return '\n'.join(out) + + +def script_group(a): + n = q(name(a['name'])) + if a.get('state', 'present') == 'absent': + return 'getent group %s >/dev/null && groupdel %s || true' % (n, n) + gid = ' -g %d' % a['gid'] if 'gid' in a else '' + return 'getent group %s >/dev/null || groupadd%s %s' % (n, gid, n) + + +def script_user(a): + n = q(name(a['name'])) + if a.get('state', 'present') == 'absent': + return 'if id %s >/dev/null 2>&1; then pkill -KILL -u %s 2>/dev/null; sleep 1; userdel -r -f %s 2>/dev/null || userdel -f %s; fi' % (n, n, n, n) + flags = '' + if 'uid' in a: + flags += ' -u %d' % a['uid'] + if 'group' in a: + flags += ' -g ' + q(name(a['group'])) + if a.get('groups'): + flags += ' -G ' + ','.join(q(name(g)) for g in a['groups']) + if 'shell' in a: + flags += ' -s ' + q(a['shell']) + flags += ' -m' if a.get('home', True) else ' -M' + cmd = 'id %s >/dev/null 2>&1 || useradd%s %s' % (n, flags, n) + if 'password' in a: + cmd += ' ; echo %s | chpasswd' % q('%s:%s' % (a['name'], a['password'])) + return cmd + + +def owner_args(a, path): + out = [] + if 'owner' in a or 'group' in a: + who = (name(a['owner']) if 'owner' in a else '') + (':' + name(a['group']) if 'group' in a else '') + out.append('chown %s %s' % (who, path)) + if 'mode' in a: + out.append('chmod %s %s' % (a['mode'], path)) + return out + + +def script_directory(a): + path = safe_path(a['path'], a.get('state') == 'absent') + p = q(path) + if a.get('state', 'present') == 'absent': + return 'rm -rf -- %s' % p + return '\n'.join(['mkdir -p -- %s' % p] + owner_args(a, p)) + + +def script_file(a): + path = safe_path(a['path'], a.get('state') == 'absent') + p = q(path) + if a.get('state', 'present') == 'absent': + return 'rm -rf -- %s' % p + import base64 + data = base64.b64encode(a.get('content', '').encode()).decode() + lines = ['mkdir -p -- "$(dirname %s)"' % p, 'echo %s | base64 -d > %s' % (data, p)] + return '\n'.join(lines + owner_args(a, p)) + + +def script_remove_lines(a): + pattern = a['matching'] + if not re.match(r'^[A-Za-z0-9 _./:,@=^$*+?|()\[\]-]{1,120}$', pattern) or a['path'] not in ('/etc/fstab', '/etc/exports', '/etc/hosts', '/etc/chrony.conf'): + raise SetupError('unsafe line pattern') + return "grep -Ev -- %s %s > /tmp/.lab-lines && cat /tmp/.lab-lines > %s; rm -f /tmp/.lab-lines" % (q(pattern), a['path'], a['path']) + + +def script_firewall(a): + fc = 'firewall-cmd --permanent' + zone = ' --zone=%s' % q(name(a['zone'])) if 'zone' in a else '' + present = a.get('state', 'present') == 'present' + out = [] + if a.get('deleteZone'): + z = q(name(a['zone'])) + out.append(('%s --new-zone=%s' % (fc, z)) if present else ('%s --delete-zone=%s' % (fc, z))) + elif a.get('deleteService'): + s = q(a['service']) + out.append(('%s --new-service=%s' % (fc, s)) if present else ('%s --delete-service=%s' % (fc, s))) + else: + verb = 'add' if present else 'remove' + for key, option in (('service', 'service'), ('port', 'port'), ('source', 'source'), ('forwardPort', 'forward-port')): + if key in a: + out.append('%s%s --%s-%s=%s' % (fc, zone, verb, option, q(a[key]))) + out.append('firewall-cmd --reload >/dev/null') + return '\n'.join(out) + + +def script_selinux(a): + present = a.get('state', 'present') == 'present' + if a['kind'] == 'port': + return 'semanage port %s -t %s -p %s %d' % ('-a' if present else '-d', q(a['type']), a['proto'], a['port']) + if a['kind'] == 'fcontext': + return 'semanage fcontext %s -t %s %s' % ('-a' if present else '-d', q(a['type']), q(a['path'])) if present else 'semanage fcontext -d %s' % q(a['path']) + return 'setsebool -P %s %s' % (q(a['name']), 'on' if a.get('value', True) else 'off') + + +def script_wipe_disk(a): + dev = q(a['device']) + return """for s in $(swapon --noheadings --show=NAME 2>/dev/null); do + case $s in %(d)s*|/dev/dm-*|/dev/mapper/*) swapoff $s 2>/dev/null;; esac +done +for m in $(lsblk -nrpo MOUNTPOINTS %(dev)s 2>/dev/null | grep '^/'); do umount -l "$m" 2>/dev/null; done +for v in $(pvs --noheadings -o vg_name %(d)s* 2>/dev/null | sort -u); do vgremove -ff -y "$v" >/dev/null 2>&1; done +for p in $(pvs --noheadings -o pv_name %(d)s* 2>/dev/null); do pvremove -ff -y "$p" >/dev/null 2>&1; done +wipefs -a -f %(d)s?* >/dev/null 2>&1 +wipefs -a -f %(dev)s >/dev/null 2>&1 +partprobe %(dev)s 2>/dev/null; udevadm settle +true""" % {'d': a['device'], 'dev': dev} + + +def script_systemd(a): + return ('systemctl daemon-reload' if a.get('reload', True) else 'true') + ('\nsystemctl daemon-reexec' if a.get('daemonReexec') else '') + + +def script_linger(a): + return 'loginctl %s %s' % ('enable-linger' if a.get('state', 'present') == 'present' else 'disable-linger', q(name(a['user']))) + + +def script_container_reset(a): + user = q(name(a['user'])) + return ('id %(u)s >/dev/null 2>&1 && { runuser -l %(u)s -c "systemctl --user stop \'*.service\' 2>/dev/null; podman system reset -f" >/dev/null 2>&1; ' + 'rm -rf /home/%(n)s/.config/containers/systemd; }; true') % {'u': user, 'n': name(a['user'])} + + +def script_restore_skel(a): + user = name(a['user']) + files = a.get('files') or ['.bashrc', '.bash_profile', '.bash_logout'] + return '\n'.join('install -o %s -g %s -m 644 /etc/skel/%s /home/%s/%s' % (q(user), q(user), q(x), q(user), q(x)) for x in files if re.match(r'^\.bash[a-z_]+$', x)) + + +def script_run_as(a): + user = q(name(a['user'])) + image = q(a['image']) + verb = {'podman-pull': 'podman pull', 'podman-rmi': 'podman rmi -f'}[a['tool']] + return 'runuser -l %s -c %s >/dev/null 2>&1' % (user, q('%s %s' % (verb, image))) + + +HOST_SCRIPTS = { + 'package': script_package, 'service': script_service, 'group': script_group, 'user': script_user, 'directory': script_directory, + 'file': script_file, 'remove-lines': script_remove_lines, 'firewall': script_firewall, 'selinux': script_selinux, + 'wipe-disk': script_wipe_disk, 'systemd': script_systemd, 'linger': script_linger, 'container-reset': script_container_reset, + 'run-as': script_run_as, 'restore-skel': script_restore_skel, +} + + +def run_on_host(host, script, tolerant): + full = 'set +e\n%s\n' % script if tolerant else 'set -e\n%s\n' % script + target = 'root@%s.lab.example.com' % host + try: + done = subprocess.run([*SSH, target, 'bash -s'], input=full, text=True, capture_output=True, timeout=600) + except subprocess.TimeoutExpired: + raise SetupError('%s: timed out' % host) + if done.returncode != 0 and not tolerant: + raise SetupError('%s: %s' % (host, (done.stderr or done.stdout).strip()[:300] or 'a setup step failed')) + return done.returncode == 0 + + +def host_action(a, ctx): + build = HOST_SCRIPTS[a['action']] + script = build(a) + for host in a['hosts']: + if not re.match(r'^server[a-d]$', host): + raise SetupError('unknown host ' + host) + ok = run_on_host(host, script, ctx.tolerant) + label = a['action'] + (' ' + a.get('name', a.get('path', '')) if a.get('name') or a.get('path') else '') + say('%s: %s%s' % (host, label.strip(), '' if ok else ' (skipped: nothing to undo)')) + + ACTIONS = { 'self-signed-cert': self_signed_cert, 'htpasswd': htpasswd, 'password-hash-var': password_hash_var, 'vault-encrypt': vault_encrypt, 'pack-installed-collection': pack_installed_collection, 'build-collection': build_collection, 'collection-requirements': collection_requirements, 'git-seed-remote': git_seed_remote, 'ssh-keypairs': ssh_keypairs, } +ACTIONS.update({kind: host_action for kind in HOST_SCRIPTS}) +CONTROL_ACTIONS = set(ACTIONS) - set(HOST_SCRIPTS) class Context: - def __init__(self, name, project, base): - self.name, self.project, self.base = name, project, base + def __init__(self, name, project, base, tolerant=False): + self.name, self.project, self.base, self.tolerant = name, project, base, tolerant def prepare(name, project, base, catalog): @@ -216,18 +445,40 @@ def prepare(name, project, base, catalog): handler(action, ctx) +def finish(name, project, base, catalog): + """Undo what the exercise did on the servers. Best effort: every action runs.""" + exercise = catalog['exercises'][name] + ctx = Context(name, project, base, tolerant=True) + count = 0 + for action in exercise.get('finish', []): + handler = ACTIONS.get(action.get('action')) + if handler is None or action.get('action') in CONTROL_ACTIONS: + say('skipped an action this version of lab does not know (run: lab update)') + continue + try: + handler(action, ctx) + except SetupError as error: + say('could not run %s: %s' % (action['action'], error)) + count += 1 + return count + + def main(): parser = argparse.ArgumentParser(description=__doc__) parser.add_argument('name') parser.add_argument('--project', type=Path, required=True) parser.add_argument('--catalog', type=Path, required=True) parser.add_argument('--base', required=True) + parser.add_argument('--finish', action='store_true', help='run the cleanup actions instead of the setup') args = parser.parse_args() try: catalog = json.loads(args.catalog.read_text()) if catalog['version'] != CATALOG_VERSION or args.name not in catalog['exercises']: raise SetupError('unknown exercise or unsupported catalog; run: lab update') - prepare(args.name, args.project, args.base, catalog) + if args.finish: + finish(args.name, args.project, args.base, catalog) + else: + prepare(args.name, args.project, args.base, catalog) except (SetupError, OSError, ValueError, KeyError) as error: print('xx ' + str(error), file=sys.stderr) return 1 diff --git a/packages/schema/catalog/components.json b/packages/schema/catalog/components.json index 93ceca2..34bf06f 100644 --- a/packages/schema/catalog/components.json +++ b/packages/schema/catalog/components.json @@ -896,6 +896,11 @@ "description": "Objectives the exercise practises.", "format": "objectiveId", "maxItems": 12 + }, + "guided": { + "type": "boolean", + "description": "A guided exercise: its tasks say exactly what to do, so it needs no challenge brief. It still has an exercise definition whose start, grading and finish the lab tool performs.", + "default": false } }, "allowedChildren": [ @@ -936,6 +941,11 @@ "type": "boolean", "description": "Also show the grading command.", "default": false + }, + "servers": { + "type": "boolean", + "description": "The exercise's cleanup steps undo its changes on the lab servers, so the Finish text says so.", + "default": false } }, "required": [ diff --git a/packages/schema/generated/validators.js b/packages/schema/generated/validators.js index 679244a..2d21e82 100644 --- a/packages/schema/generated/validators.js +++ b/packages/schema/generated/validators.js @@ -6,4 +6,4 @@ import runtime1Module from 'ajv/dist/runtime/equal.js'; const runtime1 = runtime1Module.default ?? runtime1Module; export const validateSite = validate20;const schema31 = {"$schema":"https://json-schema.org/draft/2020-12/schema","$id":"https://kernelpath.dev/schema/v1/bundle/site.schema.json","type":"object","additionalProperties":false,"required":["apiVersion","generatedAt","site","programs"],"properties":{"apiVersion":{"$ref":"../common.schema.json#/$defs/apiVersion"},"generatedAt":{"type":"string","pattern":"^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(?:\\.[0-9]+)?Z$"},"site":{"type":"object","additionalProperties":false,"required":["name","tagline"],"properties":{"name":{"$ref":"../common.schema.json#/$defs/shortText"},"tagline":{"$ref":"../common.schema.json#/$defs/shortText"},"description":{"$ref":"../common.schema.json#/$defs/text"},"repo":{"$ref":"../common.schema.json#/$defs/url"},"links":{"type":"array"},"legal":{"$ref":"../common.schema.json#/$defs/inlineMarkdown"},"theme":{"$ref":"../source/theme.schema.json"},"home":{"$ref":"#/$defs/hashedUrl"}}},"ui":{"type":"object","additionalProperties":{"type":"object","additionalProperties":{"type":"object","additionalProperties":{"type":"string","maxLength":1000}}}},"programs":{"type":"array","minItems":1,"items":{"type":"object","additionalProperties":false,"required":["id","title","label","platform","status","summary","manifest"],"properties":{"id":{"$ref":"../common.schema.json#/$defs/id"},"title":{"$ref":"../common.schema.json#/$defs/shortText"},"label":{"$ref":"../common.schema.json#/$defs/shortText"},"tagline":{"$ref":"../common.schema.json#/$defs/shortText"},"summary":{"$ref":"../common.schema.json#/$defs/text"},"platform":{"$ref":"../source/program.schema.json#/properties/platform"},"status":{"enum":["active","planned"]},"theme":{"$ref":"../source/theme.schema.json"},"sections":{"type":"integer","minimum":0},"manifest":{"$ref":"#/$defs/hashedUrl"},"showcase":{"$ref":"../source/program.schema.json#/properties/showcase"}}}},"lab":{"type":"string","const":"lab/"},"signature":{"$ref":"#/$defs/hashedUrl"},"interface":{"$ref":"#/$defs/hashedUrl"},"legacyProgram":{"$ref":"../common.schema.json#/$defs/id"}},"$defs":{"hashedUrl":{"type":"string","pattern":"^(?:p/[a-z0-9-]+/)?(?:[a-z]+/)*[a-z0-9-]+\\.[a-f0-9]{8,64}\\.(?:json|sig)$"}}};const schema33 = {"const":1};const schema35 = {"type":"string","minLength":1,"maxLength":2000,"not":{"pattern":"[\\u0000-\\u0008\\u000B\\u000C\\u000E-\\u001F]"}};const schema37 = {"type":"string","maxLength":2048,"pattern":"^(?:#[/A-Za-z0-9._~%!$&'()*+,;=:@?#-]*|lab/[a-z0-9][a-z0-9._/-]*|https://[^\\s<>\"]+|mailto:[^\\s<>\"]+)$"};const schema38 = {"type":"string","minLength":1,"maxLength":4000,"not":{"pattern":"<\\s*/?\\s*[a-zA-Z!]"}};const schema48 = {"type":"string","pattern":"^(?:p/[a-z0-9-]+/)?(?:[a-z]+/)*[a-z0-9-]+\\.[a-f0-9]{8,64}\\.(?:json|sig)$"};const schema49 = {"type":"string","pattern":"^[a-z0-9]+(?:-[a-z0-9]+)*$","maxLength":64};const func1 = Object.prototype.hasOwnProperty;const func2 = runtime0;const pattern4 = new RegExp("^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(?:\\.[0-9]+)?Z$", "u");const pattern5 = new RegExp("[\\u0000-\\u0008\\u000B\\u000C\\u000E-\\u001F]", "u");const pattern7 = new RegExp("^(?:#[/A-Za-z0-9._~%!$&'()*+,;=:@?#-]*|lab/[a-z0-9][a-z0-9._/-]*|https://[^\\s<>\"]+|mailto:[^\\s<>\"]+)$", "u");const pattern8 = new RegExp("<\\s*/?\\s*[a-zA-Z!]", "u");const pattern17 = new RegExp("^(?:p/[a-z0-9-]+/)?(?:[a-z]+/)*[a-z0-9-]+\\.[a-f0-9]{8,64}\\.(?:json|sig)$", "u");const pattern18 = new RegExp("^[a-z0-9]+(?:-[a-z0-9]+)*$", "u");const schema34 = {"$ref":"#/$defs/text","maxLength":160,"type":"string"};function validate22(data, {instancePath="", parentData, parentDataProperty, rootData=data, dynamicAnchors={}}={}){let vErrors = null;let errors = 0;const evaluated0 = validate22.evaluated;if(evaluated0.dynamicProps){evaluated0.props = undefined;}if(evaluated0.dynamicItems){evaluated0.items = undefined;}const _errs0 = errors;const _errs2 = errors;const _errs3 = errors;if(typeof data === "string"){if(!pattern5.test(data)){const err0 = {};if(vErrors === null){vErrors = [err0];}else {vErrors.push(err0);}errors++;}}var valid1 = _errs3 === errors;if(valid1){validate22.errors = [{instancePath,schemaPath:"#/$defs/text/not",keyword:"not",params:{},message:"must NOT be valid"}];return false;}else {errors = _errs2;if(vErrors !== null){if(_errs2){vErrors.length = _errs2;}else {vErrors = null;}}}if(errors === _errs0){if(typeof data === "string"){if(func2(data) > 2000){validate22.errors = [{instancePath,schemaPath:"#/$defs/text/maxLength",keyword:"maxLength",params:{limit: 2000},message:"must NOT have more than 2000 characters"}];return false;}else {if(func2(data) < 1){validate22.errors = [{instancePath,schemaPath:"#/$defs/text/minLength",keyword:"minLength",params:{limit: 1},message:"must NOT have fewer than 1 characters"}];return false;}}}else {validate22.errors = [{instancePath,schemaPath:"#/$defs/text/type",keyword:"type",params:{type: "string"},message:"must be string"}];return false;}}if(errors === 0){if(typeof data === "string"){if(func2(data) > 160){validate22.errors = [{instancePath,schemaPath:"#/maxLength",keyword:"maxLength",params:{limit: 160},message:"must NOT have more than 160 characters"}];return false;}}else {validate22.errors = [{instancePath,schemaPath:"#/type",keyword:"type",params:{type: "string"},message:"must be string"}];return false;}}validate22.errors = vErrors;return errors === 0;}validate22.evaluated = {"dynamicProps":false,"dynamicItems":false};const schema39 = {"$schema":"https://json-schema.org/draft/2020-12/schema","$id":"https://kernelpath.dev/schema/v1/source/theme.schema.json","type":"object","additionalProperties":false,"properties":{"accent":{"$ref":"../common.schema.json#/$defs/color"},"accentHover":{"$ref":"../common.schema.json#/$defs/color"},"accentSoft":{"$ref":"../common.schema.json#/$defs/color"},"accentText":{"$ref":"../common.schema.json#/$defs/color"},"dark":{"type":"object","additionalProperties":false,"properties":{"accent":{"$ref":"../common.schema.json#/$defs/color"},"accentHover":{"$ref":"../common.schema.json#/$defs/color"},"accentSoft":{"$ref":"../common.schema.json#/$defs/color"},"accentText":{"$ref":"../common.schema.json#/$defs/color"}}}}};const schema40 = {"type":"string","pattern":"^(?:#[0-9a-fA-F]{3}|#[0-9a-fA-F]{6}|oklch\\(\\s*[0-9.]+%?\\s+[0-9.]+\\s+[0-9.]+\\s*\\))$"};const pattern9 = new RegExp("^(?:#[0-9a-fA-F]{3}|#[0-9a-fA-F]{6}|oklch\\(\\s*[0-9.]+%?\\s+[0-9.]+\\s+[0-9.]+\\s*\\))$", "u");function validate25(data, {instancePath="", parentData, parentDataProperty, rootData=data, dynamicAnchors={}}={}){/*# sourceURL="https://kernelpath.dev/schema/v1/source/theme.schema.json" */;let vErrors = null;let errors = 0;const evaluated0 = validate25.evaluated;if(evaluated0.dynamicProps){evaluated0.props = undefined;}if(evaluated0.dynamicItems){evaluated0.items = undefined;}if(errors === 0){if(data && typeof data == "object" && !Array.isArray(data)){const _errs1 = errors;for(const key0 in data){if(!(((((key0 === "accent") || (key0 === "accentHover")) || (key0 === "accentSoft")) || (key0 === "accentText")) || (key0 === "dark"))){validate25.errors = [{instancePath,schemaPath:"#/additionalProperties",keyword:"additionalProperties",params:{additionalProperty: key0},message:"must NOT have additional properties"}];return false;break;}}if(_errs1 === errors){if(data.accent !== undefined){let data0 = data.accent;const _errs2 = errors;const _errs3 = errors;if(errors === _errs3){if(typeof data0 === "string"){if(!pattern9.test(data0)){validate25.errors = [{instancePath:instancePath+"/accent",schemaPath:"../common.schema.json#/$defs/color/pattern",keyword:"pattern",params:{pattern: "^(?:#[0-9a-fA-F]{3}|#[0-9a-fA-F]{6}|oklch\\(\\s*[0-9.]+%?\\s+[0-9.]+\\s+[0-9.]+\\s*\\))$"},message:"must match pattern \""+"^(?:#[0-9a-fA-F]{3}|#[0-9a-fA-F]{6}|oklch\\(\\s*[0-9.]+%?\\s+[0-9.]+\\s+[0-9.]+\\s*\\))$"+"\""}];return false;}}else {validate25.errors = [{instancePath:instancePath+"/accent",schemaPath:"../common.schema.json#/$defs/color/type",keyword:"type",params:{type: "string"},message:"must be string"}];return false;}}var valid0 = _errs2 === errors;}else {var valid0 = true;}if(valid0){if(data.accentHover !== undefined){let data1 = data.accentHover;const _errs5 = errors;const _errs6 = errors;if(errors === _errs6){if(typeof data1 === "string"){if(!pattern9.test(data1)){validate25.errors = [{instancePath:instancePath+"/accentHover",schemaPath:"../common.schema.json#/$defs/color/pattern",keyword:"pattern",params:{pattern: "^(?:#[0-9a-fA-F]{3}|#[0-9a-fA-F]{6}|oklch\\(\\s*[0-9.]+%?\\s+[0-9.]+\\s+[0-9.]+\\s*\\))$"},message:"must match pattern \""+"^(?:#[0-9a-fA-F]{3}|#[0-9a-fA-F]{6}|oklch\\(\\s*[0-9.]+%?\\s+[0-9.]+\\s+[0-9.]+\\s*\\))$"+"\""}];return false;}}else {validate25.errors = [{instancePath:instancePath+"/accentHover",schemaPath:"../common.schema.json#/$defs/color/type",keyword:"type",params:{type: "string"},message:"must be string"}];return false;}}var valid0 = _errs5 === errors;}else {var valid0 = true;}if(valid0){if(data.accentSoft !== undefined){let data2 = data.accentSoft;const _errs8 = errors;const _errs9 = errors;if(errors === _errs9){if(typeof data2 === "string"){if(!pattern9.test(data2)){validate25.errors = [{instancePath:instancePath+"/accentSoft",schemaPath:"../common.schema.json#/$defs/color/pattern",keyword:"pattern",params:{pattern: "^(?:#[0-9a-fA-F]{3}|#[0-9a-fA-F]{6}|oklch\\(\\s*[0-9.]+%?\\s+[0-9.]+\\s+[0-9.]+\\s*\\))$"},message:"must match pattern \""+"^(?:#[0-9a-fA-F]{3}|#[0-9a-fA-F]{6}|oklch\\(\\s*[0-9.]+%?\\s+[0-9.]+\\s+[0-9.]+\\s*\\))$"+"\""}];return false;}}else {validate25.errors = [{instancePath:instancePath+"/accentSoft",schemaPath:"../common.schema.json#/$defs/color/type",keyword:"type",params:{type: "string"},message:"must be string"}];return false;}}var valid0 = _errs8 === errors;}else {var valid0 = true;}if(valid0){if(data.accentText !== undefined){let data3 = data.accentText;const _errs11 = errors;const _errs12 = errors;if(errors === _errs12){if(typeof data3 === "string"){if(!pattern9.test(data3)){validate25.errors = [{instancePath:instancePath+"/accentText",schemaPath:"../common.schema.json#/$defs/color/pattern",keyword:"pattern",params:{pattern: "^(?:#[0-9a-fA-F]{3}|#[0-9a-fA-F]{6}|oklch\\(\\s*[0-9.]+%?\\s+[0-9.]+\\s+[0-9.]+\\s*\\))$"},message:"must match pattern \""+"^(?:#[0-9a-fA-F]{3}|#[0-9a-fA-F]{6}|oklch\\(\\s*[0-9.]+%?\\s+[0-9.]+\\s+[0-9.]+\\s*\\))$"+"\""}];return false;}}else {validate25.errors = [{instancePath:instancePath+"/accentText",schemaPath:"../common.schema.json#/$defs/color/type",keyword:"type",params:{type: "string"},message:"must be string"}];return false;}}var valid0 = _errs11 === errors;}else {var valid0 = true;}if(valid0){if(data.dark !== undefined){let data4 = data.dark;const _errs14 = errors;if(errors === _errs14){if(data4 && typeof data4 == "object" && !Array.isArray(data4)){const _errs16 = errors;for(const key1 in data4){if(!((((key1 === "accent") || (key1 === "accentHover")) || (key1 === "accentSoft")) || (key1 === "accentText"))){validate25.errors = [{instancePath:instancePath+"/dark",schemaPath:"#/properties/dark/additionalProperties",keyword:"additionalProperties",params:{additionalProperty: key1},message:"must NOT have additional properties"}];return false;break;}}if(_errs16 === errors){if(data4.accent !== undefined){let data5 = data4.accent;const _errs17 = errors;const _errs18 = errors;if(errors === _errs18){if(typeof data5 === "string"){if(!pattern9.test(data5)){validate25.errors = [{instancePath:instancePath+"/dark/accent",schemaPath:"../common.schema.json#/$defs/color/pattern",keyword:"pattern",params:{pattern: "^(?:#[0-9a-fA-F]{3}|#[0-9a-fA-F]{6}|oklch\\(\\s*[0-9.]+%?\\s+[0-9.]+\\s+[0-9.]+\\s*\\))$"},message:"must match pattern \""+"^(?:#[0-9a-fA-F]{3}|#[0-9a-fA-F]{6}|oklch\\(\\s*[0-9.]+%?\\s+[0-9.]+\\s+[0-9.]+\\s*\\))$"+"\""}];return false;}}else {validate25.errors = [{instancePath:instancePath+"/dark/accent",schemaPath:"../common.schema.json#/$defs/color/type",keyword:"type",params:{type: "string"},message:"must be string"}];return false;}}var valid5 = _errs17 === errors;}else {var valid5 = true;}if(valid5){if(data4.accentHover !== undefined){let data6 = data4.accentHover;const _errs20 = errors;const _errs21 = errors;if(errors === _errs21){if(typeof data6 === "string"){if(!pattern9.test(data6)){validate25.errors = [{instancePath:instancePath+"/dark/accentHover",schemaPath:"../common.schema.json#/$defs/color/pattern",keyword:"pattern",params:{pattern: "^(?:#[0-9a-fA-F]{3}|#[0-9a-fA-F]{6}|oklch\\(\\s*[0-9.]+%?\\s+[0-9.]+\\s+[0-9.]+\\s*\\))$"},message:"must match pattern \""+"^(?:#[0-9a-fA-F]{3}|#[0-9a-fA-F]{6}|oklch\\(\\s*[0-9.]+%?\\s+[0-9.]+\\s+[0-9.]+\\s*\\))$"+"\""}];return false;}}else {validate25.errors = [{instancePath:instancePath+"/dark/accentHover",schemaPath:"../common.schema.json#/$defs/color/type",keyword:"type",params:{type: "string"},message:"must be string"}];return false;}}var valid5 = _errs20 === errors;}else {var valid5 = true;}if(valid5){if(data4.accentSoft !== undefined){let data7 = data4.accentSoft;const _errs23 = errors;const _errs24 = errors;if(errors === _errs24){if(typeof data7 === "string"){if(!pattern9.test(data7)){validate25.errors = [{instancePath:instancePath+"/dark/accentSoft",schemaPath:"../common.schema.json#/$defs/color/pattern",keyword:"pattern",params:{pattern: "^(?:#[0-9a-fA-F]{3}|#[0-9a-fA-F]{6}|oklch\\(\\s*[0-9.]+%?\\s+[0-9.]+\\s+[0-9.]+\\s*\\))$"},message:"must match pattern \""+"^(?:#[0-9a-fA-F]{3}|#[0-9a-fA-F]{6}|oklch\\(\\s*[0-9.]+%?\\s+[0-9.]+\\s+[0-9.]+\\s*\\))$"+"\""}];return false;}}else {validate25.errors = [{instancePath:instancePath+"/dark/accentSoft",schemaPath:"../common.schema.json#/$defs/color/type",keyword:"type",params:{type: "string"},message:"must be string"}];return false;}}var valid5 = _errs23 === errors;}else {var valid5 = true;}if(valid5){if(data4.accentText !== undefined){let data8 = data4.accentText;const _errs26 = errors;const _errs27 = errors;if(errors === _errs27){if(typeof data8 === "string"){if(!pattern9.test(data8)){validate25.errors = [{instancePath:instancePath+"/dark/accentText",schemaPath:"../common.schema.json#/$defs/color/pattern",keyword:"pattern",params:{pattern: "^(?:#[0-9a-fA-F]{3}|#[0-9a-fA-F]{6}|oklch\\(\\s*[0-9.]+%?\\s+[0-9.]+\\s+[0-9.]+\\s*\\))$"},message:"must match pattern \""+"^(?:#[0-9a-fA-F]{3}|#[0-9a-fA-F]{6}|oklch\\(\\s*[0-9.]+%?\\s+[0-9.]+\\s+[0-9.]+\\s*\\))$"+"\""}];return false;}}else {validate25.errors = [{instancePath:instancePath+"/dark/accentText",schemaPath:"../common.schema.json#/$defs/color/type",keyword:"type",params:{type: "string"},message:"must be string"}];return false;}}var valid5 = _errs26 === errors;}else {var valid5 = true;}}}}}}else {validate25.errors = [{instancePath:instancePath+"/dark",schemaPath:"#/properties/dark/type",keyword:"type",params:{type: "object"},message:"must be object"}];return false;}}var valid0 = _errs14 === errors;}else {var valid0 = true;}}}}}}}else {validate25.errors = [{instancePath,schemaPath:"#/type",keyword:"type",params:{type: "object"},message:"must be object"}];return false;}}validate25.errors = vErrors;return errors === 0;}validate25.evaluated = {"props":true,"dynamicProps":false,"dynamicItems":false};const schema62 = {"type":"object","additionalProperties":false,"required":["family","version","label"],"properties":{"family":{"$ref":"../common.schema.json#/$defs/id"},"version":{"type":"string","pattern":"^[0-9]+(?:\\.[0-9]+)?$"},"label":{"$ref":"../common.schema.json#/$defs/shortText","maxLength":24,"type":"string"}}};const pattern24 = new RegExp("^[0-9]+(?:\\.[0-9]+)?$", "u");function validate31(data, {instancePath="", parentData, parentDataProperty, rootData=data, dynamicAnchors={}}={}){let vErrors = null;let errors = 0;const evaluated0 = validate31.evaluated;if(evaluated0.dynamicProps){evaluated0.props = undefined;}if(evaluated0.dynamicItems){evaluated0.items = undefined;}const _errs0 = errors;const _errs2 = errors;const _errs3 = errors;if(typeof data === "string"){if(!pattern5.test(data)){const err0 = {};if(vErrors === null){vErrors = [err0];}else {vErrors.push(err0);}errors++;}}var valid1 = _errs3 === errors;if(valid1){validate31.errors = [{instancePath,schemaPath:"#/$defs/text/not",keyword:"not",params:{},message:"must NOT be valid"}];return false;}else {errors = _errs2;if(vErrors !== null){if(_errs2){vErrors.length = _errs2;}else {vErrors = null;}}}if(errors === _errs0){if(typeof data === "string"){if(func2(data) > 2000){validate31.errors = [{instancePath,schemaPath:"#/$defs/text/maxLength",keyword:"maxLength",params:{limit: 2000},message:"must NOT have more than 2000 characters"}];return false;}else {if(func2(data) < 1){validate31.errors = [{instancePath,schemaPath:"#/$defs/text/minLength",keyword:"minLength",params:{limit: 1},message:"must NOT have fewer than 1 characters"}];return false;}}}else {validate31.errors = [{instancePath,schemaPath:"#/$defs/text/type",keyword:"type",params:{type: "string"},message:"must be string"}];return false;}}if(errors === 0){if(typeof data === "string"){if(func2(data) > 160){validate31.errors = [{instancePath,schemaPath:"#/maxLength",keyword:"maxLength",params:{limit: 160},message:"must NOT have more than 160 characters"}];return false;}}else {validate31.errors = [{instancePath,schemaPath:"#/type",keyword:"type",params:{type: "string"},message:"must be string"}];return false;}}validate31.errors = vErrors;return errors === 0;}validate31.evaluated = {"dynamicProps":false,"dynamicItems":false};function validate44(data, {instancePath="", parentData, parentDataProperty, rootData=data, dynamicAnchors={}}={}){let vErrors = null;let errors = 0;const evaluated0 = validate44.evaluated;if(evaluated0.dynamicProps){evaluated0.props = undefined;}if(evaluated0.dynamicItems){evaluated0.items = undefined;}if(errors === 0){if(data && typeof data == "object" && !Array.isArray(data)){let missing0;if((((data.family === undefined) && (missing0 = "family")) || ((data.version === undefined) && (missing0 = "version"))) || ((data.label === undefined) && (missing0 = "label"))){validate44.errors = [{instancePath,schemaPath:"#/required",keyword:"required",params:{missingProperty: missing0},message:"must have required property '"+missing0+"'"}];return false;}else {const _errs1 = errors;for(const key0 in data){if(!(((key0 === "family") || (key0 === "version")) || (key0 === "label"))){validate44.errors = [{instancePath,schemaPath:"#/additionalProperties",keyword:"additionalProperties",params:{additionalProperty: key0},message:"must NOT have additional properties"}];return false;break;}}if(_errs1 === errors){if(data.family !== undefined){let data0 = data.family;const _errs2 = errors;const _errs3 = errors;if(errors === _errs3){if(typeof data0 === "string"){if(func2(data0) > 64){validate44.errors = [{instancePath:instancePath+"/family",schemaPath:"../common.schema.json#/$defs/id/maxLength",keyword:"maxLength",params:{limit: 64},message:"must NOT have more than 64 characters"}];return false;}else {if(!pattern18.test(data0)){validate44.errors = [{instancePath:instancePath+"/family",schemaPath:"../common.schema.json#/$defs/id/pattern",keyword:"pattern",params:{pattern: "^[a-z0-9]+(?:-[a-z0-9]+)*$"},message:"must match pattern \""+"^[a-z0-9]+(?:-[a-z0-9]+)*$"+"\""}];return false;}}}else {validate44.errors = [{instancePath:instancePath+"/family",schemaPath:"../common.schema.json#/$defs/id/type",keyword:"type",params:{type: "string"},message:"must be string"}];return false;}}var valid0 = _errs2 === errors;}else {var valid0 = true;}if(valid0){if(data.version !== undefined){let data1 = data.version;const _errs5 = errors;if(errors === _errs5){if(typeof data1 === "string"){if(!pattern24.test(data1)){validate44.errors = [{instancePath:instancePath+"/version",schemaPath:"#/properties/version/pattern",keyword:"pattern",params:{pattern: "^[0-9]+(?:\\.[0-9]+)?$"},message:"must match pattern \""+"^[0-9]+(?:\\.[0-9]+)?$"+"\""}];return false;}}else {validate44.errors = [{instancePath:instancePath+"/version",schemaPath:"#/properties/version/type",keyword:"type",params:{type: "string"},message:"must be string"}];return false;}}var valid0 = _errs5 === errors;}else {var valid0 = true;}if(valid0){if(data.label !== undefined){let data2 = data.label;const _errs7 = errors;if(!(validate31(data2, {instancePath:instancePath+"/label",parentData:data,parentDataProperty:"label",rootData,dynamicAnchors}))){vErrors = vErrors === null ? validate31.errors : vErrors.concat(validate31.errors);errors = vErrors.length;}if(errors === _errs7){if(typeof data2 === "string"){if(func2(data2) > 24){validate44.errors = [{instancePath:instancePath+"/label",schemaPath:"#/properties/label/maxLength",keyword:"maxLength",params:{limit: 24},message:"must NOT have more than 24 characters"}];return false;}}else {validate44.errors = [{instancePath:instancePath+"/label",schemaPath:"#/properties/label/type",keyword:"type",params:{type: "string"},message:"must be string"}];return false;}}var valid0 = _errs7 === errors;}else {var valid0 = true;}}}}}}else {validate44.errors = [{instancePath,schemaPath:"#/type",keyword:"type",params:{type: "object"},message:"must be object"}];return false;}}validate44.errors = vErrors;return errors === 0;}validate44.evaluated = {"props":true,"dynamicProps":false,"dynamicItems":false};const schema65 = {"type":"object","additionalProperties":false,"required":["art"],"properties":{"art":{"enum":["automation","terminal"]},"tone":{"enum":["purple","teal","coral","pink","blue","green","amber","gray"]},"highlights":{"type":"array","maxItems":6,"items":{"$ref":"../common.schema.json#/$defs/shortText","maxLength":28,"type":"string"}},"title":{"$ref":"../common.schema.json#/$defs/shortText"},"label":{"$ref":"../common.schema.json#/$defs/shortText","maxLength":24,"type":"string"},"summary":{"$ref":"../common.schema.json#/$defs/text","maxLength":400,"type":"string"}}};function validate48(data, {instancePath="", parentData, parentDataProperty, rootData=data, dynamicAnchors={}}={}){let vErrors = null;let errors = 0;const evaluated0 = validate48.evaluated;if(evaluated0.dynamicProps){evaluated0.props = undefined;}if(evaluated0.dynamicItems){evaluated0.items = undefined;}if(errors === 0){if(data && typeof data == "object" && !Array.isArray(data)){let missing0;if((data.art === undefined) && (missing0 = "art")){validate48.errors = [{instancePath,schemaPath:"#/required",keyword:"required",params:{missingProperty: missing0},message:"must have required property '"+missing0+"'"}];return false;}else {const _errs1 = errors;for(const key0 in data){if(!((((((key0 === "art") || (key0 === "tone")) || (key0 === "highlights")) || (key0 === "title")) || (key0 === "label")) || (key0 === "summary"))){validate48.errors = [{instancePath,schemaPath:"#/additionalProperties",keyword:"additionalProperties",params:{additionalProperty: key0},message:"must NOT have additional properties"}];return false;break;}}if(_errs1 === errors){if(data.art !== undefined){let data0 = data.art;const _errs2 = errors;if(!((data0 === "automation") || (data0 === "terminal"))){validate48.errors = [{instancePath:instancePath+"/art",schemaPath:"#/properties/art/enum",keyword:"enum",params:{allowedValues: schema65.properties.art.enum},message:"must be equal to one of the allowed values"}];return false;}var valid0 = _errs2 === errors;}else {var valid0 = true;}if(valid0){if(data.tone !== undefined){let data1 = data.tone;const _errs3 = errors;if(!((((((((data1 === "purple") || (data1 === "teal")) || (data1 === "coral")) || (data1 === "pink")) || (data1 === "blue")) || (data1 === "green")) || (data1 === "amber")) || (data1 === "gray"))){validate48.errors = [{instancePath:instancePath+"/tone",schemaPath:"#/properties/tone/enum",keyword:"enum",params:{allowedValues: schema65.properties.tone.enum},message:"must be equal to one of the allowed values"}];return false;}var valid0 = _errs3 === errors;}else {var valid0 = true;}if(valid0){if(data.highlights !== undefined){let data2 = data.highlights;const _errs4 = errors;if(errors === _errs4){if(Array.isArray(data2)){if(data2.length > 6){validate48.errors = [{instancePath:instancePath+"/highlights",schemaPath:"#/properties/highlights/maxItems",keyword:"maxItems",params:{limit: 6},message:"must NOT have more than 6 items"}];return false;}else {var valid1 = true;const len0 = data2.length;for(let i0=0; i0 28){validate48.errors = [{instancePath:instancePath+"/highlights/" + i0,schemaPath:"#/properties/highlights/items/maxLength",keyword:"maxLength",params:{limit: 28},message:"must NOT have more than 28 characters"}];return false;}}else {validate48.errors = [{instancePath:instancePath+"/highlights/" + i0,schemaPath:"#/properties/highlights/items/type",keyword:"type",params:{type: "string"},message:"must be string"}];return false;}}var valid1 = _errs6 === errors;if(!valid1){break;}}}}else {validate48.errors = [{instancePath:instancePath+"/highlights",schemaPath:"#/properties/highlights/type",keyword:"type",params:{type: "array"},message:"must be array"}];return false;}}var valid0 = _errs4 === errors;}else {var valid0 = true;}if(valid0){if(data.title !== undefined){const _errs8 = errors;if(!(validate31(data.title, {instancePath:instancePath+"/title",parentData:data,parentDataProperty:"title",rootData,dynamicAnchors}))){vErrors = vErrors === null ? validate31.errors : vErrors.concat(validate31.errors);errors = vErrors.length;}var valid0 = _errs8 === errors;}else {var valid0 = true;}if(valid0){if(data.label !== undefined){let data5 = data.label;const _errs9 = errors;if(!(validate31(data5, {instancePath:instancePath+"/label",parentData:data,parentDataProperty:"label",rootData,dynamicAnchors}))){vErrors = vErrors === null ? validate31.errors : vErrors.concat(validate31.errors);errors = vErrors.length;}if(errors === _errs9){if(typeof data5 === "string"){if(func2(data5) > 24){validate48.errors = [{instancePath:instancePath+"/label",schemaPath:"#/properties/label/maxLength",keyword:"maxLength",params:{limit: 24},message:"must NOT have more than 24 characters"}];return false;}}else {validate48.errors = [{instancePath:instancePath+"/label",schemaPath:"#/properties/label/type",keyword:"type",params:{type: "string"},message:"must be string"}];return false;}}var valid0 = _errs9 === errors;}else {var valid0 = true;}if(valid0){if(data.summary !== undefined){let data6 = data.summary;const _errs11 = errors;const _errs12 = errors;const _errs14 = errors;const _errs15 = errors;if(typeof data6 === "string"){if(!pattern5.test(data6)){const err0 = {};if(vErrors === null){vErrors = [err0];}else {vErrors.push(err0);}errors++;}}var valid3 = _errs15 === errors;if(valid3){validate48.errors = [{instancePath:instancePath+"/summary",schemaPath:"../common.schema.json#/$defs/text/not",keyword:"not",params:{},message:"must NOT be valid"}];return false;}else {errors = _errs14;if(vErrors !== null){if(_errs14){vErrors.length = _errs14;}else {vErrors = null;}}}if(errors === _errs12){if(typeof data6 === "string"){if(func2(data6) > 2000){validate48.errors = [{instancePath:instancePath+"/summary",schemaPath:"../common.schema.json#/$defs/text/maxLength",keyword:"maxLength",params:{limit: 2000},message:"must NOT have more than 2000 characters"}];return false;}else {if(func2(data6) < 1){validate48.errors = [{instancePath:instancePath+"/summary",schemaPath:"../common.schema.json#/$defs/text/minLength",keyword:"minLength",params:{limit: 1},message:"must NOT have fewer than 1 characters"}];return false;}}}else {validate48.errors = [{instancePath:instancePath+"/summary",schemaPath:"../common.schema.json#/$defs/text/type",keyword:"type",params:{type: "string"},message:"must be string"}];return false;}}if(errors === _errs11){if(typeof data6 === "string"){if(func2(data6) > 400){validate48.errors = [{instancePath:instancePath+"/summary",schemaPath:"#/properties/summary/maxLength",keyword:"maxLength",params:{limit: 400},message:"must NOT have more than 400 characters"}];return false;}}else {validate48.errors = [{instancePath:instancePath+"/summary",schemaPath:"#/properties/summary/type",keyword:"type",params:{type: "string"},message:"must be string"}];return false;}}var valid0 = _errs11 === errors;}else {var valid0 = true;}}}}}}}}}else {validate48.errors = [{instancePath,schemaPath:"#/type",keyword:"type",params:{type: "object"},message:"must be object"}];return false;}}validate48.errors = vErrors;return errors === 0;}validate48.evaluated = {"props":true,"dynamicProps":false,"dynamicItems":false};function validate20(data, {instancePath="", parentData, parentDataProperty, rootData=data, dynamicAnchors={}}={}){/*# sourceURL="https://kernelpath.dev/schema/v1/bundle/site.schema.json" */;let vErrors = null;let errors = 0;const evaluated0 = validate20.evaluated;if(evaluated0.dynamicProps){evaluated0.props = undefined;}if(evaluated0.dynamicItems){evaluated0.items = undefined;}if(errors === 0){if(data && typeof data == "object" && !Array.isArray(data)){let missing0;if(((((data.apiVersion === undefined) && (missing0 = "apiVersion")) || ((data.generatedAt === undefined) && (missing0 = "generatedAt"))) || ((data.site === undefined) && (missing0 = "site"))) || ((data.programs === undefined) && (missing0 = "programs"))){validate20.errors = [{instancePath,schemaPath:"#/required",keyword:"required",params:{missingProperty: missing0},message:"must have required property '"+missing0+"'"}];return false;}else {const _errs1 = errors;for(const key0 in data){if(!(func1.call(schema31.properties, key0))){validate20.errors = [{instancePath,schemaPath:"#/additionalProperties",keyword:"additionalProperties",params:{additionalProperty: key0},message:"must NOT have additional properties"}];return false;break;}}if(_errs1 === errors){if(data.apiVersion !== undefined){const _errs2 = errors;if(1 !== data.apiVersion){validate20.errors = [{instancePath:instancePath+"/apiVersion",schemaPath:"../common.schema.json#/$defs/apiVersion/const",keyword:"const",params:{allowedValue: 1},message:"must be equal to constant"}];return false;}var valid0 = _errs2 === errors;}else {var valid0 = true;}if(valid0){if(data.generatedAt !== undefined){let data1 = data.generatedAt;const _errs4 = errors;if(errors === _errs4){if(typeof data1 === "string"){if(!pattern4.test(data1)){validate20.errors = [{instancePath:instancePath+"/generatedAt",schemaPath:"#/properties/generatedAt/pattern",keyword:"pattern",params:{pattern: "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(?:\\.[0-9]+)?Z$"},message:"must match pattern \""+"^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(?:\\.[0-9]+)?Z$"+"\""}];return false;}}else {validate20.errors = [{instancePath:instancePath+"/generatedAt",schemaPath:"#/properties/generatedAt/type",keyword:"type",params:{type: "string"},message:"must be string"}];return false;}}var valid0 = _errs4 === errors;}else {var valid0 = true;}if(valid0){if(data.site !== undefined){let data2 = data.site;const _errs6 = errors;if(errors === _errs6){if(data2 && typeof data2 == "object" && !Array.isArray(data2)){let missing1;if(((data2.name === undefined) && (missing1 = "name")) || ((data2.tagline === undefined) && (missing1 = "tagline"))){validate20.errors = [{instancePath:instancePath+"/site",schemaPath:"#/properties/site/required",keyword:"required",params:{missingProperty: missing1},message:"must have required property '"+missing1+"'"}];return false;}else {const _errs8 = errors;for(const key1 in data2){if(!((((((((key1 === "name") || (key1 === "tagline")) || (key1 === "description")) || (key1 === "repo")) || (key1 === "links")) || (key1 === "legal")) || (key1 === "theme")) || (key1 === "home"))){validate20.errors = [{instancePath:instancePath+"/site",schemaPath:"#/properties/site/additionalProperties",keyword:"additionalProperties",params:{additionalProperty: key1},message:"must NOT have additional properties"}];return false;break;}}if(_errs8 === errors){if(data2.name !== undefined){const _errs9 = errors;if(!(validate22(data2.name, {instancePath:instancePath+"/site/name",parentData:data2,parentDataProperty:"name",rootData,dynamicAnchors}))){vErrors = vErrors === null ? validate22.errors : vErrors.concat(validate22.errors);errors = vErrors.length;}var valid2 = _errs9 === errors;}else {var valid2 = true;}if(valid2){if(data2.tagline !== undefined){const _errs10 = errors;if(!(validate22(data2.tagline, {instancePath:instancePath+"/site/tagline",parentData:data2,parentDataProperty:"tagline",rootData,dynamicAnchors}))){vErrors = vErrors === null ? validate22.errors : vErrors.concat(validate22.errors);errors = vErrors.length;}var valid2 = _errs10 === errors;}else {var valid2 = true;}if(valid2){if(data2.description !== undefined){let data5 = data2.description;const _errs11 = errors;const _errs12 = errors;const _errs14 = errors;const _errs15 = errors;if(typeof data5 === "string"){if(!pattern5.test(data5)){const err0 = {};if(vErrors === null){vErrors = [err0];}else {vErrors.push(err0);}errors++;}}var valid4 = _errs15 === errors;if(valid4){validate20.errors = [{instancePath:instancePath+"/site/description",schemaPath:"../common.schema.json#/$defs/text/not",keyword:"not",params:{},message:"must NOT be valid"}];return false;}else {errors = _errs14;if(vErrors !== null){if(_errs14){vErrors.length = _errs14;}else {vErrors = null;}}}if(errors === _errs12){if(typeof data5 === "string"){if(func2(data5) > 2000){validate20.errors = [{instancePath:instancePath+"/site/description",schemaPath:"../common.schema.json#/$defs/text/maxLength",keyword:"maxLength",params:{limit: 2000},message:"must NOT have more than 2000 characters"}];return false;}else {if(func2(data5) < 1){validate20.errors = [{instancePath:instancePath+"/site/description",schemaPath:"../common.schema.json#/$defs/text/minLength",keyword:"minLength",params:{limit: 1},message:"must NOT have fewer than 1 characters"}];return false;}}}else {validate20.errors = [{instancePath:instancePath+"/site/description",schemaPath:"../common.schema.json#/$defs/text/type",keyword:"type",params:{type: "string"},message:"must be string"}];return false;}}var valid2 = _errs11 === errors;}else {var valid2 = true;}if(valid2){if(data2.repo !== undefined){let data6 = data2.repo;const _errs16 = errors;const _errs17 = errors;if(errors === _errs17){if(typeof data6 === "string"){if(func2(data6) > 2048){validate20.errors = [{instancePath:instancePath+"/site/repo",schemaPath:"../common.schema.json#/$defs/url/maxLength",keyword:"maxLength",params:{limit: 2048},message:"must NOT have more than 2048 characters"}];return false;}else {if(!pattern7.test(data6)){validate20.errors = [{instancePath:instancePath+"/site/repo",schemaPath:"../common.schema.json#/$defs/url/pattern",keyword:"pattern",params:{pattern: "^(?:#[/A-Za-z0-9._~%!$&'()*+,;=:@?#-]*|lab/[a-z0-9][a-z0-9._/-]*|https://[^\\s<>\"]+|mailto:[^\\s<>\"]+)$"},message:"must match pattern \""+"^(?:#[/A-Za-z0-9._~%!$&'()*+,;=:@?#-]*|lab/[a-z0-9][a-z0-9._/-]*|https://[^\\s<>\"]+|mailto:[^\\s<>\"]+)$"+"\""}];return false;}}}else {validate20.errors = [{instancePath:instancePath+"/site/repo",schemaPath:"../common.schema.json#/$defs/url/type",keyword:"type",params:{type: "string"},message:"must be string"}];return false;}}var valid2 = _errs16 === errors;}else {var valid2 = true;}if(valid2){if(data2.links !== undefined){const _errs19 = errors;if(!(Array.isArray(data2.links))){validate20.errors = [{instancePath:instancePath+"/site/links",schemaPath:"#/properties/site/properties/links/type",keyword:"type",params:{type: "array"},message:"must be array"}];return false;}var valid2 = _errs19 === errors;}else {var valid2 = true;}if(valid2){if(data2.legal !== undefined){let data8 = data2.legal;const _errs21 = errors;const _errs22 = errors;const _errs24 = errors;const _errs25 = errors;if(typeof data8 === "string"){if(!pattern8.test(data8)){const err1 = {};if(vErrors === null){vErrors = [err1];}else {vErrors.push(err1);}errors++;}}var valid7 = _errs25 === errors;if(valid7){validate20.errors = [{instancePath:instancePath+"/site/legal",schemaPath:"../common.schema.json#/$defs/inlineMarkdown/not",keyword:"not",params:{},message:"must NOT be valid"}];return false;}else {errors = _errs24;if(vErrors !== null){if(_errs24){vErrors.length = _errs24;}else {vErrors = null;}}}if(errors === _errs22){if(typeof data8 === "string"){if(func2(data8) > 4000){validate20.errors = [{instancePath:instancePath+"/site/legal",schemaPath:"../common.schema.json#/$defs/inlineMarkdown/maxLength",keyword:"maxLength",params:{limit: 4000},message:"must NOT have more than 4000 characters"}];return false;}else {if(func2(data8) < 1){validate20.errors = [{instancePath:instancePath+"/site/legal",schemaPath:"../common.schema.json#/$defs/inlineMarkdown/minLength",keyword:"minLength",params:{limit: 1},message:"must NOT have fewer than 1 characters"}];return false;}}}else {validate20.errors = [{instancePath:instancePath+"/site/legal",schemaPath:"../common.schema.json#/$defs/inlineMarkdown/type",keyword:"type",params:{type: "string"},message:"must be string"}];return false;}}var valid2 = _errs21 === errors;}else {var valid2 = true;}if(valid2){if(data2.theme !== undefined){const _errs26 = errors;if(!(validate25(data2.theme, {instancePath:instancePath+"/site/theme",parentData:data2,parentDataProperty:"theme",rootData,dynamicAnchors}))){vErrors = vErrors === null ? validate25.errors : vErrors.concat(validate25.errors);errors = vErrors.length;}var valid2 = _errs26 === errors;}else {var valid2 = true;}if(valid2){if(data2.home !== undefined){let data10 = data2.home;const _errs27 = errors;const _errs28 = errors;if(errors === _errs28){if(typeof data10 === "string"){if(!pattern17.test(data10)){validate20.errors = [{instancePath:instancePath+"/site/home",schemaPath:"#/$defs/hashedUrl/pattern",keyword:"pattern",params:{pattern: "^(?:p/[a-z0-9-]+/)?(?:[a-z]+/)*[a-z0-9-]+\\.[a-f0-9]{8,64}\\.(?:json|sig)$"},message:"must match pattern \""+"^(?:p/[a-z0-9-]+/)?(?:[a-z]+/)*[a-z0-9-]+\\.[a-f0-9]{8,64}\\.(?:json|sig)$"+"\""}];return false;}}else {validate20.errors = [{instancePath:instancePath+"/site/home",schemaPath:"#/$defs/hashedUrl/type",keyword:"type",params:{type: "string"},message:"must be string"}];return false;}}var valid2 = _errs27 === errors;}else {var valid2 = true;}}}}}}}}}}}else {validate20.errors = [{instancePath:instancePath+"/site",schemaPath:"#/properties/site/type",keyword:"type",params:{type: "object"},message:"must be object"}];return false;}}var valid0 = _errs6 === errors;}else {var valid0 = true;}if(valid0){if(data.ui !== undefined){let data11 = data.ui;const _errs30 = errors;if(errors === _errs30){if(data11 && typeof data11 == "object" && !Array.isArray(data11)){for(const key2 in data11){let data12 = data11[key2];const _errs33 = errors;if(errors === _errs33){if(data12 && typeof data12 == "object" && !Array.isArray(data12)){for(const key3 in data12){let data13 = data12[key3];const _errs36 = errors;if(errors === _errs36){if(data13 && typeof data13 == "object" && !Array.isArray(data13)){for(const key4 in data13){let data14 = data13[key4];const _errs39 = errors;if(errors === _errs39){if(typeof data14 === "string"){if(func2(data14) > 1000){validate20.errors = [{instancePath:instancePath+"/ui/" + key2.replace(/~/g, "~0").replace(/\//g, "~1")+"/" + key3.replace(/~/g, "~0").replace(/\//g, "~1")+"/" + key4.replace(/~/g, "~0").replace(/\//g, "~1"),schemaPath:"#/properties/ui/additionalProperties/additionalProperties/additionalProperties/maxLength",keyword:"maxLength",params:{limit: 1000},message:"must NOT have more than 1000 characters"}];return false;}}else {validate20.errors = [{instancePath:instancePath+"/ui/" + key2.replace(/~/g, "~0").replace(/\//g, "~1")+"/" + key3.replace(/~/g, "~0").replace(/\//g, "~1")+"/" + key4.replace(/~/g, "~0").replace(/\//g, "~1"),schemaPath:"#/properties/ui/additionalProperties/additionalProperties/additionalProperties/type",keyword:"type",params:{type: "string"},message:"must be string"}];return false;}}var valid11 = _errs39 === errors;if(!valid11){break;}}}else {validate20.errors = [{instancePath:instancePath+"/ui/" + key2.replace(/~/g, "~0").replace(/\//g, "~1")+"/" + key3.replace(/~/g, "~0").replace(/\//g, "~1"),schemaPath:"#/properties/ui/additionalProperties/additionalProperties/type",keyword:"type",params:{type: "object"},message:"must be object"}];return false;}}var valid10 = _errs36 === errors;if(!valid10){break;}}}else {validate20.errors = [{instancePath:instancePath+"/ui/" + key2.replace(/~/g, "~0").replace(/\//g, "~1"),schemaPath:"#/properties/ui/additionalProperties/type",keyword:"type",params:{type: "object"},message:"must be object"}];return false;}}var valid9 = _errs33 === errors;if(!valid9){break;}}}else {validate20.errors = [{instancePath:instancePath+"/ui",schemaPath:"#/properties/ui/type",keyword:"type",params:{type: "object"},message:"must be object"}];return false;}}var valid0 = _errs30 === errors;}else {var valid0 = true;}if(valid0){if(data.programs !== undefined){let data15 = data.programs;const _errs41 = errors;if(errors === _errs41){if(Array.isArray(data15)){if(data15.length < 1){validate20.errors = [{instancePath:instancePath+"/programs",schemaPath:"#/properties/programs/minItems",keyword:"minItems",params:{limit: 1},message:"must NOT have fewer than 1 items"}];return false;}else {var valid12 = true;const len0 = data15.length;for(let i0=0; i0 64){validate20.errors = [{instancePath:instancePath+"/programs/" + i0+"/id",schemaPath:"../common.schema.json#/$defs/id/maxLength",keyword:"maxLength",params:{limit: 64},message:"must NOT have more than 64 characters"}];return false;}else {if(!pattern18.test(data17)){validate20.errors = [{instancePath:instancePath+"/programs/" + i0+"/id",schemaPath:"../common.schema.json#/$defs/id/pattern",keyword:"pattern",params:{pattern: "^[a-z0-9]+(?:-[a-z0-9]+)*$"},message:"must match pattern \""+"^[a-z0-9]+(?:-[a-z0-9]+)*$"+"\""}];return false;}}}else {validate20.errors = [{instancePath:instancePath+"/programs/" + i0+"/id",schemaPath:"../common.schema.json#/$defs/id/type",keyword:"type",params:{type: "string"},message:"must be string"}];return false;}}var valid13 = _errs46 === errors;}else {var valid13 = true;}if(valid13){if(data16.title !== undefined){const _errs49 = errors;if(!(validate22(data16.title, {instancePath:instancePath+"/programs/" + i0+"/title",parentData:data16,parentDataProperty:"title",rootData,dynamicAnchors}))){vErrors = vErrors === null ? validate22.errors : vErrors.concat(validate22.errors);errors = vErrors.length;}var valid13 = _errs49 === errors;}else {var valid13 = true;}if(valid13){if(data16.label !== undefined){const _errs50 = errors;if(!(validate22(data16.label, {instancePath:instancePath+"/programs/" + i0+"/label",parentData:data16,parentDataProperty:"label",rootData,dynamicAnchors}))){vErrors = vErrors === null ? validate22.errors : vErrors.concat(validate22.errors);errors = vErrors.length;}var valid13 = _errs50 === errors;}else {var valid13 = true;}if(valid13){if(data16.tagline !== undefined){const _errs51 = errors;if(!(validate22(data16.tagline, {instancePath:instancePath+"/programs/" + i0+"/tagline",parentData:data16,parentDataProperty:"tagline",rootData,dynamicAnchors}))){vErrors = vErrors === null ? validate22.errors : vErrors.concat(validate22.errors);errors = vErrors.length;}var valid13 = _errs51 === errors;}else {var valid13 = true;}if(valid13){if(data16.summary !== undefined){let data21 = data16.summary;const _errs52 = errors;const _errs53 = errors;const _errs55 = errors;const _errs56 = errors;if(typeof data21 === "string"){if(!pattern5.test(data21)){const err2 = {};if(vErrors === null){vErrors = [err2];}else {vErrors.push(err2);}errors++;}}var valid16 = _errs56 === errors;if(valid16){validate20.errors = [{instancePath:instancePath+"/programs/" + i0+"/summary",schemaPath:"../common.schema.json#/$defs/text/not",keyword:"not",params:{},message:"must NOT be valid"}];return false;}else {errors = _errs55;if(vErrors !== null){if(_errs55){vErrors.length = _errs55;}else {vErrors = null;}}}if(errors === _errs53){if(typeof data21 === "string"){if(func2(data21) > 2000){validate20.errors = [{instancePath:instancePath+"/programs/" + i0+"/summary",schemaPath:"../common.schema.json#/$defs/text/maxLength",keyword:"maxLength",params:{limit: 2000},message:"must NOT have more than 2000 characters"}];return false;}else {if(func2(data21) < 1){validate20.errors = [{instancePath:instancePath+"/programs/" + i0+"/summary",schemaPath:"../common.schema.json#/$defs/text/minLength",keyword:"minLength",params:{limit: 1},message:"must NOT have fewer than 1 characters"}];return false;}}}else {validate20.errors = [{instancePath:instancePath+"/programs/" + i0+"/summary",schemaPath:"../common.schema.json#/$defs/text/type",keyword:"type",params:{type: "string"},message:"must be string"}];return false;}}var valid13 = _errs52 === errors;}else {var valid13 = true;}if(valid13){if(data16.platform !== undefined){const _errs57 = errors;if(!(validate44(data16.platform, {instancePath:instancePath+"/programs/" + i0+"/platform",parentData:data16,parentDataProperty:"platform",rootData,dynamicAnchors}))){vErrors = vErrors === null ? validate44.errors : vErrors.concat(validate44.errors);errors = vErrors.length;}var valid13 = _errs57 === errors;}else {var valid13 = true;}if(valid13){if(data16.status !== undefined){let data23 = data16.status;const _errs58 = errors;if(!((data23 === "active") || (data23 === "planned"))){validate20.errors = [{instancePath:instancePath+"/programs/" + i0+"/status",schemaPath:"#/properties/programs/items/properties/status/enum",keyword:"enum",params:{allowedValues: schema31.properties.programs.items.properties.status.enum},message:"must be equal to one of the allowed values"}];return false;}var valid13 = _errs58 === errors;}else {var valid13 = true;}if(valid13){if(data16.theme !== undefined){const _errs59 = errors;if(!(validate25(data16.theme, {instancePath:instancePath+"/programs/" + i0+"/theme",parentData:data16,parentDataProperty:"theme",rootData,dynamicAnchors}))){vErrors = vErrors === null ? validate25.errors : vErrors.concat(validate25.errors);errors = vErrors.length;}var valid13 = _errs59 === errors;}else {var valid13 = true;}if(valid13){if(data16.sections !== undefined){let data25 = data16.sections;const _errs60 = errors;if(!(((typeof data25 == "number") && (!(data25 % 1) && !isNaN(data25))) && (isFinite(data25)))){validate20.errors = [{instancePath:instancePath+"/programs/" + i0+"/sections",schemaPath:"#/properties/programs/items/properties/sections/type",keyword:"type",params:{type: "integer"},message:"must be integer"}];return false;}if(errors === _errs60){if((typeof data25 == "number") && (isFinite(data25))){if(data25 < 0 || isNaN(data25)){validate20.errors = [{instancePath:instancePath+"/programs/" + i0+"/sections",schemaPath:"#/properties/programs/items/properties/sections/minimum",keyword:"minimum",params:{comparison: ">=", limit: 0},message:"must be >= 0"}];return false;}}}var valid13 = _errs60 === errors;}else {var valid13 = true;}if(valid13){if(data16.manifest !== undefined){let data26 = data16.manifest;const _errs62 = errors;const _errs63 = errors;if(errors === _errs63){if(typeof data26 === "string"){if(!pattern17.test(data26)){validate20.errors = [{instancePath:instancePath+"/programs/" + i0+"/manifest",schemaPath:"#/$defs/hashedUrl/pattern",keyword:"pattern",params:{pattern: "^(?:p/[a-z0-9-]+/)?(?:[a-z]+/)*[a-z0-9-]+\\.[a-f0-9]{8,64}\\.(?:json|sig)$"},message:"must match pattern \""+"^(?:p/[a-z0-9-]+/)?(?:[a-z]+/)*[a-z0-9-]+\\.[a-f0-9]{8,64}\\.(?:json|sig)$"+"\""}];return false;}}else {validate20.errors = [{instancePath:instancePath+"/programs/" + i0+"/manifest",schemaPath:"#/$defs/hashedUrl/type",keyword:"type",params:{type: "string"},message:"must be string"}];return false;}}var valid13 = _errs62 === errors;}else {var valid13 = true;}if(valid13){if(data16.showcase !== undefined){const _errs65 = errors;if(!(validate48(data16.showcase, {instancePath:instancePath+"/programs/" + i0+"/showcase",parentData:data16,parentDataProperty:"showcase",rootData,dynamicAnchors}))){vErrors = vErrors === null ? validate48.errors : vErrors.concat(validate48.errors);errors = vErrors.length;}var valid13 = _errs65 === errors;}else {var valid13 = true;}}}}}}}}}}}}}}else {validate20.errors = [{instancePath:instancePath+"/programs/" + i0,schemaPath:"#/properties/programs/items/type",keyword:"type",params:{type: "object"},message:"must be object"}];return false;}}var valid12 = _errs43 === errors;if(!valid12){break;}}}}else {validate20.errors = [{instancePath:instancePath+"/programs",schemaPath:"#/properties/programs/type",keyword:"type",params:{type: "array"},message:"must be array"}];return false;}}var valid0 = _errs41 === errors;}else {var valid0 = true;}if(valid0){if(data.lab !== undefined){let data28 = data.lab;const _errs66 = errors;if(typeof data28 !== "string"){validate20.errors = [{instancePath:instancePath+"/lab",schemaPath:"#/properties/lab/type",keyword:"type",params:{type: "string"},message:"must be string"}];return false;}if("lab/" !== data28){validate20.errors = [{instancePath:instancePath+"/lab",schemaPath:"#/properties/lab/const",keyword:"const",params:{allowedValue: "lab/"},message:"must be equal to constant"}];return false;}var valid0 = _errs66 === errors;}else {var valid0 = true;}if(valid0){if(data.signature !== undefined){let data29 = data.signature;const _errs68 = errors;const _errs69 = errors;if(errors === _errs69){if(typeof data29 === "string"){if(!pattern17.test(data29)){validate20.errors = [{instancePath:instancePath+"/signature",schemaPath:"#/$defs/hashedUrl/pattern",keyword:"pattern",params:{pattern: "^(?:p/[a-z0-9-]+/)?(?:[a-z]+/)*[a-z0-9-]+\\.[a-f0-9]{8,64}\\.(?:json|sig)$"},message:"must match pattern \""+"^(?:p/[a-z0-9-]+/)?(?:[a-z]+/)*[a-z0-9-]+\\.[a-f0-9]{8,64}\\.(?:json|sig)$"+"\""}];return false;}}else {validate20.errors = [{instancePath:instancePath+"/signature",schemaPath:"#/$defs/hashedUrl/type",keyword:"type",params:{type: "string"},message:"must be string"}];return false;}}var valid0 = _errs68 === errors;}else {var valid0 = true;}if(valid0){if(data.interface !== undefined){let data30 = data.interface;const _errs71 = errors;const _errs72 = errors;if(errors === _errs72){if(typeof data30 === "string"){if(!pattern17.test(data30)){validate20.errors = [{instancePath:instancePath+"/interface",schemaPath:"#/$defs/hashedUrl/pattern",keyword:"pattern",params:{pattern: "^(?:p/[a-z0-9-]+/)?(?:[a-z]+/)*[a-z0-9-]+\\.[a-f0-9]{8,64}\\.(?:json|sig)$"},message:"must match pattern \""+"^(?:p/[a-z0-9-]+/)?(?:[a-z]+/)*[a-z0-9-]+\\.[a-f0-9]{8,64}\\.(?:json|sig)$"+"\""}];return false;}}else {validate20.errors = [{instancePath:instancePath+"/interface",schemaPath:"#/$defs/hashedUrl/type",keyword:"type",params:{type: "string"},message:"must be string"}];return false;}}var valid0 = _errs71 === errors;}else {var valid0 = true;}if(valid0){if(data.legacyProgram !== undefined){let data31 = data.legacyProgram;const _errs74 = errors;const _errs75 = errors;if(errors === _errs75){if(typeof data31 === "string"){if(func2(data31) > 64){validate20.errors = [{instancePath:instancePath+"/legacyProgram",schemaPath:"../common.schema.json#/$defs/id/maxLength",keyword:"maxLength",params:{limit: 64},message:"must NOT have more than 64 characters"}];return false;}else {if(!pattern18.test(data31)){validate20.errors = [{instancePath:instancePath+"/legacyProgram",schemaPath:"../common.schema.json#/$defs/id/pattern",keyword:"pattern",params:{pattern: "^[a-z0-9]+(?:-[a-z0-9]+)*$"},message:"must match pattern \""+"^[a-z0-9]+(?:-[a-z0-9]+)*$"+"\""}];return false;}}}else {validate20.errors = [{instancePath:instancePath+"/legacyProgram",schemaPath:"../common.schema.json#/$defs/id/type",keyword:"type",params:{type: "string"},message:"must be string"}];return false;}}var valid0 = _errs74 === errors;}else {var valid0 = true;}}}}}}}}}}}}else {validate20.errors = [{instancePath,schemaPath:"#/type",keyword:"type",params:{type: "object"},message:"must be object"}];return false;}}validate20.errors = vErrors;return errors === 0;}validate20.evaluated = {"props":true,"dynamicProps":false,"dynamicItems":false};export const validateManifest = validate53;const schema70 = {"$schema":"https://json-schema.org/draft/2020-12/schema","$id":"https://kernelpath.dev/schema/v1/bundle/manifest.schema.json","type":"object","additionalProperties":false,"required":["apiVersion","program","chapters","objectives","pages","search"],"properties":{"apiVersion":{"$ref":"../common.schema.json#/$defs/apiVersion"},"program":{"$ref":"../source/program.schema.json"},"chapters":{"type":"array","maxItems":99,"items":{"type":"object","additionalProperties":false,"required":["id","number","title","status","sections"],"properties":{"id":{"$ref":"../common.schema.json#/$defs/chapterId"},"number":{"type":"integer","minimum":1,"maximum":99},"title":{"$ref":"../common.schema.json#/$defs/shortText"},"goal":{"$ref":"../common.schema.json#/$defs/text"},"objectives":{"type":"array","items":{"$ref":"../common.schema.json#/$defs/text"}},"objectiveIds":{"type":"array","items":{"$ref":"../common.schema.json#/$defs/objectiveId"}},"topics":{"type":"array","items":{"$ref":"../common.schema.json#/$defs/shortText"}},"setup":{"type":"boolean"},"status":{"enum":["active","planned"]},"sections":{"type":"array","maxItems":60,"items":{"type":"object","additionalProperties":false,"required":["slug","title","kind","minutes"],"properties":{"slug":{"$ref":"../common.schema.json#/$defs/id"},"title":{"$ref":"../common.schema.json#/$defs/shortText"},"kind":{"enum":["lesson","lab","quiz","summary","reference"]},"minutes":{"$ref":"../common.schema.json#/$defs/minutes"},"activities":{"type":"object","additionalProperties":false,"properties":{"quizzes":{"type":"array","items":{"type":"object","additionalProperties":false,"required":["id","quizId","prompt","revision"],"properties":{"id":{"type":"string","pattern":"^q-[a-z0-9]{6,32}$"},"quizId":{"$ref":"../common.schema.json#/$defs/id"},"prompt":{"$ref":"../common.schema.json#/$defs/inlineMarkdown"},"revision":{"type":"string","pattern":"^[a-f0-9]{1,16}$"}}}},"tasks":{"type":"array","items":{"type":"object","additionalProperties":false,"required":["id","labId","title"],"properties":{"id":{"$ref":"../common.schema.json#/$defs/id"},"labId":{"$ref":"../common.schema.json#/$defs/id"},"title":{"$ref":"../common.schema.json#/$defs/shortText"}}}},"practice":{"type":"array","items":{"type":"object","additionalProperties":false,"required":["id","objective","title"],"properties":{"id":{"$ref":"../common.schema.json#/$defs/id"},"objective":{"$ref":"../common.schema.json#/$defs/objectiveId"},"title":{"$ref":"../common.schema.json#/$defs/shortText"}}}}}}}}}}}},"objectives":{"$ref":"../source/objectives.schema.json"},"pages":{"type":"object","propertyNames":{"pattern":"^(?:ch[0-9]{2}/[a-z0-9]+(?:-[a-z0-9]+)*|home|details)$"},"additionalProperties":{"$ref":"site.schema.json#/$defs/hashedUrl"}},"search":{"$ref":"site.schema.json#/$defs/hashedUrl"},"legacy":{"$ref":"site.schema.json#/$defs/hashedUrl"},"labReports":{"type":"object","additionalProperties":false,"required":["exercises"],"properties":{"exercises":{"type":"object","maxProperties":500,"propertyNames":{"$ref":"../common.schema.json#/$defs/id"},"additionalProperties":{"type":"object","additionalProperties":false,"required":["version","lesson","checkpoints"],"properties":{"version":{"type":"integer","minimum":1},"lesson":{"type":"string","pattern":"^#/ch[0-9]{2}/[a-z0-9-]+$"},"checkpoints":{"type":"object","required":["final"],"additionalProperties":{"type":"object","additionalProperties":false,"required":["files","groups","local","probes"],"properties":{"files":{"type":"array","maxItems":500,"items":{"type":"string","maxLength":240}},"groups":{"type":"object","additionalProperties":{"type":"array","maxItems":500,"items":{"type":"string","maxLength":240}}},"local":{"type":"array","maxItems":40,"items":{"type":"object","additionalProperties":false,"required":["id"],"properties":{"id":{"$ref":"../common.schema.json#/$defs/id"}}}},"probes":{"type":"array","maxItems":40,"items":{"type":"object","additionalProperties":false,"required":["id","targets"],"properties":{"id":{"$ref":"../common.schema.json#/$defs/id"},"targets":{"type":"array","maxItems":500,"items":{"type":"string","maxLength":240}}}}}}}}}}}}}}};const schema72 = {"type":"string","pattern":"^ch[0-9]{2}$"};const schema77 = {"type":"string","pattern":"^ch[0-9]{2}\\.[a-z0-9]+(?:-[a-z0-9]+)*$"};const schema79 = {"type":"integer","minimum":1,"maximum":600};const schema51 = {"$schema":"https://json-schema.org/draft/2020-12/schema","$id":"https://kernelpath.dev/schema/v1/source/program.schema.json","type":"object","additionalProperties":false,"required":["apiVersion","id","title","label","platform","status","summary"],"properties":{"apiVersion":{"$ref":"../common.schema.json#/$defs/apiVersion"},"id":{"$ref":"../common.schema.json#/$defs/id"},"title":{"$ref":"../common.schema.json#/$defs/shortText"},"label":{"$ref":"../common.schema.json#/$defs/shortText","maxLength":32,"type":"string"},"tagline":{"$ref":"../common.schema.json#/$defs/shortText"},"summary":{"$ref":"../common.schema.json#/$defs/text","maxLength":400,"type":"string"},"platform":{"type":"object","additionalProperties":false,"required":["family","version","label"],"properties":{"family":{"$ref":"../common.schema.json#/$defs/id"},"version":{"type":"string","pattern":"^[0-9]+(?:\\.[0-9]+)?$"},"label":{"$ref":"../common.schema.json#/$defs/shortText","maxLength":24,"type":"string"}}},"status":{"enum":["active","planned"]},"stages":{"type":"array","maxItems":12,"items":{"type":"object","additionalProperties":false,"required":["title","chapters"],"properties":{"title":{"$ref":"../common.schema.json#/$defs/shortText"},"chapters":{"type":"array","minItems":1,"uniqueItems":true,"items":{"type":"integer","minimum":1,"maximum":99}},"text":{"$ref":"../common.schema.json#/$defs/text","maxLength":300,"type":"string"}}}},"variants":{"type":"array","maxItems":6,"items":{"type":"object","additionalProperties":false,"required":["id","label"],"properties":{"id":{"$ref":"../common.schema.json#/$defs/id"},"label":{"$ref":"../common.schema.json#/$defs/shortText"},"icon":{"enum":["school","house","server","laptop","cloud"]},"default":{"type":"boolean"}}}},"readerVariables":{"type":"array","maxItems":12,"items":{"type":"object","additionalProperties":false,"required":["key","label"],"properties":{"key":{"$ref":"../common.schema.json#/$defs/variableKey"},"label":{"$ref":"../common.schema.json#/$defs/shortText"},"hint":{"$ref":"../common.schema.json#/$defs/shortText"},"example":{"type":"string","maxLength":64},"pattern":{"enum":["ipv4","hostname","username","text"]}}}},"lab":{"type":"boolean"},"labGuide":{"type":"string","pattern":"^ch[0-9]{2}/[a-z0-9]+(?:-[a-z0-9]+)*$"},"theme":{"$ref":"theme.schema.json"},"showcase":{"type":"object","additionalProperties":false,"required":["art"],"properties":{"art":{"enum":["automation","terminal"]},"tone":{"enum":["purple","teal","coral","pink","blue","green","amber","gray"]},"highlights":{"type":"array","maxItems":6,"items":{"$ref":"../common.schema.json#/$defs/shortText","maxLength":28,"type":"string"}},"title":{"$ref":"../common.schema.json#/$defs/shortText"},"label":{"$ref":"../common.schema.json#/$defs/shortText","maxLength":24,"type":"string"},"summary":{"$ref":"../common.schema.json#/$defs/text","maxLength":400,"type":"string"}}}}};const schema60 = {"type":"string","pattern":"^[A-Z][A-Z0-9_]{1,31}$"};const pattern27 = new RegExp("^[A-Z][A-Z0-9_]{1,31}$", "u");const pattern28 = new RegExp("^ch[0-9]{2}/[a-z0-9]+(?:-[a-z0-9]+)*$", "u");function validate30(data, {instancePath="", parentData, parentDataProperty, rootData=data, dynamicAnchors={}}={}){/*# sourceURL="https://kernelpath.dev/schema/v1/source/program.schema.json" */;let vErrors = null;let errors = 0;const evaluated0 = validate30.evaluated;if(evaluated0.dynamicProps){evaluated0.props = undefined;}if(evaluated0.dynamicItems){evaluated0.items = undefined;}if(errors === 0){if(data && typeof data == "object" && !Array.isArray(data)){let missing0;if((((((((data.apiVersion === undefined) && (missing0 = "apiVersion")) || ((data.id === undefined) && (missing0 = "id"))) || ((data.title === undefined) && (missing0 = "title"))) || ((data.label === undefined) && (missing0 = "label"))) || ((data.platform === undefined) && (missing0 = "platform"))) || ((data.status === undefined) && (missing0 = "status"))) || ((data.summary === undefined) && (missing0 = "summary"))){validate30.errors = [{instancePath,schemaPath:"#/required",keyword:"required",params:{missingProperty: missing0},message:"must have required property '"+missing0+"'"}];return false;}else {const _errs1 = errors;for(const key0 in data){if(!(func1.call(schema51.properties, key0))){validate30.errors = [{instancePath,schemaPath:"#/additionalProperties",keyword:"additionalProperties",params:{additionalProperty: key0},message:"must NOT have additional properties"}];return false;break;}}if(_errs1 === errors){if(data.apiVersion !== undefined){const _errs2 = errors;if(1 !== data.apiVersion){validate30.errors = [{instancePath:instancePath+"/apiVersion",schemaPath:"../common.schema.json#/$defs/apiVersion/const",keyword:"const",params:{allowedValue: 1},message:"must be equal to constant"}];return false;}var valid0 = _errs2 === errors;}else {var valid0 = true;}if(valid0){if(data.id !== undefined){let data1 = data.id;const _errs4 = errors;const _errs5 = errors;if(errors === _errs5){if(typeof data1 === "string"){if(func2(data1) > 64){validate30.errors = [{instancePath:instancePath+"/id",schemaPath:"../common.schema.json#/$defs/id/maxLength",keyword:"maxLength",params:{limit: 64},message:"must NOT have more than 64 characters"}];return false;}else {if(!pattern18.test(data1)){validate30.errors = [{instancePath:instancePath+"/id",schemaPath:"../common.schema.json#/$defs/id/pattern",keyword:"pattern",params:{pattern: "^[a-z0-9]+(?:-[a-z0-9]+)*$"},message:"must match pattern \""+"^[a-z0-9]+(?:-[a-z0-9]+)*$"+"\""}];return false;}}}else {validate30.errors = [{instancePath:instancePath+"/id",schemaPath:"../common.schema.json#/$defs/id/type",keyword:"type",params:{type: "string"},message:"must be string"}];return false;}}var valid0 = _errs4 === errors;}else {var valid0 = true;}if(valid0){if(data.title !== undefined){const _errs7 = errors;if(!(validate31(data.title, {instancePath:instancePath+"/title",parentData:data,parentDataProperty:"title",rootData,dynamicAnchors}))){vErrors = vErrors === null ? validate31.errors : vErrors.concat(validate31.errors);errors = vErrors.length;}var valid0 = _errs7 === errors;}else {var valid0 = true;}if(valid0){if(data.label !== undefined){let data3 = data.label;const _errs8 = errors;if(!(validate31(data3, {instancePath:instancePath+"/label",parentData:data,parentDataProperty:"label",rootData,dynamicAnchors}))){vErrors = vErrors === null ? validate31.errors : vErrors.concat(validate31.errors);errors = vErrors.length;}if(errors === _errs8){if(typeof data3 === "string"){if(func2(data3) > 32){validate30.errors = [{instancePath:instancePath+"/label",schemaPath:"#/properties/label/maxLength",keyword:"maxLength",params:{limit: 32},message:"must NOT have more than 32 characters"}];return false;}}else {validate30.errors = [{instancePath:instancePath+"/label",schemaPath:"#/properties/label/type",keyword:"type",params:{type: "string"},message:"must be string"}];return false;}}var valid0 = _errs8 === errors;}else {var valid0 = true;}if(valid0){if(data.tagline !== undefined){const _errs10 = errors;if(!(validate31(data.tagline, {instancePath:instancePath+"/tagline",parentData:data,parentDataProperty:"tagline",rootData,dynamicAnchors}))){vErrors = vErrors === null ? validate31.errors : vErrors.concat(validate31.errors);errors = vErrors.length;}var valid0 = _errs10 === errors;}else {var valid0 = true;}if(valid0){if(data.summary !== undefined){let data5 = data.summary;const _errs11 = errors;const _errs12 = errors;const _errs14 = errors;const _errs15 = errors;if(typeof data5 === "string"){if(!pattern5.test(data5)){const err0 = {};if(vErrors === null){vErrors = [err0];}else {vErrors.push(err0);}errors++;}}var valid4 = _errs15 === errors;if(valid4){validate30.errors = [{instancePath:instancePath+"/summary",schemaPath:"../common.schema.json#/$defs/text/not",keyword:"not",params:{},message:"must NOT be valid"}];return false;}else {errors = _errs14;if(vErrors !== null){if(_errs14){vErrors.length = _errs14;}else {vErrors = null;}}}if(errors === _errs12){if(typeof data5 === "string"){if(func2(data5) > 2000){validate30.errors = [{instancePath:instancePath+"/summary",schemaPath:"../common.schema.json#/$defs/text/maxLength",keyword:"maxLength",params:{limit: 2000},message:"must NOT have more than 2000 characters"}];return false;}else {if(func2(data5) < 1){validate30.errors = [{instancePath:instancePath+"/summary",schemaPath:"../common.schema.json#/$defs/text/minLength",keyword:"minLength",params:{limit: 1},message:"must NOT have fewer than 1 characters"}];return false;}}}else {validate30.errors = [{instancePath:instancePath+"/summary",schemaPath:"../common.schema.json#/$defs/text/type",keyword:"type",params:{type: "string"},message:"must be string"}];return false;}}if(errors === _errs11){if(typeof data5 === "string"){if(func2(data5) > 400){validate30.errors = [{instancePath:instancePath+"/summary",schemaPath:"#/properties/summary/maxLength",keyword:"maxLength",params:{limit: 400},message:"must NOT have more than 400 characters"}];return false;}}else {validate30.errors = [{instancePath:instancePath+"/summary",schemaPath:"#/properties/summary/type",keyword:"type",params:{type: "string"},message:"must be string"}];return false;}}var valid0 = _errs11 === errors;}else {var valid0 = true;}if(valid0){if(data.platform !== undefined){let data6 = data.platform;const _errs17 = errors;if(errors === _errs17){if(data6 && typeof data6 == "object" && !Array.isArray(data6)){let missing1;if((((data6.family === undefined) && (missing1 = "family")) || ((data6.version === undefined) && (missing1 = "version"))) || ((data6.label === undefined) && (missing1 = "label"))){validate30.errors = [{instancePath:instancePath+"/platform",schemaPath:"#/properties/platform/required",keyword:"required",params:{missingProperty: missing1},message:"must have required property '"+missing1+"'"}];return false;}else {const _errs19 = errors;for(const key1 in data6){if(!(((key1 === "family") || (key1 === "version")) || (key1 === "label"))){validate30.errors = [{instancePath:instancePath+"/platform",schemaPath:"#/properties/platform/additionalProperties",keyword:"additionalProperties",params:{additionalProperty: key1},message:"must NOT have additional properties"}];return false;break;}}if(_errs19 === errors){if(data6.family !== undefined){let data7 = data6.family;const _errs20 = errors;const _errs21 = errors;if(errors === _errs21){if(typeof data7 === "string"){if(func2(data7) > 64){validate30.errors = [{instancePath:instancePath+"/platform/family",schemaPath:"../common.schema.json#/$defs/id/maxLength",keyword:"maxLength",params:{limit: 64},message:"must NOT have more than 64 characters"}];return false;}else {if(!pattern18.test(data7)){validate30.errors = [{instancePath:instancePath+"/platform/family",schemaPath:"../common.schema.json#/$defs/id/pattern",keyword:"pattern",params:{pattern: "^[a-z0-9]+(?:-[a-z0-9]+)*$"},message:"must match pattern \""+"^[a-z0-9]+(?:-[a-z0-9]+)*$"+"\""}];return false;}}}else {validate30.errors = [{instancePath:instancePath+"/platform/family",schemaPath:"../common.schema.json#/$defs/id/type",keyword:"type",params:{type: "string"},message:"must be string"}];return false;}}var valid5 = _errs20 === errors;}else {var valid5 = true;}if(valid5){if(data6.version !== undefined){let data8 = data6.version;const _errs23 = errors;if(errors === _errs23){if(typeof data8 === "string"){if(!pattern24.test(data8)){validate30.errors = [{instancePath:instancePath+"/platform/version",schemaPath:"#/properties/platform/properties/version/pattern",keyword:"pattern",params:{pattern: "^[0-9]+(?:\\.[0-9]+)?$"},message:"must match pattern \""+"^[0-9]+(?:\\.[0-9]+)?$"+"\""}];return false;}}else {validate30.errors = [{instancePath:instancePath+"/platform/version",schemaPath:"#/properties/platform/properties/version/type",keyword:"type",params:{type: "string"},message:"must be string"}];return false;}}var valid5 = _errs23 === errors;}else {var valid5 = true;}if(valid5){if(data6.label !== undefined){let data9 = data6.label;const _errs25 = errors;if(!(validate31(data9, {instancePath:instancePath+"/platform/label",parentData:data6,parentDataProperty:"label",rootData,dynamicAnchors}))){vErrors = vErrors === null ? validate31.errors : vErrors.concat(validate31.errors);errors = vErrors.length;}if(errors === _errs25){if(typeof data9 === "string"){if(func2(data9) > 24){validate30.errors = [{instancePath:instancePath+"/platform/label",schemaPath:"#/properties/platform/properties/label/maxLength",keyword:"maxLength",params:{limit: 24},message:"must NOT have more than 24 characters"}];return false;}}else {validate30.errors = [{instancePath:instancePath+"/platform/label",schemaPath:"#/properties/platform/properties/label/type",keyword:"type",params:{type: "string"},message:"must be string"}];return false;}}var valid5 = _errs25 === errors;}else {var valid5 = true;}}}}}}else {validate30.errors = [{instancePath:instancePath+"/platform",schemaPath:"#/properties/platform/type",keyword:"type",params:{type: "object"},message:"must be object"}];return false;}}var valid0 = _errs17 === errors;}else {var valid0 = true;}if(valid0){if(data.status !== undefined){let data10 = data.status;const _errs27 = errors;if(!((data10 === "active") || (data10 === "planned"))){validate30.errors = [{instancePath:instancePath+"/status",schemaPath:"#/properties/status/enum",keyword:"enum",params:{allowedValues: schema51.properties.status.enum},message:"must be equal to one of the allowed values"}];return false;}var valid0 = _errs27 === errors;}else {var valid0 = true;}if(valid0){if(data.stages !== undefined){let data11 = data.stages;const _errs28 = errors;if(errors === _errs28){if(Array.isArray(data11)){if(data11.length > 12){validate30.errors = [{instancePath:instancePath+"/stages",schemaPath:"#/properties/stages/maxItems",keyword:"maxItems",params:{limit: 12},message:"must NOT have more than 12 items"}];return false;}else {var valid7 = true;const len0 = data11.length;for(let i0=0; i0 99 || isNaN(data15)){validate30.errors = [{instancePath:instancePath+"/stages/" + i0+"/chapters/" + i1,schemaPath:"#/properties/stages/items/properties/chapters/items/maximum",keyword:"maximum",params:{comparison: "<=", limit: 99},message:"must be <= 99"}];return false;}else {if(data15 < 1 || isNaN(data15)){validate30.errors = [{instancePath:instancePath+"/stages/" + i0+"/chapters/" + i1,schemaPath:"#/properties/stages/items/properties/chapters/items/minimum",keyword:"minimum",params:{comparison: ">=", limit: 1},message:"must be >= 1"}];return false;}}}}var valid9 = _errs36 === errors;if(!valid9){break;}}if(valid9){let i2 = data14.length;let j0;if(i2 > 1){const indices0 = {};for(;i2--;){let item0 = data14[i2];if(!(((typeof item0 == "number") && (!(item0 % 1) && !isNaN(item0))) && (isFinite(item0)))){continue;}if(typeof indices0[item0] == "number"){j0 = indices0[item0];validate30.errors = [{instancePath:instancePath+"/stages/" + i0+"/chapters",schemaPath:"#/properties/stages/items/properties/chapters/uniqueItems",keyword:"uniqueItems",params:{i: i2, j: j0},message:"must NOT have duplicate items (items ## "+j0+" and "+i2+" are identical)"}];return false;break;}indices0[item0] = i2;}}}}}else {validate30.errors = [{instancePath:instancePath+"/stages/" + i0+"/chapters",schemaPath:"#/properties/stages/items/properties/chapters/type",keyword:"type",params:{type: "array"},message:"must be array"}];return false;}}var valid8 = _errs34 === errors;}else {var valid8 = true;}if(valid8){if(data12.text !== undefined){let data16 = data12.text;const _errs38 = errors;const _errs39 = errors;const _errs41 = errors;const _errs42 = errors;if(typeof data16 === "string"){if(!pattern5.test(data16)){const err1 = {};if(vErrors === null){vErrors = [err1];}else {vErrors.push(err1);}errors++;}}var valid12 = _errs42 === errors;if(valid12){validate30.errors = [{instancePath:instancePath+"/stages/" + i0+"/text",schemaPath:"../common.schema.json#/$defs/text/not",keyword:"not",params:{},message:"must NOT be valid"}];return false;}else {errors = _errs41;if(vErrors !== null){if(_errs41){vErrors.length = _errs41;}else {vErrors = null;}}}if(errors === _errs39){if(typeof data16 === "string"){if(func2(data16) > 2000){validate30.errors = [{instancePath:instancePath+"/stages/" + i0+"/text",schemaPath:"../common.schema.json#/$defs/text/maxLength",keyword:"maxLength",params:{limit: 2000},message:"must NOT have more than 2000 characters"}];return false;}else {if(func2(data16) < 1){validate30.errors = [{instancePath:instancePath+"/stages/" + i0+"/text",schemaPath:"../common.schema.json#/$defs/text/minLength",keyword:"minLength",params:{limit: 1},message:"must NOT have fewer than 1 characters"}];return false;}}}else {validate30.errors = [{instancePath:instancePath+"/stages/" + i0+"/text",schemaPath:"../common.schema.json#/$defs/text/type",keyword:"type",params:{type: "string"},message:"must be string"}];return false;}}if(errors === _errs38){if(typeof data16 === "string"){if(func2(data16) > 300){validate30.errors = [{instancePath:instancePath+"/stages/" + i0+"/text",schemaPath:"#/properties/stages/items/properties/text/maxLength",keyword:"maxLength",params:{limit: 300},message:"must NOT have more than 300 characters"}];return false;}}else {validate30.errors = [{instancePath:instancePath+"/stages/" + i0+"/text",schemaPath:"#/properties/stages/items/properties/text/type",keyword:"type",params:{type: "string"},message:"must be string"}];return false;}}var valid8 = _errs38 === errors;}else {var valid8 = true;}}}}}}else {validate30.errors = [{instancePath:instancePath+"/stages/" + i0,schemaPath:"#/properties/stages/items/type",keyword:"type",params:{type: "object"},message:"must be object"}];return false;}}var valid7 = _errs30 === errors;if(!valid7){break;}}}}else {validate30.errors = [{instancePath:instancePath+"/stages",schemaPath:"#/properties/stages/type",keyword:"type",params:{type: "array"},message:"must be array"}];return false;}}var valid0 = _errs28 === errors;}else {var valid0 = true;}if(valid0){if(data.variants !== undefined){let data17 = data.variants;const _errs44 = errors;if(errors === _errs44){if(Array.isArray(data17)){if(data17.length > 6){validate30.errors = [{instancePath:instancePath+"/variants",schemaPath:"#/properties/variants/maxItems",keyword:"maxItems",params:{limit: 6},message:"must NOT have more than 6 items"}];return false;}else {var valid13 = true;const len2 = data17.length;for(let i3=0; i3 64){validate30.errors = [{instancePath:instancePath+"/variants/" + i3+"/id",schemaPath:"../common.schema.json#/$defs/id/maxLength",keyword:"maxLength",params:{limit: 64},message:"must NOT have more than 64 characters"}];return false;}else {if(!pattern18.test(data19)){validate30.errors = [{instancePath:instancePath+"/variants/" + i3+"/id",schemaPath:"../common.schema.json#/$defs/id/pattern",keyword:"pattern",params:{pattern: "^[a-z0-9]+(?:-[a-z0-9]+)*$"},message:"must match pattern \""+"^[a-z0-9]+(?:-[a-z0-9]+)*$"+"\""}];return false;}}}else {validate30.errors = [{instancePath:instancePath+"/variants/" + i3+"/id",schemaPath:"../common.schema.json#/$defs/id/type",keyword:"type",params:{type: "string"},message:"must be string"}];return false;}}var valid14 = _errs49 === errors;}else {var valid14 = true;}if(valid14){if(data18.label !== undefined){const _errs52 = errors;if(!(validate31(data18.label, {instancePath:instancePath+"/variants/" + i3+"/label",parentData:data18,parentDataProperty:"label",rootData,dynamicAnchors}))){vErrors = vErrors === null ? validate31.errors : vErrors.concat(validate31.errors);errors = vErrors.length;}var valid14 = _errs52 === errors;}else {var valid14 = true;}if(valid14){if(data18.icon !== undefined){let data21 = data18.icon;const _errs53 = errors;if(!(((((data21 === "school") || (data21 === "house")) || (data21 === "server")) || (data21 === "laptop")) || (data21 === "cloud"))){validate30.errors = [{instancePath:instancePath+"/variants/" + i3+"/icon",schemaPath:"#/properties/variants/items/properties/icon/enum",keyword:"enum",params:{allowedValues: schema51.properties.variants.items.properties.icon.enum},message:"must be equal to one of the allowed values"}];return false;}var valid14 = _errs53 === errors;}else {var valid14 = true;}if(valid14){if(data18.default !== undefined){const _errs54 = errors;if(typeof data18.default !== "boolean"){validate30.errors = [{instancePath:instancePath+"/variants/" + i3+"/default",schemaPath:"#/properties/variants/items/properties/default/type",keyword:"type",params:{type: "boolean"},message:"must be boolean"}];return false;}var valid14 = _errs54 === errors;}else {var valid14 = true;}}}}}}}else {validate30.errors = [{instancePath:instancePath+"/variants/" + i3,schemaPath:"#/properties/variants/items/type",keyword:"type",params:{type: "object"},message:"must be object"}];return false;}}var valid13 = _errs46 === errors;if(!valid13){break;}}}}else {validate30.errors = [{instancePath:instancePath+"/variants",schemaPath:"#/properties/variants/type",keyword:"type",params:{type: "array"},message:"must be array"}];return false;}}var valid0 = _errs44 === errors;}else {var valid0 = true;}if(valid0){if(data.readerVariables !== undefined){let data23 = data.readerVariables;const _errs56 = errors;if(errors === _errs56){if(Array.isArray(data23)){if(data23.length > 12){validate30.errors = [{instancePath:instancePath+"/readerVariables",schemaPath:"#/properties/readerVariables/maxItems",keyword:"maxItems",params:{limit: 12},message:"must NOT have more than 12 items"}];return false;}else {var valid16 = true;const len3 = data23.length;for(let i4=0; i4 64){validate30.errors = [{instancePath:instancePath+"/readerVariables/" + i4+"/example",schemaPath:"#/properties/readerVariables/items/properties/example/maxLength",keyword:"maxLength",params:{limit: 64},message:"must NOT have more than 64 characters"}];return false;}}else {validate30.errors = [{instancePath:instancePath+"/readerVariables/" + i4+"/example",schemaPath:"#/properties/readerVariables/items/properties/example/type",keyword:"type",params:{type: "string"},message:"must be string"}];return false;}}var valid17 = _errs66 === errors;}else {var valid17 = true;}if(valid17){if(data24.pattern !== undefined){let data29 = data24.pattern;const _errs68 = errors;if(!((((data29 === "ipv4") || (data29 === "hostname")) || (data29 === "username")) || (data29 === "text"))){validate30.errors = [{instancePath:instancePath+"/readerVariables/" + i4+"/pattern",schemaPath:"#/properties/readerVariables/items/properties/pattern/enum",keyword:"enum",params:{allowedValues: schema51.properties.readerVariables.items.properties.pattern.enum},message:"must be equal to one of the allowed values"}];return false;}var valid17 = _errs68 === errors;}else {var valid17 = true;}}}}}}}}else {validate30.errors = [{instancePath:instancePath+"/readerVariables/" + i4,schemaPath:"#/properties/readerVariables/items/type",keyword:"type",params:{type: "object"},message:"must be object"}];return false;}}var valid16 = _errs58 === errors;if(!valid16){break;}}}}else {validate30.errors = [{instancePath:instancePath+"/readerVariables",schemaPath:"#/properties/readerVariables/type",keyword:"type",params:{type: "array"},message:"must be array"}];return false;}}var valid0 = _errs56 === errors;}else {var valid0 = true;}if(valid0){if(data.lab !== undefined){const _errs69 = errors;if(typeof data.lab !== "boolean"){validate30.errors = [{instancePath:instancePath+"/lab",schemaPath:"#/properties/lab/type",keyword:"type",params:{type: "boolean"},message:"must be boolean"}];return false;}var valid0 = _errs69 === errors;}else {var valid0 = true;}if(valid0){if(data.labGuide !== undefined){let data31 = data.labGuide;const _errs71 = errors;if(errors === _errs71){if(typeof data31 === "string"){if(!pattern28.test(data31)){validate30.errors = [{instancePath:instancePath+"/labGuide",schemaPath:"#/properties/labGuide/pattern",keyword:"pattern",params:{pattern: "^ch[0-9]{2}/[a-z0-9]+(?:-[a-z0-9]+)*$"},message:"must match pattern \""+"^ch[0-9]{2}/[a-z0-9]+(?:-[a-z0-9]+)*$"+"\""}];return false;}}else {validate30.errors = [{instancePath:instancePath+"/labGuide",schemaPath:"#/properties/labGuide/type",keyword:"type",params:{type: "string"},message:"must be string"}];return false;}}var valid0 = _errs71 === errors;}else {var valid0 = true;}if(valid0){if(data.theme !== undefined){const _errs73 = errors;if(!(validate25(data.theme, {instancePath:instancePath+"/theme",parentData:data,parentDataProperty:"theme",rootData,dynamicAnchors}))){vErrors = vErrors === null ? validate25.errors : vErrors.concat(validate25.errors);errors = vErrors.length;}var valid0 = _errs73 === errors;}else {var valid0 = true;}if(valid0){if(data.showcase !== undefined){let data33 = data.showcase;const _errs74 = errors;if(errors === _errs74){if(data33 && typeof data33 == "object" && !Array.isArray(data33)){let missing5;if((data33.art === undefined) && (missing5 = "art")){validate30.errors = [{instancePath:instancePath+"/showcase",schemaPath:"#/properties/showcase/required",keyword:"required",params:{missingProperty: missing5},message:"must have required property '"+missing5+"'"}];return false;}else {const _errs76 = errors;for(const key5 in data33){if(!((((((key5 === "art") || (key5 === "tone")) || (key5 === "highlights")) || (key5 === "title")) || (key5 === "label")) || (key5 === "summary"))){validate30.errors = [{instancePath:instancePath+"/showcase",schemaPath:"#/properties/showcase/additionalProperties",keyword:"additionalProperties",params:{additionalProperty: key5},message:"must NOT have additional properties"}];return false;break;}}if(_errs76 === errors){if(data33.art !== undefined){let data34 = data33.art;const _errs77 = errors;if(!((data34 === "automation") || (data34 === "terminal"))){validate30.errors = [{instancePath:instancePath+"/showcase/art",schemaPath:"#/properties/showcase/properties/art/enum",keyword:"enum",params:{allowedValues: schema51.properties.showcase.properties.art.enum},message:"must be equal to one of the allowed values"}];return false;}var valid19 = _errs77 === errors;}else {var valid19 = true;}if(valid19){if(data33.tone !== undefined){let data35 = data33.tone;const _errs78 = errors;if(!((((((((data35 === "purple") || (data35 === "teal")) || (data35 === "coral")) || (data35 === "pink")) || (data35 === "blue")) || (data35 === "green")) || (data35 === "amber")) || (data35 === "gray"))){validate30.errors = [{instancePath:instancePath+"/showcase/tone",schemaPath:"#/properties/showcase/properties/tone/enum",keyword:"enum",params:{allowedValues: schema51.properties.showcase.properties.tone.enum},message:"must be equal to one of the allowed values"}];return false;}var valid19 = _errs78 === errors;}else {var valid19 = true;}if(valid19){if(data33.highlights !== undefined){let data36 = data33.highlights;const _errs79 = errors;if(errors === _errs79){if(Array.isArray(data36)){if(data36.length > 6){validate30.errors = [{instancePath:instancePath+"/showcase/highlights",schemaPath:"#/properties/showcase/properties/highlights/maxItems",keyword:"maxItems",params:{limit: 6},message:"must NOT have more than 6 items"}];return false;}else {var valid20 = true;const len4 = data36.length;for(let i5=0; i5 28){validate30.errors = [{instancePath:instancePath+"/showcase/highlights/" + i5,schemaPath:"#/properties/showcase/properties/highlights/items/maxLength",keyword:"maxLength",params:{limit: 28},message:"must NOT have more than 28 characters"}];return false;}}else {validate30.errors = [{instancePath:instancePath+"/showcase/highlights/" + i5,schemaPath:"#/properties/showcase/properties/highlights/items/type",keyword:"type",params:{type: "string"},message:"must be string"}];return false;}}var valid20 = _errs81 === errors;if(!valid20){break;}}}}else {validate30.errors = [{instancePath:instancePath+"/showcase/highlights",schemaPath:"#/properties/showcase/properties/highlights/type",keyword:"type",params:{type: "array"},message:"must be array"}];return false;}}var valid19 = _errs79 === errors;}else {var valid19 = true;}if(valid19){if(data33.title !== undefined){const _errs83 = errors;if(!(validate31(data33.title, {instancePath:instancePath+"/showcase/title",parentData:data33,parentDataProperty:"title",rootData,dynamicAnchors}))){vErrors = vErrors === null ? validate31.errors : vErrors.concat(validate31.errors);errors = vErrors.length;}var valid19 = _errs83 === errors;}else {var valid19 = true;}if(valid19){if(data33.label !== undefined){let data39 = data33.label;const _errs84 = errors;if(!(validate31(data39, {instancePath:instancePath+"/showcase/label",parentData:data33,parentDataProperty:"label",rootData,dynamicAnchors}))){vErrors = vErrors === null ? validate31.errors : vErrors.concat(validate31.errors);errors = vErrors.length;}if(errors === _errs84){if(typeof data39 === "string"){if(func2(data39) > 24){validate30.errors = [{instancePath:instancePath+"/showcase/label",schemaPath:"#/properties/showcase/properties/label/maxLength",keyword:"maxLength",params:{limit: 24},message:"must NOT have more than 24 characters"}];return false;}}else {validate30.errors = [{instancePath:instancePath+"/showcase/label",schemaPath:"#/properties/showcase/properties/label/type",keyword:"type",params:{type: "string"},message:"must be string"}];return false;}}var valid19 = _errs84 === errors;}else {var valid19 = true;}if(valid19){if(data33.summary !== undefined){let data40 = data33.summary;const _errs86 = errors;const _errs87 = errors;const _errs89 = errors;const _errs90 = errors;if(typeof data40 === "string"){if(!pattern5.test(data40)){const err2 = {};if(vErrors === null){vErrors = [err2];}else {vErrors.push(err2);}errors++;}}var valid22 = _errs90 === errors;if(valid22){validate30.errors = [{instancePath:instancePath+"/showcase/summary",schemaPath:"../common.schema.json#/$defs/text/not",keyword:"not",params:{},message:"must NOT be valid"}];return false;}else {errors = _errs89;if(vErrors !== null){if(_errs89){vErrors.length = _errs89;}else {vErrors = null;}}}if(errors === _errs87){if(typeof data40 === "string"){if(func2(data40) > 2000){validate30.errors = [{instancePath:instancePath+"/showcase/summary",schemaPath:"../common.schema.json#/$defs/text/maxLength",keyword:"maxLength",params:{limit: 2000},message:"must NOT have more than 2000 characters"}];return false;}else {if(func2(data40) < 1){validate30.errors = [{instancePath:instancePath+"/showcase/summary",schemaPath:"../common.schema.json#/$defs/text/minLength",keyword:"minLength",params:{limit: 1},message:"must NOT have fewer than 1 characters"}];return false;}}}else {validate30.errors = [{instancePath:instancePath+"/showcase/summary",schemaPath:"../common.schema.json#/$defs/text/type",keyword:"type",params:{type: "string"},message:"must be string"}];return false;}}if(errors === _errs86){if(typeof data40 === "string"){if(func2(data40) > 400){validate30.errors = [{instancePath:instancePath+"/showcase/summary",schemaPath:"#/properties/showcase/properties/summary/maxLength",keyword:"maxLength",params:{limit: 400},message:"must NOT have more than 400 characters"}];return false;}}else {validate30.errors = [{instancePath:instancePath+"/showcase/summary",schemaPath:"#/properties/showcase/properties/summary/type",keyword:"type",params:{type: "string"},message:"must be string"}];return false;}}var valid19 = _errs86 === errors;}else {var valid19 = true;}}}}}}}}}else {validate30.errors = [{instancePath:instancePath+"/showcase",schemaPath:"#/properties/showcase/type",keyword:"type",params:{type: "object"},message:"must be object"}];return false;}}var valid0 = _errs74 === errors;}else {var valid0 = true;}}}}}}}}}}}}}}}}}}else {validate30.errors = [{instancePath,schemaPath:"#/type",keyword:"type",params:{type: "object"},message:"must be object"}];return false;}}validate30.errors = vErrors;return errors === 0;}validate30.evaluated = {"props":true,"dynamicProps":false,"dynamicItems":false};function validate55(data, {instancePath="", parentData, parentDataProperty, rootData=data, dynamicAnchors={}}={}){let vErrors = null;let errors = 0;const evaluated0 = validate55.evaluated;if(evaluated0.dynamicProps){evaluated0.props = undefined;}if(evaluated0.dynamicItems){evaluated0.items = undefined;}const _errs0 = errors;const _errs2 = errors;const _errs3 = errors;if(typeof data === "string"){if(!pattern5.test(data)){const err0 = {};if(vErrors === null){vErrors = [err0];}else {vErrors.push(err0);}errors++;}}var valid1 = _errs3 === errors;if(valid1){validate55.errors = [{instancePath,schemaPath:"#/$defs/text/not",keyword:"not",params:{},message:"must NOT be valid"}];return false;}else {errors = _errs2;if(vErrors !== null){if(_errs2){vErrors.length = _errs2;}else {vErrors = null;}}}if(errors === _errs0){if(typeof data === "string"){if(func2(data) > 2000){validate55.errors = [{instancePath,schemaPath:"#/$defs/text/maxLength",keyword:"maxLength",params:{limit: 2000},message:"must NOT have more than 2000 characters"}];return false;}else {if(func2(data) < 1){validate55.errors = [{instancePath,schemaPath:"#/$defs/text/minLength",keyword:"minLength",params:{limit: 1},message:"must NOT have fewer than 1 characters"}];return false;}}}else {validate55.errors = [{instancePath,schemaPath:"#/$defs/text/type",keyword:"type",params:{type: "string"},message:"must be string"}];return false;}}if(errors === 0){if(typeof data === "string"){if(func2(data) > 160){validate55.errors = [{instancePath,schemaPath:"#/maxLength",keyword:"maxLength",params:{limit: 160},message:"must NOT have more than 160 characters"}];return false;}}else {validate55.errors = [{instancePath,schemaPath:"#/type",keyword:"type",params:{type: "string"},message:"must be string"}];return false;}}validate55.errors = vErrors;return errors === 0;}validate55.evaluated = {"dynamicProps":false,"dynamicItems":false};const schema86 = {"$schema":"https://json-schema.org/draft/2020-12/schema","$id":"https://kernelpath.dev/schema/v1/source/objectives.schema.json","type":"array","items":{"type":"object","additionalProperties":false,"required":["id","chapter","lessons","labs","challenges"],"properties":{"id":{"type":"string","pattern":"^ch[0-9]{2}\\.[a-z0-9]+(?:-[a-z0-9]+)*$"},"chapter":{"$ref":"../common.schema.json#/$defs/chapterId"},"lessons":{"type":"array","items":{"$ref":"#/$defs/pageRef"}},"labs":{"type":"array","items":{"$ref":"#/$defs/pageRef"}},"challenges":{"type":"array","items":{"$ref":"../common.schema.json#/$defs/id"}}}},"$defs":{"pageRef":{"type":"string","pattern":"^ch[0-9]{2}/[a-z0-9]+(?:-[a-z0-9]+)*$"}}};const schema88 = {"type":"string","pattern":"^ch[0-9]{2}/[a-z0-9]+(?:-[a-z0-9]+)*$"};const pattern41 = new RegExp("^ch[0-9]{2}\\.[a-z0-9]+(?:-[a-z0-9]+)*$", "u");const pattern37 = new RegExp("^ch[0-9]{2}$", "u");function validate61(data, {instancePath="", parentData, parentDataProperty, rootData=data, dynamicAnchors={}}={}){/*# sourceURL="https://kernelpath.dev/schema/v1/source/objectives.schema.json" */;let vErrors = null;let errors = 0;const evaluated0 = validate61.evaluated;if(evaluated0.dynamicProps){evaluated0.props = undefined;}if(evaluated0.dynamicItems){evaluated0.items = undefined;}if(errors === 0){if(Array.isArray(data)){var valid0 = true;const len0 = data.length;for(let i0=0; i0 64){validate61.errors = [{instancePath:instancePath+"/" + i0+"/challenges/" + i3,schemaPath:"../common.schema.json#/$defs/id/maxLength",keyword:"maxLength",params:{limit: 64},message:"must NOT have more than 64 characters"}];return false;}else {if(!pattern18.test(data8)){validate61.errors = [{instancePath:instancePath+"/" + i0+"/challenges/" + i3,schemaPath:"../common.schema.json#/$defs/id/pattern",keyword:"pattern",params:{pattern: "^[a-z0-9]+(?:-[a-z0-9]+)*$"},message:"must match pattern \""+"^[a-z0-9]+(?:-[a-z0-9]+)*$"+"\""}];return false;}}}else {validate61.errors = [{instancePath:instancePath+"/" + i0+"/challenges/" + i3,schemaPath:"../common.schema.json#/$defs/id/type",keyword:"type",params:{type: "string"},message:"must be string"}];return false;}}var valid7 = _errs21 === errors;if(!valid7){break;}}}else {validate61.errors = [{instancePath:instancePath+"/" + i0+"/challenges",schemaPath:"#/items/properties/challenges/type",keyword:"type",params:{type: "array"},message:"must be array"}];return false;}}var valid1 = _errs19 === errors;}else {var valid1 = true;}}}}}}}}else {validate61.errors = [{instancePath:instancePath+"/" + i0,schemaPath:"#/items/type",keyword:"type",params:{type: "object"},message:"must be object"}];return false;}}var valid0 = _errs1 === errors;if(!valid0){break;}}}else {validate61.errors = [{instancePath,schemaPath:"#/type",keyword:"type",params:{type: "array"},message:"must be array"}];return false;}}validate61.errors = vErrors;return errors === 0;}validate61.evaluated = {"items":true,"dynamicProps":false,"dynamicItems":false};const pattern43 = new RegExp("^q-[a-z0-9]{6,32}$", "u");const pattern46 = new RegExp("^[a-f0-9]{1,16}$", "u");const pattern56 = new RegExp("^(?:ch[0-9]{2}/[a-z0-9]+(?:-[a-z0-9]+)*|home|details)$", "u");const pattern61 = new RegExp("^#/ch[0-9]{2}/[a-z0-9-]+$", "u");function validate53(data, {instancePath="", parentData, parentDataProperty, rootData=data, dynamicAnchors={}}={}){/*# sourceURL="https://kernelpath.dev/schema/v1/bundle/manifest.schema.json" */;let vErrors = null;let errors = 0;const evaluated0 = validate53.evaluated;if(evaluated0.dynamicProps){evaluated0.props = undefined;}if(evaluated0.dynamicItems){evaluated0.items = undefined;}if(errors === 0){if(data && typeof data == "object" && !Array.isArray(data)){let missing0;if(((((((data.apiVersion === undefined) && (missing0 = "apiVersion")) || ((data.program === undefined) && (missing0 = "program"))) || ((data.chapters === undefined) && (missing0 = "chapters"))) || ((data.objectives === undefined) && (missing0 = "objectives"))) || ((data.pages === undefined) && (missing0 = "pages"))) || ((data.search === undefined) && (missing0 = "search"))){validate53.errors = [{instancePath,schemaPath:"#/required",keyword:"required",params:{missingProperty: missing0},message:"must have required property '"+missing0+"'"}];return false;}else {const _errs1 = errors;for(const key0 in data){if(!((((((((key0 === "apiVersion") || (key0 === "program")) || (key0 === "chapters")) || (key0 === "objectives")) || (key0 === "pages")) || (key0 === "search")) || (key0 === "legacy")) || (key0 === "labReports"))){validate53.errors = [{instancePath,schemaPath:"#/additionalProperties",keyword:"additionalProperties",params:{additionalProperty: key0},message:"must NOT have additional properties"}];return false;break;}}if(_errs1 === errors){if(data.apiVersion !== undefined){const _errs2 = errors;if(1 !== data.apiVersion){validate53.errors = [{instancePath:instancePath+"/apiVersion",schemaPath:"../common.schema.json#/$defs/apiVersion/const",keyword:"const",params:{allowedValue: 1},message:"must be equal to constant"}];return false;}var valid0 = _errs2 === errors;}else {var valid0 = true;}if(valid0){if(data.program !== undefined){const _errs4 = errors;if(!(validate30(data.program, {instancePath:instancePath+"/program",parentData:data,parentDataProperty:"program",rootData,dynamicAnchors}))){vErrors = vErrors === null ? validate30.errors : vErrors.concat(validate30.errors);errors = vErrors.length;}var valid0 = _errs4 === errors;}else {var valid0 = true;}if(valid0){if(data.chapters !== undefined){let data2 = data.chapters;const _errs5 = errors;if(errors === _errs5){if(Array.isArray(data2)){if(data2.length > 99){validate53.errors = [{instancePath:instancePath+"/chapters",schemaPath:"#/properties/chapters/maxItems",keyword:"maxItems",params:{limit: 99},message:"must NOT have more than 99 items"}];return false;}else {var valid2 = true;const len0 = data2.length;for(let i0=0; i0 99 || isNaN(data5)){validate53.errors = [{instancePath:instancePath+"/chapters/" + i0+"/number",schemaPath:"#/properties/chapters/items/properties/number/maximum",keyword:"maximum",params:{comparison: "<=", limit: 99},message:"must be <= 99"}];return false;}else {if(data5 < 1 || isNaN(data5)){validate53.errors = [{instancePath:instancePath+"/chapters/" + i0+"/number",schemaPath:"#/properties/chapters/items/properties/number/minimum",keyword:"minimum",params:{comparison: ">=", limit: 1},message:"must be >= 1"}];return false;}}}}var valid3 = _errs13 === errors;}else {var valid3 = true;}if(valid3){if(data3.title !== undefined){const _errs15 = errors;if(!(validate55(data3.title, {instancePath:instancePath+"/chapters/" + i0+"/title",parentData:data3,parentDataProperty:"title",rootData,dynamicAnchors}))){vErrors = vErrors === null ? validate55.errors : vErrors.concat(validate55.errors);errors = vErrors.length;}var valid3 = _errs15 === errors;}else {var valid3 = true;}if(valid3){if(data3.goal !== undefined){let data7 = data3.goal;const _errs16 = errors;const _errs17 = errors;const _errs19 = errors;const _errs20 = errors;if(typeof data7 === "string"){if(!pattern5.test(data7)){const err0 = {};if(vErrors === null){vErrors = [err0];}else {vErrors.push(err0);}errors++;}}var valid6 = _errs20 === errors;if(valid6){validate53.errors = [{instancePath:instancePath+"/chapters/" + i0+"/goal",schemaPath:"../common.schema.json#/$defs/text/not",keyword:"not",params:{},message:"must NOT be valid"}];return false;}else {errors = _errs19;if(vErrors !== null){if(_errs19){vErrors.length = _errs19;}else {vErrors = null;}}}if(errors === _errs17){if(typeof data7 === "string"){if(func2(data7) > 2000){validate53.errors = [{instancePath:instancePath+"/chapters/" + i0+"/goal",schemaPath:"../common.schema.json#/$defs/text/maxLength",keyword:"maxLength",params:{limit: 2000},message:"must NOT have more than 2000 characters"}];return false;}else {if(func2(data7) < 1){validate53.errors = [{instancePath:instancePath+"/chapters/" + i0+"/goal",schemaPath:"../common.schema.json#/$defs/text/minLength",keyword:"minLength",params:{limit: 1},message:"must NOT have fewer than 1 characters"}];return false;}}}else {validate53.errors = [{instancePath:instancePath+"/chapters/" + i0+"/goal",schemaPath:"../common.schema.json#/$defs/text/type",keyword:"type",params:{type: "string"},message:"must be string"}];return false;}}var valid3 = _errs16 === errors;}else {var valid3 = true;}if(valid3){if(data3.objectives !== undefined){let data8 = data3.objectives;const _errs21 = errors;if(errors === _errs21){if(Array.isArray(data8)){var valid7 = true;const len1 = data8.length;for(let i1=0; i1 2000){validate53.errors = [{instancePath:instancePath+"/chapters/" + i0+"/objectives/" + i1,schemaPath:"../common.schema.json#/$defs/text/maxLength",keyword:"maxLength",params:{limit: 2000},message:"must NOT have more than 2000 characters"}];return false;}else {if(func2(data9) < 1){validate53.errors = [{instancePath:instancePath+"/chapters/" + i0+"/objectives/" + i1,schemaPath:"../common.schema.json#/$defs/text/minLength",keyword:"minLength",params:{limit: 1},message:"must NOT have fewer than 1 characters"}];return false;}}}else {validate53.errors = [{instancePath:instancePath+"/chapters/" + i0+"/objectives/" + i1,schemaPath:"../common.schema.json#/$defs/text/type",keyword:"type",params:{type: "string"},message:"must be string"}];return false;}}var valid7 = _errs23 === errors;if(!valid7){break;}}}else {validate53.errors = [{instancePath:instancePath+"/chapters/" + i0+"/objectives",schemaPath:"#/properties/chapters/items/properties/objectives/type",keyword:"type",params:{type: "array"},message:"must be array"}];return false;}}var valid3 = _errs21 === errors;}else {var valid3 = true;}if(valid3){if(data3.objectiveIds !== undefined){let data10 = data3.objectiveIds;const _errs28 = errors;if(errors === _errs28){if(Array.isArray(data10)){var valid10 = true;const len2 = data10.length;for(let i2=0; i2 60){validate53.errors = [{instancePath:instancePath+"/chapters/" + i0+"/sections",schemaPath:"#/properties/chapters/items/properties/sections/maxItems",keyword:"maxItems",params:{limit: 60},message:"must NOT have more than 60 items"}];return false;}else {var valid13 = true;const len4 = data16.length;for(let i4=0; i4 64){validate53.errors = [{instancePath:instancePath+"/chapters/" + i0+"/sections/" + i4+"/slug",schemaPath:"../common.schema.json#/$defs/id/maxLength",keyword:"maxLength",params:{limit: 64},message:"must NOT have more than 64 characters"}];return false;}else {if(!pattern18.test(data18)){validate53.errors = [{instancePath:instancePath+"/chapters/" + i0+"/sections/" + i4+"/slug",schemaPath:"../common.schema.json#/$defs/id/pattern",keyword:"pattern",params:{pattern: "^[a-z0-9]+(?:-[a-z0-9]+)*$"},message:"must match pattern \""+"^[a-z0-9]+(?:-[a-z0-9]+)*$"+"\""}];return false;}}}else {validate53.errors = [{instancePath:instancePath+"/chapters/" + i0+"/sections/" + i4+"/slug",schemaPath:"../common.schema.json#/$defs/id/type",keyword:"type",params:{type: "string"},message:"must be string"}];return false;}}var valid14 = _errs44 === errors;}else {var valid14 = true;}if(valid14){if(data17.title !== undefined){const _errs47 = errors;if(!(validate55(data17.title, {instancePath:instancePath+"/chapters/" + i0+"/sections/" + i4+"/title",parentData:data17,parentDataProperty:"title",rootData,dynamicAnchors}))){vErrors = vErrors === null ? validate55.errors : vErrors.concat(validate55.errors);errors = vErrors.length;}var valid14 = _errs47 === errors;}else {var valid14 = true;}if(valid14){if(data17.kind !== undefined){let data20 = data17.kind;const _errs48 = errors;if(!(((((data20 === "lesson") || (data20 === "lab")) || (data20 === "quiz")) || (data20 === "summary")) || (data20 === "reference"))){validate53.errors = [{instancePath:instancePath+"/chapters/" + i0+"/sections/" + i4+"/kind",schemaPath:"#/properties/chapters/items/properties/sections/items/properties/kind/enum",keyword:"enum",params:{allowedValues: schema70.properties.chapters.items.properties.sections.items.properties.kind.enum},message:"must be equal to one of the allowed values"}];return false;}var valid14 = _errs48 === errors;}else {var valid14 = true;}if(valid14){if(data17.minutes !== undefined){let data21 = data17.minutes;const _errs49 = errors;const _errs50 = errors;if(!(((typeof data21 == "number") && (!(data21 % 1) && !isNaN(data21))) && (isFinite(data21)))){validate53.errors = [{instancePath:instancePath+"/chapters/" + i0+"/sections/" + i4+"/minutes",schemaPath:"../common.schema.json#/$defs/minutes/type",keyword:"type",params:{type: "integer"},message:"must be integer"}];return false;}if(errors === _errs50){if((typeof data21 == "number") && (isFinite(data21))){if(data21 > 600 || isNaN(data21)){validate53.errors = [{instancePath:instancePath+"/chapters/" + i0+"/sections/" + i4+"/minutes",schemaPath:"../common.schema.json#/$defs/minutes/maximum",keyword:"maximum",params:{comparison: "<=", limit: 600},message:"must be <= 600"}];return false;}else {if(data21 < 1 || isNaN(data21)){validate53.errors = [{instancePath:instancePath+"/chapters/" + i0+"/sections/" + i4+"/minutes",schemaPath:"../common.schema.json#/$defs/minutes/minimum",keyword:"minimum",params:{comparison: ">=", limit: 1},message:"must be >= 1"}];return false;}}}}var valid14 = _errs49 === errors;}else {var valid14 = true;}if(valid14){if(data17.activities !== undefined){let data22 = data17.activities;const _errs52 = errors;if(errors === _errs52){if(data22 && typeof data22 == "object" && !Array.isArray(data22)){const _errs54 = errors;for(const key3 in data22){if(!(((key3 === "quizzes") || (key3 === "tasks")) || (key3 === "practice"))){validate53.errors = [{instancePath:instancePath+"/chapters/" + i0+"/sections/" + i4+"/activities",schemaPath:"#/properties/chapters/items/properties/sections/items/properties/activities/additionalProperties",keyword:"additionalProperties",params:{additionalProperty: key3},message:"must NOT have additional properties"}];return false;break;}}if(_errs54 === errors){if(data22.quizzes !== undefined){let data23 = data22.quizzes;const _errs55 = errors;if(errors === _errs55){if(Array.isArray(data23)){var valid18 = true;const len5 = data23.length;for(let i5=0; i5 64){validate53.errors = [{instancePath:instancePath+"/chapters/" + i0+"/sections/" + i4+"/activities/quizzes/" + i5+"/quizId",schemaPath:"../common.schema.json#/$defs/id/maxLength",keyword:"maxLength",params:{limit: 64},message:"must NOT have more than 64 characters"}];return false;}else {if(!pattern18.test(data26)){validate53.errors = [{instancePath:instancePath+"/chapters/" + i0+"/sections/" + i4+"/activities/quizzes/" + i5+"/quizId",schemaPath:"../common.schema.json#/$defs/id/pattern",keyword:"pattern",params:{pattern: "^[a-z0-9]+(?:-[a-z0-9]+)*$"},message:"must match pattern \""+"^[a-z0-9]+(?:-[a-z0-9]+)*$"+"\""}];return false;}}}else {validate53.errors = [{instancePath:instancePath+"/chapters/" + i0+"/sections/" + i4+"/activities/quizzes/" + i5+"/quizId",schemaPath:"../common.schema.json#/$defs/id/type",keyword:"type",params:{type: "string"},message:"must be string"}];return false;}}var valid19 = _errs62 === errors;}else {var valid19 = true;}if(valid19){if(data24.prompt !== undefined){let data27 = data24.prompt;const _errs65 = errors;const _errs66 = errors;const _errs68 = errors;const _errs69 = errors;if(typeof data27 === "string"){if(!pattern8.test(data27)){const err2 = {};if(vErrors === null){vErrors = [err2];}else {vErrors.push(err2);}errors++;}}var valid22 = _errs69 === errors;if(valid22){validate53.errors = [{instancePath:instancePath+"/chapters/" + i0+"/sections/" + i4+"/activities/quizzes/" + i5+"/prompt",schemaPath:"../common.schema.json#/$defs/inlineMarkdown/not",keyword:"not",params:{},message:"must NOT be valid"}];return false;}else {errors = _errs68;if(vErrors !== null){if(_errs68){vErrors.length = _errs68;}else {vErrors = null;}}}if(errors === _errs66){if(typeof data27 === "string"){if(func2(data27) > 4000){validate53.errors = [{instancePath:instancePath+"/chapters/" + i0+"/sections/" + i4+"/activities/quizzes/" + i5+"/prompt",schemaPath:"../common.schema.json#/$defs/inlineMarkdown/maxLength",keyword:"maxLength",params:{limit: 4000},message:"must NOT have more than 4000 characters"}];return false;}else {if(func2(data27) < 1){validate53.errors = [{instancePath:instancePath+"/chapters/" + i0+"/sections/" + i4+"/activities/quizzes/" + i5+"/prompt",schemaPath:"../common.schema.json#/$defs/inlineMarkdown/minLength",keyword:"minLength",params:{limit: 1},message:"must NOT have fewer than 1 characters"}];return false;}}}else {validate53.errors = [{instancePath:instancePath+"/chapters/" + i0+"/sections/" + i4+"/activities/quizzes/" + i5+"/prompt",schemaPath:"../common.schema.json#/$defs/inlineMarkdown/type",keyword:"type",params:{type: "string"},message:"must be string"}];return false;}}var valid19 = _errs65 === errors;}else {var valid19 = true;}if(valid19){if(data24.revision !== undefined){let data28 = data24.revision;const _errs70 = errors;if(errors === _errs70){if(typeof data28 === "string"){if(!pattern46.test(data28)){validate53.errors = [{instancePath:instancePath+"/chapters/" + i0+"/sections/" + i4+"/activities/quizzes/" + i5+"/revision",schemaPath:"#/properties/chapters/items/properties/sections/items/properties/activities/properties/quizzes/items/properties/revision/pattern",keyword:"pattern",params:{pattern: "^[a-f0-9]{1,16}$"},message:"must match pattern \""+"^[a-f0-9]{1,16}$"+"\""}];return false;}}else {validate53.errors = [{instancePath:instancePath+"/chapters/" + i0+"/sections/" + i4+"/activities/quizzes/" + i5+"/revision",schemaPath:"#/properties/chapters/items/properties/sections/items/properties/activities/properties/quizzes/items/properties/revision/type",keyword:"type",params:{type: "string"},message:"must be string"}];return false;}}var valid19 = _errs70 === errors;}else {var valid19 = true;}}}}}}}else {validate53.errors = [{instancePath:instancePath+"/chapters/" + i0+"/sections/" + i4+"/activities/quizzes/" + i5,schemaPath:"#/properties/chapters/items/properties/sections/items/properties/activities/properties/quizzes/items/type",keyword:"type",params:{type: "object"},message:"must be object"}];return false;}}var valid18 = _errs57 === errors;if(!valid18){break;}}}else {validate53.errors = [{instancePath:instancePath+"/chapters/" + i0+"/sections/" + i4+"/activities/quizzes",schemaPath:"#/properties/chapters/items/properties/sections/items/properties/activities/properties/quizzes/type",keyword:"type",params:{type: "array"},message:"must be array"}];return false;}}var valid17 = _errs55 === errors;}else {var valid17 = true;}if(valid17){if(data22.tasks !== undefined){let data29 = data22.tasks;const _errs72 = errors;if(errors === _errs72){if(Array.isArray(data29)){var valid23 = true;const len6 = data29.length;for(let i6=0; i6 64){validate53.errors = [{instancePath:instancePath+"/chapters/" + i0+"/sections/" + i4+"/activities/tasks/" + i6+"/id",schemaPath:"../common.schema.json#/$defs/id/maxLength",keyword:"maxLength",params:{limit: 64},message:"must NOT have more than 64 characters"}];return false;}else {if(!pattern18.test(data31)){validate53.errors = [{instancePath:instancePath+"/chapters/" + i0+"/sections/" + i4+"/activities/tasks/" + i6+"/id",schemaPath:"../common.schema.json#/$defs/id/pattern",keyword:"pattern",params:{pattern: "^[a-z0-9]+(?:-[a-z0-9]+)*$"},message:"must match pattern \""+"^[a-z0-9]+(?:-[a-z0-9]+)*$"+"\""}];return false;}}}else {validate53.errors = [{instancePath:instancePath+"/chapters/" + i0+"/sections/" + i4+"/activities/tasks/" + i6+"/id",schemaPath:"../common.schema.json#/$defs/id/type",keyword:"type",params:{type: "string"},message:"must be string"}];return false;}}var valid24 = _errs77 === errors;}else {var valid24 = true;}if(valid24){if(data30.labId !== undefined){let data32 = data30.labId;const _errs80 = errors;const _errs81 = errors;if(errors === _errs81){if(typeof data32 === "string"){if(func2(data32) > 64){validate53.errors = [{instancePath:instancePath+"/chapters/" + i0+"/sections/" + i4+"/activities/tasks/" + i6+"/labId",schemaPath:"../common.schema.json#/$defs/id/maxLength",keyword:"maxLength",params:{limit: 64},message:"must NOT have more than 64 characters"}];return false;}else {if(!pattern18.test(data32)){validate53.errors = [{instancePath:instancePath+"/chapters/" + i0+"/sections/" + i4+"/activities/tasks/" + i6+"/labId",schemaPath:"../common.schema.json#/$defs/id/pattern",keyword:"pattern",params:{pattern: "^[a-z0-9]+(?:-[a-z0-9]+)*$"},message:"must match pattern \""+"^[a-z0-9]+(?:-[a-z0-9]+)*$"+"\""}];return false;}}}else {validate53.errors = [{instancePath:instancePath+"/chapters/" + i0+"/sections/" + i4+"/activities/tasks/" + i6+"/labId",schemaPath:"../common.schema.json#/$defs/id/type",keyword:"type",params:{type: "string"},message:"must be string"}];return false;}}var valid24 = _errs80 === errors;}else {var valid24 = true;}if(valid24){if(data30.title !== undefined){const _errs83 = errors;if(!(validate55(data30.title, {instancePath:instancePath+"/chapters/" + i0+"/sections/" + i4+"/activities/tasks/" + i6+"/title",parentData:data30,parentDataProperty:"title",rootData,dynamicAnchors}))){vErrors = vErrors === null ? validate55.errors : vErrors.concat(validate55.errors);errors = vErrors.length;}var valid24 = _errs83 === errors;}else {var valid24 = true;}}}}}}else {validate53.errors = [{instancePath:instancePath+"/chapters/" + i0+"/sections/" + i4+"/activities/tasks/" + i6,schemaPath:"#/properties/chapters/items/properties/sections/items/properties/activities/properties/tasks/items/type",keyword:"type",params:{type: "object"},message:"must be object"}];return false;}}var valid23 = _errs74 === errors;if(!valid23){break;}}}else {validate53.errors = [{instancePath:instancePath+"/chapters/" + i0+"/sections/" + i4+"/activities/tasks",schemaPath:"#/properties/chapters/items/properties/sections/items/properties/activities/properties/tasks/type",keyword:"type",params:{type: "array"},message:"must be array"}];return false;}}var valid17 = _errs72 === errors;}else {var valid17 = true;}if(valid17){if(data22.practice !== undefined){let data34 = data22.practice;const _errs84 = errors;if(errors === _errs84){if(Array.isArray(data34)){var valid27 = true;const len7 = data34.length;for(let i7=0; i7 64){validate53.errors = [{instancePath:instancePath+"/chapters/" + i0+"/sections/" + i4+"/activities/practice/" + i7+"/id",schemaPath:"../common.schema.json#/$defs/id/maxLength",keyword:"maxLength",params:{limit: 64},message:"must NOT have more than 64 characters"}];return false;}else {if(!pattern18.test(data36)){validate53.errors = [{instancePath:instancePath+"/chapters/" + i0+"/sections/" + i4+"/activities/practice/" + i7+"/id",schemaPath:"../common.schema.json#/$defs/id/pattern",keyword:"pattern",params:{pattern: "^[a-z0-9]+(?:-[a-z0-9]+)*$"},message:"must match pattern \""+"^[a-z0-9]+(?:-[a-z0-9]+)*$"+"\""}];return false;}}}else {validate53.errors = [{instancePath:instancePath+"/chapters/" + i0+"/sections/" + i4+"/activities/practice/" + i7+"/id",schemaPath:"../common.schema.json#/$defs/id/type",keyword:"type",params:{type: "string"},message:"must be string"}];return false;}}var valid28 = _errs89 === errors;}else {var valid28 = true;}if(valid28){if(data35.objective !== undefined){let data37 = data35.objective;const _errs92 = errors;const _errs93 = errors;if(errors === _errs93){if(typeof data37 === "string"){if(!pattern41.test(data37)){validate53.errors = [{instancePath:instancePath+"/chapters/" + i0+"/sections/" + i4+"/activities/practice/" + i7+"/objective",schemaPath:"../common.schema.json#/$defs/objectiveId/pattern",keyword:"pattern",params:{pattern: "^ch[0-9]{2}\\.[a-z0-9]+(?:-[a-z0-9]+)*$"},message:"must match pattern \""+"^ch[0-9]{2}\\.[a-z0-9]+(?:-[a-z0-9]+)*$"+"\""}];return false;}}else {validate53.errors = [{instancePath:instancePath+"/chapters/" + i0+"/sections/" + i4+"/activities/practice/" + i7+"/objective",schemaPath:"../common.schema.json#/$defs/objectiveId/type",keyword:"type",params:{type: "string"},message:"must be string"}];return false;}}var valid28 = _errs92 === errors;}else {var valid28 = true;}if(valid28){if(data35.title !== undefined){const _errs95 = errors;if(!(validate55(data35.title, {instancePath:instancePath+"/chapters/" + i0+"/sections/" + i4+"/activities/practice/" + i7+"/title",parentData:data35,parentDataProperty:"title",rootData,dynamicAnchors}))){vErrors = vErrors === null ? validate55.errors : vErrors.concat(validate55.errors);errors = vErrors.length;}var valid28 = _errs95 === errors;}else {var valid28 = true;}}}}}}else {validate53.errors = [{instancePath:instancePath+"/chapters/" + i0+"/sections/" + i4+"/activities/practice/" + i7,schemaPath:"#/properties/chapters/items/properties/sections/items/properties/activities/properties/practice/items/type",keyword:"type",params:{type: "object"},message:"must be object"}];return false;}}var valid27 = _errs86 === errors;if(!valid27){break;}}}else {validate53.errors = [{instancePath:instancePath+"/chapters/" + i0+"/sections/" + i4+"/activities/practice",schemaPath:"#/properties/chapters/items/properties/sections/items/properties/activities/properties/practice/type",keyword:"type",params:{type: "array"},message:"must be array"}];return false;}}var valid17 = _errs84 === errors;}else {var valid17 = true;}}}}}else {validate53.errors = [{instancePath:instancePath+"/chapters/" + i0+"/sections/" + i4+"/activities",schemaPath:"#/properties/chapters/items/properties/sections/items/properties/activities/type",keyword:"type",params:{type: "object"},message:"must be object"}];return false;}}var valid14 = _errs52 === errors;}else {var valid14 = true;}}}}}}}}else {validate53.errors = [{instancePath:instancePath+"/chapters/" + i0+"/sections/" + i4,schemaPath:"#/properties/chapters/items/properties/sections/items/type",keyword:"type",params:{type: "object"},message:"must be object"}];return false;}}var valid13 = _errs41 === errors;if(!valid13){break;}}}}else {validate53.errors = [{instancePath:instancePath+"/chapters/" + i0+"/sections",schemaPath:"#/properties/chapters/items/properties/sections/type",keyword:"type",params:{type: "array"},message:"must be array"}];return false;}}var valid3 = _errs39 === errors;}else {var valid3 = true;}}}}}}}}}}}}}else {validate53.errors = [{instancePath:instancePath+"/chapters/" + i0,schemaPath:"#/properties/chapters/items/type",keyword:"type",params:{type: "object"},message:"must be object"}];return false;}}var valid2 = _errs7 === errors;if(!valid2){break;}}}}else {validate53.errors = [{instancePath:instancePath+"/chapters",schemaPath:"#/properties/chapters/type",keyword:"type",params:{type: "array"},message:"must be array"}];return false;}}var valid0 = _errs5 === errors;}else {var valid0 = true;}if(valid0){if(data.objectives !== undefined){const _errs96 = errors;if(!(validate61(data.objectives, {instancePath:instancePath+"/objectives",parentData:data,parentDataProperty:"objectives",rootData,dynamicAnchors}))){vErrors = vErrors === null ? validate61.errors : vErrors.concat(validate61.errors);errors = vErrors.length;}var valid0 = _errs96 === errors;}else {var valid0 = true;}if(valid0){if(data.pages !== undefined){let data40 = data.pages;const _errs97 = errors;if(errors === _errs97){if(data40 && typeof data40 == "object" && !Array.isArray(data40)){for(const key7 in data40){const _errs99 = errors;if(typeof key7 === "string"){if(!pattern56.test(key7)){const err3 = {instancePath:instancePath+"/pages",schemaPath:"#/properties/pages/propertyNames/pattern",keyword:"pattern",params:{pattern: "^(?:ch[0-9]{2}/[a-z0-9]+(?:-[a-z0-9]+)*|home|details)$"},message:"must match pattern \""+"^(?:ch[0-9]{2}/[a-z0-9]+(?:-[a-z0-9]+)*|home|details)$"+"\"",propertyName:key7};if(vErrors === null){vErrors = [err3];}else {vErrors.push(err3);}errors++;}}var valid31 = _errs99 === errors;if(!valid31){const err4 = {instancePath:instancePath+"/pages",schemaPath:"#/properties/pages/propertyNames",keyword:"propertyNames",params:{propertyName: key7},message:"property name must be valid"};if(vErrors === null){vErrors = [err4];}else {vErrors.push(err4);}errors++;validate53.errors = vErrors;return false;break;}}if(valid31){for(const key8 in data40){let data41 = data40[key8];const _errs101 = errors;const _errs102 = errors;if(errors === _errs102){if(typeof data41 === "string"){if(!pattern17.test(data41)){validate53.errors = [{instancePath:instancePath+"/pages/" + key8.replace(/~/g, "~0").replace(/\//g, "~1"),schemaPath:"site.schema.json#/$defs/hashedUrl/pattern",keyword:"pattern",params:{pattern: "^(?:p/[a-z0-9-]+/)?(?:[a-z]+/)*[a-z0-9-]+\\.[a-f0-9]{8,64}\\.(?:json|sig)$"},message:"must match pattern \""+"^(?:p/[a-z0-9-]+/)?(?:[a-z]+/)*[a-z0-9-]+\\.[a-f0-9]{8,64}\\.(?:json|sig)$"+"\""}];return false;}}else {validate53.errors = [{instancePath:instancePath+"/pages/" + key8.replace(/~/g, "~0").replace(/\//g, "~1"),schemaPath:"site.schema.json#/$defs/hashedUrl/type",keyword:"type",params:{type: "string"},message:"must be string"}];return false;}}var valid32 = _errs101 === errors;if(!valid32){break;}}}}else {validate53.errors = [{instancePath:instancePath+"/pages",schemaPath:"#/properties/pages/type",keyword:"type",params:{type: "object"},message:"must be object"}];return false;}}var valid0 = _errs97 === errors;}else {var valid0 = true;}if(valid0){if(data.search !== undefined){let data42 = data.search;const _errs104 = errors;const _errs105 = errors;if(errors === _errs105){if(typeof data42 === "string"){if(!pattern17.test(data42)){validate53.errors = [{instancePath:instancePath+"/search",schemaPath:"site.schema.json#/$defs/hashedUrl/pattern",keyword:"pattern",params:{pattern: "^(?:p/[a-z0-9-]+/)?(?:[a-z]+/)*[a-z0-9-]+\\.[a-f0-9]{8,64}\\.(?:json|sig)$"},message:"must match pattern \""+"^(?:p/[a-z0-9-]+/)?(?:[a-z]+/)*[a-z0-9-]+\\.[a-f0-9]{8,64}\\.(?:json|sig)$"+"\""}];return false;}}else {validate53.errors = [{instancePath:instancePath+"/search",schemaPath:"site.schema.json#/$defs/hashedUrl/type",keyword:"type",params:{type: "string"},message:"must be string"}];return false;}}var valid0 = _errs104 === errors;}else {var valid0 = true;}if(valid0){if(data.legacy !== undefined){let data43 = data.legacy;const _errs107 = errors;const _errs108 = errors;if(errors === _errs108){if(typeof data43 === "string"){if(!pattern17.test(data43)){validate53.errors = [{instancePath:instancePath+"/legacy",schemaPath:"site.schema.json#/$defs/hashedUrl/pattern",keyword:"pattern",params:{pattern: "^(?:p/[a-z0-9-]+/)?(?:[a-z]+/)*[a-z0-9-]+\\.[a-f0-9]{8,64}\\.(?:json|sig)$"},message:"must match pattern \""+"^(?:p/[a-z0-9-]+/)?(?:[a-z]+/)*[a-z0-9-]+\\.[a-f0-9]{8,64}\\.(?:json|sig)$"+"\""}];return false;}}else {validate53.errors = [{instancePath:instancePath+"/legacy",schemaPath:"site.schema.json#/$defs/hashedUrl/type",keyword:"type",params:{type: "string"},message:"must be string"}];return false;}}var valid0 = _errs107 === errors;}else {var valid0 = true;}if(valid0){if(data.labReports !== undefined){let data44 = data.labReports;const _errs110 = errors;if(errors === _errs110){if(data44 && typeof data44 == "object" && !Array.isArray(data44)){let missing6;if((data44.exercises === undefined) && (missing6 = "exercises")){validate53.errors = [{instancePath:instancePath+"/labReports",schemaPath:"#/properties/labReports/required",keyword:"required",params:{missingProperty: missing6},message:"must have required property '"+missing6+"'"}];return false;}else {const _errs112 = errors;for(const key9 in data44){if(!(key9 === "exercises")){validate53.errors = [{instancePath:instancePath+"/labReports",schemaPath:"#/properties/labReports/additionalProperties",keyword:"additionalProperties",params:{additionalProperty: key9},message:"must NOT have additional properties"}];return false;break;}}if(_errs112 === errors){if(data44.exercises !== undefined){let data45 = data44.exercises;const _errs113 = errors;if(errors === _errs113){if(data45 && typeof data45 == "object" && !Array.isArray(data45)){if(Object.keys(data45).length > 500){validate53.errors = [{instancePath:instancePath+"/labReports/exercises",schemaPath:"#/properties/labReports/properties/exercises/maxProperties",keyword:"maxProperties",params:{limit: 500},message:"must NOT have more than 500 properties"}];return false;}else {for(const key10 in data45){const _errs115 = errors;const _errs116 = errors;if(errors === _errs116){if(typeof key10 === "string"){if(func2(key10) > 64){const err5 = {instancePath:instancePath+"/labReports/exercises",schemaPath:"../common.schema.json#/$defs/id/maxLength",keyword:"maxLength",params:{limit: 64},message:"must NOT have more than 64 characters",propertyName:key10};if(vErrors === null){vErrors = [err5];}else {vErrors.push(err5);}errors++;}else {if(!pattern18.test(key10)){const err6 = {instancePath:instancePath+"/labReports/exercises",schemaPath:"../common.schema.json#/$defs/id/pattern",keyword:"pattern",params:{pattern: "^[a-z0-9]+(?:-[a-z0-9]+)*$"},message:"must match pattern \""+"^[a-z0-9]+(?:-[a-z0-9]+)*$"+"\"",propertyName:key10};if(vErrors === null){vErrors = [err6];}else {vErrors.push(err6);}errors++;}}}else {const err7 = {instancePath:instancePath+"/labReports/exercises",schemaPath:"../common.schema.json#/$defs/id/type",keyword:"type",params:{type: "string"},message:"must be string",propertyName:key10};if(vErrors === null){vErrors = [err7];}else {vErrors.push(err7);}errors++;}}var valid37 = _errs115 === errors;if(!valid37){const err8 = {instancePath:instancePath+"/labReports/exercises",schemaPath:"#/properties/labReports/properties/exercises/propertyNames",keyword:"propertyNames",params:{propertyName: key10},message:"property name must be valid"};if(vErrors === null){vErrors = [err8];}else {vErrors.push(err8);}errors++;validate53.errors = vErrors;return false;break;}}if(valid37){for(const key11 in data45){let data46 = data45[key11];const _errs119 = errors;if(errors === _errs119){if(data46 && typeof data46 == "object" && !Array.isArray(data46)){let missing7;if((((data46.version === undefined) && (missing7 = "version")) || ((data46.lesson === undefined) && (missing7 = "lesson"))) || ((data46.checkpoints === undefined) && (missing7 = "checkpoints"))){validate53.errors = [{instancePath:instancePath+"/labReports/exercises/" + key11.replace(/~/g, "~0").replace(/\//g, "~1"),schemaPath:"#/properties/labReports/properties/exercises/additionalProperties/required",keyword:"required",params:{missingProperty: missing7},message:"must have required property '"+missing7+"'"}];return false;}else {const _errs121 = errors;for(const key12 in data46){if(!(((key12 === "version") || (key12 === "lesson")) || (key12 === "checkpoints"))){validate53.errors = [{instancePath:instancePath+"/labReports/exercises/" + key11.replace(/~/g, "~0").replace(/\//g, "~1"),schemaPath:"#/properties/labReports/properties/exercises/additionalProperties/additionalProperties",keyword:"additionalProperties",params:{additionalProperty: key12},message:"must NOT have additional properties"}];return false;break;}}if(_errs121 === errors){if(data46.version !== undefined){let data47 = data46.version;const _errs122 = errors;if(!(((typeof data47 == "number") && (!(data47 % 1) && !isNaN(data47))) && (isFinite(data47)))){validate53.errors = [{instancePath:instancePath+"/labReports/exercises/" + key11.replace(/~/g, "~0").replace(/\//g, "~1")+"/version",schemaPath:"#/properties/labReports/properties/exercises/additionalProperties/properties/version/type",keyword:"type",params:{type: "integer"},message:"must be integer"}];return false;}if(errors === _errs122){if((typeof data47 == "number") && (isFinite(data47))){if(data47 < 1 || isNaN(data47)){validate53.errors = [{instancePath:instancePath+"/labReports/exercises/" + key11.replace(/~/g, "~0").replace(/\//g, "~1")+"/version",schemaPath:"#/properties/labReports/properties/exercises/additionalProperties/properties/version/minimum",keyword:"minimum",params:{comparison: ">=", limit: 1},message:"must be >= 1"}];return false;}}}var valid40 = _errs122 === errors;}else {var valid40 = true;}if(valid40){if(data46.lesson !== undefined){let data48 = data46.lesson;const _errs124 = errors;if(errors === _errs124){if(typeof data48 === "string"){if(!pattern61.test(data48)){validate53.errors = [{instancePath:instancePath+"/labReports/exercises/" + key11.replace(/~/g, "~0").replace(/\//g, "~1")+"/lesson",schemaPath:"#/properties/labReports/properties/exercises/additionalProperties/properties/lesson/pattern",keyword:"pattern",params:{pattern: "^#/ch[0-9]{2}/[a-z0-9-]+$"},message:"must match pattern \""+"^#/ch[0-9]{2}/[a-z0-9-]+$"+"\""}];return false;}}else {validate53.errors = [{instancePath:instancePath+"/labReports/exercises/" + key11.replace(/~/g, "~0").replace(/\//g, "~1")+"/lesson",schemaPath:"#/properties/labReports/properties/exercises/additionalProperties/properties/lesson/type",keyword:"type",params:{type: "string"},message:"must be string"}];return false;}}var valid40 = _errs124 === errors;}else {var valid40 = true;}if(valid40){if(data46.checkpoints !== undefined){let data49 = data46.checkpoints;const _errs126 = errors;if(errors === _errs126){if(data49 && typeof data49 == "object" && !Array.isArray(data49)){let missing8;if((data49.final === undefined) && (missing8 = "final")){validate53.errors = [{instancePath:instancePath+"/labReports/exercises/" + key11.replace(/~/g, "~0").replace(/\//g, "~1")+"/checkpoints",schemaPath:"#/properties/labReports/properties/exercises/additionalProperties/properties/checkpoints/required",keyword:"required",params:{missingProperty: missing8},message:"must have required property '"+missing8+"'"}];return false;}else {for(const key13 in data49){let data50 = data49[key13];const _errs129 = errors;if(errors === _errs129){if(data50 && typeof data50 == "object" && !Array.isArray(data50)){let missing9;if(((((data50.files === undefined) && (missing9 = "files")) || ((data50.groups === undefined) && (missing9 = "groups"))) || ((data50.local === undefined) && (missing9 = "local"))) || ((data50.probes === undefined) && (missing9 = "probes"))){validate53.errors = [{instancePath:instancePath+"/labReports/exercises/" + key11.replace(/~/g, "~0").replace(/\//g, "~1")+"/checkpoints/" + key13.replace(/~/g, "~0").replace(/\//g, "~1"),schemaPath:"#/properties/labReports/properties/exercises/additionalProperties/properties/checkpoints/additionalProperties/required",keyword:"required",params:{missingProperty: missing9},message:"must have required property '"+missing9+"'"}];return false;}else {const _errs131 = errors;for(const key14 in data50){if(!((((key14 === "files") || (key14 === "groups")) || (key14 === "local")) || (key14 === "probes"))){validate53.errors = [{instancePath:instancePath+"/labReports/exercises/" + key11.replace(/~/g, "~0").replace(/\//g, "~1")+"/checkpoints/" + key13.replace(/~/g, "~0").replace(/\//g, "~1"),schemaPath:"#/properties/labReports/properties/exercises/additionalProperties/properties/checkpoints/additionalProperties/additionalProperties",keyword:"additionalProperties",params:{additionalProperty: key14},message:"must NOT have additional properties"}];return false;break;}}if(_errs131 === errors){if(data50.files !== undefined){let data51 = data50.files;const _errs132 = errors;if(errors === _errs132){if(Array.isArray(data51)){if(data51.length > 500){validate53.errors = [{instancePath:instancePath+"/labReports/exercises/" + key11.replace(/~/g, "~0").replace(/\//g, "~1")+"/checkpoints/" + key13.replace(/~/g, "~0").replace(/\//g, "~1")+"/files",schemaPath:"#/properties/labReports/properties/exercises/additionalProperties/properties/checkpoints/additionalProperties/properties/files/maxItems",keyword:"maxItems",params:{limit: 500},message:"must NOT have more than 500 items"}];return false;}else {var valid43 = true;const len8 = data51.length;for(let i8=0; i8 240){validate53.errors = [{instancePath:instancePath+"/labReports/exercises/" + key11.replace(/~/g, "~0").replace(/\//g, "~1")+"/checkpoints/" + key13.replace(/~/g, "~0").replace(/\//g, "~1")+"/files/" + i8,schemaPath:"#/properties/labReports/properties/exercises/additionalProperties/properties/checkpoints/additionalProperties/properties/files/items/maxLength",keyword:"maxLength",params:{limit: 240},message:"must NOT have more than 240 characters"}];return false;}}else {validate53.errors = [{instancePath:instancePath+"/labReports/exercises/" + key11.replace(/~/g, "~0").replace(/\//g, "~1")+"/checkpoints/" + key13.replace(/~/g, "~0").replace(/\//g, "~1")+"/files/" + i8,schemaPath:"#/properties/labReports/properties/exercises/additionalProperties/properties/checkpoints/additionalProperties/properties/files/items/type",keyword:"type",params:{type: "string"},message:"must be string"}];return false;}}var valid43 = _errs134 === errors;if(!valid43){break;}}}}else {validate53.errors = [{instancePath:instancePath+"/labReports/exercises/" + key11.replace(/~/g, "~0").replace(/\//g, "~1")+"/checkpoints/" + key13.replace(/~/g, "~0").replace(/\//g, "~1")+"/files",schemaPath:"#/properties/labReports/properties/exercises/additionalProperties/properties/checkpoints/additionalProperties/properties/files/type",keyword:"type",params:{type: "array"},message:"must be array"}];return false;}}var valid42 = _errs132 === errors;}else {var valid42 = true;}if(valid42){if(data50.groups !== undefined){let data53 = data50.groups;const _errs136 = errors;if(errors === _errs136){if(data53 && typeof data53 == "object" && !Array.isArray(data53)){for(const key15 in data53){let data54 = data53[key15];const _errs139 = errors;if(errors === _errs139){if(Array.isArray(data54)){if(data54.length > 500){validate53.errors = [{instancePath:instancePath+"/labReports/exercises/" + key11.replace(/~/g, "~0").replace(/\//g, "~1")+"/checkpoints/" + key13.replace(/~/g, "~0").replace(/\//g, "~1")+"/groups/" + key15.replace(/~/g, "~0").replace(/\//g, "~1"),schemaPath:"#/properties/labReports/properties/exercises/additionalProperties/properties/checkpoints/additionalProperties/properties/groups/additionalProperties/maxItems",keyword:"maxItems",params:{limit: 500},message:"must NOT have more than 500 items"}];return false;}else {var valid45 = true;const len9 = data54.length;for(let i9=0; i9 240){validate53.errors = [{instancePath:instancePath+"/labReports/exercises/" + key11.replace(/~/g, "~0").replace(/\//g, "~1")+"/checkpoints/" + key13.replace(/~/g, "~0").replace(/\//g, "~1")+"/groups/" + key15.replace(/~/g, "~0").replace(/\//g, "~1")+"/" + i9,schemaPath:"#/properties/labReports/properties/exercises/additionalProperties/properties/checkpoints/additionalProperties/properties/groups/additionalProperties/items/maxLength",keyword:"maxLength",params:{limit: 240},message:"must NOT have more than 240 characters"}];return false;}}else {validate53.errors = [{instancePath:instancePath+"/labReports/exercises/" + key11.replace(/~/g, "~0").replace(/\//g, "~1")+"/checkpoints/" + key13.replace(/~/g, "~0").replace(/\//g, "~1")+"/groups/" + key15.replace(/~/g, "~0").replace(/\//g, "~1")+"/" + i9,schemaPath:"#/properties/labReports/properties/exercises/additionalProperties/properties/checkpoints/additionalProperties/properties/groups/additionalProperties/items/type",keyword:"type",params:{type: "string"},message:"must be string"}];return false;}}var valid45 = _errs141 === errors;if(!valid45){break;}}}}else {validate53.errors = [{instancePath:instancePath+"/labReports/exercises/" + key11.replace(/~/g, "~0").replace(/\//g, "~1")+"/checkpoints/" + key13.replace(/~/g, "~0").replace(/\//g, "~1")+"/groups/" + key15.replace(/~/g, "~0").replace(/\//g, "~1"),schemaPath:"#/properties/labReports/properties/exercises/additionalProperties/properties/checkpoints/additionalProperties/properties/groups/additionalProperties/type",keyword:"type",params:{type: "array"},message:"must be array"}];return false;}}var valid44 = _errs139 === errors;if(!valid44){break;}}}else {validate53.errors = [{instancePath:instancePath+"/labReports/exercises/" + key11.replace(/~/g, "~0").replace(/\//g, "~1")+"/checkpoints/" + key13.replace(/~/g, "~0").replace(/\//g, "~1")+"/groups",schemaPath:"#/properties/labReports/properties/exercises/additionalProperties/properties/checkpoints/additionalProperties/properties/groups/type",keyword:"type",params:{type: "object"},message:"must be object"}];return false;}}var valid42 = _errs136 === errors;}else {var valid42 = true;}if(valid42){if(data50.local !== undefined){let data56 = data50.local;const _errs143 = errors;if(errors === _errs143){if(Array.isArray(data56)){if(data56.length > 40){validate53.errors = [{instancePath:instancePath+"/labReports/exercises/" + key11.replace(/~/g, "~0").replace(/\//g, "~1")+"/checkpoints/" + key13.replace(/~/g, "~0").replace(/\//g, "~1")+"/local",schemaPath:"#/properties/labReports/properties/exercises/additionalProperties/properties/checkpoints/additionalProperties/properties/local/maxItems",keyword:"maxItems",params:{limit: 40},message:"must NOT have more than 40 items"}];return false;}else {var valid46 = true;const len10 = data56.length;for(let i10=0; i10 64){validate53.errors = [{instancePath:instancePath+"/labReports/exercises/" + key11.replace(/~/g, "~0").replace(/\//g, "~1")+"/checkpoints/" + key13.replace(/~/g, "~0").replace(/\//g, "~1")+"/local/" + i10+"/id",schemaPath:"../common.schema.json#/$defs/id/maxLength",keyword:"maxLength",params:{limit: 64},message:"must NOT have more than 64 characters"}];return false;}else {if(!pattern18.test(data58)){validate53.errors = [{instancePath:instancePath+"/labReports/exercises/" + key11.replace(/~/g, "~0").replace(/\//g, "~1")+"/checkpoints/" + key13.replace(/~/g, "~0").replace(/\//g, "~1")+"/local/" + i10+"/id",schemaPath:"../common.schema.json#/$defs/id/pattern",keyword:"pattern",params:{pattern: "^[a-z0-9]+(?:-[a-z0-9]+)*$"},message:"must match pattern \""+"^[a-z0-9]+(?:-[a-z0-9]+)*$"+"\""}];return false;}}}else {validate53.errors = [{instancePath:instancePath+"/labReports/exercises/" + key11.replace(/~/g, "~0").replace(/\//g, "~1")+"/checkpoints/" + key13.replace(/~/g, "~0").replace(/\//g, "~1")+"/local/" + i10+"/id",schemaPath:"../common.schema.json#/$defs/id/type",keyword:"type",params:{type: "string"},message:"must be string"}];return false;}}}}}}else {validate53.errors = [{instancePath:instancePath+"/labReports/exercises/" + key11.replace(/~/g, "~0").replace(/\//g, "~1")+"/checkpoints/" + key13.replace(/~/g, "~0").replace(/\//g, "~1")+"/local/" + i10,schemaPath:"#/properties/labReports/properties/exercises/additionalProperties/properties/checkpoints/additionalProperties/properties/local/items/type",keyword:"type",params:{type: "object"},message:"must be object"}];return false;}}var valid46 = _errs145 === errors;if(!valid46){break;}}}}else {validate53.errors = [{instancePath:instancePath+"/labReports/exercises/" + key11.replace(/~/g, "~0").replace(/\//g, "~1")+"/checkpoints/" + key13.replace(/~/g, "~0").replace(/\//g, "~1")+"/local",schemaPath:"#/properties/labReports/properties/exercises/additionalProperties/properties/checkpoints/additionalProperties/properties/local/type",keyword:"type",params:{type: "array"},message:"must be array"}];return false;}}var valid42 = _errs143 === errors;}else {var valid42 = true;}if(valid42){if(data50.probes !== undefined){let data59 = data50.probes;const _errs151 = errors;if(errors === _errs151){if(Array.isArray(data59)){if(data59.length > 40){validate53.errors = [{instancePath:instancePath+"/labReports/exercises/" + key11.replace(/~/g, "~0").replace(/\//g, "~1")+"/checkpoints/" + key13.replace(/~/g, "~0").replace(/\//g, "~1")+"/probes",schemaPath:"#/properties/labReports/properties/exercises/additionalProperties/properties/checkpoints/additionalProperties/properties/probes/maxItems",keyword:"maxItems",params:{limit: 40},message:"must NOT have more than 40 items"}];return false;}else {var valid49 = true;const len11 = data59.length;for(let i11=0; i11 64){validate53.errors = [{instancePath:instancePath+"/labReports/exercises/" + key11.replace(/~/g, "~0").replace(/\//g, "~1")+"/checkpoints/" + key13.replace(/~/g, "~0").replace(/\//g, "~1")+"/probes/" + i11+"/id",schemaPath:"../common.schema.json#/$defs/id/maxLength",keyword:"maxLength",params:{limit: 64},message:"must NOT have more than 64 characters"}];return false;}else {if(!pattern18.test(data61)){validate53.errors = [{instancePath:instancePath+"/labReports/exercises/" + key11.replace(/~/g, "~0").replace(/\//g, "~1")+"/checkpoints/" + key13.replace(/~/g, "~0").replace(/\//g, "~1")+"/probes/" + i11+"/id",schemaPath:"../common.schema.json#/$defs/id/pattern",keyword:"pattern",params:{pattern: "^[a-z0-9]+(?:-[a-z0-9]+)*$"},message:"must match pattern \""+"^[a-z0-9]+(?:-[a-z0-9]+)*$"+"\""}];return false;}}}else {validate53.errors = [{instancePath:instancePath+"/labReports/exercises/" + key11.replace(/~/g, "~0").replace(/\//g, "~1")+"/checkpoints/" + key13.replace(/~/g, "~0").replace(/\//g, "~1")+"/probes/" + i11+"/id",schemaPath:"../common.schema.json#/$defs/id/type",keyword:"type",params:{type: "string"},message:"must be string"}];return false;}}var valid50 = _errs156 === errors;}else {var valid50 = true;}if(valid50){if(data60.targets !== undefined){let data62 = data60.targets;const _errs159 = errors;if(errors === _errs159){if(Array.isArray(data62)){if(data62.length > 500){validate53.errors = [{instancePath:instancePath+"/labReports/exercises/" + key11.replace(/~/g, "~0").replace(/\//g, "~1")+"/checkpoints/" + key13.replace(/~/g, "~0").replace(/\//g, "~1")+"/probes/" + i11+"/targets",schemaPath:"#/properties/labReports/properties/exercises/additionalProperties/properties/checkpoints/additionalProperties/properties/probes/items/properties/targets/maxItems",keyword:"maxItems",params:{limit: 500},message:"must NOT have more than 500 items"}];return false;}else {var valid52 = true;const len12 = data62.length;for(let i12=0; i12 240){validate53.errors = [{instancePath:instancePath+"/labReports/exercises/" + key11.replace(/~/g, "~0").replace(/\//g, "~1")+"/checkpoints/" + key13.replace(/~/g, "~0").replace(/\//g, "~1")+"/probes/" + i11+"/targets/" + i12,schemaPath:"#/properties/labReports/properties/exercises/additionalProperties/properties/checkpoints/additionalProperties/properties/probes/items/properties/targets/items/maxLength",keyword:"maxLength",params:{limit: 240},message:"must NOT have more than 240 characters"}];return false;}}else {validate53.errors = [{instancePath:instancePath+"/labReports/exercises/" + key11.replace(/~/g, "~0").replace(/\//g, "~1")+"/checkpoints/" + key13.replace(/~/g, "~0").replace(/\//g, "~1")+"/probes/" + i11+"/targets/" + i12,schemaPath:"#/properties/labReports/properties/exercises/additionalProperties/properties/checkpoints/additionalProperties/properties/probes/items/properties/targets/items/type",keyword:"type",params:{type: "string"},message:"must be string"}];return false;}}var valid52 = _errs161 === errors;if(!valid52){break;}}}}else {validate53.errors = [{instancePath:instancePath+"/labReports/exercises/" + key11.replace(/~/g, "~0").replace(/\//g, "~1")+"/checkpoints/" + key13.replace(/~/g, "~0").replace(/\//g, "~1")+"/probes/" + i11+"/targets",schemaPath:"#/properties/labReports/properties/exercises/additionalProperties/properties/checkpoints/additionalProperties/properties/probes/items/properties/targets/type",keyword:"type",params:{type: "array"},message:"must be array"}];return false;}}var valid50 = _errs159 === errors;}else {var valid50 = true;}}}}}else {validate53.errors = [{instancePath:instancePath+"/labReports/exercises/" + key11.replace(/~/g, "~0").replace(/\//g, "~1")+"/checkpoints/" + key13.replace(/~/g, "~0").replace(/\//g, "~1")+"/probes/" + i11,schemaPath:"#/properties/labReports/properties/exercises/additionalProperties/properties/checkpoints/additionalProperties/properties/probes/items/type",keyword:"type",params:{type: "object"},message:"must be object"}];return false;}}var valid49 = _errs153 === errors;if(!valid49){break;}}}}else {validate53.errors = [{instancePath:instancePath+"/labReports/exercises/" + key11.replace(/~/g, "~0").replace(/\//g, "~1")+"/checkpoints/" + key13.replace(/~/g, "~0").replace(/\//g, "~1")+"/probes",schemaPath:"#/properties/labReports/properties/exercises/additionalProperties/properties/checkpoints/additionalProperties/properties/probes/type",keyword:"type",params:{type: "array"},message:"must be array"}];return false;}}var valid42 = _errs151 === errors;}else {var valid42 = true;}}}}}}}else {validate53.errors = [{instancePath:instancePath+"/labReports/exercises/" + key11.replace(/~/g, "~0").replace(/\//g, "~1")+"/checkpoints/" + key13.replace(/~/g, "~0").replace(/\//g, "~1"),schemaPath:"#/properties/labReports/properties/exercises/additionalProperties/properties/checkpoints/additionalProperties/type",keyword:"type",params:{type: "object"},message:"must be object"}];return false;}}var valid41 = _errs129 === errors;if(!valid41){break;}}}}else {validate53.errors = [{instancePath:instancePath+"/labReports/exercises/" + key11.replace(/~/g, "~0").replace(/\//g, "~1")+"/checkpoints",schemaPath:"#/properties/labReports/properties/exercises/additionalProperties/properties/checkpoints/type",keyword:"type",params:{type: "object"},message:"must be object"}];return false;}}var valid40 = _errs126 === errors;}else {var valid40 = true;}}}}}}else {validate53.errors = [{instancePath:instancePath+"/labReports/exercises/" + key11.replace(/~/g, "~0").replace(/\//g, "~1"),schemaPath:"#/properties/labReports/properties/exercises/additionalProperties/type",keyword:"type",params:{type: "object"},message:"must be object"}];return false;}}var valid39 = _errs119 === errors;if(!valid39){break;}}}}}else {validate53.errors = [{instancePath:instancePath+"/labReports/exercises",schemaPath:"#/properties/labReports/properties/exercises/type",keyword:"type",params:{type: "object"},message:"must be object"}];return false;}}}}}}else {validate53.errors = [{instancePath:instancePath+"/labReports",schemaPath:"#/properties/labReports/type",keyword:"type",params:{type: "object"},message:"must be object"}];return false;}}var valid0 = _errs110 === errors;}else {var valid0 = true;}}}}}}}}}}}else {validate53.errors = [{instancePath,schemaPath:"#/type",keyword:"type",params:{type: "object"},message:"must be object"}];return false;}}validate53.errors = vErrors;return errors === 0;}validate53.evaluated = {"props":true,"dynamicProps":false,"dynamicItems":false};export const validatePage = validate63;const schema97 = {"$schema":"https://json-schema.org/draft/2020-12/schema","$id":"https://kernelpath.dev/schema/v1/bundle/page.schema.json","type":"object","additionalProperties":false,"required":["apiVersion","key","title","kind","toc","data","tree"],"properties":{"apiVersion":{"$ref":"../common.schema.json#/$defs/apiVersion"},"key":{"type":"string","pattern":"^(?:ch[0-9]{2}/[a-z0-9]+(?:-[a-z0-9]+)*|home|details|site-home)$"},"title":{"$ref":"../common.schema.json#/$defs/shortText"},"kind":{"enum":["lesson","lab","quiz","summary","reference","landing"]},"minutes":{"$ref":"../common.schema.json#/$defs/minutes"},"description":{"$ref":"../common.schema.json#/$defs/text"},"toc":{"type":"array","maxItems":200,"items":{"type":"object","additionalProperties":false,"required":["id","text","depth"],"properties":{"id":{"$ref":"#/$defs/anchorId"},"text":{"$ref":"../common.schema.json#/$defs/text"},"depth":{"enum":[2,3]}}}},"data":{"type":"object","propertyNames":{"$ref":"../common.schema.json#/$defs/dataRef"}},"tree":{"$ref":"#/$defs/children"},"eyebrow":{"$ref":"../common.schema.json#/$defs/shortText"}},"$defs":{"anchorId":{"type":"string","pattern":"^[a-z0-9_-][a-z0-9_-]{0,119}$"},"children":{"type":"array","maxItems":5000,"items":{"$ref":"#/$defs/node"}},"node":{"oneOf":[{"$ref":"#/$defs/text"},{"$ref":"#/$defs/element"},{"$ref":"#/$defs/code"},{"$ref":"#/$defs/tag"}]},"text":{"type":"object","additionalProperties":false,"required":["t","v"],"properties":{"t":{"const":"text"},"v":{"type":"string","maxLength":20000}}},"element":{"type":"object","additionalProperties":false,"required":["t","tag"],"properties":{"t":{"const":"el"},"tag":{"enum":["p","h2","h3","h4","ul","ol","li","blockquote","strong","em","del","code","a","br","hr","table","thead","tbody","tr","th","td","img"]},"attrs":{"type":"object","additionalProperties":false,"properties":{"id":{"$ref":"#/$defs/anchorId"},"href":{"$ref":"../common.schema.json#/$defs/url"},"src":{"type":"string","pattern":"^assets/[a-f0-9]{8,64}\\.(?:png|jpe?g|webp|avif|gif|svg)$"},"alt":{"type":"string","maxLength":300},"align":{"enum":["left","center","right"]},"start":{"type":"integer","minimum":0},"checked":{"type":"boolean"}}},"c":{"$ref":"#/$defs/children"}}},"code":{"type":"object","additionalProperties":false,"required":["t","lang","lines"],"properties":{"t":{"const":"code"},"lang":{"type":"string","pattern":"^[a-z0-9+#-]{1,24}$"},"title":{"$ref":"../common.schema.json#/$defs/shortText"},"highlight":{"type":"array","items":{"type":"integer","minimum":1}},"lines":{"type":"array","maxItems":5000,"items":{"type":"array","maxItems":2000,"items":{"type":"object","additionalProperties":false,"required":["v"],"properties":{"v":{"type":"string","maxLength":20000},"l":{"$ref":"#/$defs/tokenColor"},"d":{"$ref":"#/$defs/tokenColor"},"i":{"const":true}}}}}}},"tokenColor":{"type":"string","pattern":"^#[0-9a-fA-F]{6}$"},"tag":{"type":"object","additionalProperties":false,"required":["t","name"],"properties":{"t":{"const":"tag"},"name":{"type":"string","enum":["admin-module-finder","annotated-yaml","assessment-timer","block-flow","build-roadmap","callout","card","cards","check-mode-sim","column","columns","command-vs-module","condition-playground","cron-builder","custom-fact-builder","data-explorer","diagram","error-decoder","fact-gathering","facts-explorer","feature-grid","fetch-layout","file-edit-simulator","file-module-chooser","flashcards","flow-map","fqcn-explorer","glob-tester","glossary","grep-tester","group-vars-resolver","handler-timeline","hero","host-pattern-tester","idempotency-demo","inventory-explorer","kbd","lab","lab-challenge","lab-finish","lab-network-map","lab-notes","lab-setup","lead","loop-unroller","magic-variables","mode-calculator","module-explorer","objectives","play-order","play-recap","practice","precedence-resolver","program-cards","project-tree","quiz","range-expander","reader-variables","readiness-checklist","reuse-simulator","reveal","role-anatomy","role-var-resolver","run-stages","shell-practice","snapshot-chain","starter-files","stat-explorer","step","steps","tab","tabs","task","task-outcome","template-playground","term","terminal-demo","variable-name-checker","variable-precedence","variable-substitution","variant","variant-group","variant-switch","vault-commands","verbosity","yaml-multiline"]},"attrs":{"type":"object","propertyNames":{"pattern":"^[a-z][a-zA-Z0-9]*$"},"additionalProperties":{"oneOf":[{"type":"string","maxLength":4000},{"type":"number"},{"type":"boolean"},{"type":"array","maxItems":50,"items":{"type":"string","maxLength":400}}]}},"c":{"$ref":"#/$defs/children"}}}}};const schema103 = {"type":"string","pattern":"^[a-z0-9_-][a-z0-9_-]{0,119}$"};const schema105 = {"type":"string","pattern":"^[a-z0-9]+(?:-[a-z0-9]+)*$","maxLength":64};const pattern64 = new RegExp("^(?:ch[0-9]{2}/[a-z0-9]+(?:-[a-z0-9]+)*|home|details|site-home)$", "u");const pattern67 = new RegExp("^[a-z0-9_-][a-z0-9_-]{0,119}$", "u");function validate64(data, {instancePath="", parentData, parentDataProperty, rootData=data, dynamicAnchors={}}={}){let vErrors = null;let errors = 0;const evaluated0 = validate64.evaluated;if(evaluated0.dynamicProps){evaluated0.props = undefined;}if(evaluated0.dynamicItems){evaluated0.items = undefined;}const _errs0 = errors;const _errs2 = errors;const _errs3 = errors;if(typeof data === "string"){if(!pattern5.test(data)){const err0 = {};if(vErrors === null){vErrors = [err0];}else {vErrors.push(err0);}errors++;}}var valid1 = _errs3 === errors;if(valid1){validate64.errors = [{instancePath,schemaPath:"#/$defs/text/not",keyword:"not",params:{},message:"must NOT be valid"}];return false;}else {errors = _errs2;if(vErrors !== null){if(_errs2){vErrors.length = _errs2;}else {vErrors = null;}}}if(errors === _errs0){if(typeof data === "string"){if(func2(data) > 2000){validate64.errors = [{instancePath,schemaPath:"#/$defs/text/maxLength",keyword:"maxLength",params:{limit: 2000},message:"must NOT have more than 2000 characters"}];return false;}else {if(func2(data) < 1){validate64.errors = [{instancePath,schemaPath:"#/$defs/text/minLength",keyword:"minLength",params:{limit: 1},message:"must NOT have fewer than 1 characters"}];return false;}}}else {validate64.errors = [{instancePath,schemaPath:"#/$defs/text/type",keyword:"type",params:{type: "string"},message:"must be string"}];return false;}}if(errors === 0){if(typeof data === "string"){if(func2(data) > 160){validate64.errors = [{instancePath,schemaPath:"#/maxLength",keyword:"maxLength",params:{limit: 160},message:"must NOT have more than 160 characters"}];return false;}}else {validate64.errors = [{instancePath,schemaPath:"#/type",keyword:"type",params:{type: "string"},message:"must be string"}];return false;}}validate64.errors = vErrors;return errors === 0;}validate64.evaluated = {"dynamicProps":false,"dynamicItems":false};const schema106 = {"type":"array","maxItems":5000,"items":{"$ref":"#/$defs/node"}};const schema107 = {"oneOf":[{"$ref":"#/$defs/text"},{"$ref":"#/$defs/element"},{"$ref":"#/$defs/code"},{"$ref":"#/$defs/tag"}]};const schema108 = {"type":"object","additionalProperties":false,"required":["t","v"],"properties":{"t":{"const":"text"},"v":{"type":"string","maxLength":20000}}};const schema109 = {"type":"object","additionalProperties":false,"required":["t","tag"],"properties":{"t":{"const":"el"},"tag":{"enum":["p","h2","h3","h4","ul","ol","li","blockquote","strong","em","del","code","a","br","hr","table","thead","tbody","tr","th","td","img"]},"attrs":{"type":"object","additionalProperties":false,"properties":{"id":{"$ref":"#/$defs/anchorId"},"href":{"$ref":"../common.schema.json#/$defs/url"},"src":{"type":"string","pattern":"^assets/[a-f0-9]{8,64}\\.(?:png|jpe?g|webp|avif|gif|svg)$"},"alt":{"type":"string","maxLength":300},"align":{"enum":["left","center","right"]},"start":{"type":"integer","minimum":0},"checked":{"type":"boolean"}}},"c":{"$ref":"#/$defs/children"}}};const pattern72 = new RegExp("^assets/[a-f0-9]{8,64}\\.(?:png|jpe?g|webp|avif|gif|svg)$", "u");const wrapper0 = {validate: validate66};function validate68(data, {instancePath="", parentData, parentDataProperty, rootData=data, dynamicAnchors={}}={}){let vErrors = null;let errors = 0;const evaluated0 = validate68.evaluated;if(evaluated0.dynamicProps){evaluated0.props = undefined;}if(evaluated0.dynamicItems){evaluated0.items = undefined;}if(errors === 0){if(data && typeof data == "object" && !Array.isArray(data)){let missing0;if(((data.t === undefined) && (missing0 = "t")) || ((data.tag === undefined) && (missing0 = "tag"))){validate68.errors = [{instancePath,schemaPath:"#/required",keyword:"required",params:{missingProperty: missing0},message:"must have required property '"+missing0+"'"}];return false;}else {const _errs1 = errors;for(const key0 in data){if(!((((key0 === "t") || (key0 === "tag")) || (key0 === "attrs")) || (key0 === "c"))){validate68.errors = [{instancePath,schemaPath:"#/additionalProperties",keyword:"additionalProperties",params:{additionalProperty: key0},message:"must NOT have additional properties"}];return false;break;}}if(_errs1 === errors){if(data.t !== undefined){const _errs2 = errors;if("el" !== data.t){validate68.errors = [{instancePath:instancePath+"/t",schemaPath:"#/properties/t/const",keyword:"const",params:{allowedValue: "el"},message:"must be equal to constant"}];return false;}var valid0 = _errs2 === errors;}else {var valid0 = true;}if(valid0){if(data.tag !== undefined){let data1 = data.tag;const _errs3 = errors;if(!((((((((((((((((((((((data1 === "p") || (data1 === "h2")) || (data1 === "h3")) || (data1 === "h4")) || (data1 === "ul")) || (data1 === "ol")) || (data1 === "li")) || (data1 === "blockquote")) || (data1 === "strong")) || (data1 === "em")) || (data1 === "del")) || (data1 === "code")) || (data1 === "a")) || (data1 === "br")) || (data1 === "hr")) || (data1 === "table")) || (data1 === "thead")) || (data1 === "tbody")) || (data1 === "tr")) || (data1 === "th")) || (data1 === "td")) || (data1 === "img"))){validate68.errors = [{instancePath:instancePath+"/tag",schemaPath:"#/properties/tag/enum",keyword:"enum",params:{allowedValues: schema109.properties.tag.enum},message:"must be equal to one of the allowed values"}];return false;}var valid0 = _errs3 === errors;}else {var valid0 = true;}if(valid0){if(data.attrs !== undefined){let data2 = data.attrs;const _errs4 = errors;if(errors === _errs4){if(data2 && typeof data2 == "object" && !Array.isArray(data2)){const _errs6 = errors;for(const key1 in data2){if(!(((((((key1 === "id") || (key1 === "href")) || (key1 === "src")) || (key1 === "alt")) || (key1 === "align")) || (key1 === "start")) || (key1 === "checked"))){validate68.errors = [{instancePath:instancePath+"/attrs",schemaPath:"#/properties/attrs/additionalProperties",keyword:"additionalProperties",params:{additionalProperty: key1},message:"must NOT have additional properties"}];return false;break;}}if(_errs6 === errors){if(data2.id !== undefined){let data3 = data2.id;const _errs7 = errors;const _errs8 = errors;if(errors === _errs8){if(typeof data3 === "string"){if(!pattern67.test(data3)){validate68.errors = [{instancePath:instancePath+"/attrs/id",schemaPath:"#/$defs/anchorId/pattern",keyword:"pattern",params:{pattern: "^[a-z0-9_-][a-z0-9_-]{0,119}$"},message:"must match pattern \""+"^[a-z0-9_-][a-z0-9_-]{0,119}$"+"\""}];return false;}}else {validate68.errors = [{instancePath:instancePath+"/attrs/id",schemaPath:"#/$defs/anchorId/type",keyword:"type",params:{type: "string"},message:"must be string"}];return false;}}var valid1 = _errs7 === errors;}else {var valid1 = true;}if(valid1){if(data2.href !== undefined){let data4 = data2.href;const _errs10 = errors;const _errs11 = errors;if(errors === _errs11){if(typeof data4 === "string"){if(func2(data4) > 2048){validate68.errors = [{instancePath:instancePath+"/attrs/href",schemaPath:"../common.schema.json#/$defs/url/maxLength",keyword:"maxLength",params:{limit: 2048},message:"must NOT have more than 2048 characters"}];return false;}else {if(!pattern7.test(data4)){validate68.errors = [{instancePath:instancePath+"/attrs/href",schemaPath:"../common.schema.json#/$defs/url/pattern",keyword:"pattern",params:{pattern: "^(?:#[/A-Za-z0-9._~%!$&'()*+,;=:@?#-]*|lab/[a-z0-9][a-z0-9._/-]*|https://[^\\s<>\"]+|mailto:[^\\s<>\"]+)$"},message:"must match pattern \""+"^(?:#[/A-Za-z0-9._~%!$&'()*+,;=:@?#-]*|lab/[a-z0-9][a-z0-9._/-]*|https://[^\\s<>\"]+|mailto:[^\\s<>\"]+)$"+"\""}];return false;}}}else {validate68.errors = [{instancePath:instancePath+"/attrs/href",schemaPath:"../common.schema.json#/$defs/url/type",keyword:"type",params:{type: "string"},message:"must be string"}];return false;}}var valid1 = _errs10 === errors;}else {var valid1 = true;}if(valid1){if(data2.src !== undefined){let data5 = data2.src;const _errs13 = errors;if(errors === _errs13){if(typeof data5 === "string"){if(!pattern72.test(data5)){validate68.errors = [{instancePath:instancePath+"/attrs/src",schemaPath:"#/properties/attrs/properties/src/pattern",keyword:"pattern",params:{pattern: "^assets/[a-f0-9]{8,64}\\.(?:png|jpe?g|webp|avif|gif|svg)$"},message:"must match pattern \""+"^assets/[a-f0-9]{8,64}\\.(?:png|jpe?g|webp|avif|gif|svg)$"+"\""}];return false;}}else {validate68.errors = [{instancePath:instancePath+"/attrs/src",schemaPath:"#/properties/attrs/properties/src/type",keyword:"type",params:{type: "string"},message:"must be string"}];return false;}}var valid1 = _errs13 === errors;}else {var valid1 = true;}if(valid1){if(data2.alt !== undefined){let data6 = data2.alt;const _errs15 = errors;if(errors === _errs15){if(typeof data6 === "string"){if(func2(data6) > 300){validate68.errors = [{instancePath:instancePath+"/attrs/alt",schemaPath:"#/properties/attrs/properties/alt/maxLength",keyword:"maxLength",params:{limit: 300},message:"must NOT have more than 300 characters"}];return false;}}else {validate68.errors = [{instancePath:instancePath+"/attrs/alt",schemaPath:"#/properties/attrs/properties/alt/type",keyword:"type",params:{type: "string"},message:"must be string"}];return false;}}var valid1 = _errs15 === errors;}else {var valid1 = true;}if(valid1){if(data2.align !== undefined){let data7 = data2.align;const _errs17 = errors;if(!(((data7 === "left") || (data7 === "center")) || (data7 === "right"))){validate68.errors = [{instancePath:instancePath+"/attrs/align",schemaPath:"#/properties/attrs/properties/align/enum",keyword:"enum",params:{allowedValues: schema109.properties.attrs.properties.align.enum},message:"must be equal to one of the allowed values"}];return false;}var valid1 = _errs17 === errors;}else {var valid1 = true;}if(valid1){if(data2.start !== undefined){let data8 = data2.start;const _errs18 = errors;if(!(((typeof data8 == "number") && (!(data8 % 1) && !isNaN(data8))) && (isFinite(data8)))){validate68.errors = [{instancePath:instancePath+"/attrs/start",schemaPath:"#/properties/attrs/properties/start/type",keyword:"type",params:{type: "integer"},message:"must be integer"}];return false;}if(errors === _errs18){if((typeof data8 == "number") && (isFinite(data8))){if(data8 < 0 || isNaN(data8)){validate68.errors = [{instancePath:instancePath+"/attrs/start",schemaPath:"#/properties/attrs/properties/start/minimum",keyword:"minimum",params:{comparison: ">=", limit: 0},message:"must be >= 0"}];return false;}}}var valid1 = _errs18 === errors;}else {var valid1 = true;}if(valid1){if(data2.checked !== undefined){const _errs20 = errors;if(typeof data2.checked !== "boolean"){validate68.errors = [{instancePath:instancePath+"/attrs/checked",schemaPath:"#/properties/attrs/properties/checked/type",keyword:"type",params:{type: "boolean"},message:"must be boolean"}];return false;}var valid1 = _errs20 === errors;}else {var valid1 = true;}}}}}}}}}else {validate68.errors = [{instancePath:instancePath+"/attrs",schemaPath:"#/properties/attrs/type",keyword:"type",params:{type: "object"},message:"must be object"}];return false;}}var valid0 = _errs4 === errors;}else {var valid0 = true;}if(valid0){if(data.c !== undefined){const _errs22 = errors;if(!(wrapper0.validate(data.c, {instancePath:instancePath+"/c",parentData:data,parentDataProperty:"c",rootData,dynamicAnchors}))){vErrors = vErrors === null ? wrapper0.validate.errors : vErrors.concat(wrapper0.validate.errors);errors = vErrors.length;}var valid0 = _errs22 === errors;}else {var valid0 = true;}}}}}}}else {validate68.errors = [{instancePath,schemaPath:"#/type",keyword:"type",params:{type: "object"},message:"must be object"}];return false;}}validate68.errors = vErrors;return errors === 0;}validate68.evaluated = {"props":true,"dynamicProps":false,"dynamicItems":false};const schema112 = {"type":"object","additionalProperties":false,"required":["t","lang","lines"],"properties":{"t":{"const":"code"},"lang":{"type":"string","pattern":"^[a-z0-9+#-]{1,24}$"},"title":{"$ref":"../common.schema.json#/$defs/shortText"},"highlight":{"type":"array","items":{"type":"integer","minimum":1}},"lines":{"type":"array","maxItems":5000,"items":{"type":"array","maxItems":2000,"items":{"type":"object","additionalProperties":false,"required":["v"],"properties":{"v":{"type":"string","maxLength":20000},"l":{"$ref":"#/$defs/tokenColor"},"d":{"$ref":"#/$defs/tokenColor"},"i":{"const":true}}}}}}};const schema113 = {"type":"string","pattern":"^#[0-9a-fA-F]{6}$"};const pattern73 = new RegExp("^[a-z0-9+#-]{1,24}$", "u");const pattern74 = new RegExp("^#[0-9a-fA-F]{6}$", "u");function validate70(data, {instancePath="", parentData, parentDataProperty, rootData=data, dynamicAnchors={}}={}){let vErrors = null;let errors = 0;const evaluated0 = validate70.evaluated;if(evaluated0.dynamicProps){evaluated0.props = undefined;}if(evaluated0.dynamicItems){evaluated0.items = undefined;}if(errors === 0){if(data && typeof data == "object" && !Array.isArray(data)){let missing0;if((((data.t === undefined) && (missing0 = "t")) || ((data.lang === undefined) && (missing0 = "lang"))) || ((data.lines === undefined) && (missing0 = "lines"))){validate70.errors = [{instancePath,schemaPath:"#/required",keyword:"required",params:{missingProperty: missing0},message:"must have required property '"+missing0+"'"}];return false;}else {const _errs1 = errors;for(const key0 in data){if(!(((((key0 === "t") || (key0 === "lang")) || (key0 === "title")) || (key0 === "highlight")) || (key0 === "lines"))){validate70.errors = [{instancePath,schemaPath:"#/additionalProperties",keyword:"additionalProperties",params:{additionalProperty: key0},message:"must NOT have additional properties"}];return false;break;}}if(_errs1 === errors){if(data.t !== undefined){const _errs2 = errors;if("code" !== data.t){validate70.errors = [{instancePath:instancePath+"/t",schemaPath:"#/properties/t/const",keyword:"const",params:{allowedValue: "code"},message:"must be equal to constant"}];return false;}var valid0 = _errs2 === errors;}else {var valid0 = true;}if(valid0){if(data.lang !== undefined){let data1 = data.lang;const _errs3 = errors;if(errors === _errs3){if(typeof data1 === "string"){if(!pattern73.test(data1)){validate70.errors = [{instancePath:instancePath+"/lang",schemaPath:"#/properties/lang/pattern",keyword:"pattern",params:{pattern: "^[a-z0-9+#-]{1,24}$"},message:"must match pattern \""+"^[a-z0-9+#-]{1,24}$"+"\""}];return false;}}else {validate70.errors = [{instancePath:instancePath+"/lang",schemaPath:"#/properties/lang/type",keyword:"type",params:{type: "string"},message:"must be string"}];return false;}}var valid0 = _errs3 === errors;}else {var valid0 = true;}if(valid0){if(data.title !== undefined){const _errs5 = errors;if(!(validate64(data.title, {instancePath:instancePath+"/title",parentData:data,parentDataProperty:"title",rootData,dynamicAnchors}))){vErrors = vErrors === null ? validate64.errors : vErrors.concat(validate64.errors);errors = vErrors.length;}var valid0 = _errs5 === errors;}else {var valid0 = true;}if(valid0){if(data.highlight !== undefined){let data3 = data.highlight;const _errs6 = errors;if(errors === _errs6){if(Array.isArray(data3)){var valid1 = true;const len0 = data3.length;for(let i0=0; i0=", limit: 1},message:"must be >= 1"}];return false;}}}var valid1 = _errs8 === errors;if(!valid1){break;}}}else {validate70.errors = [{instancePath:instancePath+"/highlight",schemaPath:"#/properties/highlight/type",keyword:"type",params:{type: "array"},message:"must be array"}];return false;}}var valid0 = _errs6 === errors;}else {var valid0 = true;}if(valid0){if(data.lines !== undefined){let data5 = data.lines;const _errs10 = errors;if(errors === _errs10){if(Array.isArray(data5)){if(data5.length > 5000){validate70.errors = [{instancePath:instancePath+"/lines",schemaPath:"#/properties/lines/maxItems",keyword:"maxItems",params:{limit: 5000},message:"must NOT have more than 5000 items"}];return false;}else {var valid2 = true;const len1 = data5.length;for(let i1=0; i1 2000){validate70.errors = [{instancePath:instancePath+"/lines/" + i1,schemaPath:"#/properties/lines/items/maxItems",keyword:"maxItems",params:{limit: 2000},message:"must NOT have more than 2000 items"}];return false;}else {var valid3 = true;const len2 = data6.length;for(let i2=0; i2 20000){validate70.errors = [{instancePath:instancePath+"/lines/" + i1+"/" + i2+"/v",schemaPath:"#/properties/lines/items/items/properties/v/maxLength",keyword:"maxLength",params:{limit: 20000},message:"must NOT have more than 20000 characters"}];return false;}}else {validate70.errors = [{instancePath:instancePath+"/lines/" + i1+"/" + i2+"/v",schemaPath:"#/properties/lines/items/items/properties/v/type",keyword:"type",params:{type: "string"},message:"must be string"}];return false;}}var valid4 = _errs17 === errors;}else {var valid4 = true;}if(valid4){if(data7.l !== undefined){let data9 = data7.l;const _errs19 = errors;const _errs20 = errors;if(errors === _errs20){if(typeof data9 === "string"){if(!pattern74.test(data9)){validate70.errors = [{instancePath:instancePath+"/lines/" + i1+"/" + i2+"/l",schemaPath:"#/$defs/tokenColor/pattern",keyword:"pattern",params:{pattern: "^#[0-9a-fA-F]{6}$"},message:"must match pattern \""+"^#[0-9a-fA-F]{6}$"+"\""}];return false;}}else {validate70.errors = [{instancePath:instancePath+"/lines/" + i1+"/" + i2+"/l",schemaPath:"#/$defs/tokenColor/type",keyword:"type",params:{type: "string"},message:"must be string"}];return false;}}var valid4 = _errs19 === errors;}else {var valid4 = true;}if(valid4){if(data7.d !== undefined){let data10 = data7.d;const _errs22 = errors;const _errs23 = errors;if(errors === _errs23){if(typeof data10 === "string"){if(!pattern74.test(data10)){validate70.errors = [{instancePath:instancePath+"/lines/" + i1+"/" + i2+"/d",schemaPath:"#/$defs/tokenColor/pattern",keyword:"pattern",params:{pattern: "^#[0-9a-fA-F]{6}$"},message:"must match pattern \""+"^#[0-9a-fA-F]{6}$"+"\""}];return false;}}else {validate70.errors = [{instancePath:instancePath+"/lines/" + i1+"/" + i2+"/d",schemaPath:"#/$defs/tokenColor/type",keyword:"type",params:{type: "string"},message:"must be string"}];return false;}}var valid4 = _errs22 === errors;}else {var valid4 = true;}if(valid4){if(data7.i !== undefined){const _errs25 = errors;if(true !== data7.i){validate70.errors = [{instancePath:instancePath+"/lines/" + i1+"/" + i2+"/i",schemaPath:"#/properties/lines/items/items/properties/i/const",keyword:"const",params:{allowedValue: true},message:"must be equal to constant"}];return false;}var valid4 = _errs25 === errors;}else {var valid4 = true;}}}}}}}else {validate70.errors = [{instancePath:instancePath+"/lines/" + i1+"/" + i2,schemaPath:"#/properties/lines/items/items/type",keyword:"type",params:{type: "object"},message:"must be object"}];return false;}}var valid3 = _errs14 === errors;if(!valid3){break;}}}}else {validate70.errors = [{instancePath:instancePath+"/lines/" + i1,schemaPath:"#/properties/lines/items/type",keyword:"type",params:{type: "array"},message:"must be array"}];return false;}}var valid2 = _errs12 === errors;if(!valid2){break;}}}}else {validate70.errors = [{instancePath:instancePath+"/lines",schemaPath:"#/properties/lines/type",keyword:"type",params:{type: "array"},message:"must be array"}];return false;}}var valid0 = _errs10 === errors;}else {var valid0 = true;}}}}}}}}else {validate70.errors = [{instancePath,schemaPath:"#/type",keyword:"type",params:{type: "object"},message:"must be object"}];return false;}}validate70.errors = vErrors;return errors === 0;}validate70.evaluated = {"props":true,"dynamicProps":false,"dynamicItems":false};const schema115 = {"type":"object","additionalProperties":false,"required":["t","name"],"properties":{"t":{"const":"tag"},"name":{"type":"string","enum":["admin-module-finder","annotated-yaml","assessment-timer","block-flow","build-roadmap","callout","card","cards","check-mode-sim","column","columns","command-vs-module","condition-playground","cron-builder","custom-fact-builder","data-explorer","diagram","error-decoder","fact-gathering","facts-explorer","feature-grid","fetch-layout","file-edit-simulator","file-module-chooser","flashcards","flow-map","fqcn-explorer","glob-tester","glossary","grep-tester","group-vars-resolver","handler-timeline","hero","host-pattern-tester","idempotency-demo","inventory-explorer","kbd","lab","lab-challenge","lab-finish","lab-network-map","lab-notes","lab-setup","lead","loop-unroller","magic-variables","mode-calculator","module-explorer","objectives","play-order","play-recap","practice","precedence-resolver","program-cards","project-tree","quiz","range-expander","reader-variables","readiness-checklist","reuse-simulator","reveal","role-anatomy","role-var-resolver","run-stages","shell-practice","snapshot-chain","starter-files","stat-explorer","step","steps","tab","tabs","task","task-outcome","template-playground","term","terminal-demo","variable-name-checker","variable-precedence","variable-substitution","variant","variant-group","variant-switch","vault-commands","verbosity","yaml-multiline"]},"attrs":{"type":"object","propertyNames":{"pattern":"^[a-z][a-zA-Z0-9]*$"},"additionalProperties":{"oneOf":[{"type":"string","maxLength":4000},{"type":"number"},{"type":"boolean"},{"type":"array","maxItems":50,"items":{"type":"string","maxLength":400}}]}},"c":{"$ref":"#/$defs/children"}}};const pattern76 = new RegExp("^[a-z][a-zA-Z0-9]*$", "u");function validate73(data, {instancePath="", parentData, parentDataProperty, rootData=data, dynamicAnchors={}}={}){let vErrors = null;let errors = 0;const evaluated0 = validate73.evaluated;if(evaluated0.dynamicProps){evaluated0.props = undefined;}if(evaluated0.dynamicItems){evaluated0.items = undefined;}if(errors === 0){if(data && typeof data == "object" && !Array.isArray(data)){let missing0;if(((data.t === undefined) && (missing0 = "t")) || ((data.name === undefined) && (missing0 = "name"))){validate73.errors = [{instancePath,schemaPath:"#/required",keyword:"required",params:{missingProperty: missing0},message:"must have required property '"+missing0+"'"}];return false;}else {const _errs1 = errors;for(const key0 in data){if(!((((key0 === "t") || (key0 === "name")) || (key0 === "attrs")) || (key0 === "c"))){validate73.errors = [{instancePath,schemaPath:"#/additionalProperties",keyword:"additionalProperties",params:{additionalProperty: key0},message:"must NOT have additional properties"}];return false;break;}}if(_errs1 === errors){if(data.t !== undefined){const _errs2 = errors;if("tag" !== data.t){validate73.errors = [{instancePath:instancePath+"/t",schemaPath:"#/properties/t/const",keyword:"const",params:{allowedValue: "tag"},message:"must be equal to constant"}];return false;}var valid0 = _errs2 === errors;}else {var valid0 = true;}if(valid0){if(data.name !== undefined){let data1 = data.name;const _errs3 = errors;if(typeof data1 !== "string"){validate73.errors = [{instancePath:instancePath+"/name",schemaPath:"#/properties/name/type",keyword:"type",params:{type: "string"},message:"must be string"}];return false;}if(!((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((data1 === "admin-module-finder") || (data1 === "annotated-yaml")) || (data1 === "assessment-timer")) || (data1 === "block-flow")) || (data1 === "build-roadmap")) || (data1 === "callout")) || (data1 === "card")) || (data1 === "cards")) || (data1 === "check-mode-sim")) || (data1 === "column")) || (data1 === "columns")) || (data1 === "command-vs-module")) || (data1 === "condition-playground")) || (data1 === "cron-builder")) || (data1 === "custom-fact-builder")) || (data1 === "data-explorer")) || (data1 === "diagram")) || (data1 === "error-decoder")) || (data1 === "fact-gathering")) || (data1 === "facts-explorer")) || (data1 === "feature-grid")) || (data1 === "fetch-layout")) || (data1 === "file-edit-simulator")) || (data1 === "file-module-chooser")) || (data1 === "flashcards")) || (data1 === "flow-map")) || (data1 === "fqcn-explorer")) || (data1 === "glob-tester")) || (data1 === "glossary")) || (data1 === "grep-tester")) || (data1 === "group-vars-resolver")) || (data1 === "handler-timeline")) || (data1 === "hero")) || (data1 === "host-pattern-tester")) || (data1 === "idempotency-demo")) || (data1 === "inventory-explorer")) || (data1 === "kbd")) || (data1 === "lab")) || (data1 === "lab-challenge")) || (data1 === "lab-finish")) || (data1 === "lab-network-map")) || (data1 === "lab-notes")) || (data1 === "lab-setup")) || (data1 === "lead")) || (data1 === "loop-unroller")) || (data1 === "magic-variables")) || (data1 === "mode-calculator")) || (data1 === "module-explorer")) || (data1 === "objectives")) || (data1 === "play-order")) || (data1 === "play-recap")) || (data1 === "practice")) || (data1 === "precedence-resolver")) || (data1 === "program-cards")) || (data1 === "project-tree")) || (data1 === "quiz")) || (data1 === "range-expander")) || (data1 === "reader-variables")) || (data1 === "readiness-checklist")) || (data1 === "reuse-simulator")) || (data1 === "reveal")) || (data1 === "role-anatomy")) || (data1 === "role-var-resolver")) || (data1 === "run-stages")) || (data1 === "shell-practice")) || (data1 === "snapshot-chain")) || (data1 === "starter-files")) || (data1 === "stat-explorer")) || (data1 === "step")) || (data1 === "steps")) || (data1 === "tab")) || (data1 === "tabs")) || (data1 === "task")) || (data1 === "task-outcome")) || (data1 === "template-playground")) || (data1 === "term")) || (data1 === "terminal-demo")) || (data1 === "variable-name-checker")) || (data1 === "variable-precedence")) || (data1 === "variable-substitution")) || (data1 === "variant")) || (data1 === "variant-group")) || (data1 === "variant-switch")) || (data1 === "vault-commands")) || (data1 === "verbosity")) || (data1 === "yaml-multiline"))){validate73.errors = [{instancePath:instancePath+"/name",schemaPath:"#/properties/name/enum",keyword:"enum",params:{allowedValues: schema115.properties.name.enum},message:"must be equal to one of the allowed values"}];return false;}var valid0 = _errs3 === errors;}else {var valid0 = true;}if(valid0){if(data.attrs !== undefined){let data2 = data.attrs;const _errs5 = errors;if(errors === _errs5){if(data2 && typeof data2 == "object" && !Array.isArray(data2)){for(const key1 in data2){const _errs7 = errors;if(typeof key1 === "string"){if(!pattern76.test(key1)){const err0 = {instancePath:instancePath+"/attrs",schemaPath:"#/properties/attrs/propertyNames/pattern",keyword:"pattern",params:{pattern: "^[a-z][a-zA-Z0-9]*$"},message:"must match pattern \""+"^[a-z][a-zA-Z0-9]*$"+"\"",propertyName:key1};if(vErrors === null){vErrors = [err0];}else {vErrors.push(err0);}errors++;}}var valid1 = _errs7 === errors;if(!valid1){const err1 = {instancePath:instancePath+"/attrs",schemaPath:"#/properties/attrs/propertyNames",keyword:"propertyNames",params:{propertyName: key1},message:"property name must be valid"};if(vErrors === null){vErrors = [err1];}else {vErrors.push(err1);}errors++;validate73.errors = vErrors;return false;break;}}if(valid1){for(const key2 in data2){let data3 = data2[key2];const _errs9 = errors;const _errs10 = errors;let valid3 = false;let passing0 = null;const _errs11 = errors;if(errors === _errs11){if(typeof data3 === "string"){if(func2(data3) > 4000){const err2 = {instancePath:instancePath+"/attrs/" + key2.replace(/~/g, "~0").replace(/\//g, "~1"),schemaPath:"#/properties/attrs/additionalProperties/oneOf/0/maxLength",keyword:"maxLength",params:{limit: 4000},message:"must NOT have more than 4000 characters"};if(vErrors === null){vErrors = [err2];}else {vErrors.push(err2);}errors++;}}else {const err3 = {instancePath:instancePath+"/attrs/" + key2.replace(/~/g, "~0").replace(/\//g, "~1"),schemaPath:"#/properties/attrs/additionalProperties/oneOf/0/type",keyword:"type",params:{type: "string"},message:"must be string"};if(vErrors === null){vErrors = [err3];}else {vErrors.push(err3);}errors++;}}var _valid0 = _errs11 === errors;if(_valid0){valid3 = true;passing0 = 0;}const _errs13 = errors;if(!((typeof data3 == "number") && (isFinite(data3)))){const err4 = {instancePath:instancePath+"/attrs/" + key2.replace(/~/g, "~0").replace(/\//g, "~1"),schemaPath:"#/properties/attrs/additionalProperties/oneOf/1/type",keyword:"type",params:{type: "number"},message:"must be number"};if(vErrors === null){vErrors = [err4];}else {vErrors.push(err4);}errors++;}var _valid0 = _errs13 === errors;if(_valid0 && valid3){valid3 = false;passing0 = [passing0, 1];}else {if(_valid0){valid3 = true;passing0 = 1;}const _errs15 = errors;if(typeof data3 !== "boolean"){const err5 = {instancePath:instancePath+"/attrs/" + key2.replace(/~/g, "~0").replace(/\//g, "~1"),schemaPath:"#/properties/attrs/additionalProperties/oneOf/2/type",keyword:"type",params:{type: "boolean"},message:"must be boolean"};if(vErrors === null){vErrors = [err5];}else {vErrors.push(err5);}errors++;}var _valid0 = _errs15 === errors;if(_valid0 && valid3){valid3 = false;passing0 = [passing0, 2];}else {if(_valid0){valid3 = true;passing0 = 2;}const _errs17 = errors;if(errors === _errs17){if(Array.isArray(data3)){if(data3.length > 50){const err6 = {instancePath:instancePath+"/attrs/" + key2.replace(/~/g, "~0").replace(/\//g, "~1"),schemaPath:"#/properties/attrs/additionalProperties/oneOf/3/maxItems",keyword:"maxItems",params:{limit: 50},message:"must NOT have more than 50 items"};if(vErrors === null){vErrors = [err6];}else {vErrors.push(err6);}errors++;}else {var valid4 = true;const len0 = data3.length;for(let i0=0; i0 400){const err7 = {instancePath:instancePath+"/attrs/" + key2.replace(/~/g, "~0").replace(/\//g, "~1")+"/" + i0,schemaPath:"#/properties/attrs/additionalProperties/oneOf/3/items/maxLength",keyword:"maxLength",params:{limit: 400},message:"must NOT have more than 400 characters"};if(vErrors === null){vErrors = [err7];}else {vErrors.push(err7);}errors++;}}else {const err8 = {instancePath:instancePath+"/attrs/" + key2.replace(/~/g, "~0").replace(/\//g, "~1")+"/" + i0,schemaPath:"#/properties/attrs/additionalProperties/oneOf/3/items/type",keyword:"type",params:{type: "string"},message:"must be string"};if(vErrors === null){vErrors = [err8];}else {vErrors.push(err8);}errors++;}}var valid4 = _errs19 === errors;if(!valid4){break;}}}}else {const err9 = {instancePath:instancePath+"/attrs/" + key2.replace(/~/g, "~0").replace(/\//g, "~1"),schemaPath:"#/properties/attrs/additionalProperties/oneOf/3/type",keyword:"type",params:{type: "array"},message:"must be array"};if(vErrors === null){vErrors = [err9];}else {vErrors.push(err9);}errors++;}}var _valid0 = _errs17 === errors;if(_valid0 && valid3){valid3 = false;passing0 = [passing0, 3];}else {if(_valid0){valid3 = true;passing0 = 3;}}}}if(!valid3){const err10 = {instancePath:instancePath+"/attrs/" + key2.replace(/~/g, "~0").replace(/\//g, "~1"),schemaPath:"#/properties/attrs/additionalProperties/oneOf",keyword:"oneOf",params:{passingSchemas: passing0},message:"must match exactly one schema in oneOf"};if(vErrors === null){vErrors = [err10];}else {vErrors.push(err10);}errors++;validate73.errors = vErrors;return false;}else {errors = _errs10;if(vErrors !== null){if(_errs10){vErrors.length = _errs10;}else {vErrors = null;}}}var valid2 = _errs9 === errors;if(!valid2){break;}}}}else {validate73.errors = [{instancePath:instancePath+"/attrs",schemaPath:"#/properties/attrs/type",keyword:"type",params:{type: "object"},message:"must be object"}];return false;}}var valid0 = _errs5 === errors;}else {var valid0 = true;}if(valid0){if(data.c !== undefined){const _errs21 = errors;if(!(wrapper0.validate(data.c, {instancePath:instancePath+"/c",parentData:data,parentDataProperty:"c",rootData,dynamicAnchors}))){vErrors = vErrors === null ? wrapper0.validate.errors : vErrors.concat(wrapper0.validate.errors);errors = vErrors.length;}var valid0 = _errs21 === errors;}else {var valid0 = true;}}}}}}}else {validate73.errors = [{instancePath,schemaPath:"#/type",keyword:"type",params:{type: "object"},message:"must be object"}];return false;}}validate73.errors = vErrors;return errors === 0;}validate73.evaluated = {"props":true,"dynamicProps":false,"dynamicItems":false};function validate67(data, {instancePath="", parentData, parentDataProperty, rootData=data, dynamicAnchors={}}={}){let vErrors = null;let errors = 0;const evaluated0 = validate67.evaluated;if(evaluated0.dynamicProps){evaluated0.props = undefined;}if(evaluated0.dynamicItems){evaluated0.items = undefined;}const _errs0 = errors;let valid0 = false;let passing0 = null;const _errs1 = errors;const _errs2 = errors;if(errors === _errs2){if(data && typeof data == "object" && !Array.isArray(data)){let missing0;if(((data.t === undefined) && (missing0 = "t")) || ((data.v === undefined) && (missing0 = "v"))){const err0 = {instancePath,schemaPath:"#/$defs/text/required",keyword:"required",params:{missingProperty: missing0},message:"must have required property '"+missing0+"'"};if(vErrors === null){vErrors = [err0];}else {vErrors.push(err0);}errors++;}else {const _errs4 = errors;for(const key0 in data){if(!((key0 === "t") || (key0 === "v"))){const err1 = {instancePath,schemaPath:"#/$defs/text/additionalProperties",keyword:"additionalProperties",params:{additionalProperty: key0},message:"must NOT have additional properties"};if(vErrors === null){vErrors = [err1];}else {vErrors.push(err1);}errors++;break;}}if(_errs4 === errors){if(data.t !== undefined){const _errs5 = errors;if("text" !== data.t){const err2 = {instancePath:instancePath+"/t",schemaPath:"#/$defs/text/properties/t/const",keyword:"const",params:{allowedValue: "text"},message:"must be equal to constant"};if(vErrors === null){vErrors = [err2];}else {vErrors.push(err2);}errors++;}var valid2 = _errs5 === errors;}else {var valid2 = true;}if(valid2){if(data.v !== undefined){let data1 = data.v;const _errs6 = errors;if(errors === _errs6){if(typeof data1 === "string"){if(func2(data1) > 20000){const err3 = {instancePath:instancePath+"/v",schemaPath:"#/$defs/text/properties/v/maxLength",keyword:"maxLength",params:{limit: 20000},message:"must NOT have more than 20000 characters"};if(vErrors === null){vErrors = [err3];}else {vErrors.push(err3);}errors++;}}else {const err4 = {instancePath:instancePath+"/v",schemaPath:"#/$defs/text/properties/v/type",keyword:"type",params:{type: "string"},message:"must be string"};if(vErrors === null){vErrors = [err4];}else {vErrors.push(err4);}errors++;}}var valid2 = _errs6 === errors;}else {var valid2 = true;}}}}}else {const err5 = {instancePath,schemaPath:"#/$defs/text/type",keyword:"type",params:{type: "object"},message:"must be object"};if(vErrors === null){vErrors = [err5];}else {vErrors.push(err5);}errors++;}}var _valid0 = _errs1 === errors;if(_valid0){valid0 = true;passing0 = 0;var props0 = true;}const _errs8 = errors;if(!(validate68(data, {instancePath,parentData,parentDataProperty,rootData,dynamicAnchors}))){vErrors = vErrors === null ? validate68.errors : vErrors.concat(validate68.errors);errors = vErrors.length;}var _valid0 = _errs8 === errors;if(_valid0 && valid0){valid0 = false;passing0 = [passing0, 1];}else {if(_valid0){valid0 = true;passing0 = 1;if(props0 !== true){props0 = true;}}const _errs9 = errors;if(!(validate70(data, {instancePath,parentData,parentDataProperty,rootData,dynamicAnchors}))){vErrors = vErrors === null ? validate70.errors : vErrors.concat(validate70.errors);errors = vErrors.length;}var _valid0 = _errs9 === errors;if(_valid0 && valid0){valid0 = false;passing0 = [passing0, 2];}else {if(_valid0){valid0 = true;passing0 = 2;if(props0 !== true){props0 = true;}}const _errs10 = errors;if(!(validate73(data, {instancePath,parentData,parentDataProperty,rootData,dynamicAnchors}))){vErrors = vErrors === null ? validate73.errors : vErrors.concat(validate73.errors);errors = vErrors.length;}var _valid0 = _errs10 === errors;if(_valid0 && valid0){valid0 = false;passing0 = [passing0, 3];}else {if(_valid0){valid0 = true;passing0 = 3;if(props0 !== true){props0 = true;}}}}}if(!valid0){const err6 = {instancePath,schemaPath:"#/oneOf",keyword:"oneOf",params:{passingSchemas: passing0},message:"must match exactly one schema in oneOf"};if(vErrors === null){vErrors = [err6];}else {vErrors.push(err6);}errors++;validate67.errors = vErrors;return false;}else {errors = _errs0;if(vErrors !== null){if(_errs0){vErrors.length = _errs0;}else {vErrors = null;}}}validate67.errors = vErrors;evaluated0.props = props0;return errors === 0;}validate67.evaluated = {"dynamicProps":true,"dynamicItems":false};function validate66(data, {instancePath="", parentData, parentDataProperty, rootData=data, dynamicAnchors={}}={}){let vErrors = null;let errors = 0;const evaluated0 = validate66.evaluated;if(evaluated0.dynamicProps){evaluated0.props = undefined;}if(evaluated0.dynamicItems){evaluated0.items = undefined;}if(errors === 0){if(Array.isArray(data)){if(data.length > 5000){validate66.errors = [{instancePath,schemaPath:"#/maxItems",keyword:"maxItems",params:{limit: 5000},message:"must NOT have more than 5000 items"}];return false;}else {var valid0 = true;const len0 = data.length;for(let i0=0; i0 600 || isNaN(data4)){validate63.errors = [{instancePath:instancePath+"/minutes",schemaPath:"../common.schema.json#/$defs/minutes/maximum",keyword:"maximum",params:{comparison: "<=", limit: 600},message:"must be <= 600"}];return false;}else {if(data4 < 1 || isNaN(data4)){validate63.errors = [{instancePath:instancePath+"/minutes",schemaPath:"../common.schema.json#/$defs/minutes/minimum",keyword:"minimum",params:{comparison: ">=", limit: 1},message:"must be >= 1"}];return false;}}}}var valid0 = _errs8 === errors;}else {var valid0 = true;}if(valid0){if(data.description !== undefined){let data5 = data.description;const _errs11 = errors;const _errs12 = errors;const _errs14 = errors;const _errs15 = errors;if(typeof data5 === "string"){if(!pattern5.test(data5)){const err0 = {};if(vErrors === null){vErrors = [err0];}else {vErrors.push(err0);}errors++;}}var valid4 = _errs15 === errors;if(valid4){validate63.errors = [{instancePath:instancePath+"/description",schemaPath:"../common.schema.json#/$defs/text/not",keyword:"not",params:{},message:"must NOT be valid"}];return false;}else {errors = _errs14;if(vErrors !== null){if(_errs14){vErrors.length = _errs14;}else {vErrors = null;}}}if(errors === _errs12){if(typeof data5 === "string"){if(func2(data5) > 2000){validate63.errors = [{instancePath:instancePath+"/description",schemaPath:"../common.schema.json#/$defs/text/maxLength",keyword:"maxLength",params:{limit: 2000},message:"must NOT have more than 2000 characters"}];return false;}else {if(func2(data5) < 1){validate63.errors = [{instancePath:instancePath+"/description",schemaPath:"../common.schema.json#/$defs/text/minLength",keyword:"minLength",params:{limit: 1},message:"must NOT have fewer than 1 characters"}];return false;}}}else {validate63.errors = [{instancePath:instancePath+"/description",schemaPath:"../common.schema.json#/$defs/text/type",keyword:"type",params:{type: "string"},message:"must be string"}];return false;}}var valid0 = _errs11 === errors;}else {var valid0 = true;}if(valid0){if(data.toc !== undefined){let data6 = data.toc;const _errs16 = errors;if(errors === _errs16){if(Array.isArray(data6)){if(data6.length > 200){validate63.errors = [{instancePath:instancePath+"/toc",schemaPath:"#/properties/toc/maxItems",keyword:"maxItems",params:{limit: 200},message:"must NOT have more than 200 items"}];return false;}else {var valid5 = true;const len0 = data6.length;for(let i0=0; i0 2000){validate63.errors = [{instancePath:instancePath+"/toc/" + i0+"/text",schemaPath:"../common.schema.json#/$defs/text/maxLength",keyword:"maxLength",params:{limit: 2000},message:"must NOT have more than 2000 characters"}];return false;}else {if(func2(data9) < 1){validate63.errors = [{instancePath:instancePath+"/toc/" + i0+"/text",schemaPath:"../common.schema.json#/$defs/text/minLength",keyword:"minLength",params:{limit: 1},message:"must NOT have fewer than 1 characters"}];return false;}}}else {validate63.errors = [{instancePath:instancePath+"/toc/" + i0+"/text",schemaPath:"../common.schema.json#/$defs/text/type",keyword:"type",params:{type: "string"},message:"must be string"}];return false;}}var valid6 = _errs24 === errors;}else {var valid6 = true;}if(valid6){if(data7.depth !== undefined){let data10 = data7.depth;const _errs29 = errors;if(!((data10 === 2) || (data10 === 3))){validate63.errors = [{instancePath:instancePath+"/toc/" + i0+"/depth",schemaPath:"#/properties/toc/items/properties/depth/enum",keyword:"enum",params:{allowedValues: schema97.properties.toc.items.properties.depth.enum},message:"must be equal to one of the allowed values"}];return false;}var valid6 = _errs29 === errors;}else {var valid6 = true;}}}}}}else {validate63.errors = [{instancePath:instancePath+"/toc/" + i0,schemaPath:"#/properties/toc/items/type",keyword:"type",params:{type: "object"},message:"must be object"}];return false;}}var valid5 = _errs18 === errors;if(!valid5){break;}}}}else {validate63.errors = [{instancePath:instancePath+"/toc",schemaPath:"#/properties/toc/type",keyword:"type",params:{type: "array"},message:"must be array"}];return false;}}var valid0 = _errs16 === errors;}else {var valid0 = true;}if(valid0){if(data.data !== undefined){let data11 = data.data;const _errs30 = errors;if(errors === _errs30){if(data11 && typeof data11 == "object" && !Array.isArray(data11)){for(const key2 in data11){const _errs32 = errors;const _errs33 = errors;if(errors === _errs33){if(typeof key2 === "string"){if(func2(key2) > 64){const err2 = {instancePath:instancePath+"/data",schemaPath:"../common.schema.json#/$defs/dataRef/maxLength",keyword:"maxLength",params:{limit: 64},message:"must NOT have more than 64 characters",propertyName:key2};if(vErrors === null){vErrors = [err2];}else {vErrors.push(err2);}errors++;}else {if(!pattern18.test(key2)){const err3 = {instancePath:instancePath+"/data",schemaPath:"../common.schema.json#/$defs/dataRef/pattern",keyword:"pattern",params:{pattern: "^[a-z0-9]+(?:-[a-z0-9]+)*$"},message:"must match pattern \""+"^[a-z0-9]+(?:-[a-z0-9]+)*$"+"\"",propertyName:key2};if(vErrors === null){vErrors = [err3];}else {vErrors.push(err3);}errors++;}}}else {const err4 = {instancePath:instancePath+"/data",schemaPath:"../common.schema.json#/$defs/dataRef/type",keyword:"type",params:{type: "string"},message:"must be string",propertyName:key2};if(vErrors === null){vErrors = [err4];}else {vErrors.push(err4);}errors++;}}var valid10 = _errs32 === errors;if(!valid10){const err5 = {instancePath:instancePath+"/data",schemaPath:"#/properties/data/propertyNames",keyword:"propertyNames",params:{propertyName: key2},message:"property name must be valid"};if(vErrors === null){vErrors = [err5];}else {vErrors.push(err5);}errors++;validate63.errors = vErrors;return false;break;}}}else {validate63.errors = [{instancePath:instancePath+"/data",schemaPath:"#/properties/data/type",keyword:"type",params:{type: "object"},message:"must be object"}];return false;}}var valid0 = _errs30 === errors;}else {var valid0 = true;}if(valid0){if(data.tree !== undefined){const _errs35 = errors;if(!(validate66(data.tree, {instancePath:instancePath+"/tree",parentData:data,parentDataProperty:"tree",rootData,dynamicAnchors}))){vErrors = vErrors === null ? validate66.errors : vErrors.concat(validate66.errors);errors = vErrors.length;}var valid0 = _errs35 === errors;}else {var valid0 = true;}if(valid0){if(data.eyebrow !== undefined){const _errs36 = errors;if(!(validate64(data.eyebrow, {instancePath:instancePath+"/eyebrow",parentData:data,parentDataProperty:"eyebrow",rootData,dynamicAnchors}))){vErrors = vErrors === null ? validate64.errors : vErrors.concat(validate64.errors);errors = vErrors.length;}var valid0 = _errs36 === errors;}else {var valid0 = true;}}}}}}}}}}}}}else {validate63.errors = [{instancePath,schemaPath:"#/type",keyword:"type",params:{type: "object"},message:"must be object"}];return false;}}validate63.errors = vErrors;return errors === 0;}validate63.evaluated = {"props":true,"dynamicProps":false,"dynamicItems":false};export const validateSearch = validate78;const schema116 = {"$schema":"https://json-schema.org/draft/2020-12/schema","$id":"https://kernelpath.dev/schema/v1/bundle/search.schema.json","type":"object","additionalProperties":false,"required":["apiVersion","entries"],"properties":{"apiVersion":{"$ref":"../common.schema.json#/$defs/apiVersion"},"entries":{"type":"array","maxItems":20000,"items":{"type":"object","additionalProperties":false,"required":["page","text"],"properties":{"page":{"type":"string","pattern":"^(?:ch[0-9]{2}/[a-z0-9]+(?:-[a-z0-9]+)*|home|details)$"},"heading":{"type":"string","maxLength":300},"anchor":{"type":"string","pattern":"^[a-z0-9_-][a-z0-9_-]{0,119}$"},"text":{"type":"string","maxLength":100000}}}}}};function validate78(data, {instancePath="", parentData, parentDataProperty, rootData=data, dynamicAnchors={}}={}){/*# sourceURL="https://kernelpath.dev/schema/v1/bundle/search.schema.json" */;let vErrors = null;let errors = 0;const evaluated0 = validate78.evaluated;if(evaluated0.dynamicProps){evaluated0.props = undefined;}if(evaluated0.dynamicItems){evaluated0.items = undefined;}if(errors === 0){if(data && typeof data == "object" && !Array.isArray(data)){let missing0;if(((data.apiVersion === undefined) && (missing0 = "apiVersion")) || ((data.entries === undefined) && (missing0 = "entries"))){validate78.errors = [{instancePath,schemaPath:"#/required",keyword:"required",params:{missingProperty: missing0},message:"must have required property '"+missing0+"'"}];return false;}else {const _errs1 = errors;for(const key0 in data){if(!((key0 === "apiVersion") || (key0 === "entries"))){validate78.errors = [{instancePath,schemaPath:"#/additionalProperties",keyword:"additionalProperties",params:{additionalProperty: key0},message:"must NOT have additional properties"}];return false;break;}}if(_errs1 === errors){if(data.apiVersion !== undefined){const _errs2 = errors;if(1 !== data.apiVersion){validate78.errors = [{instancePath:instancePath+"/apiVersion",schemaPath:"../common.schema.json#/$defs/apiVersion/const",keyword:"const",params:{allowedValue: 1},message:"must be equal to constant"}];return false;}var valid0 = _errs2 === errors;}else {var valid0 = true;}if(valid0){if(data.entries !== undefined){let data1 = data.entries;const _errs4 = errors;if(errors === _errs4){if(Array.isArray(data1)){if(data1.length > 20000){validate78.errors = [{instancePath:instancePath+"/entries",schemaPath:"#/properties/entries/maxItems",keyword:"maxItems",params:{limit: 20000},message:"must NOT have more than 20000 items"}];return false;}else {var valid2 = true;const len0 = data1.length;for(let i0=0; i0 300){validate78.errors = [{instancePath:instancePath+"/entries/" + i0+"/heading",schemaPath:"#/properties/entries/items/properties/heading/maxLength",keyword:"maxLength",params:{limit: 300},message:"must NOT have more than 300 characters"}];return false;}}else {validate78.errors = [{instancePath:instancePath+"/entries/" + i0+"/heading",schemaPath:"#/properties/entries/items/properties/heading/type",keyword:"type",params:{type: "string"},message:"must be string"}];return false;}}var valid3 = _errs11 === errors;}else {var valid3 = true;}if(valid3){if(data2.anchor !== undefined){let data5 = data2.anchor;const _errs13 = errors;if(errors === _errs13){if(typeof data5 === "string"){if(!pattern67.test(data5)){validate78.errors = [{instancePath:instancePath+"/entries/" + i0+"/anchor",schemaPath:"#/properties/entries/items/properties/anchor/pattern",keyword:"pattern",params:{pattern: "^[a-z0-9_-][a-z0-9_-]{0,119}$"},message:"must match pattern \""+"^[a-z0-9_-][a-z0-9_-]{0,119}$"+"\""}];return false;}}else {validate78.errors = [{instancePath:instancePath+"/entries/" + i0+"/anchor",schemaPath:"#/properties/entries/items/properties/anchor/type",keyword:"type",params:{type: "string"},message:"must be string"}];return false;}}var valid3 = _errs13 === errors;}else {var valid3 = true;}if(valid3){if(data2.text !== undefined){let data6 = data2.text;const _errs15 = errors;if(errors === _errs15){if(typeof data6 === "string"){if(func2(data6) > 100000){validate78.errors = [{instancePath:instancePath+"/entries/" + i0+"/text",schemaPath:"#/properties/entries/items/properties/text/maxLength",keyword:"maxLength",params:{limit: 100000},message:"must NOT have more than 100000 characters"}];return false;}}else {validate78.errors = [{instancePath:instancePath+"/entries/" + i0+"/text",schemaPath:"#/properties/entries/items/properties/text/type",keyword:"type",params:{type: "string"},message:"must be string"}];return false;}}var valid3 = _errs15 === errors;}else {var valid3 = true;}}}}}}}else {validate78.errors = [{instancePath:instancePath+"/entries/" + i0,schemaPath:"#/properties/entries/items/type",keyword:"type",params:{type: "object"},message:"must be object"}];return false;}}var valid2 = _errs6 === errors;if(!valid2){break;}}}}else {validate78.errors = [{instancePath:instancePath+"/entries",schemaPath:"#/properties/entries/type",keyword:"type",params:{type: "array"},message:"must be array"}];return false;}}var valid0 = _errs4 === errors;}else {var valid0 = true;}}}}}else {validate78.errors = [{instancePath,schemaPath:"#/type",keyword:"type",params:{type: "object"},message:"must be object"}];return false;}}validate78.errors = vErrors;return errors === 0;}validate78.evaluated = {"props":true,"dynamicProps":false,"dynamicItems":false};export const validateLegacy = validate79;const schema118 = {"$schema":"https://json-schema.org/draft/2020-12/schema","$id":"https://kernelpath.dev/schema/v1/bundle/legacy.schema.json","type":"object","additionalProperties":false,"required":["apiVersion","course","track","interface"],"properties":{"apiVersion":{"$ref":"../common.schema.json#/$defs/apiVersion"},"course":{"type":"object","additionalProperties":false,"required":["title","tagline"],"properties":{"title":{"$ref":"../common.schema.json#/$defs/shortText"},"tagline":{"$ref":"../common.schema.json#/$defs/shortText"},"repo":{"$ref":"../common.schema.json#/$defs/url"}}},"track":{"type":"object","additionalProperties":{"type":"string","maxLength":200},"required":["id","label","platform"],"properties":{"platform":{"type":"object","additionalProperties":false,"required":["title","path"],"properties":{"title":{"$ref":"../common.schema.json#/$defs/shortText"},"eyebrow":{"$ref":"../common.schema.json#/$defs/shortText"},"description":{"$ref":"../common.schema.json#/$defs/text"},"path":{"const":"/platform"}}}}},"interface":{"$ref":"interface.schema.json#/properties/interface"}}};const schema126 = {"type":"object","propertyNames":{"pattern":"^[A-Z][A-Za-z0-9]*$"},"additionalProperties":{"type":"object","additionalProperties":false,"properties":{"text":{"type":"object","additionalProperties":{"type":"string","maxLength":4000}},"data":{"type":"object"}}}};function validate80(data, {instancePath="", parentData, parentDataProperty, rootData=data, dynamicAnchors={}}={}){let vErrors = null;let errors = 0;const evaluated0 = validate80.evaluated;if(evaluated0.dynamicProps){evaluated0.props = undefined;}if(evaluated0.dynamicItems){evaluated0.items = undefined;}const _errs0 = errors;const _errs2 = errors;const _errs3 = errors;if(typeof data === "string"){if(!pattern5.test(data)){const err0 = {};if(vErrors === null){vErrors = [err0];}else {vErrors.push(err0);}errors++;}}var valid1 = _errs3 === errors;if(valid1){validate80.errors = [{instancePath,schemaPath:"#/$defs/text/not",keyword:"not",params:{},message:"must NOT be valid"}];return false;}else {errors = _errs2;if(vErrors !== null){if(_errs2){vErrors.length = _errs2;}else {vErrors = null;}}}if(errors === _errs0){if(typeof data === "string"){if(func2(data) > 2000){validate80.errors = [{instancePath,schemaPath:"#/$defs/text/maxLength",keyword:"maxLength",params:{limit: 2000},message:"must NOT have more than 2000 characters"}];return false;}else {if(func2(data) < 1){validate80.errors = [{instancePath,schemaPath:"#/$defs/text/minLength",keyword:"minLength",params:{limit: 1},message:"must NOT have fewer than 1 characters"}];return false;}}}else {validate80.errors = [{instancePath,schemaPath:"#/$defs/text/type",keyword:"type",params:{type: "string"},message:"must be string"}];return false;}}if(errors === 0){if(typeof data === "string"){if(func2(data) > 160){validate80.errors = [{instancePath,schemaPath:"#/maxLength",keyword:"maxLength",params:{limit: 160},message:"must NOT have more than 160 characters"}];return false;}}else {validate80.errors = [{instancePath,schemaPath:"#/type",keyword:"type",params:{type: "string"},message:"must be string"}];return false;}}validate80.errors = vErrors;return errors === 0;}validate80.evaluated = {"dynamicProps":false,"dynamicItems":false};const pattern82 = new RegExp("^[A-Z][A-Za-z0-9]*$", "u");function validate79(data, {instancePath="", parentData, parentDataProperty, rootData=data, dynamicAnchors={}}={}){/*# sourceURL="https://kernelpath.dev/schema/v1/bundle/legacy.schema.json" */;let vErrors = null;let errors = 0;const evaluated0 = validate79.evaluated;if(evaluated0.dynamicProps){evaluated0.props = undefined;}if(evaluated0.dynamicItems){evaluated0.items = undefined;}if(errors === 0){if(data && typeof data == "object" && !Array.isArray(data)){let missing0;if(((((data.apiVersion === undefined) && (missing0 = "apiVersion")) || ((data.course === undefined) && (missing0 = "course"))) || ((data.track === undefined) && (missing0 = "track"))) || ((data.interface === undefined) && (missing0 = "interface"))){validate79.errors = [{instancePath,schemaPath:"#/required",keyword:"required",params:{missingProperty: missing0},message:"must have required property '"+missing0+"'"}];return false;}else {const _errs1 = errors;for(const key0 in data){if(!((((key0 === "apiVersion") || (key0 === "course")) || (key0 === "track")) || (key0 === "interface"))){validate79.errors = [{instancePath,schemaPath:"#/additionalProperties",keyword:"additionalProperties",params:{additionalProperty: key0},message:"must NOT have additional properties"}];return false;break;}}if(_errs1 === errors){if(data.apiVersion !== undefined){const _errs2 = errors;if(1 !== data.apiVersion){validate79.errors = [{instancePath:instancePath+"/apiVersion",schemaPath:"../common.schema.json#/$defs/apiVersion/const",keyword:"const",params:{allowedValue: 1},message:"must be equal to constant"}];return false;}var valid0 = _errs2 === errors;}else {var valid0 = true;}if(valid0){if(data.course !== undefined){let data1 = data.course;const _errs4 = errors;if(errors === _errs4){if(data1 && typeof data1 == "object" && !Array.isArray(data1)){let missing1;if(((data1.title === undefined) && (missing1 = "title")) || ((data1.tagline === undefined) && (missing1 = "tagline"))){validate79.errors = [{instancePath:instancePath+"/course",schemaPath:"#/properties/course/required",keyword:"required",params:{missingProperty: missing1},message:"must have required property '"+missing1+"'"}];return false;}else {const _errs6 = errors;for(const key1 in data1){if(!(((key1 === "title") || (key1 === "tagline")) || (key1 === "repo"))){validate79.errors = [{instancePath:instancePath+"/course",schemaPath:"#/properties/course/additionalProperties",keyword:"additionalProperties",params:{additionalProperty: key1},message:"must NOT have additional properties"}];return false;break;}}if(_errs6 === errors){if(data1.title !== undefined){const _errs7 = errors;if(!(validate80(data1.title, {instancePath:instancePath+"/course/title",parentData:data1,parentDataProperty:"title",rootData,dynamicAnchors}))){vErrors = vErrors === null ? validate80.errors : vErrors.concat(validate80.errors);errors = vErrors.length;}var valid2 = _errs7 === errors;}else {var valid2 = true;}if(valid2){if(data1.tagline !== undefined){const _errs8 = errors;if(!(validate80(data1.tagline, {instancePath:instancePath+"/course/tagline",parentData:data1,parentDataProperty:"tagline",rootData,dynamicAnchors}))){vErrors = vErrors === null ? validate80.errors : vErrors.concat(validate80.errors);errors = vErrors.length;}var valid2 = _errs8 === errors;}else {var valid2 = true;}if(valid2){if(data1.repo !== undefined){let data4 = data1.repo;const _errs9 = errors;const _errs10 = errors;if(errors === _errs10){if(typeof data4 === "string"){if(func2(data4) > 2048){validate79.errors = [{instancePath:instancePath+"/course/repo",schemaPath:"../common.schema.json#/$defs/url/maxLength",keyword:"maxLength",params:{limit: 2048},message:"must NOT have more than 2048 characters"}];return false;}else {if(!pattern7.test(data4)){validate79.errors = [{instancePath:instancePath+"/course/repo",schemaPath:"../common.schema.json#/$defs/url/pattern",keyword:"pattern",params:{pattern: "^(?:#[/A-Za-z0-9._~%!$&'()*+,;=:@?#-]*|lab/[a-z0-9][a-z0-9._/-]*|https://[^\\s<>\"]+|mailto:[^\\s<>\"]+)$"},message:"must match pattern \""+"^(?:#[/A-Za-z0-9._~%!$&'()*+,;=:@?#-]*|lab/[a-z0-9][a-z0-9._/-]*|https://[^\\s<>\"]+|mailto:[^\\s<>\"]+)$"+"\""}];return false;}}}else {validate79.errors = [{instancePath:instancePath+"/course/repo",schemaPath:"../common.schema.json#/$defs/url/type",keyword:"type",params:{type: "string"},message:"must be string"}];return false;}}var valid2 = _errs9 === errors;}else {var valid2 = true;}}}}}}else {validate79.errors = [{instancePath:instancePath+"/course",schemaPath:"#/properties/course/type",keyword:"type",params:{type: "object"},message:"must be object"}];return false;}}var valid0 = _errs4 === errors;}else {var valid0 = true;}if(valid0){if(data.track !== undefined){let data5 = data.track;const _errs12 = errors;if(errors === _errs12){if(data5 && typeof data5 == "object" && !Array.isArray(data5)){let missing2;if((((data5.id === undefined) && (missing2 = "id")) || ((data5.label === undefined) && (missing2 = "label"))) || ((data5.platform === undefined) && (missing2 = "platform"))){validate79.errors = [{instancePath:instancePath+"/track",schemaPath:"#/properties/track/required",keyword:"required",params:{missingProperty: missing2},message:"must have required property '"+missing2+"'"}];return false;}else {const _errs14 = errors;for(const key2 in data5){if(!(key2 === "platform")){let data6 = data5[key2];const _errs15 = errors;if(errors === _errs15){if(typeof data6 === "string"){if(func2(data6) > 200){validate79.errors = [{instancePath:instancePath+"/track/" + key2.replace(/~/g, "~0").replace(/\//g, "~1"),schemaPath:"#/properties/track/additionalProperties/maxLength",keyword:"maxLength",params:{limit: 200},message:"must NOT have more than 200 characters"}];return false;}}else {validate79.errors = [{instancePath:instancePath+"/track/" + key2.replace(/~/g, "~0").replace(/\//g, "~1"),schemaPath:"#/properties/track/additionalProperties/type",keyword:"type",params:{type: "string"},message:"must be string"}];return false;}}var valid4 = _errs15 === errors;if(!valid4){break;}}}if(_errs14 === errors){if(data5.platform !== undefined){let data7 = data5.platform;const _errs17 = errors;if(errors === _errs17){if(data7 && typeof data7 == "object" && !Array.isArray(data7)){let missing3;if(((data7.title === undefined) && (missing3 = "title")) || ((data7.path === undefined) && (missing3 = "path"))){validate79.errors = [{instancePath:instancePath+"/track/platform",schemaPath:"#/properties/track/properties/platform/required",keyword:"required",params:{missingProperty: missing3},message:"must have required property '"+missing3+"'"}];return false;}else {const _errs19 = errors;for(const key3 in data7){if(!((((key3 === "title") || (key3 === "eyebrow")) || (key3 === "description")) || (key3 === "path"))){validate79.errors = [{instancePath:instancePath+"/track/platform",schemaPath:"#/properties/track/properties/platform/additionalProperties",keyword:"additionalProperties",params:{additionalProperty: key3},message:"must NOT have additional properties"}];return false;break;}}if(_errs19 === errors){if(data7.title !== undefined){const _errs20 = errors;if(!(validate80(data7.title, {instancePath:instancePath+"/track/platform/title",parentData:data7,parentDataProperty:"title",rootData,dynamicAnchors}))){vErrors = vErrors === null ? validate80.errors : vErrors.concat(validate80.errors);errors = vErrors.length;}var valid6 = _errs20 === errors;}else {var valid6 = true;}if(valid6){if(data7.eyebrow !== undefined){const _errs21 = errors;if(!(validate80(data7.eyebrow, {instancePath:instancePath+"/track/platform/eyebrow",parentData:data7,parentDataProperty:"eyebrow",rootData,dynamicAnchors}))){vErrors = vErrors === null ? validate80.errors : vErrors.concat(validate80.errors);errors = vErrors.length;}var valid6 = _errs21 === errors;}else {var valid6 = true;}if(valid6){if(data7.description !== undefined){let data10 = data7.description;const _errs22 = errors;const _errs23 = errors;const _errs25 = errors;const _errs26 = errors;if(typeof data10 === "string"){if(!pattern5.test(data10)){const err0 = {};if(vErrors === null){vErrors = [err0];}else {vErrors.push(err0);}errors++;}}var valid8 = _errs26 === errors;if(valid8){validate79.errors = [{instancePath:instancePath+"/track/platform/description",schemaPath:"../common.schema.json#/$defs/text/not",keyword:"not",params:{},message:"must NOT be valid"}];return false;}else {errors = _errs25;if(vErrors !== null){if(_errs25){vErrors.length = _errs25;}else {vErrors = null;}}}if(errors === _errs23){if(typeof data10 === "string"){if(func2(data10) > 2000){validate79.errors = [{instancePath:instancePath+"/track/platform/description",schemaPath:"../common.schema.json#/$defs/text/maxLength",keyword:"maxLength",params:{limit: 2000},message:"must NOT have more than 2000 characters"}];return false;}else {if(func2(data10) < 1){validate79.errors = [{instancePath:instancePath+"/track/platform/description",schemaPath:"../common.schema.json#/$defs/text/minLength",keyword:"minLength",params:{limit: 1},message:"must NOT have fewer than 1 characters"}];return false;}}}else {validate79.errors = [{instancePath:instancePath+"/track/platform/description",schemaPath:"../common.schema.json#/$defs/text/type",keyword:"type",params:{type: "string"},message:"must be string"}];return false;}}var valid6 = _errs22 === errors;}else {var valid6 = true;}if(valid6){if(data7.path !== undefined){const _errs27 = errors;if("/platform" !== data7.path){validate79.errors = [{instancePath:instancePath+"/track/platform/path",schemaPath:"#/properties/track/properties/platform/properties/path/const",keyword:"const",params:{allowedValue: "/platform"},message:"must be equal to constant"}];return false;}var valid6 = _errs27 === errors;}else {var valid6 = true;}}}}}}}else {validate79.errors = [{instancePath:instancePath+"/track/platform",schemaPath:"#/properties/track/properties/platform/type",keyword:"type",params:{type: "object"},message:"must be object"}];return false;}}}}}}else {validate79.errors = [{instancePath:instancePath+"/track",schemaPath:"#/properties/track/type",keyword:"type",params:{type: "object"},message:"must be object"}];return false;}}var valid0 = _errs12 === errors;}else {var valid0 = true;}if(valid0){if(data.interface !== undefined){let data12 = data.interface;const _errs28 = errors;const _errs29 = errors;if(errors === _errs29){if(data12 && typeof data12 == "object" && !Array.isArray(data12)){for(const key4 in data12){const _errs31 = errors;if(typeof key4 === "string"){if(!pattern82.test(key4)){const err1 = {instancePath:instancePath+"/interface",schemaPath:"interface.schema.json#/properties/interface/propertyNames/pattern",keyword:"pattern",params:{pattern: "^[A-Z][A-Za-z0-9]*$"},message:"must match pattern \""+"^[A-Z][A-Za-z0-9]*$"+"\"",propertyName:key4};if(vErrors === null){vErrors = [err1];}else {vErrors.push(err1);}errors++;}}var valid10 = _errs31 === errors;if(!valid10){const err2 = {instancePath:instancePath+"/interface",schemaPath:"interface.schema.json#/properties/interface/propertyNames",keyword:"propertyNames",params:{propertyName: key4},message:"property name must be valid"};if(vErrors === null){vErrors = [err2];}else {vErrors.push(err2);}errors++;validate79.errors = vErrors;return false;break;}}if(valid10){for(const key5 in data12){let data13 = data12[key5];const _errs33 = errors;if(errors === _errs33){if(data13 && typeof data13 == "object" && !Array.isArray(data13)){const _errs35 = errors;for(const key6 in data13){if(!((key6 === "text") || (key6 === "data"))){validate79.errors = [{instancePath:instancePath+"/interface/" + key5.replace(/~/g, "~0").replace(/\//g, "~1"),schemaPath:"interface.schema.json#/properties/interface/additionalProperties/additionalProperties",keyword:"additionalProperties",params:{additionalProperty: key6},message:"must NOT have additional properties"}];return false;break;}}if(_errs35 === errors){if(data13.text !== undefined){let data14 = data13.text;const _errs36 = errors;if(errors === _errs36){if(data14 && typeof data14 == "object" && !Array.isArray(data14)){for(const key7 in data14){let data15 = data14[key7];const _errs39 = errors;if(errors === _errs39){if(typeof data15 === "string"){if(func2(data15) > 4000){validate79.errors = [{instancePath:instancePath+"/interface/" + key5.replace(/~/g, "~0").replace(/\//g, "~1")+"/text/" + key7.replace(/~/g, "~0").replace(/\//g, "~1"),schemaPath:"interface.schema.json#/properties/interface/additionalProperties/properties/text/additionalProperties/maxLength",keyword:"maxLength",params:{limit: 4000},message:"must NOT have more than 4000 characters"}];return false;}}else {validate79.errors = [{instancePath:instancePath+"/interface/" + key5.replace(/~/g, "~0").replace(/\//g, "~1")+"/text/" + key7.replace(/~/g, "~0").replace(/\//g, "~1"),schemaPath:"interface.schema.json#/properties/interface/additionalProperties/properties/text/additionalProperties/type",keyword:"type",params:{type: "string"},message:"must be string"}];return false;}}var valid13 = _errs39 === errors;if(!valid13){break;}}}else {validate79.errors = [{instancePath:instancePath+"/interface/" + key5.replace(/~/g, "~0").replace(/\//g, "~1")+"/text",schemaPath:"interface.schema.json#/properties/interface/additionalProperties/properties/text/type",keyword:"type",params:{type: "object"},message:"must be object"}];return false;}}var valid12 = _errs36 === errors;}else {var valid12 = true;}if(valid12){if(data13.data !== undefined){let data16 = data13.data;const _errs41 = errors;if(!(data16 && typeof data16 == "object" && !Array.isArray(data16))){validate79.errors = [{instancePath:instancePath+"/interface/" + key5.replace(/~/g, "~0").replace(/\//g, "~1")+"/data",schemaPath:"interface.schema.json#/properties/interface/additionalProperties/properties/data/type",keyword:"type",params:{type: "object"},message:"must be object"}];return false;}var valid12 = _errs41 === errors;}else {var valid12 = true;}}}}else {validate79.errors = [{instancePath:instancePath+"/interface/" + key5.replace(/~/g, "~0").replace(/\//g, "~1"),schemaPath:"interface.schema.json#/properties/interface/additionalProperties/type",keyword:"type",params:{type: "object"},message:"must be object"}];return false;}}var valid11 = _errs33 === errors;if(!valid11){break;}}}}else {validate79.errors = [{instancePath:instancePath+"/interface",schemaPath:"interface.schema.json#/properties/interface/type",keyword:"type",params:{type: "object"},message:"must be object"}];return false;}}var valid0 = _errs28 === errors;}else {var valid0 = true;}}}}}}}else {validate79.errors = [{instancePath,schemaPath:"#/type",keyword:"type",params:{type: "object"},message:"must be object"}];return false;}}validate79.errors = vErrors;return errors === 0;}validate79.evaluated = {"props":true,"dynamicProps":false,"dynamicItems":false};export const validateInterface = validate85;const schema124 = {"$schema":"https://json-schema.org/draft/2020-12/schema","$id":"https://kernelpath.dev/schema/v1/bundle/interface.schema.json","type":"object","additionalProperties":false,"required":["apiVersion","interface"],"properties":{"apiVersion":{"$ref":"../common.schema.json#/$defs/apiVersion"},"interface":{"type":"object","propertyNames":{"pattern":"^[A-Z][A-Za-z0-9]*$"},"additionalProperties":{"type":"object","additionalProperties":false,"properties":{"text":{"type":"object","additionalProperties":{"type":"string","maxLength":4000}},"data":{"type":"object"}}}}}};function validate85(data, {instancePath="", parentData, parentDataProperty, rootData=data, dynamicAnchors={}}={}){/*# sourceURL="https://kernelpath.dev/schema/v1/bundle/interface.schema.json" */;let vErrors = null;let errors = 0;const evaluated0 = validate85.evaluated;if(evaluated0.dynamicProps){evaluated0.props = undefined;}if(evaluated0.dynamicItems){evaluated0.items = undefined;}if(errors === 0){if(data && typeof data == "object" && !Array.isArray(data)){let missing0;if(((data.apiVersion === undefined) && (missing0 = "apiVersion")) || ((data.interface === undefined) && (missing0 = "interface"))){validate85.errors = [{instancePath,schemaPath:"#/required",keyword:"required",params:{missingProperty: missing0},message:"must have required property '"+missing0+"'"}];return false;}else {const _errs1 = errors;for(const key0 in data){if(!((key0 === "apiVersion") || (key0 === "interface"))){validate85.errors = [{instancePath,schemaPath:"#/additionalProperties",keyword:"additionalProperties",params:{additionalProperty: key0},message:"must NOT have additional properties"}];return false;break;}}if(_errs1 === errors){if(data.apiVersion !== undefined){const _errs2 = errors;if(1 !== data.apiVersion){validate85.errors = [{instancePath:instancePath+"/apiVersion",schemaPath:"../common.schema.json#/$defs/apiVersion/const",keyword:"const",params:{allowedValue: 1},message:"must be equal to constant"}];return false;}var valid0 = _errs2 === errors;}else {var valid0 = true;}if(valid0){if(data.interface !== undefined){let data1 = data.interface;const _errs4 = errors;if(errors === _errs4){if(data1 && typeof data1 == "object" && !Array.isArray(data1)){for(const key1 in data1){const _errs6 = errors;if(typeof key1 === "string"){if(!pattern82.test(key1)){const err0 = {instancePath:instancePath+"/interface",schemaPath:"#/properties/interface/propertyNames/pattern",keyword:"pattern",params:{pattern: "^[A-Z][A-Za-z0-9]*$"},message:"must match pattern \""+"^[A-Z][A-Za-z0-9]*$"+"\"",propertyName:key1};if(vErrors === null){vErrors = [err0];}else {vErrors.push(err0);}errors++;}}var valid2 = _errs6 === errors;if(!valid2){const err1 = {instancePath:instancePath+"/interface",schemaPath:"#/properties/interface/propertyNames",keyword:"propertyNames",params:{propertyName: key1},message:"property name must be valid"};if(vErrors === null){vErrors = [err1];}else {vErrors.push(err1);}errors++;validate85.errors = vErrors;return false;break;}}if(valid2){for(const key2 in data1){let data2 = data1[key2];const _errs8 = errors;if(errors === _errs8){if(data2 && typeof data2 == "object" && !Array.isArray(data2)){const _errs10 = errors;for(const key3 in data2){if(!((key3 === "text") || (key3 === "data"))){validate85.errors = [{instancePath:instancePath+"/interface/" + key2.replace(/~/g, "~0").replace(/\//g, "~1"),schemaPath:"#/properties/interface/additionalProperties/additionalProperties",keyword:"additionalProperties",params:{additionalProperty: key3},message:"must NOT have additional properties"}];return false;break;}}if(_errs10 === errors){if(data2.text !== undefined){let data3 = data2.text;const _errs11 = errors;if(errors === _errs11){if(data3 && typeof data3 == "object" && !Array.isArray(data3)){for(const key4 in data3){let data4 = data3[key4];const _errs14 = errors;if(errors === _errs14){if(typeof data4 === "string"){if(func2(data4) > 4000){validate85.errors = [{instancePath:instancePath+"/interface/" + key2.replace(/~/g, "~0").replace(/\//g, "~1")+"/text/" + key4.replace(/~/g, "~0").replace(/\//g, "~1"),schemaPath:"#/properties/interface/additionalProperties/properties/text/additionalProperties/maxLength",keyword:"maxLength",params:{limit: 4000},message:"must NOT have more than 4000 characters"}];return false;}}else {validate85.errors = [{instancePath:instancePath+"/interface/" + key2.replace(/~/g, "~0").replace(/\//g, "~1")+"/text/" + key4.replace(/~/g, "~0").replace(/\//g, "~1"),schemaPath:"#/properties/interface/additionalProperties/properties/text/additionalProperties/type",keyword:"type",params:{type: "string"},message:"must be string"}];return false;}}var valid5 = _errs14 === errors;if(!valid5){break;}}}else {validate85.errors = [{instancePath:instancePath+"/interface/" + key2.replace(/~/g, "~0").replace(/\//g, "~1")+"/text",schemaPath:"#/properties/interface/additionalProperties/properties/text/type",keyword:"type",params:{type: "object"},message:"must be object"}];return false;}}var valid4 = _errs11 === errors;}else {var valid4 = true;}if(valid4){if(data2.data !== undefined){let data5 = data2.data;const _errs16 = errors;if(!(data5 && typeof data5 == "object" && !Array.isArray(data5))){validate85.errors = [{instancePath:instancePath+"/interface/" + key2.replace(/~/g, "~0").replace(/\//g, "~1")+"/data",schemaPath:"#/properties/interface/additionalProperties/properties/data/type",keyword:"type",params:{type: "object"},message:"must be object"}];return false;}var valid4 = _errs16 === errors;}else {var valid4 = true;}}}}else {validate85.errors = [{instancePath:instancePath+"/interface/" + key2.replace(/~/g, "~0").replace(/\//g, "~1"),schemaPath:"#/properties/interface/additionalProperties/type",keyword:"type",params:{type: "object"},message:"must be object"}];return false;}}var valid3 = _errs8 === errors;if(!valid3){break;}}}}else {validate85.errors = [{instancePath:instancePath+"/interface",schemaPath:"#/properties/interface/type",keyword:"type",params:{type: "object"},message:"must be object"}];return false;}}var valid0 = _errs4 === errors;}else {var valid0 = true;}}}}}else {validate85.errors = [{instancePath,schemaPath:"#/type",keyword:"type",params:{type: "object"},message:"must be object"}];return false;}}validate85.errors = vErrors;return errors === 0;}validate85.evaluated = {"props":true,"dynamicProps":false,"dynamicItems":false};export const validateQuizData = validate86;const schema127 = {"$schema":"https://json-schema.org/draft/2020-12/schema","$id":"https://kernelpath.dev/schema/v1/data/quiz.schema.json","type":"object","additionalProperties":false,"required":["questions"],"properties":{"questions":{"type":"array","minItems":1,"maxItems":60,"items":{"type":"object","additionalProperties":false,"required":["id","q","options","answer","explain"],"properties":{"id":{"type":"string","pattern":"^q-[a-z0-9]{6,32}$"},"legacyIndex":{"type":"integer","minimum":0},"q":{"$ref":"../common.schema.json#/$defs/inlineMarkdown"},"code":{"type":"string","maxLength":4000},"options":{"type":"array","minItems":2,"maxItems":6,"uniqueItems":true,"items":{"$ref":"../common.schema.json#/$defs/inlineMarkdown"}},"answer":{"type":"integer","minimum":0,"maximum":5},"explain":{"$ref":"../common.schema.json#/$defs/inlineMarkdown"}}}}}};const func0 = runtime1;function validate86(data, {instancePath="", parentData, parentDataProperty, rootData=data, dynamicAnchors={}}={}){/*# sourceURL="https://kernelpath.dev/schema/v1/data/quiz.schema.json" */;let vErrors = null;let errors = 0;const evaluated0 = validate86.evaluated;if(evaluated0.dynamicProps){evaluated0.props = undefined;}if(evaluated0.dynamicItems){evaluated0.items = undefined;}if(errors === 0){if(data && typeof data == "object" && !Array.isArray(data)){let missing0;if((data.questions === undefined) && (missing0 = "questions")){validate86.errors = [{instancePath,schemaPath:"#/required",keyword:"required",params:{missingProperty: missing0},message:"must have required property '"+missing0+"'"}];return false;}else {const _errs1 = errors;for(const key0 in data){if(!(key0 === "questions")){validate86.errors = [{instancePath,schemaPath:"#/additionalProperties",keyword:"additionalProperties",params:{additionalProperty: key0},message:"must NOT have additional properties"}];return false;break;}}if(_errs1 === errors){if(data.questions !== undefined){let data0 = data.questions;const _errs2 = errors;if(errors === _errs2){if(Array.isArray(data0)){if(data0.length > 60){validate86.errors = [{instancePath:instancePath+"/questions",schemaPath:"#/properties/questions/maxItems",keyword:"maxItems",params:{limit: 60},message:"must NOT have more than 60 items"}];return false;}else {if(data0.length < 1){validate86.errors = [{instancePath:instancePath+"/questions",schemaPath:"#/properties/questions/minItems",keyword:"minItems",params:{limit: 1},message:"must NOT have fewer than 1 items"}];return false;}else {var valid1 = true;const len0 = data0.length;for(let i0=0; i0=", limit: 0},message:"must be >= 0"}];return false;}}}var valid2 = _errs9 === errors;}else {var valid2 = true;}if(valid2){if(data1.q !== undefined){let data4 = data1.q;const _errs11 = errors;const _errs12 = errors;const _errs14 = errors;const _errs15 = errors;if(typeof data4 === "string"){if(!pattern8.test(data4)){const err0 = {};if(vErrors === null){vErrors = [err0];}else {vErrors.push(err0);}errors++;}}var valid4 = _errs15 === errors;if(valid4){validate86.errors = [{instancePath:instancePath+"/questions/" + i0+"/q",schemaPath:"../common.schema.json#/$defs/inlineMarkdown/not",keyword:"not",params:{},message:"must NOT be valid"}];return false;}else {errors = _errs14;if(vErrors !== null){if(_errs14){vErrors.length = _errs14;}else {vErrors = null;}}}if(errors === _errs12){if(typeof data4 === "string"){if(func2(data4) > 4000){validate86.errors = [{instancePath:instancePath+"/questions/" + i0+"/q",schemaPath:"../common.schema.json#/$defs/inlineMarkdown/maxLength",keyword:"maxLength",params:{limit: 4000},message:"must NOT have more than 4000 characters"}];return false;}else {if(func2(data4) < 1){validate86.errors = [{instancePath:instancePath+"/questions/" + i0+"/q",schemaPath:"../common.schema.json#/$defs/inlineMarkdown/minLength",keyword:"minLength",params:{limit: 1},message:"must NOT have fewer than 1 characters"}];return false;}}}else {validate86.errors = [{instancePath:instancePath+"/questions/" + i0+"/q",schemaPath:"../common.schema.json#/$defs/inlineMarkdown/type",keyword:"type",params:{type: "string"},message:"must be string"}];return false;}}var valid2 = _errs11 === errors;}else {var valid2 = true;}if(valid2){if(data1.code !== undefined){let data5 = data1.code;const _errs16 = errors;if(errors === _errs16){if(typeof data5 === "string"){if(func2(data5) > 4000){validate86.errors = [{instancePath:instancePath+"/questions/" + i0+"/code",schemaPath:"#/properties/questions/items/properties/code/maxLength",keyword:"maxLength",params:{limit: 4000},message:"must NOT have more than 4000 characters"}];return false;}}else {validate86.errors = [{instancePath:instancePath+"/questions/" + i0+"/code",schemaPath:"#/properties/questions/items/properties/code/type",keyword:"type",params:{type: "string"},message:"must be string"}];return false;}}var valid2 = _errs16 === errors;}else {var valid2 = true;}if(valid2){if(data1.options !== undefined){let data6 = data1.options;const _errs18 = errors;if(errors === _errs18){if(Array.isArray(data6)){if(data6.length > 6){validate86.errors = [{instancePath:instancePath+"/questions/" + i0+"/options",schemaPath:"#/properties/questions/items/properties/options/maxItems",keyword:"maxItems",params:{limit: 6},message:"must NOT have more than 6 items"}];return false;}else {if(data6.length < 2){validate86.errors = [{instancePath:instancePath+"/questions/" + i0+"/options",schemaPath:"#/properties/questions/items/properties/options/minItems",keyword:"minItems",params:{limit: 2},message:"must NOT have fewer than 2 items"}];return false;}else {var valid5 = true;const len1 = data6.length;for(let i1=0; i1 4000){validate86.errors = [{instancePath:instancePath+"/questions/" + i0+"/options/" + i1,schemaPath:"../common.schema.json#/$defs/inlineMarkdown/maxLength",keyword:"maxLength",params:{limit: 4000},message:"must NOT have more than 4000 characters"}];return false;}else {if(func2(data7) < 1){validate86.errors = [{instancePath:instancePath+"/questions/" + i0+"/options/" + i1,schemaPath:"../common.schema.json#/$defs/inlineMarkdown/minLength",keyword:"minLength",params:{limit: 1},message:"must NOT have fewer than 1 characters"}];return false;}}}else {validate86.errors = [{instancePath:instancePath+"/questions/" + i0+"/options/" + i1,schemaPath:"../common.schema.json#/$defs/inlineMarkdown/type",keyword:"type",params:{type: "string"},message:"must be string"}];return false;}}var valid5 = _errs20 === errors;if(!valid5){break;}}if(valid5){let i2 = data6.length;let j0;if(i2 > 1){outer0:for(;i2--;){for(j0 = i2; j0--;){if(func0(data6[i2], data6[j0])){validate86.errors = [{instancePath:instancePath+"/questions/" + i0+"/options",schemaPath:"#/properties/questions/items/properties/options/uniqueItems",keyword:"uniqueItems",params:{i: i2, j: j0},message:"must NOT have duplicate items (items ## "+j0+" and "+i2+" are identical)"}];return false;break outer0;}}}}}}}}else {validate86.errors = [{instancePath:instancePath+"/questions/" + i0+"/options",schemaPath:"#/properties/questions/items/properties/options/type",keyword:"type",params:{type: "array"},message:"must be array"}];return false;}}var valid2 = _errs18 === errors;}else {var valid2 = true;}if(valid2){if(data1.answer !== undefined){let data8 = data1.answer;const _errs25 = errors;if(!(((typeof data8 == "number") && (!(data8 % 1) && !isNaN(data8))) && (isFinite(data8)))){validate86.errors = [{instancePath:instancePath+"/questions/" + i0+"/answer",schemaPath:"#/properties/questions/items/properties/answer/type",keyword:"type",params:{type: "integer"},message:"must be integer"}];return false;}if(errors === _errs25){if((typeof data8 == "number") && (isFinite(data8))){if(data8 > 5 || isNaN(data8)){validate86.errors = [{instancePath:instancePath+"/questions/" + i0+"/answer",schemaPath:"#/properties/questions/items/properties/answer/maximum",keyword:"maximum",params:{comparison: "<=", limit: 5},message:"must be <= 5"}];return false;}else {if(data8 < 0 || isNaN(data8)){validate86.errors = [{instancePath:instancePath+"/questions/" + i0+"/answer",schemaPath:"#/properties/questions/items/properties/answer/minimum",keyword:"minimum",params:{comparison: ">=", limit: 0},message:"must be >= 0"}];return false;}}}}var valid2 = _errs25 === errors;}else {var valid2 = true;}if(valid2){if(data1.explain !== undefined){let data9 = data1.explain;const _errs27 = errors;const _errs28 = errors;const _errs30 = errors;const _errs31 = errors;if(typeof data9 === "string"){if(!pattern8.test(data9)){const err2 = {};if(vErrors === null){vErrors = [err2];}else {vErrors.push(err2);}errors++;}}var valid10 = _errs31 === errors;if(valid10){validate86.errors = [{instancePath:instancePath+"/questions/" + i0+"/explain",schemaPath:"../common.schema.json#/$defs/inlineMarkdown/not",keyword:"not",params:{},message:"must NOT be valid"}];return false;}else {errors = _errs30;if(vErrors !== null){if(_errs30){vErrors.length = _errs30;}else {vErrors = null;}}}if(errors === _errs28){if(typeof data9 === "string"){if(func2(data9) > 4000){validate86.errors = [{instancePath:instancePath+"/questions/" + i0+"/explain",schemaPath:"../common.schema.json#/$defs/inlineMarkdown/maxLength",keyword:"maxLength",params:{limit: 4000},message:"must NOT have more than 4000 characters"}];return false;}else {if(func2(data9) < 1){validate86.errors = [{instancePath:instancePath+"/questions/" + i0+"/explain",schemaPath:"../common.schema.json#/$defs/inlineMarkdown/minLength",keyword:"minLength",params:{limit: 1},message:"must NOT have fewer than 1 characters"}];return false;}}}else {validate86.errors = [{instancePath:instancePath+"/questions/" + i0+"/explain",schemaPath:"../common.schema.json#/$defs/inlineMarkdown/type",keyword:"type",params:{type: "string"},message:"must be string"}];return false;}}var valid2 = _errs27 === errors;}else {var valid2 = true;}}}}}}}}}}else {validate86.errors = [{instancePath:instancePath+"/questions/" + i0,schemaPath:"#/properties/questions/items/type",keyword:"type",params:{type: "object"},message:"must be object"}];return false;}}var valid1 = _errs4 === errors;if(!valid1){break;}}}}}else {validate86.errors = [{instancePath:instancePath+"/questions",schemaPath:"#/properties/questions/type",keyword:"type",params:{type: "array"},message:"must be array"}];return false;}}}}}}else {validate86.errors = [{instancePath,schemaPath:"#/type",keyword:"type",params:{type: "object"},message:"must be object"}];return false;}}validate86.errors = vErrors;return errors === 0;}validate86.evaluated = {"props":true,"dynamicProps":false,"dynamicItems":false};export const validatePracticeData = validate87;const schema131 = {"$schema":"https://json-schema.org/draft/2020-12/schema","$id":"https://kernelpath.dev/schema/v1/data/practice.schema.json","type":"object","additionalProperties":false,"required":["questions"],"properties":{"questions":{"type":"array","minItems":1,"maxItems":20,"items":{"type":"object","additionalProperties":false,"required":["id","objective","title","prompt","options","expected","explain"],"properties":{"id":{"$ref":"../common.schema.json#/$defs/id"},"objective":{"$ref":"../common.schema.json#/$defs/objectiveId"},"title":{"$ref":"../common.schema.json#/$defs/shortText"},"prompt":{"$ref":"../common.schema.json#/$defs/inlineMarkdown"},"code":{"type":"string","maxLength":4000},"options":{"type":"array","minItems":3,"maxItems":6,"uniqueItems":true,"items":{"$ref":"../common.schema.json#/$defs/inlineMarkdown"}},"expected":{"$ref":"../common.schema.json#/$defs/inlineMarkdown"},"explain":{"$ref":"../common.schema.json#/$defs/inlineMarkdown"}}}}}};function validate88(data, {instancePath="", parentData, parentDataProperty, rootData=data, dynamicAnchors={}}={}){let vErrors = null;let errors = 0;const evaluated0 = validate88.evaluated;if(evaluated0.dynamicProps){evaluated0.props = undefined;}if(evaluated0.dynamicItems){evaluated0.items = undefined;}const _errs0 = errors;const _errs2 = errors;const _errs3 = errors;if(typeof data === "string"){if(!pattern5.test(data)){const err0 = {};if(vErrors === null){vErrors = [err0];}else {vErrors.push(err0);}errors++;}}var valid1 = _errs3 === errors;if(valid1){validate88.errors = [{instancePath,schemaPath:"#/$defs/text/not",keyword:"not",params:{},message:"must NOT be valid"}];return false;}else {errors = _errs2;if(vErrors !== null){if(_errs2){vErrors.length = _errs2;}else {vErrors = null;}}}if(errors === _errs0){if(typeof data === "string"){if(func2(data) > 2000){validate88.errors = [{instancePath,schemaPath:"#/$defs/text/maxLength",keyword:"maxLength",params:{limit: 2000},message:"must NOT have more than 2000 characters"}];return false;}else {if(func2(data) < 1){validate88.errors = [{instancePath,schemaPath:"#/$defs/text/minLength",keyword:"minLength",params:{limit: 1},message:"must NOT have fewer than 1 characters"}];return false;}}}else {validate88.errors = [{instancePath,schemaPath:"#/$defs/text/type",keyword:"type",params:{type: "string"},message:"must be string"}];return false;}}if(errors === 0){if(typeof data === "string"){if(func2(data) > 160){validate88.errors = [{instancePath,schemaPath:"#/maxLength",keyword:"maxLength",params:{limit: 160},message:"must NOT have more than 160 characters"}];return false;}}else {validate88.errors = [{instancePath,schemaPath:"#/type",keyword:"type",params:{type: "string"},message:"must be string"}];return false;}}validate88.errors = vErrors;return errors === 0;}validate88.evaluated = {"dynamicProps":false,"dynamicItems":false};function validate87(data, {instancePath="", parentData, parentDataProperty, rootData=data, dynamicAnchors={}}={}){/*# sourceURL="https://kernelpath.dev/schema/v1/data/practice.schema.json" */;let vErrors = null;let errors = 0;const evaluated0 = validate87.evaluated;if(evaluated0.dynamicProps){evaluated0.props = undefined;}if(evaluated0.dynamicItems){evaluated0.items = undefined;}if(errors === 0){if(data && typeof data == "object" && !Array.isArray(data)){let missing0;if((data.questions === undefined) && (missing0 = "questions")){validate87.errors = [{instancePath,schemaPath:"#/required",keyword:"required",params:{missingProperty: missing0},message:"must have required property '"+missing0+"'"}];return false;}else {const _errs1 = errors;for(const key0 in data){if(!(key0 === "questions")){validate87.errors = [{instancePath,schemaPath:"#/additionalProperties",keyword:"additionalProperties",params:{additionalProperty: key0},message:"must NOT have additional properties"}];return false;break;}}if(_errs1 === errors){if(data.questions !== undefined){let data0 = data.questions;const _errs2 = errors;if(errors === _errs2){if(Array.isArray(data0)){if(data0.length > 20){validate87.errors = [{instancePath:instancePath+"/questions",schemaPath:"#/properties/questions/maxItems",keyword:"maxItems",params:{limit: 20},message:"must NOT have more than 20 items"}];return false;}else {if(data0.length < 1){validate87.errors = [{instancePath:instancePath+"/questions",schemaPath:"#/properties/questions/minItems",keyword:"minItems",params:{limit: 1},message:"must NOT have fewer than 1 items"}];return false;}else {var valid1 = true;const len0 = data0.length;for(let i0=0; i0 64){validate87.errors = [{instancePath:instancePath+"/questions/" + i0+"/id",schemaPath:"../common.schema.json#/$defs/id/maxLength",keyword:"maxLength",params:{limit: 64},message:"must NOT have more than 64 characters"}];return false;}else {if(!pattern18.test(data2)){validate87.errors = [{instancePath:instancePath+"/questions/" + i0+"/id",schemaPath:"../common.schema.json#/$defs/id/pattern",keyword:"pattern",params:{pattern: "^[a-z0-9]+(?:-[a-z0-9]+)*$"},message:"must match pattern \""+"^[a-z0-9]+(?:-[a-z0-9]+)*$"+"\""}];return false;}}}else {validate87.errors = [{instancePath:instancePath+"/questions/" + i0+"/id",schemaPath:"../common.schema.json#/$defs/id/type",keyword:"type",params:{type: "string"},message:"must be string"}];return false;}}var valid2 = _errs7 === errors;}else {var valid2 = true;}if(valid2){if(data1.objective !== undefined){let data3 = data1.objective;const _errs10 = errors;const _errs11 = errors;if(errors === _errs11){if(typeof data3 === "string"){if(!pattern41.test(data3)){validate87.errors = [{instancePath:instancePath+"/questions/" + i0+"/objective",schemaPath:"../common.schema.json#/$defs/objectiveId/pattern",keyword:"pattern",params:{pattern: "^ch[0-9]{2}\\.[a-z0-9]+(?:-[a-z0-9]+)*$"},message:"must match pattern \""+"^ch[0-9]{2}\\.[a-z0-9]+(?:-[a-z0-9]+)*$"+"\""}];return false;}}else {validate87.errors = [{instancePath:instancePath+"/questions/" + i0+"/objective",schemaPath:"../common.schema.json#/$defs/objectiveId/type",keyword:"type",params:{type: "string"},message:"must be string"}];return false;}}var valid2 = _errs10 === errors;}else {var valid2 = true;}if(valid2){if(data1.title !== undefined){const _errs13 = errors;if(!(validate88(data1.title, {instancePath:instancePath+"/questions/" + i0+"/title",parentData:data1,parentDataProperty:"title",rootData,dynamicAnchors}))){vErrors = vErrors === null ? validate88.errors : vErrors.concat(validate88.errors);errors = vErrors.length;}var valid2 = _errs13 === errors;}else {var valid2 = true;}if(valid2){if(data1.prompt !== undefined){let data5 = data1.prompt;const _errs14 = errors;const _errs15 = errors;const _errs17 = errors;const _errs18 = errors;if(typeof data5 === "string"){if(!pattern8.test(data5)){const err0 = {};if(vErrors === null){vErrors = [err0];}else {vErrors.push(err0);}errors++;}}var valid6 = _errs18 === errors;if(valid6){validate87.errors = [{instancePath:instancePath+"/questions/" + i0+"/prompt",schemaPath:"../common.schema.json#/$defs/inlineMarkdown/not",keyword:"not",params:{},message:"must NOT be valid"}];return false;}else {errors = _errs17;if(vErrors !== null){if(_errs17){vErrors.length = _errs17;}else {vErrors = null;}}}if(errors === _errs15){if(typeof data5 === "string"){if(func2(data5) > 4000){validate87.errors = [{instancePath:instancePath+"/questions/" + i0+"/prompt",schemaPath:"../common.schema.json#/$defs/inlineMarkdown/maxLength",keyword:"maxLength",params:{limit: 4000},message:"must NOT have more than 4000 characters"}];return false;}else {if(func2(data5) < 1){validate87.errors = [{instancePath:instancePath+"/questions/" + i0+"/prompt",schemaPath:"../common.schema.json#/$defs/inlineMarkdown/minLength",keyword:"minLength",params:{limit: 1},message:"must NOT have fewer than 1 characters"}];return false;}}}else {validate87.errors = [{instancePath:instancePath+"/questions/" + i0+"/prompt",schemaPath:"../common.schema.json#/$defs/inlineMarkdown/type",keyword:"type",params:{type: "string"},message:"must be string"}];return false;}}var valid2 = _errs14 === errors;}else {var valid2 = true;}if(valid2){if(data1.code !== undefined){let data6 = data1.code;const _errs19 = errors;if(errors === _errs19){if(typeof data6 === "string"){if(func2(data6) > 4000){validate87.errors = [{instancePath:instancePath+"/questions/" + i0+"/code",schemaPath:"#/properties/questions/items/properties/code/maxLength",keyword:"maxLength",params:{limit: 4000},message:"must NOT have more than 4000 characters"}];return false;}}else {validate87.errors = [{instancePath:instancePath+"/questions/" + i0+"/code",schemaPath:"#/properties/questions/items/properties/code/type",keyword:"type",params:{type: "string"},message:"must be string"}];return false;}}var valid2 = _errs19 === errors;}else {var valid2 = true;}if(valid2){if(data1.options !== undefined){let data7 = data1.options;const _errs21 = errors;if(errors === _errs21){if(Array.isArray(data7)){if(data7.length > 6){validate87.errors = [{instancePath:instancePath+"/questions/" + i0+"/options",schemaPath:"#/properties/questions/items/properties/options/maxItems",keyword:"maxItems",params:{limit: 6},message:"must NOT have more than 6 items"}];return false;}else {if(data7.length < 3){validate87.errors = [{instancePath:instancePath+"/questions/" + i0+"/options",schemaPath:"#/properties/questions/items/properties/options/minItems",keyword:"minItems",params:{limit: 3},message:"must NOT have fewer than 3 items"}];return false;}else {var valid7 = true;const len1 = data7.length;for(let i1=0; i1 4000){validate87.errors = [{instancePath:instancePath+"/questions/" + i0+"/options/" + i1,schemaPath:"../common.schema.json#/$defs/inlineMarkdown/maxLength",keyword:"maxLength",params:{limit: 4000},message:"must NOT have more than 4000 characters"}];return false;}else {if(func2(data8) < 1){validate87.errors = [{instancePath:instancePath+"/questions/" + i0+"/options/" + i1,schemaPath:"../common.schema.json#/$defs/inlineMarkdown/minLength",keyword:"minLength",params:{limit: 1},message:"must NOT have fewer than 1 characters"}];return false;}}}else {validate87.errors = [{instancePath:instancePath+"/questions/" + i0+"/options/" + i1,schemaPath:"../common.schema.json#/$defs/inlineMarkdown/type",keyword:"type",params:{type: "string"},message:"must be string"}];return false;}}var valid7 = _errs23 === errors;if(!valid7){break;}}if(valid7){let i2 = data7.length;let j0;if(i2 > 1){outer0:for(;i2--;){for(j0 = i2; j0--;){if(func0(data7[i2], data7[j0])){validate87.errors = [{instancePath:instancePath+"/questions/" + i0+"/options",schemaPath:"#/properties/questions/items/properties/options/uniqueItems",keyword:"uniqueItems",params:{i: i2, j: j0},message:"must NOT have duplicate items (items ## "+j0+" and "+i2+" are identical)"}];return false;break outer0;}}}}}}}}else {validate87.errors = [{instancePath:instancePath+"/questions/" + i0+"/options",schemaPath:"#/properties/questions/items/properties/options/type",keyword:"type",params:{type: "array"},message:"must be array"}];return false;}}var valid2 = _errs21 === errors;}else {var valid2 = true;}if(valid2){if(data1.expected !== undefined){let data9 = data1.expected;const _errs28 = errors;const _errs29 = errors;const _errs31 = errors;const _errs32 = errors;if(typeof data9 === "string"){if(!pattern8.test(data9)){const err2 = {};if(vErrors === null){vErrors = [err2];}else {vErrors.push(err2);}errors++;}}var valid12 = _errs32 === errors;if(valid12){validate87.errors = [{instancePath:instancePath+"/questions/" + i0+"/expected",schemaPath:"../common.schema.json#/$defs/inlineMarkdown/not",keyword:"not",params:{},message:"must NOT be valid"}];return false;}else {errors = _errs31;if(vErrors !== null){if(_errs31){vErrors.length = _errs31;}else {vErrors = null;}}}if(errors === _errs29){if(typeof data9 === "string"){if(func2(data9) > 4000){validate87.errors = [{instancePath:instancePath+"/questions/" + i0+"/expected",schemaPath:"../common.schema.json#/$defs/inlineMarkdown/maxLength",keyword:"maxLength",params:{limit: 4000},message:"must NOT have more than 4000 characters"}];return false;}else {if(func2(data9) < 1){validate87.errors = [{instancePath:instancePath+"/questions/" + i0+"/expected",schemaPath:"../common.schema.json#/$defs/inlineMarkdown/minLength",keyword:"minLength",params:{limit: 1},message:"must NOT have fewer than 1 characters"}];return false;}}}else {validate87.errors = [{instancePath:instancePath+"/questions/" + i0+"/expected",schemaPath:"../common.schema.json#/$defs/inlineMarkdown/type",keyword:"type",params:{type: "string"},message:"must be string"}];return false;}}var valid2 = _errs28 === errors;}else {var valid2 = true;}if(valid2){if(data1.explain !== undefined){let data10 = data1.explain;const _errs33 = errors;const _errs34 = errors;const _errs36 = errors;const _errs37 = errors;if(typeof data10 === "string"){if(!pattern8.test(data10)){const err3 = {};if(vErrors === null){vErrors = [err3];}else {vErrors.push(err3);}errors++;}}var valid14 = _errs37 === errors;if(valid14){validate87.errors = [{instancePath:instancePath+"/questions/" + i0+"/explain",schemaPath:"../common.schema.json#/$defs/inlineMarkdown/not",keyword:"not",params:{},message:"must NOT be valid"}];return false;}else {errors = _errs36;if(vErrors !== null){if(_errs36){vErrors.length = _errs36;}else {vErrors = null;}}}if(errors === _errs34){if(typeof data10 === "string"){if(func2(data10) > 4000){validate87.errors = [{instancePath:instancePath+"/questions/" + i0+"/explain",schemaPath:"../common.schema.json#/$defs/inlineMarkdown/maxLength",keyword:"maxLength",params:{limit: 4000},message:"must NOT have more than 4000 characters"}];return false;}else {if(func2(data10) < 1){validate87.errors = [{instancePath:instancePath+"/questions/" + i0+"/explain",schemaPath:"../common.schema.json#/$defs/inlineMarkdown/minLength",keyword:"minLength",params:{limit: 1},message:"must NOT have fewer than 1 characters"}];return false;}}}else {validate87.errors = [{instancePath:instancePath+"/questions/" + i0+"/explain",schemaPath:"../common.schema.json#/$defs/inlineMarkdown/type",keyword:"type",params:{type: "string"},message:"must be string"}];return false;}}var valid2 = _errs33 === errors;}else {var valid2 = true;}}}}}}}}}}}else {validate87.errors = [{instancePath:instancePath+"/questions/" + i0,schemaPath:"#/properties/questions/items/type",keyword:"type",params:{type: "object"},message:"must be object"}];return false;}}var valid1 = _errs4 === errors;if(!valid1){break;}}}}}else {validate87.errors = [{instancePath:instancePath+"/questions",schemaPath:"#/properties/questions/type",keyword:"type",params:{type: "array"},message:"must be array"}];return false;}}}}}}else {validate87.errors = [{instancePath,schemaPath:"#/type",keyword:"type",params:{type: "object"},message:"must be object"}];return false;}}validate87.errors = vErrors;return errors === 0;}validate87.evaluated = {"props":true,"dynamicProps":false,"dynamicItems":false};export const validateFlashcardsData = validate90;const schema140 = {"$schema":"https://json-schema.org/draft/2020-12/schema","$id":"https://kernelpath.dev/schema/v1/data/flashcards.schema.json","type":"object","additionalProperties":false,"required":["cards"],"properties":{"cards":{"type":"array","minItems":1,"maxItems":60,"items":{"type":"object","additionalProperties":false,"required":["front","back"],"properties":{"front":{"$ref":"../common.schema.json#/$defs/inlineMarkdown"},"back":{"$ref":"../common.schema.json#/$defs/inlineMarkdown"}}}}}};function validate90(data, {instancePath="", parentData, parentDataProperty, rootData=data, dynamicAnchors={}}={}){/*# sourceURL="https://kernelpath.dev/schema/v1/data/flashcards.schema.json" */;let vErrors = null;let errors = 0;const evaluated0 = validate90.evaluated;if(evaluated0.dynamicProps){evaluated0.props = undefined;}if(evaluated0.dynamicItems){evaluated0.items = undefined;}if(errors === 0){if(data && typeof data == "object" && !Array.isArray(data)){let missing0;if((data.cards === undefined) && (missing0 = "cards")){validate90.errors = [{instancePath,schemaPath:"#/required",keyword:"required",params:{missingProperty: missing0},message:"must have required property '"+missing0+"'"}];return false;}else {const _errs1 = errors;for(const key0 in data){if(!(key0 === "cards")){validate90.errors = [{instancePath,schemaPath:"#/additionalProperties",keyword:"additionalProperties",params:{additionalProperty: key0},message:"must NOT have additional properties"}];return false;break;}}if(_errs1 === errors){if(data.cards !== undefined){let data0 = data.cards;const _errs2 = errors;if(errors === _errs2){if(Array.isArray(data0)){if(data0.length > 60){validate90.errors = [{instancePath:instancePath+"/cards",schemaPath:"#/properties/cards/maxItems",keyword:"maxItems",params:{limit: 60},message:"must NOT have more than 60 items"}];return false;}else {if(data0.length < 1){validate90.errors = [{instancePath:instancePath+"/cards",schemaPath:"#/properties/cards/minItems",keyword:"minItems",params:{limit: 1},message:"must NOT have fewer than 1 items"}];return false;}else {var valid1 = true;const len0 = data0.length;for(let i0=0; i0 4000){validate90.errors = [{instancePath:instancePath+"/cards/" + i0+"/front",schemaPath:"../common.schema.json#/$defs/inlineMarkdown/maxLength",keyword:"maxLength",params:{limit: 4000},message:"must NOT have more than 4000 characters"}];return false;}else {if(func2(data2) < 1){validate90.errors = [{instancePath:instancePath+"/cards/" + i0+"/front",schemaPath:"../common.schema.json#/$defs/inlineMarkdown/minLength",keyword:"minLength",params:{limit: 1},message:"must NOT have fewer than 1 characters"}];return false;}}}else {validate90.errors = [{instancePath:instancePath+"/cards/" + i0+"/front",schemaPath:"../common.schema.json#/$defs/inlineMarkdown/type",keyword:"type",params:{type: "string"},message:"must be string"}];return false;}}var valid2 = _errs7 === errors;}else {var valid2 = true;}if(valid2){if(data1.back !== undefined){let data3 = data1.back;const _errs12 = errors;const _errs13 = errors;const _errs15 = errors;const _errs16 = errors;if(typeof data3 === "string"){if(!pattern8.test(data3)){const err1 = {};if(vErrors === null){vErrors = [err1];}else {vErrors.push(err1);}errors++;}}var valid6 = _errs16 === errors;if(valid6){validate90.errors = [{instancePath:instancePath+"/cards/" + i0+"/back",schemaPath:"../common.schema.json#/$defs/inlineMarkdown/not",keyword:"not",params:{},message:"must NOT be valid"}];return false;}else {errors = _errs15;if(vErrors !== null){if(_errs15){vErrors.length = _errs15;}else {vErrors = null;}}}if(errors === _errs13){if(typeof data3 === "string"){if(func2(data3) > 4000){validate90.errors = [{instancePath:instancePath+"/cards/" + i0+"/back",schemaPath:"../common.schema.json#/$defs/inlineMarkdown/maxLength",keyword:"maxLength",params:{limit: 4000},message:"must NOT have more than 4000 characters"}];return false;}else {if(func2(data3) < 1){validate90.errors = [{instancePath:instancePath+"/cards/" + i0+"/back",schemaPath:"../common.schema.json#/$defs/inlineMarkdown/minLength",keyword:"minLength",params:{limit: 1},message:"must NOT have fewer than 1 characters"}];return false;}}}else {validate90.errors = [{instancePath:instancePath+"/cards/" + i0+"/back",schemaPath:"../common.schema.json#/$defs/inlineMarkdown/type",keyword:"type",params:{type: "string"},message:"must be string"}];return false;}}var valid2 = _errs12 === errors;}else {var valid2 = true;}}}}}else {validate90.errors = [{instancePath:instancePath+"/cards/" + i0,schemaPath:"#/properties/cards/items/type",keyword:"type",params:{type: "object"},message:"must be object"}];return false;}}var valid1 = _errs4 === errors;if(!valid1){break;}}}}}else {validate90.errors = [{instancePath:instancePath+"/cards",schemaPath:"#/properties/cards/type",keyword:"type",params:{type: "array"},message:"must be array"}];return false;}}}}}}else {validate90.errors = [{instancePath,schemaPath:"#/type",keyword:"type",params:{type: "object"},message:"must be object"}];return false;}}validate90.errors = vErrors;return errors === 0;}validate90.evaluated = {"props":true,"dynamicProps":false,"dynamicItems":false};export const validateFlowMapData = validate91;const schema143 = {"$schema":"https://json-schema.org/draft/2020-12/schema","$id":"https://kernelpath.dev/schema/v1/data/flow-map.schema.json","type":"object","additionalProperties":false,"required":["steps"],"properties":{"connections":{"type":"array","maxItems":7,"items":{"$ref":"../common.schema.json#/$defs/shortText"}},"steps":{"type":"array","minItems":2,"maxItems":8,"items":{"type":"object","additionalProperties":false,"required":["id","title","text"],"properties":{"id":{"$ref":"../common.schema.json#/$defs/id"},"title":{"$ref":"../common.schema.json#/$defs/shortText"},"sub":{"oneOf":[{"$ref":"../common.schema.json#/$defs/shortText"},{"type":"array","minItems":1,"maxItems":3,"items":{"$ref":"../common.schema.json#/$defs/shortText"}}]},"text":{"$ref":"../common.schema.json#/$defs/inlineMarkdown"},"tone":{"$ref":"../common.schema.json#/$defs/tone"}}}}}};const schema148 = {"enum":["gray","purple","teal","coral","pink","blue","green","amber","red"]};function validate92(data, {instancePath="", parentData, parentDataProperty, rootData=data, dynamicAnchors={}}={}){let vErrors = null;let errors = 0;const evaluated0 = validate92.evaluated;if(evaluated0.dynamicProps){evaluated0.props = undefined;}if(evaluated0.dynamicItems){evaluated0.items = undefined;}const _errs0 = errors;const _errs2 = errors;const _errs3 = errors;if(typeof data === "string"){if(!pattern5.test(data)){const err0 = {};if(vErrors === null){vErrors = [err0];}else {vErrors.push(err0);}errors++;}}var valid1 = _errs3 === errors;if(valid1){validate92.errors = [{instancePath,schemaPath:"#/$defs/text/not",keyword:"not",params:{},message:"must NOT be valid"}];return false;}else {errors = _errs2;if(vErrors !== null){if(_errs2){vErrors.length = _errs2;}else {vErrors = null;}}}if(errors === _errs0){if(typeof data === "string"){if(func2(data) > 2000){validate92.errors = [{instancePath,schemaPath:"#/$defs/text/maxLength",keyword:"maxLength",params:{limit: 2000},message:"must NOT have more than 2000 characters"}];return false;}else {if(func2(data) < 1){validate92.errors = [{instancePath,schemaPath:"#/$defs/text/minLength",keyword:"minLength",params:{limit: 1},message:"must NOT have fewer than 1 characters"}];return false;}}}else {validate92.errors = [{instancePath,schemaPath:"#/$defs/text/type",keyword:"type",params:{type: "string"},message:"must be string"}];return false;}}if(errors === 0){if(typeof data === "string"){if(func2(data) > 160){validate92.errors = [{instancePath,schemaPath:"#/maxLength",keyword:"maxLength",params:{limit: 160},message:"must NOT have more than 160 characters"}];return false;}}else {validate92.errors = [{instancePath,schemaPath:"#/type",keyword:"type",params:{type: "string"},message:"must be string"}];return false;}}validate92.errors = vErrors;return errors === 0;}validate92.evaluated = {"dynamicProps":false,"dynamicItems":false};function validate91(data, {instancePath="", parentData, parentDataProperty, rootData=data, dynamicAnchors={}}={}){/*# sourceURL="https://kernelpath.dev/schema/v1/data/flow-map.schema.json" */;let vErrors = null;let errors = 0;const evaluated0 = validate91.evaluated;if(evaluated0.dynamicProps){evaluated0.props = undefined;}if(evaluated0.dynamicItems){evaluated0.items = undefined;}if(errors === 0){if(data && typeof data == "object" && !Array.isArray(data)){let missing0;if((data.steps === undefined) && (missing0 = "steps")){validate91.errors = [{instancePath,schemaPath:"#/required",keyword:"required",params:{missingProperty: missing0},message:"must have required property '"+missing0+"'"}];return false;}else {const _errs1 = errors;for(const key0 in data){if(!((key0 === "connections") || (key0 === "steps"))){validate91.errors = [{instancePath,schemaPath:"#/additionalProperties",keyword:"additionalProperties",params:{additionalProperty: key0},message:"must NOT have additional properties"}];return false;break;}}if(_errs1 === errors){if(data.connections !== undefined){let data0 = data.connections;const _errs2 = errors;if(errors === _errs2){if(Array.isArray(data0)){if(data0.length > 7){validate91.errors = [{instancePath:instancePath+"/connections",schemaPath:"#/properties/connections/maxItems",keyword:"maxItems",params:{limit: 7},message:"must NOT have more than 7 items"}];return false;}else {var valid1 = true;const len0 = data0.length;for(let i0=0; i0 8){validate91.errors = [{instancePath:instancePath+"/steps",schemaPath:"#/properties/steps/maxItems",keyword:"maxItems",params:{limit: 8},message:"must NOT have more than 8 items"}];return false;}else {if(data2.length < 2){validate91.errors = [{instancePath:instancePath+"/steps",schemaPath:"#/properties/steps/minItems",keyword:"minItems",params:{limit: 2},message:"must NOT have fewer than 2 items"}];return false;}else {var valid2 = true;const len1 = data2.length;for(let i1=0; i1 64){validate91.errors = [{instancePath:instancePath+"/steps/" + i1+"/id",schemaPath:"../common.schema.json#/$defs/id/maxLength",keyword:"maxLength",params:{limit: 64},message:"must NOT have more than 64 characters"}];return false;}else {if(!pattern18.test(data4)){validate91.errors = [{instancePath:instancePath+"/steps/" + i1+"/id",schemaPath:"../common.schema.json#/$defs/id/pattern",keyword:"pattern",params:{pattern: "^[a-z0-9]+(?:-[a-z0-9]+)*$"},message:"must match pattern \""+"^[a-z0-9]+(?:-[a-z0-9]+)*$"+"\""}];return false;}}}else {validate91.errors = [{instancePath:instancePath+"/steps/" + i1+"/id",schemaPath:"../common.schema.json#/$defs/id/type",keyword:"type",params:{type: "string"},message:"must be string"}];return false;}}var valid3 = _errs10 === errors;}else {var valid3 = true;}if(valid3){if(data3.title !== undefined){const _errs13 = errors;if(!(validate92(data3.title, {instancePath:instancePath+"/steps/" + i1+"/title",parentData:data3,parentDataProperty:"title",rootData,dynamicAnchors}))){vErrors = vErrors === null ? validate92.errors : vErrors.concat(validate92.errors);errors = vErrors.length;}var valid3 = _errs13 === errors;}else {var valid3 = true;}if(valid3){if(data3.sub !== undefined){let data6 = data3.sub;const _errs14 = errors;const _errs15 = errors;let valid5 = false;let passing0 = null;const _errs16 = errors;if(!(validate92(data6, {instancePath:instancePath+"/steps/" + i1+"/sub",parentData:data3,parentDataProperty:"sub",rootData,dynamicAnchors}))){vErrors = vErrors === null ? validate92.errors : vErrors.concat(validate92.errors);errors = vErrors.length;}var _valid0 = _errs16 === errors;if(_valid0){valid5 = true;passing0 = 0;}const _errs17 = errors;if(errors === _errs17){if(Array.isArray(data6)){if(data6.length > 3){const err0 = {instancePath:instancePath+"/steps/" + i1+"/sub",schemaPath:"#/properties/steps/items/properties/sub/oneOf/1/maxItems",keyword:"maxItems",params:{limit: 3},message:"must NOT have more than 3 items"};if(vErrors === null){vErrors = [err0];}else {vErrors.push(err0);}errors++;}else {if(data6.length < 1){const err1 = {instancePath:instancePath+"/steps/" + i1+"/sub",schemaPath:"#/properties/steps/items/properties/sub/oneOf/1/minItems",keyword:"minItems",params:{limit: 1},message:"must NOT have fewer than 1 items"};if(vErrors === null){vErrors = [err1];}else {vErrors.push(err1);}errors++;}else {var valid6 = true;const len2 = data6.length;for(let i2=0; i2 4000){validate91.errors = [{instancePath:instancePath+"/steps/" + i1+"/text",schemaPath:"../common.schema.json#/$defs/inlineMarkdown/maxLength",keyword:"maxLength",params:{limit: 4000},message:"must NOT have more than 4000 characters"}];return false;}else {if(func2(data8) < 1){validate91.errors = [{instancePath:instancePath+"/steps/" + i1+"/text",schemaPath:"../common.schema.json#/$defs/inlineMarkdown/minLength",keyword:"minLength",params:{limit: 1},message:"must NOT have fewer than 1 characters"}];return false;}}}else {validate91.errors = [{instancePath:instancePath+"/steps/" + i1+"/text",schemaPath:"../common.schema.json#/$defs/inlineMarkdown/type",keyword:"type",params:{type: "string"},message:"must be string"}];return false;}}var valid3 = _errs20 === errors;}else {var valid3 = true;}if(valid3){if(data3.tone !== undefined){let data9 = data3.tone;const _errs25 = errors;if(!(((((((((data9 === "gray") || (data9 === "purple")) || (data9 === "teal")) || (data9 === "coral")) || (data9 === "pink")) || (data9 === "blue")) || (data9 === "green")) || (data9 === "amber")) || (data9 === "red"))){validate91.errors = [{instancePath:instancePath+"/steps/" + i1+"/tone",schemaPath:"../common.schema.json#/$defs/tone/enum",keyword:"enum",params:{allowedValues: schema148.enum},message:"must be equal to one of the allowed values"}];return false;}var valid3 = _errs25 === errors;}else {var valid3 = true;}}}}}}}}else {validate91.errors = [{instancePath:instancePath+"/steps/" + i1,schemaPath:"#/properties/steps/items/type",keyword:"type",params:{type: "object"},message:"must be object"}];return false;}}var valid2 = _errs7 === errors;if(!valid2){break;}}}}}else {validate91.errors = [{instancePath:instancePath+"/steps",schemaPath:"#/properties/steps/type",keyword:"type",params:{type: "array"},message:"must be array"}];return false;}}var valid0 = _errs5 === errors;}else {var valid0 = true;}}}}}else {validate91.errors = [{instancePath,schemaPath:"#/type",keyword:"type",params:{type: "object"},message:"must be object"}];return false;}}validate91.errors = vErrors;return errors === 0;}validate91.evaluated = {"props":true,"dynamicProps":false,"dynamicItems":false};export const validateDiagramData = validate97;const schema149 = {"$schema":"https://json-schema.org/draft/2020-12/schema","$id":"https://kernelpath.dev/schema/v1/data/diagram.schema.json","type":"object","additionalProperties":false,"required":["title","height","elements"],"properties":{"title":{"type":"string","minLength":1,"maxLength":200},"caption":{"type":"string","minLength":0,"maxLength":600},"height":{"type":"integer","minimum":40,"maximum":1200},"width":{"type":"integer","minimum":200,"maximum":1600},"expandable":{"type":"boolean"},"mode":{"enum":["static","select","steps"]},"initial":{"$ref":"#/$defs/viewKey"},"hint":{"type":"string","minLength":0,"maxLength":300},"elements":{"type":"array","minItems":1,"maxItems":120,"items":{"type":"object","required":["kind"],"properties":{"kind":{"enum":["node","group","arrow","label","badge"]}},"allOf":[{"if":{"properties":{"kind":{"const":"node"}},"required":["kind"]},"then":{"$ref":"#/$defs/node"}},{"if":{"properties":{"kind":{"const":"group"}},"required":["kind"]},"then":{"$ref":"#/$defs/group"}},{"if":{"properties":{"kind":{"const":"arrow"}},"required":["kind"]},"then":{"$ref":"#/$defs/arrow"}},{"if":{"properties":{"kind":{"const":"label"}},"required":["kind"]},"then":{"$ref":"#/$defs/label"}},{"if":{"properties":{"kind":{"const":"badge"}},"required":["kind"]},"then":{"$ref":"#/$defs/badge"}}]}},"info":{"type":"object","maxProperties":30,"propertyNames":{"$ref":"../common.schema.json#/$defs/id"},"additionalProperties":{"type":"object","additionalProperties":false,"required":["title","text"],"properties":{"title":{"type":"string","minLength":1,"maxLength":200},"text":{"type":"string","minLength":1,"maxLength":1500},"tone":{"$ref":"../common.schema.json#/$defs/tone"},"code":{"type":"string","minLength":0,"maxLength":2000}}}},"steps":{"type":"array","minItems":2,"maxItems":12,"items":{"type":"object","additionalProperties":false,"required":["title"],"properties":{"title":{"type":"string","minLength":1,"maxLength":200},"text":{"type":"string","minLength":0,"maxLength":600}}}}},"$defs":{"viewKey":{"$ref":"../common.schema.json#/$defs/id"},"node":{"type":"object","additionalProperties":false,"required":["kind","x","y","w","h"],"properties":{"kind":{"const":"node"},"x":{"type":"number","minimum":-400,"maximum":2400},"y":{"type":"number","minimum":-400,"maximum":2400},"w":{"type":"number","minimum":1,"maximum":2400},"h":{"type":"number","minimum":1,"maximum":2400},"tone":{"$ref":"../common.schema.json#/$defs/tone"},"title":{"type":"string","minLength":0,"maxLength":200},"sub":{"oneOf":[{"type":"string","minLength":0,"maxLength":300},{"type":"array","minItems":1,"maxItems":6,"items":{"type":"string","minLength":0,"maxLength":300}}]},"mono":{"type":"boolean"},"rx":{"type":"number","minimum":0,"maximum":60},"titleSize":{"type":"number","minimum":8,"maximum":32},"align":{"enum":["start","middle"]},"select":{"$ref":"#/$defs/viewKey"},"active":{"oneOf":[{"type":"boolean"},{"type":"array","maxItems":20,"items":{"oneOf":[{"type":"integer","minimum":0,"maximum":15},{"$ref":"../common.schema.json#/$defs/id"}]}}]},"hot":{"oneOf":[{"type":"boolean"},{"type":"array","maxItems":20,"items":{"oneOf":[{"type":"integer","minimum":0,"maximum":15},{"$ref":"../common.schema.json#/$defs/id"}]}}]},"dim":{"type":"boolean"},"visible":{"type":"array","maxItems":20,"items":{"oneOf":[{"type":"integer","minimum":0,"maximum":15},{"$ref":"../common.schema.json#/$defs/id"}]}},"overrides":{"type":"object","maxProperties":6,"propertyNames":{"enum":["title","sub","label","text","n","dashed","muted"]},"additionalProperties":{"type":"object","maxProperties":20,"additionalProperties":{"oneOf":[{"type":"string","minLength":0,"maxLength":300},{"type":"number"},{"type":"boolean"},{"type":"array","maxItems":3,"items":{"type":"string","minLength":0,"maxLength":300}}]}}}}},"group":{"type":"object","additionalProperties":false,"required":["kind","x","y","w","h"],"properties":{"kind":{"const":"group"},"x":{"type":"number","minimum":-400,"maximum":2400},"y":{"type":"number","minimum":-400,"maximum":2400},"w":{"type":"number","minimum":1,"maximum":2400},"h":{"type":"number","minimum":1,"maximum":2400},"tone":{"$ref":"../common.schema.json#/$defs/tone"},"label":{"type":"string","minLength":0,"maxLength":200},"sub":{"type":"string","minLength":0,"maxLength":200},"solid":{"type":"boolean"},"select":{"$ref":"#/$defs/viewKey"},"active":{"oneOf":[{"type":"boolean"},{"type":"array","maxItems":20,"items":{"oneOf":[{"type":"integer","minimum":0,"maximum":15},{"$ref":"../common.schema.json#/$defs/id"}]}}]},"hot":{"oneOf":[{"type":"boolean"},{"type":"array","maxItems":20,"items":{"oneOf":[{"type":"integer","minimum":0,"maximum":15},{"$ref":"../common.schema.json#/$defs/id"}]}}]},"dim":{"type":"boolean"},"visible":{"type":"array","maxItems":20,"items":{"oneOf":[{"type":"integer","minimum":0,"maximum":15},{"$ref":"../common.schema.json#/$defs/id"}]}},"overrides":{"type":"object","maxProperties":6,"propertyNames":{"enum":["title","sub","label","text","n","dashed","muted"]},"additionalProperties":{"type":"object","maxProperties":20,"additionalProperties":{"oneOf":[{"type":"string","minLength":0,"maxLength":300},{"type":"number"},{"type":"boolean"},{"type":"array","maxItems":3,"items":{"type":"string","minLength":0,"maxLength":300}}]}}}}},"arrow":{"type":"object","additionalProperties":false,"required":["kind","points"],"properties":{"kind":{"const":"arrow"},"points":{"type":"array","minItems":2,"maxItems":12,"items":{"type":"array","minItems":2,"maxItems":2,"items":{"type":"number","minimum":-400,"maximum":2400}}},"label":{"type":"string","minLength":0,"maxLength":120},"labelAt":{"type":"number","minimum":0,"maximum":1},"labelDx":{"type":"number","minimum":-400,"maximum":2400},"labelDy":{"type":"number","minimum":-400,"maximum":2400},"dashed":{"type":"boolean"},"both":{"type":"boolean"},"variant":{"enum":["ok","bad"]},"labelAnchor":{"enum":["start","middle","end"]},"active":{"oneOf":[{"type":"boolean"},{"type":"array","maxItems":20,"items":{"oneOf":[{"type":"integer","minimum":0,"maximum":15},{"$ref":"../common.schema.json#/$defs/id"}]}}]},"hot":{"oneOf":[{"type":"boolean"},{"type":"array","maxItems":20,"items":{"oneOf":[{"type":"integer","minimum":0,"maximum":15},{"$ref":"../common.schema.json#/$defs/id"}]}}]},"dim":{"type":"boolean"},"visible":{"type":"array","maxItems":20,"items":{"oneOf":[{"type":"integer","minimum":0,"maximum":15},{"$ref":"../common.schema.json#/$defs/id"}]}},"overrides":{"type":"object","maxProperties":6,"propertyNames":{"enum":["title","sub","label","text","n","dashed","muted"]},"additionalProperties":{"type":"object","maxProperties":20,"additionalProperties":{"oneOf":[{"type":"string","minLength":0,"maxLength":300},{"type":"number"},{"type":"boolean"},{"type":"array","maxItems":3,"items":{"type":"string","minLength":0,"maxLength":300}}]}}}}},"label":{"type":"object","additionalProperties":false,"required":["kind","x","y","text"],"properties":{"kind":{"const":"label"},"x":{"type":"number","minimum":-400,"maximum":2400},"y":{"type":"number","minimum":-400,"maximum":2400},"text":{"type":"string","minLength":0,"maxLength":200},"anchor":{"enum":["start","middle","end"]},"size":{"type":"number","minimum":8,"maximum":32},"muted":{"type":"boolean"},"weight":{"type":"number","minimum":100,"maximum":900},"mono":{"type":"boolean"},"active":{"oneOf":[{"type":"boolean"},{"type":"array","maxItems":20,"items":{"oneOf":[{"type":"integer","minimum":0,"maximum":15},{"$ref":"../common.schema.json#/$defs/id"}]}}]},"hot":{"oneOf":[{"type":"boolean"},{"type":"array","maxItems":20,"items":{"oneOf":[{"type":"integer","minimum":0,"maximum":15},{"$ref":"../common.schema.json#/$defs/id"}]}}]},"dim":{"type":"boolean"},"visible":{"type":"array","maxItems":20,"items":{"oneOf":[{"type":"integer","minimum":0,"maximum":15},{"$ref":"../common.schema.json#/$defs/id"}]}},"overrides":{"type":"object","maxProperties":6,"propertyNames":{"enum":["title","sub","label","text","n","dashed","muted"]},"additionalProperties":{"type":"object","maxProperties":20,"additionalProperties":{"oneOf":[{"type":"string","minLength":0,"maxLength":300},{"type":"number"},{"type":"boolean"},{"type":"array","maxItems":3,"items":{"type":"string","minLength":0,"maxLength":300}}]}}}}},"badge":{"type":"object","additionalProperties":false,"required":["kind","x","y"],"properties":{"kind":{"const":"badge"},"x":{"type":"number","minimum":-400,"maximum":2400},"y":{"type":"number","minimum":-400,"maximum":2400},"n":{"type":"integer","minimum":0,"maximum":99},"active":{"oneOf":[{"type":"boolean"},{"type":"array","maxItems":20,"items":{"oneOf":[{"type":"integer","minimum":0,"maximum":15},{"$ref":"../common.schema.json#/$defs/id"}]}}]},"hot":{"oneOf":[{"type":"boolean"},{"type":"array","maxItems":20,"items":{"oneOf":[{"type":"integer","minimum":0,"maximum":15},{"$ref":"../common.schema.json#/$defs/id"}]}}]},"dim":{"type":"boolean"},"visible":{"type":"array","maxItems":20,"items":{"oneOf":[{"type":"integer","minimum":0,"maximum":15},{"$ref":"../common.schema.json#/$defs/id"}]}},"overrides":{"type":"object","maxProperties":6,"propertyNames":{"enum":["title","sub","label","text","n","dashed","muted"]},"additionalProperties":{"type":"object","maxProperties":20,"additionalProperties":{"oneOf":[{"type":"string","minLength":0,"maxLength":300},{"type":"number"},{"type":"boolean"},{"type":"array","maxItems":3,"items":{"type":"string","minLength":0,"maxLength":300}}]}}}}}}};const schema151 = {"type":"object","additionalProperties":false,"required":["kind","x","y","w","h"],"properties":{"kind":{"const":"node"},"x":{"type":"number","minimum":-400,"maximum":2400},"y":{"type":"number","minimum":-400,"maximum":2400},"w":{"type":"number","minimum":1,"maximum":2400},"h":{"type":"number","minimum":1,"maximum":2400},"tone":{"$ref":"../common.schema.json#/$defs/tone"},"title":{"type":"string","minLength":0,"maxLength":200},"sub":{"oneOf":[{"type":"string","minLength":0,"maxLength":300},{"type":"array","minItems":1,"maxItems":6,"items":{"type":"string","minLength":0,"maxLength":300}}]},"mono":{"type":"boolean"},"rx":{"type":"number","minimum":0,"maximum":60},"titleSize":{"type":"number","minimum":8,"maximum":32},"align":{"enum":["start","middle"]},"select":{"$ref":"#/$defs/viewKey"},"active":{"oneOf":[{"type":"boolean"},{"type":"array","maxItems":20,"items":{"oneOf":[{"type":"integer","minimum":0,"maximum":15},{"$ref":"../common.schema.json#/$defs/id"}]}}]},"hot":{"oneOf":[{"type":"boolean"},{"type":"array","maxItems":20,"items":{"oneOf":[{"type":"integer","minimum":0,"maximum":15},{"$ref":"../common.schema.json#/$defs/id"}]}}]},"dim":{"type":"boolean"},"visible":{"type":"array","maxItems":20,"items":{"oneOf":[{"type":"integer","minimum":0,"maximum":15},{"$ref":"../common.schema.json#/$defs/id"}]}},"overrides":{"type":"object","maxProperties":6,"propertyNames":{"enum":["title","sub","label","text","n","dashed","muted"]},"additionalProperties":{"type":"object","maxProperties":20,"additionalProperties":{"oneOf":[{"type":"string","minLength":0,"maxLength":300},{"type":"number"},{"type":"boolean"},{"type":"array","maxItems":3,"items":{"type":"string","minLength":0,"maxLength":300}}]}}}}};function validate98(data, {instancePath="", parentData, parentDataProperty, rootData=data, dynamicAnchors={}}={}){let vErrors = null;let errors = 0;const evaluated0 = validate98.evaluated;if(evaluated0.dynamicProps){evaluated0.props = undefined;}if(evaluated0.dynamicItems){evaluated0.items = undefined;}if(errors === 0){if(data && typeof data == "object" && !Array.isArray(data)){let missing0;if((((((data.kind === undefined) && (missing0 = "kind")) || ((data.x === undefined) && (missing0 = "x"))) || ((data.y === undefined) && (missing0 = "y"))) || ((data.w === undefined) && (missing0 = "w"))) || ((data.h === undefined) && (missing0 = "h"))){validate98.errors = [{instancePath,schemaPath:"#/required",keyword:"required",params:{missingProperty: missing0},message:"must have required property '"+missing0+"'"}];return false;}else {const _errs1 = errors;for(const key0 in data){if(!(func1.call(schema151.properties, key0))){validate98.errors = [{instancePath,schemaPath:"#/additionalProperties",keyword:"additionalProperties",params:{additionalProperty: key0},message:"must NOT have additional properties"}];return false;break;}}if(_errs1 === errors){if(data.kind !== undefined){const _errs2 = errors;if("node" !== data.kind){validate98.errors = [{instancePath:instancePath+"/kind",schemaPath:"#/properties/kind/const",keyword:"const",params:{allowedValue: "node"},message:"must be equal to constant"}];return false;}var valid0 = _errs2 === errors;}else {var valid0 = true;}if(valid0){if(data.x !== undefined){let data1 = data.x;const _errs3 = errors;if(errors === _errs3){if((typeof data1 == "number") && (isFinite(data1))){if(data1 > 2400 || isNaN(data1)){validate98.errors = [{instancePath:instancePath+"/x",schemaPath:"#/properties/x/maximum",keyword:"maximum",params:{comparison: "<=", limit: 2400},message:"must be <= 2400"}];return false;}else {if(data1 < -400 || isNaN(data1)){validate98.errors = [{instancePath:instancePath+"/x",schemaPath:"#/properties/x/minimum",keyword:"minimum",params:{comparison: ">=", limit: -400},message:"must be >= -400"}];return false;}}}else {validate98.errors = [{instancePath:instancePath+"/x",schemaPath:"#/properties/x/type",keyword:"type",params:{type: "number"},message:"must be number"}];return false;}}var valid0 = _errs3 === errors;}else {var valid0 = true;}if(valid0){if(data.y !== undefined){let data2 = data.y;const _errs5 = errors;if(errors === _errs5){if((typeof data2 == "number") && (isFinite(data2))){if(data2 > 2400 || isNaN(data2)){validate98.errors = [{instancePath:instancePath+"/y",schemaPath:"#/properties/y/maximum",keyword:"maximum",params:{comparison: "<=", limit: 2400},message:"must be <= 2400"}];return false;}else {if(data2 < -400 || isNaN(data2)){validate98.errors = [{instancePath:instancePath+"/y",schemaPath:"#/properties/y/minimum",keyword:"minimum",params:{comparison: ">=", limit: -400},message:"must be >= -400"}];return false;}}}else {validate98.errors = [{instancePath:instancePath+"/y",schemaPath:"#/properties/y/type",keyword:"type",params:{type: "number"},message:"must be number"}];return false;}}var valid0 = _errs5 === errors;}else {var valid0 = true;}if(valid0){if(data.w !== undefined){let data3 = data.w;const _errs7 = errors;if(errors === _errs7){if((typeof data3 == "number") && (isFinite(data3))){if(data3 > 2400 || isNaN(data3)){validate98.errors = [{instancePath:instancePath+"/w",schemaPath:"#/properties/w/maximum",keyword:"maximum",params:{comparison: "<=", limit: 2400},message:"must be <= 2400"}];return false;}else {if(data3 < 1 || isNaN(data3)){validate98.errors = [{instancePath:instancePath+"/w",schemaPath:"#/properties/w/minimum",keyword:"minimum",params:{comparison: ">=", limit: 1},message:"must be >= 1"}];return false;}}}else {validate98.errors = [{instancePath:instancePath+"/w",schemaPath:"#/properties/w/type",keyword:"type",params:{type: "number"},message:"must be number"}];return false;}}var valid0 = _errs7 === errors;}else {var valid0 = true;}if(valid0){if(data.h !== undefined){let data4 = data.h;const _errs9 = errors;if(errors === _errs9){if((typeof data4 == "number") && (isFinite(data4))){if(data4 > 2400 || isNaN(data4)){validate98.errors = [{instancePath:instancePath+"/h",schemaPath:"#/properties/h/maximum",keyword:"maximum",params:{comparison: "<=", limit: 2400},message:"must be <= 2400"}];return false;}else {if(data4 < 1 || isNaN(data4)){validate98.errors = [{instancePath:instancePath+"/h",schemaPath:"#/properties/h/minimum",keyword:"minimum",params:{comparison: ">=", limit: 1},message:"must be >= 1"}];return false;}}}else {validate98.errors = [{instancePath:instancePath+"/h",schemaPath:"#/properties/h/type",keyword:"type",params:{type: "number"},message:"must be number"}];return false;}}var valid0 = _errs9 === errors;}else {var valid0 = true;}if(valid0){if(data.tone !== undefined){let data5 = data.tone;const _errs11 = errors;if(!(((((((((data5 === "gray") || (data5 === "purple")) || (data5 === "teal")) || (data5 === "coral")) || (data5 === "pink")) || (data5 === "blue")) || (data5 === "green")) || (data5 === "amber")) || (data5 === "red"))){validate98.errors = [{instancePath:instancePath+"/tone",schemaPath:"../common.schema.json#/$defs/tone/enum",keyword:"enum",params:{allowedValues: schema148.enum},message:"must be equal to one of the allowed values"}];return false;}var valid0 = _errs11 === errors;}else {var valid0 = true;}if(valid0){if(data.title !== undefined){let data6 = data.title;const _errs13 = errors;if(errors === _errs13){if(typeof data6 === "string"){if(func2(data6) > 200){validate98.errors = [{instancePath:instancePath+"/title",schemaPath:"#/properties/title/maxLength",keyword:"maxLength",params:{limit: 200},message:"must NOT have more than 200 characters"}];return false;}else {if(func2(data6) < 0){validate98.errors = [{instancePath:instancePath+"/title",schemaPath:"#/properties/title/minLength",keyword:"minLength",params:{limit: 0},message:"must NOT have fewer than 0 characters"}];return false;}}}else {validate98.errors = [{instancePath:instancePath+"/title",schemaPath:"#/properties/title/type",keyword:"type",params:{type: "string"},message:"must be string"}];return false;}}var valid0 = _errs13 === errors;}else {var valid0 = true;}if(valid0){if(data.sub !== undefined){let data7 = data.sub;const _errs15 = errors;const _errs16 = errors;let valid2 = false;let passing0 = null;const _errs17 = errors;if(errors === _errs17){if(typeof data7 === "string"){if(func2(data7) > 300){const err0 = {instancePath:instancePath+"/sub",schemaPath:"#/properties/sub/oneOf/0/maxLength",keyword:"maxLength",params:{limit: 300},message:"must NOT have more than 300 characters"};if(vErrors === null){vErrors = [err0];}else {vErrors.push(err0);}errors++;}else {if(func2(data7) < 0){const err1 = {instancePath:instancePath+"/sub",schemaPath:"#/properties/sub/oneOf/0/minLength",keyword:"minLength",params:{limit: 0},message:"must NOT have fewer than 0 characters"};if(vErrors === null){vErrors = [err1];}else {vErrors.push(err1);}errors++;}}}else {const err2 = {instancePath:instancePath+"/sub",schemaPath:"#/properties/sub/oneOf/0/type",keyword:"type",params:{type: "string"},message:"must be string"};if(vErrors === null){vErrors = [err2];}else {vErrors.push(err2);}errors++;}}var _valid0 = _errs17 === errors;if(_valid0){valid2 = true;passing0 = 0;}const _errs19 = errors;if(errors === _errs19){if(Array.isArray(data7)){if(data7.length > 6){const err3 = {instancePath:instancePath+"/sub",schemaPath:"#/properties/sub/oneOf/1/maxItems",keyword:"maxItems",params:{limit: 6},message:"must NOT have more than 6 items"};if(vErrors === null){vErrors = [err3];}else {vErrors.push(err3);}errors++;}else {if(data7.length < 1){const err4 = {instancePath:instancePath+"/sub",schemaPath:"#/properties/sub/oneOf/1/minItems",keyword:"minItems",params:{limit: 1},message:"must NOT have fewer than 1 items"};if(vErrors === null){vErrors = [err4];}else {vErrors.push(err4);}errors++;}else {var valid3 = true;const len0 = data7.length;for(let i0=0; i0 300){const err5 = {instancePath:instancePath+"/sub/" + i0,schemaPath:"#/properties/sub/oneOf/1/items/maxLength",keyword:"maxLength",params:{limit: 300},message:"must NOT have more than 300 characters"};if(vErrors === null){vErrors = [err5];}else {vErrors.push(err5);}errors++;}else {if(func2(data8) < 0){const err6 = {instancePath:instancePath+"/sub/" + i0,schemaPath:"#/properties/sub/oneOf/1/items/minLength",keyword:"minLength",params:{limit: 0},message:"must NOT have fewer than 0 characters"};if(vErrors === null){vErrors = [err6];}else {vErrors.push(err6);}errors++;}}}else {const err7 = {instancePath:instancePath+"/sub/" + i0,schemaPath:"#/properties/sub/oneOf/1/items/type",keyword:"type",params:{type: "string"},message:"must be string"};if(vErrors === null){vErrors = [err7];}else {vErrors.push(err7);}errors++;}}var valid3 = _errs21 === errors;if(!valid3){break;}}}}}else {const err8 = {instancePath:instancePath+"/sub",schemaPath:"#/properties/sub/oneOf/1/type",keyword:"type",params:{type: "array"},message:"must be array"};if(vErrors === null){vErrors = [err8];}else {vErrors.push(err8);}errors++;}}var _valid0 = _errs19 === errors;if(_valid0 && valid2){valid2 = false;passing0 = [passing0, 1];}else {if(_valid0){valid2 = true;passing0 = 1;}}if(!valid2){const err9 = {instancePath:instancePath+"/sub",schemaPath:"#/properties/sub/oneOf",keyword:"oneOf",params:{passingSchemas: passing0},message:"must match exactly one schema in oneOf"};if(vErrors === null){vErrors = [err9];}else {vErrors.push(err9);}errors++;validate98.errors = vErrors;return false;}else {errors = _errs16;if(vErrors !== null){if(_errs16){vErrors.length = _errs16;}else {vErrors = null;}}}var valid0 = _errs15 === errors;}else {var valid0 = true;}if(valid0){if(data.mono !== undefined){const _errs23 = errors;if(typeof data.mono !== "boolean"){validate98.errors = [{instancePath:instancePath+"/mono",schemaPath:"#/properties/mono/type",keyword:"type",params:{type: "boolean"},message:"must be boolean"}];return false;}var valid0 = _errs23 === errors;}else {var valid0 = true;}if(valid0){if(data.rx !== undefined){let data10 = data.rx;const _errs25 = errors;if(errors === _errs25){if((typeof data10 == "number") && (isFinite(data10))){if(data10 > 60 || isNaN(data10)){validate98.errors = [{instancePath:instancePath+"/rx",schemaPath:"#/properties/rx/maximum",keyword:"maximum",params:{comparison: "<=", limit: 60},message:"must be <= 60"}];return false;}else {if(data10 < 0 || isNaN(data10)){validate98.errors = [{instancePath:instancePath+"/rx",schemaPath:"#/properties/rx/minimum",keyword:"minimum",params:{comparison: ">=", limit: 0},message:"must be >= 0"}];return false;}}}else {validate98.errors = [{instancePath:instancePath+"/rx",schemaPath:"#/properties/rx/type",keyword:"type",params:{type: "number"},message:"must be number"}];return false;}}var valid0 = _errs25 === errors;}else {var valid0 = true;}if(valid0){if(data.titleSize !== undefined){let data11 = data.titleSize;const _errs27 = errors;if(errors === _errs27){if((typeof data11 == "number") && (isFinite(data11))){if(data11 > 32 || isNaN(data11)){validate98.errors = [{instancePath:instancePath+"/titleSize",schemaPath:"#/properties/titleSize/maximum",keyword:"maximum",params:{comparison: "<=", limit: 32},message:"must be <= 32"}];return false;}else {if(data11 < 8 || isNaN(data11)){validate98.errors = [{instancePath:instancePath+"/titleSize",schemaPath:"#/properties/titleSize/minimum",keyword:"minimum",params:{comparison: ">=", limit: 8},message:"must be >= 8"}];return false;}}}else {validate98.errors = [{instancePath:instancePath+"/titleSize",schemaPath:"#/properties/titleSize/type",keyword:"type",params:{type: "number"},message:"must be number"}];return false;}}var valid0 = _errs27 === errors;}else {var valid0 = true;}if(valid0){if(data.align !== undefined){let data12 = data.align;const _errs29 = errors;if(!((data12 === "start") || (data12 === "middle"))){validate98.errors = [{instancePath:instancePath+"/align",schemaPath:"#/properties/align/enum",keyword:"enum",params:{allowedValues: schema151.properties.align.enum},message:"must be equal to one of the allowed values"}];return false;}var valid0 = _errs29 === errors;}else {var valid0 = true;}if(valid0){if(data.select !== undefined){let data13 = data.select;const _errs30 = errors;const _errs31 = errors;if(errors === _errs31){if(typeof data13 === "string"){if(func2(data13) > 64){validate98.errors = [{instancePath:instancePath+"/select",schemaPath:"#/$defs/viewKey/maxLength",keyword:"maxLength",params:{limit: 64},message:"must NOT have more than 64 characters"}];return false;}else {if(!pattern18.test(data13)){validate98.errors = [{instancePath:instancePath+"/select",schemaPath:"#/$defs/viewKey/pattern",keyword:"pattern",params:{pattern: "^[a-z0-9]+(?:-[a-z0-9]+)*$"},message:"must match pattern \""+"^[a-z0-9]+(?:-[a-z0-9]+)*$"+"\""}];return false;}}}else {validate98.errors = [{instancePath:instancePath+"/select",schemaPath:"#/$defs/viewKey/type",keyword:"type",params:{type: "string"},message:"must be string"}];return false;}}var valid0 = _errs30 === errors;}else {var valid0 = true;}if(valid0){if(data.active !== undefined){let data14 = data.active;const _errs33 = errors;const _errs34 = errors;let valid5 = false;let passing1 = null;const _errs35 = errors;if(typeof data14 !== "boolean"){const err10 = {instancePath:instancePath+"/active",schemaPath:"#/properties/active/oneOf/0/type",keyword:"type",params:{type: "boolean"},message:"must be boolean"};if(vErrors === null){vErrors = [err10];}else {vErrors.push(err10);}errors++;}var _valid1 = _errs35 === errors;if(_valid1){valid5 = true;passing1 = 0;}const _errs37 = errors;if(errors === _errs37){if(Array.isArray(data14)){if(data14.length > 20){const err11 = {instancePath:instancePath+"/active",schemaPath:"#/properties/active/oneOf/1/maxItems",keyword:"maxItems",params:{limit: 20},message:"must NOT have more than 20 items"};if(vErrors === null){vErrors = [err11];}else {vErrors.push(err11);}errors++;}else {var valid6 = true;const len1 = data14.length;for(let i1=0; i1 15 || isNaN(data15)){const err13 = {instancePath:instancePath+"/active/" + i1,schemaPath:"#/properties/active/oneOf/1/items/oneOf/0/maximum",keyword:"maximum",params:{comparison: "<=", limit: 15},message:"must be <= 15"};if(vErrors === null){vErrors = [err13];}else {vErrors.push(err13);}errors++;}else {if(data15 < 0 || isNaN(data15)){const err14 = {instancePath:instancePath+"/active/" + i1,schemaPath:"#/properties/active/oneOf/1/items/oneOf/0/minimum",keyword:"minimum",params:{comparison: ">=", limit: 0},message:"must be >= 0"};if(vErrors === null){vErrors = [err14];}else {vErrors.push(err14);}errors++;}}}}var _valid2 = _errs41 === errors;if(_valid2){valid7 = true;passing2 = 0;}const _errs43 = errors;const _errs44 = errors;if(errors === _errs44){if(typeof data15 === "string"){if(func2(data15) > 64){const err15 = {instancePath:instancePath+"/active/" + i1,schemaPath:"../common.schema.json#/$defs/id/maxLength",keyword:"maxLength",params:{limit: 64},message:"must NOT have more than 64 characters"};if(vErrors === null){vErrors = [err15];}else {vErrors.push(err15);}errors++;}else {if(!pattern18.test(data15)){const err16 = {instancePath:instancePath+"/active/" + i1,schemaPath:"../common.schema.json#/$defs/id/pattern",keyword:"pattern",params:{pattern: "^[a-z0-9]+(?:-[a-z0-9]+)*$"},message:"must match pattern \""+"^[a-z0-9]+(?:-[a-z0-9]+)*$"+"\""};if(vErrors === null){vErrors = [err16];}else {vErrors.push(err16);}errors++;}}}else {const err17 = {instancePath:instancePath+"/active/" + i1,schemaPath:"../common.schema.json#/$defs/id/type",keyword:"type",params:{type: "string"},message:"must be string"};if(vErrors === null){vErrors = [err17];}else {vErrors.push(err17);}errors++;}}var _valid2 = _errs43 === errors;if(_valid2 && valid7){valid7 = false;passing2 = [passing2, 1];}else {if(_valid2){valid7 = true;passing2 = 1;}}if(!valid7){const err18 = {instancePath:instancePath+"/active/" + i1,schemaPath:"#/properties/active/oneOf/1/items/oneOf",keyword:"oneOf",params:{passingSchemas: passing2},message:"must match exactly one schema in oneOf"};if(vErrors === null){vErrors = [err18];}else {vErrors.push(err18);}errors++;}else {errors = _errs40;if(vErrors !== null){if(_errs40){vErrors.length = _errs40;}else {vErrors = null;}}}var valid6 = _errs39 === errors;if(!valid6){break;}}}}else {const err19 = {instancePath:instancePath+"/active",schemaPath:"#/properties/active/oneOf/1/type",keyword:"type",params:{type: "array"},message:"must be array"};if(vErrors === null){vErrors = [err19];}else {vErrors.push(err19);}errors++;}}var _valid1 = _errs37 === errors;if(_valid1 && valid5){valid5 = false;passing1 = [passing1, 1];}else {if(_valid1){valid5 = true;passing1 = 1;}}if(!valid5){const err20 = {instancePath:instancePath+"/active",schemaPath:"#/properties/active/oneOf",keyword:"oneOf",params:{passingSchemas: passing1},message:"must match exactly one schema in oneOf"};if(vErrors === null){vErrors = [err20];}else {vErrors.push(err20);}errors++;validate98.errors = vErrors;return false;}else {errors = _errs34;if(vErrors !== null){if(_errs34){vErrors.length = _errs34;}else {vErrors = null;}}}var valid0 = _errs33 === errors;}else {var valid0 = true;}if(valid0){if(data.hot !== undefined){let data16 = data.hot;const _errs46 = errors;const _errs47 = errors;let valid9 = false;let passing3 = null;const _errs48 = errors;if(typeof data16 !== "boolean"){const err21 = {instancePath:instancePath+"/hot",schemaPath:"#/properties/hot/oneOf/0/type",keyword:"type",params:{type: "boolean"},message:"must be boolean"};if(vErrors === null){vErrors = [err21];}else {vErrors.push(err21);}errors++;}var _valid3 = _errs48 === errors;if(_valid3){valid9 = true;passing3 = 0;}const _errs50 = errors;if(errors === _errs50){if(Array.isArray(data16)){if(data16.length > 20){const err22 = {instancePath:instancePath+"/hot",schemaPath:"#/properties/hot/oneOf/1/maxItems",keyword:"maxItems",params:{limit: 20},message:"must NOT have more than 20 items"};if(vErrors === null){vErrors = [err22];}else {vErrors.push(err22);}errors++;}else {var valid10 = true;const len2 = data16.length;for(let i2=0; i2 15 || isNaN(data17)){const err24 = {instancePath:instancePath+"/hot/" + i2,schemaPath:"#/properties/hot/oneOf/1/items/oneOf/0/maximum",keyword:"maximum",params:{comparison: "<=", limit: 15},message:"must be <= 15"};if(vErrors === null){vErrors = [err24];}else {vErrors.push(err24);}errors++;}else {if(data17 < 0 || isNaN(data17)){const err25 = {instancePath:instancePath+"/hot/" + i2,schemaPath:"#/properties/hot/oneOf/1/items/oneOf/0/minimum",keyword:"minimum",params:{comparison: ">=", limit: 0},message:"must be >= 0"};if(vErrors === null){vErrors = [err25];}else {vErrors.push(err25);}errors++;}}}}var _valid4 = _errs54 === errors;if(_valid4){valid11 = true;passing4 = 0;}const _errs56 = errors;const _errs57 = errors;if(errors === _errs57){if(typeof data17 === "string"){if(func2(data17) > 64){const err26 = {instancePath:instancePath+"/hot/" + i2,schemaPath:"../common.schema.json#/$defs/id/maxLength",keyword:"maxLength",params:{limit: 64},message:"must NOT have more than 64 characters"};if(vErrors === null){vErrors = [err26];}else {vErrors.push(err26);}errors++;}else {if(!pattern18.test(data17)){const err27 = {instancePath:instancePath+"/hot/" + i2,schemaPath:"../common.schema.json#/$defs/id/pattern",keyword:"pattern",params:{pattern: "^[a-z0-9]+(?:-[a-z0-9]+)*$"},message:"must match pattern \""+"^[a-z0-9]+(?:-[a-z0-9]+)*$"+"\""};if(vErrors === null){vErrors = [err27];}else {vErrors.push(err27);}errors++;}}}else {const err28 = {instancePath:instancePath+"/hot/" + i2,schemaPath:"../common.schema.json#/$defs/id/type",keyword:"type",params:{type: "string"},message:"must be string"};if(vErrors === null){vErrors = [err28];}else {vErrors.push(err28);}errors++;}}var _valid4 = _errs56 === errors;if(_valid4 && valid11){valid11 = false;passing4 = [passing4, 1];}else {if(_valid4){valid11 = true;passing4 = 1;}}if(!valid11){const err29 = {instancePath:instancePath+"/hot/" + i2,schemaPath:"#/properties/hot/oneOf/1/items/oneOf",keyword:"oneOf",params:{passingSchemas: passing4},message:"must match exactly one schema in oneOf"};if(vErrors === null){vErrors = [err29];}else {vErrors.push(err29);}errors++;}else {errors = _errs53;if(vErrors !== null){if(_errs53){vErrors.length = _errs53;}else {vErrors = null;}}}var valid10 = _errs52 === errors;if(!valid10){break;}}}}else {const err30 = {instancePath:instancePath+"/hot",schemaPath:"#/properties/hot/oneOf/1/type",keyword:"type",params:{type: "array"},message:"must be array"};if(vErrors === null){vErrors = [err30];}else {vErrors.push(err30);}errors++;}}var _valid3 = _errs50 === errors;if(_valid3 && valid9){valid9 = false;passing3 = [passing3, 1];}else {if(_valid3){valid9 = true;passing3 = 1;}}if(!valid9){const err31 = {instancePath:instancePath+"/hot",schemaPath:"#/properties/hot/oneOf",keyword:"oneOf",params:{passingSchemas: passing3},message:"must match exactly one schema in oneOf"};if(vErrors === null){vErrors = [err31];}else {vErrors.push(err31);}errors++;validate98.errors = vErrors;return false;}else {errors = _errs47;if(vErrors !== null){if(_errs47){vErrors.length = _errs47;}else {vErrors = null;}}}var valid0 = _errs46 === errors;}else {var valid0 = true;}if(valid0){if(data.dim !== undefined){const _errs59 = errors;if(typeof data.dim !== "boolean"){validate98.errors = [{instancePath:instancePath+"/dim",schemaPath:"#/properties/dim/type",keyword:"type",params:{type: "boolean"},message:"must be boolean"}];return false;}var valid0 = _errs59 === errors;}else {var valid0 = true;}if(valid0){if(data.visible !== undefined){let data19 = data.visible;const _errs61 = errors;if(errors === _errs61){if(Array.isArray(data19)){if(data19.length > 20){validate98.errors = [{instancePath:instancePath+"/visible",schemaPath:"#/properties/visible/maxItems",keyword:"maxItems",params:{limit: 20},message:"must NOT have more than 20 items"}];return false;}else {var valid13 = true;const len3 = data19.length;for(let i3=0; i3 15 || isNaN(data20)){const err33 = {instancePath:instancePath+"/visible/" + i3,schemaPath:"#/properties/visible/items/oneOf/0/maximum",keyword:"maximum",params:{comparison: "<=", limit: 15},message:"must be <= 15"};if(vErrors === null){vErrors = [err33];}else {vErrors.push(err33);}errors++;}else {if(data20 < 0 || isNaN(data20)){const err34 = {instancePath:instancePath+"/visible/" + i3,schemaPath:"#/properties/visible/items/oneOf/0/minimum",keyword:"minimum",params:{comparison: ">=", limit: 0},message:"must be >= 0"};if(vErrors === null){vErrors = [err34];}else {vErrors.push(err34);}errors++;}}}}var _valid5 = _errs65 === errors;if(_valid5){valid14 = true;passing5 = 0;}const _errs67 = errors;const _errs68 = errors;if(errors === _errs68){if(typeof data20 === "string"){if(func2(data20) > 64){const err35 = {instancePath:instancePath+"/visible/" + i3,schemaPath:"../common.schema.json#/$defs/id/maxLength",keyword:"maxLength",params:{limit: 64},message:"must NOT have more than 64 characters"};if(vErrors === null){vErrors = [err35];}else {vErrors.push(err35);}errors++;}else {if(!pattern18.test(data20)){const err36 = {instancePath:instancePath+"/visible/" + i3,schemaPath:"../common.schema.json#/$defs/id/pattern",keyword:"pattern",params:{pattern: "^[a-z0-9]+(?:-[a-z0-9]+)*$"},message:"must match pattern \""+"^[a-z0-9]+(?:-[a-z0-9]+)*$"+"\""};if(vErrors === null){vErrors = [err36];}else {vErrors.push(err36);}errors++;}}}else {const err37 = {instancePath:instancePath+"/visible/" + i3,schemaPath:"../common.schema.json#/$defs/id/type",keyword:"type",params:{type: "string"},message:"must be string"};if(vErrors === null){vErrors = [err37];}else {vErrors.push(err37);}errors++;}}var _valid5 = _errs67 === errors;if(_valid5 && valid14){valid14 = false;passing5 = [passing5, 1];}else {if(_valid5){valid14 = true;passing5 = 1;}}if(!valid14){const err38 = {instancePath:instancePath+"/visible/" + i3,schemaPath:"#/properties/visible/items/oneOf",keyword:"oneOf",params:{passingSchemas: passing5},message:"must match exactly one schema in oneOf"};if(vErrors === null){vErrors = [err38];}else {vErrors.push(err38);}errors++;validate98.errors = vErrors;return false;}else {errors = _errs64;if(vErrors !== null){if(_errs64){vErrors.length = _errs64;}else {vErrors = null;}}}var valid13 = _errs63 === errors;if(!valid13){break;}}}}else {validate98.errors = [{instancePath:instancePath+"/visible",schemaPath:"#/properties/visible/type",keyword:"type",params:{type: "array"},message:"must be array"}];return false;}}var valid0 = _errs61 === errors;}else {var valid0 = true;}if(valid0){if(data.overrides !== undefined){let data21 = data.overrides;const _errs70 = errors;if(errors === _errs70){if(data21 && typeof data21 == "object" && !Array.isArray(data21)){if(Object.keys(data21).length > 6){validate98.errors = [{instancePath:instancePath+"/overrides",schemaPath:"#/properties/overrides/maxProperties",keyword:"maxProperties",params:{limit: 6},message:"must NOT have more than 6 properties"}];return false;}else {for(const key1 in data21){const _errs72 = errors;if(!(((((((key1 === "title") || (key1 === "sub")) || (key1 === "label")) || (key1 === "text")) || (key1 === "n")) || (key1 === "dashed")) || (key1 === "muted"))){const err39 = {instancePath:instancePath+"/overrides",schemaPath:"#/properties/overrides/propertyNames/enum",keyword:"enum",params:{allowedValues: schema151.properties.overrides.propertyNames.enum},message:"must be equal to one of the allowed values",propertyName:key1};if(vErrors === null){vErrors = [err39];}else {vErrors.push(err39);}errors++;}var valid16 = _errs72 === errors;if(!valid16){const err40 = {instancePath:instancePath+"/overrides",schemaPath:"#/properties/overrides/propertyNames",keyword:"propertyNames",params:{propertyName: key1},message:"property name must be valid"};if(vErrors === null){vErrors = [err40];}else {vErrors.push(err40);}errors++;validate98.errors = vErrors;return false;break;}}if(valid16){for(const key2 in data21){let data22 = data21[key2];const _errs74 = errors;if(errors === _errs74){if(data22 && typeof data22 == "object" && !Array.isArray(data22)){if(Object.keys(data22).length > 20){validate98.errors = [{instancePath:instancePath+"/overrides/" + key2.replace(/~/g, "~0").replace(/\//g, "~1"),schemaPath:"#/properties/overrides/additionalProperties/maxProperties",keyword:"maxProperties",params:{limit: 20},message:"must NOT have more than 20 properties"}];return false;}else {for(const key3 in data22){let data23 = data22[key3];const _errs77 = errors;const _errs78 = errors;let valid19 = false;let passing6 = null;const _errs79 = errors;if(errors === _errs79){if(typeof data23 === "string"){if(func2(data23) > 300){const err41 = {instancePath:instancePath+"/overrides/" + key2.replace(/~/g, "~0").replace(/\//g, "~1")+"/" + key3.replace(/~/g, "~0").replace(/\//g, "~1"),schemaPath:"#/properties/overrides/additionalProperties/additionalProperties/oneOf/0/maxLength",keyword:"maxLength",params:{limit: 300},message:"must NOT have more than 300 characters"};if(vErrors === null){vErrors = [err41];}else {vErrors.push(err41);}errors++;}else {if(func2(data23) < 0){const err42 = {instancePath:instancePath+"/overrides/" + key2.replace(/~/g, "~0").replace(/\//g, "~1")+"/" + key3.replace(/~/g, "~0").replace(/\//g, "~1"),schemaPath:"#/properties/overrides/additionalProperties/additionalProperties/oneOf/0/minLength",keyword:"minLength",params:{limit: 0},message:"must NOT have fewer than 0 characters"};if(vErrors === null){vErrors = [err42];}else {vErrors.push(err42);}errors++;}}}else {const err43 = {instancePath:instancePath+"/overrides/" + key2.replace(/~/g, "~0").replace(/\//g, "~1")+"/" + key3.replace(/~/g, "~0").replace(/\//g, "~1"),schemaPath:"#/properties/overrides/additionalProperties/additionalProperties/oneOf/0/type",keyword:"type",params:{type: "string"},message:"must be string"};if(vErrors === null){vErrors = [err43];}else {vErrors.push(err43);}errors++;}}var _valid6 = _errs79 === errors;if(_valid6){valid19 = true;passing6 = 0;}const _errs81 = errors;if(!((typeof data23 == "number") && (isFinite(data23)))){const err44 = {instancePath:instancePath+"/overrides/" + key2.replace(/~/g, "~0").replace(/\//g, "~1")+"/" + key3.replace(/~/g, "~0").replace(/\//g, "~1"),schemaPath:"#/properties/overrides/additionalProperties/additionalProperties/oneOf/1/type",keyword:"type",params:{type: "number"},message:"must be number"};if(vErrors === null){vErrors = [err44];}else {vErrors.push(err44);}errors++;}var _valid6 = _errs81 === errors;if(_valid6 && valid19){valid19 = false;passing6 = [passing6, 1];}else {if(_valid6){valid19 = true;passing6 = 1;}const _errs83 = errors;if(typeof data23 !== "boolean"){const err45 = {instancePath:instancePath+"/overrides/" + key2.replace(/~/g, "~0").replace(/\//g, "~1")+"/" + key3.replace(/~/g, "~0").replace(/\//g, "~1"),schemaPath:"#/properties/overrides/additionalProperties/additionalProperties/oneOf/2/type",keyword:"type",params:{type: "boolean"},message:"must be boolean"};if(vErrors === null){vErrors = [err45];}else {vErrors.push(err45);}errors++;}var _valid6 = _errs83 === errors;if(_valid6 && valid19){valid19 = false;passing6 = [passing6, 2];}else {if(_valid6){valid19 = true;passing6 = 2;}const _errs85 = errors;if(errors === _errs85){if(Array.isArray(data23)){if(data23.length > 3){const err46 = {instancePath:instancePath+"/overrides/" + key2.replace(/~/g, "~0").replace(/\//g, "~1")+"/" + key3.replace(/~/g, "~0").replace(/\//g, "~1"),schemaPath:"#/properties/overrides/additionalProperties/additionalProperties/oneOf/3/maxItems",keyword:"maxItems",params:{limit: 3},message:"must NOT have more than 3 items"};if(vErrors === null){vErrors = [err46];}else {vErrors.push(err46);}errors++;}else {var valid20 = true;const len4 = data23.length;for(let i4=0; i4 300){const err47 = {instancePath:instancePath+"/overrides/" + key2.replace(/~/g, "~0").replace(/\//g, "~1")+"/" + key3.replace(/~/g, "~0").replace(/\//g, "~1")+"/" + i4,schemaPath:"#/properties/overrides/additionalProperties/additionalProperties/oneOf/3/items/maxLength",keyword:"maxLength",params:{limit: 300},message:"must NOT have more than 300 characters"};if(vErrors === null){vErrors = [err47];}else {vErrors.push(err47);}errors++;}else {if(func2(data24) < 0){const err48 = {instancePath:instancePath+"/overrides/" + key2.replace(/~/g, "~0").replace(/\//g, "~1")+"/" + key3.replace(/~/g, "~0").replace(/\//g, "~1")+"/" + i4,schemaPath:"#/properties/overrides/additionalProperties/additionalProperties/oneOf/3/items/minLength",keyword:"minLength",params:{limit: 0},message:"must NOT have fewer than 0 characters"};if(vErrors === null){vErrors = [err48];}else {vErrors.push(err48);}errors++;}}}else {const err49 = {instancePath:instancePath+"/overrides/" + key2.replace(/~/g, "~0").replace(/\//g, "~1")+"/" + key3.replace(/~/g, "~0").replace(/\//g, "~1")+"/" + i4,schemaPath:"#/properties/overrides/additionalProperties/additionalProperties/oneOf/3/items/type",keyword:"type",params:{type: "string"},message:"must be string"};if(vErrors === null){vErrors = [err49];}else {vErrors.push(err49);}errors++;}}var valid20 = _errs87 === errors;if(!valid20){break;}}}}else {const err50 = {instancePath:instancePath+"/overrides/" + key2.replace(/~/g, "~0").replace(/\//g, "~1")+"/" + key3.replace(/~/g, "~0").replace(/\//g, "~1"),schemaPath:"#/properties/overrides/additionalProperties/additionalProperties/oneOf/3/type",keyword:"type",params:{type: "array"},message:"must be array"};if(vErrors === null){vErrors = [err50];}else {vErrors.push(err50);}errors++;}}var _valid6 = _errs85 === errors;if(_valid6 && valid19){valid19 = false;passing6 = [passing6, 3];}else {if(_valid6){valid19 = true;passing6 = 3;}}}}if(!valid19){const err51 = {instancePath:instancePath+"/overrides/" + key2.replace(/~/g, "~0").replace(/\//g, "~1")+"/" + key3.replace(/~/g, "~0").replace(/\//g, "~1"),schemaPath:"#/properties/overrides/additionalProperties/additionalProperties/oneOf",keyword:"oneOf",params:{passingSchemas: passing6},message:"must match exactly one schema in oneOf"};if(vErrors === null){vErrors = [err51];}else {vErrors.push(err51);}errors++;validate98.errors = vErrors;return false;}else {errors = _errs78;if(vErrors !== null){if(_errs78){vErrors.length = _errs78;}else {vErrors = null;}}}var valid18 = _errs77 === errors;if(!valid18){break;}}}}else {validate98.errors = [{instancePath:instancePath+"/overrides/" + key2.replace(/~/g, "~0").replace(/\//g, "~1"),schemaPath:"#/properties/overrides/additionalProperties/type",keyword:"type",params:{type: "object"},message:"must be object"}];return false;}}var valid17 = _errs74 === errors;if(!valid17){break;}}}}}else {validate98.errors = [{instancePath:instancePath+"/overrides",schemaPath:"#/properties/overrides/type",keyword:"type",params:{type: "object"},message:"must be object"}];return false;}}var valid0 = _errs70 === errors;}else {var valid0 = true;}}}}}}}}}}}}}}}}}}}}}else {validate98.errors = [{instancePath,schemaPath:"#/type",keyword:"type",params:{type: "object"},message:"must be object"}];return false;}}validate98.errors = vErrors;return errors === 0;}validate98.evaluated = {"props":true,"dynamicProps":false,"dynamicItems":false};const schema157 = {"type":"object","additionalProperties":false,"required":["kind","x","y","w","h"],"properties":{"kind":{"const":"group"},"x":{"type":"number","minimum":-400,"maximum":2400},"y":{"type":"number","minimum":-400,"maximum":2400},"w":{"type":"number","minimum":1,"maximum":2400},"h":{"type":"number","minimum":1,"maximum":2400},"tone":{"$ref":"../common.schema.json#/$defs/tone"},"label":{"type":"string","minLength":0,"maxLength":200},"sub":{"type":"string","minLength":0,"maxLength":200},"solid":{"type":"boolean"},"select":{"$ref":"#/$defs/viewKey"},"active":{"oneOf":[{"type":"boolean"},{"type":"array","maxItems":20,"items":{"oneOf":[{"type":"integer","minimum":0,"maximum":15},{"$ref":"../common.schema.json#/$defs/id"}]}}]},"hot":{"oneOf":[{"type":"boolean"},{"type":"array","maxItems":20,"items":{"oneOf":[{"type":"integer","minimum":0,"maximum":15},{"$ref":"../common.schema.json#/$defs/id"}]}}]},"dim":{"type":"boolean"},"visible":{"type":"array","maxItems":20,"items":{"oneOf":[{"type":"integer","minimum":0,"maximum":15},{"$ref":"../common.schema.json#/$defs/id"}]}},"overrides":{"type":"object","maxProperties":6,"propertyNames":{"enum":["title","sub","label","text","n","dashed","muted"]},"additionalProperties":{"type":"object","maxProperties":20,"additionalProperties":{"oneOf":[{"type":"string","minLength":0,"maxLength":300},{"type":"number"},{"type":"boolean"},{"type":"array","maxItems":3,"items":{"type":"string","minLength":0,"maxLength":300}}]}}}}};function validate100(data, {instancePath="", parentData, parentDataProperty, rootData=data, dynamicAnchors={}}={}){let vErrors = null;let errors = 0;const evaluated0 = validate100.evaluated;if(evaluated0.dynamicProps){evaluated0.props = undefined;}if(evaluated0.dynamicItems){evaluated0.items = undefined;}if(errors === 0){if(data && typeof data == "object" && !Array.isArray(data)){let missing0;if((((((data.kind === undefined) && (missing0 = "kind")) || ((data.x === undefined) && (missing0 = "x"))) || ((data.y === undefined) && (missing0 = "y"))) || ((data.w === undefined) && (missing0 = "w"))) || ((data.h === undefined) && (missing0 = "h"))){validate100.errors = [{instancePath,schemaPath:"#/required",keyword:"required",params:{missingProperty: missing0},message:"must have required property '"+missing0+"'"}];return false;}else {const _errs1 = errors;for(const key0 in data){if(!(func1.call(schema157.properties, key0))){validate100.errors = [{instancePath,schemaPath:"#/additionalProperties",keyword:"additionalProperties",params:{additionalProperty: key0},message:"must NOT have additional properties"}];return false;break;}}if(_errs1 === errors){if(data.kind !== undefined){const _errs2 = errors;if("group" !== data.kind){validate100.errors = [{instancePath:instancePath+"/kind",schemaPath:"#/properties/kind/const",keyword:"const",params:{allowedValue: "group"},message:"must be equal to constant"}];return false;}var valid0 = _errs2 === errors;}else {var valid0 = true;}if(valid0){if(data.x !== undefined){let data1 = data.x;const _errs3 = errors;if(errors === _errs3){if((typeof data1 == "number") && (isFinite(data1))){if(data1 > 2400 || isNaN(data1)){validate100.errors = [{instancePath:instancePath+"/x",schemaPath:"#/properties/x/maximum",keyword:"maximum",params:{comparison: "<=", limit: 2400},message:"must be <= 2400"}];return false;}else {if(data1 < -400 || isNaN(data1)){validate100.errors = [{instancePath:instancePath+"/x",schemaPath:"#/properties/x/minimum",keyword:"minimum",params:{comparison: ">=", limit: -400},message:"must be >= -400"}];return false;}}}else {validate100.errors = [{instancePath:instancePath+"/x",schemaPath:"#/properties/x/type",keyword:"type",params:{type: "number"},message:"must be number"}];return false;}}var valid0 = _errs3 === errors;}else {var valid0 = true;}if(valid0){if(data.y !== undefined){let data2 = data.y;const _errs5 = errors;if(errors === _errs5){if((typeof data2 == "number") && (isFinite(data2))){if(data2 > 2400 || isNaN(data2)){validate100.errors = [{instancePath:instancePath+"/y",schemaPath:"#/properties/y/maximum",keyword:"maximum",params:{comparison: "<=", limit: 2400},message:"must be <= 2400"}];return false;}else {if(data2 < -400 || isNaN(data2)){validate100.errors = [{instancePath:instancePath+"/y",schemaPath:"#/properties/y/minimum",keyword:"minimum",params:{comparison: ">=", limit: -400},message:"must be >= -400"}];return false;}}}else {validate100.errors = [{instancePath:instancePath+"/y",schemaPath:"#/properties/y/type",keyword:"type",params:{type: "number"},message:"must be number"}];return false;}}var valid0 = _errs5 === errors;}else {var valid0 = true;}if(valid0){if(data.w !== undefined){let data3 = data.w;const _errs7 = errors;if(errors === _errs7){if((typeof data3 == "number") && (isFinite(data3))){if(data3 > 2400 || isNaN(data3)){validate100.errors = [{instancePath:instancePath+"/w",schemaPath:"#/properties/w/maximum",keyword:"maximum",params:{comparison: "<=", limit: 2400},message:"must be <= 2400"}];return false;}else {if(data3 < 1 || isNaN(data3)){validate100.errors = [{instancePath:instancePath+"/w",schemaPath:"#/properties/w/minimum",keyword:"minimum",params:{comparison: ">=", limit: 1},message:"must be >= 1"}];return false;}}}else {validate100.errors = [{instancePath:instancePath+"/w",schemaPath:"#/properties/w/type",keyword:"type",params:{type: "number"},message:"must be number"}];return false;}}var valid0 = _errs7 === errors;}else {var valid0 = true;}if(valid0){if(data.h !== undefined){let data4 = data.h;const _errs9 = errors;if(errors === _errs9){if((typeof data4 == "number") && (isFinite(data4))){if(data4 > 2400 || isNaN(data4)){validate100.errors = [{instancePath:instancePath+"/h",schemaPath:"#/properties/h/maximum",keyword:"maximum",params:{comparison: "<=", limit: 2400},message:"must be <= 2400"}];return false;}else {if(data4 < 1 || isNaN(data4)){validate100.errors = [{instancePath:instancePath+"/h",schemaPath:"#/properties/h/minimum",keyword:"minimum",params:{comparison: ">=", limit: 1},message:"must be >= 1"}];return false;}}}else {validate100.errors = [{instancePath:instancePath+"/h",schemaPath:"#/properties/h/type",keyword:"type",params:{type: "number"},message:"must be number"}];return false;}}var valid0 = _errs9 === errors;}else {var valid0 = true;}if(valid0){if(data.tone !== undefined){let data5 = data.tone;const _errs11 = errors;if(!(((((((((data5 === "gray") || (data5 === "purple")) || (data5 === "teal")) || (data5 === "coral")) || (data5 === "pink")) || (data5 === "blue")) || (data5 === "green")) || (data5 === "amber")) || (data5 === "red"))){validate100.errors = [{instancePath:instancePath+"/tone",schemaPath:"../common.schema.json#/$defs/tone/enum",keyword:"enum",params:{allowedValues: schema148.enum},message:"must be equal to one of the allowed values"}];return false;}var valid0 = _errs11 === errors;}else {var valid0 = true;}if(valid0){if(data.label !== undefined){let data6 = data.label;const _errs13 = errors;if(errors === _errs13){if(typeof data6 === "string"){if(func2(data6) > 200){validate100.errors = [{instancePath:instancePath+"/label",schemaPath:"#/properties/label/maxLength",keyword:"maxLength",params:{limit: 200},message:"must NOT have more than 200 characters"}];return false;}else {if(func2(data6) < 0){validate100.errors = [{instancePath:instancePath+"/label",schemaPath:"#/properties/label/minLength",keyword:"minLength",params:{limit: 0},message:"must NOT have fewer than 0 characters"}];return false;}}}else {validate100.errors = [{instancePath:instancePath+"/label",schemaPath:"#/properties/label/type",keyword:"type",params:{type: "string"},message:"must be string"}];return false;}}var valid0 = _errs13 === errors;}else {var valid0 = true;}if(valid0){if(data.sub !== undefined){let data7 = data.sub;const _errs15 = errors;if(errors === _errs15){if(typeof data7 === "string"){if(func2(data7) > 200){validate100.errors = [{instancePath:instancePath+"/sub",schemaPath:"#/properties/sub/maxLength",keyword:"maxLength",params:{limit: 200},message:"must NOT have more than 200 characters"}];return false;}else {if(func2(data7) < 0){validate100.errors = [{instancePath:instancePath+"/sub",schemaPath:"#/properties/sub/minLength",keyword:"minLength",params:{limit: 0},message:"must NOT have fewer than 0 characters"}];return false;}}}else {validate100.errors = [{instancePath:instancePath+"/sub",schemaPath:"#/properties/sub/type",keyword:"type",params:{type: "string"},message:"must be string"}];return false;}}var valid0 = _errs15 === errors;}else {var valid0 = true;}if(valid0){if(data.solid !== undefined){const _errs17 = errors;if(typeof data.solid !== "boolean"){validate100.errors = [{instancePath:instancePath+"/solid",schemaPath:"#/properties/solid/type",keyword:"type",params:{type: "boolean"},message:"must be boolean"}];return false;}var valid0 = _errs17 === errors;}else {var valid0 = true;}if(valid0){if(data.select !== undefined){let data9 = data.select;const _errs19 = errors;const _errs20 = errors;if(errors === _errs20){if(typeof data9 === "string"){if(func2(data9) > 64){validate100.errors = [{instancePath:instancePath+"/select",schemaPath:"#/$defs/viewKey/maxLength",keyword:"maxLength",params:{limit: 64},message:"must NOT have more than 64 characters"}];return false;}else {if(!pattern18.test(data9)){validate100.errors = [{instancePath:instancePath+"/select",schemaPath:"#/$defs/viewKey/pattern",keyword:"pattern",params:{pattern: "^[a-z0-9]+(?:-[a-z0-9]+)*$"},message:"must match pattern \""+"^[a-z0-9]+(?:-[a-z0-9]+)*$"+"\""}];return false;}}}else {validate100.errors = [{instancePath:instancePath+"/select",schemaPath:"#/$defs/viewKey/type",keyword:"type",params:{type: "string"},message:"must be string"}];return false;}}var valid0 = _errs19 === errors;}else {var valid0 = true;}if(valid0){if(data.active !== undefined){let data10 = data.active;const _errs22 = errors;const _errs23 = errors;let valid3 = false;let passing0 = null;const _errs24 = errors;if(typeof data10 !== "boolean"){const err0 = {instancePath:instancePath+"/active",schemaPath:"#/properties/active/oneOf/0/type",keyword:"type",params:{type: "boolean"},message:"must be boolean"};if(vErrors === null){vErrors = [err0];}else {vErrors.push(err0);}errors++;}var _valid0 = _errs24 === errors;if(_valid0){valid3 = true;passing0 = 0;}const _errs26 = errors;if(errors === _errs26){if(Array.isArray(data10)){if(data10.length > 20){const err1 = {instancePath:instancePath+"/active",schemaPath:"#/properties/active/oneOf/1/maxItems",keyword:"maxItems",params:{limit: 20},message:"must NOT have more than 20 items"};if(vErrors === null){vErrors = [err1];}else {vErrors.push(err1);}errors++;}else {var valid4 = true;const len0 = data10.length;for(let i0=0; i0 15 || isNaN(data11)){const err3 = {instancePath:instancePath+"/active/" + i0,schemaPath:"#/properties/active/oneOf/1/items/oneOf/0/maximum",keyword:"maximum",params:{comparison: "<=", limit: 15},message:"must be <= 15"};if(vErrors === null){vErrors = [err3];}else {vErrors.push(err3);}errors++;}else {if(data11 < 0 || isNaN(data11)){const err4 = {instancePath:instancePath+"/active/" + i0,schemaPath:"#/properties/active/oneOf/1/items/oneOf/0/minimum",keyword:"minimum",params:{comparison: ">=", limit: 0},message:"must be >= 0"};if(vErrors === null){vErrors = [err4];}else {vErrors.push(err4);}errors++;}}}}var _valid1 = _errs30 === errors;if(_valid1){valid5 = true;passing1 = 0;}const _errs32 = errors;const _errs33 = errors;if(errors === _errs33){if(typeof data11 === "string"){if(func2(data11) > 64){const err5 = {instancePath:instancePath+"/active/" + i0,schemaPath:"../common.schema.json#/$defs/id/maxLength",keyword:"maxLength",params:{limit: 64},message:"must NOT have more than 64 characters"};if(vErrors === null){vErrors = [err5];}else {vErrors.push(err5);}errors++;}else {if(!pattern18.test(data11)){const err6 = {instancePath:instancePath+"/active/" + i0,schemaPath:"../common.schema.json#/$defs/id/pattern",keyword:"pattern",params:{pattern: "^[a-z0-9]+(?:-[a-z0-9]+)*$"},message:"must match pattern \""+"^[a-z0-9]+(?:-[a-z0-9]+)*$"+"\""};if(vErrors === null){vErrors = [err6];}else {vErrors.push(err6);}errors++;}}}else {const err7 = {instancePath:instancePath+"/active/" + i0,schemaPath:"../common.schema.json#/$defs/id/type",keyword:"type",params:{type: "string"},message:"must be string"};if(vErrors === null){vErrors = [err7];}else {vErrors.push(err7);}errors++;}}var _valid1 = _errs32 === errors;if(_valid1 && valid5){valid5 = false;passing1 = [passing1, 1];}else {if(_valid1){valid5 = true;passing1 = 1;}}if(!valid5){const err8 = {instancePath:instancePath+"/active/" + i0,schemaPath:"#/properties/active/oneOf/1/items/oneOf",keyword:"oneOf",params:{passingSchemas: passing1},message:"must match exactly one schema in oneOf"};if(vErrors === null){vErrors = [err8];}else {vErrors.push(err8);}errors++;}else {errors = _errs29;if(vErrors !== null){if(_errs29){vErrors.length = _errs29;}else {vErrors = null;}}}var valid4 = _errs28 === errors;if(!valid4){break;}}}}else {const err9 = {instancePath:instancePath+"/active",schemaPath:"#/properties/active/oneOf/1/type",keyword:"type",params:{type: "array"},message:"must be array"};if(vErrors === null){vErrors = [err9];}else {vErrors.push(err9);}errors++;}}var _valid0 = _errs26 === errors;if(_valid0 && valid3){valid3 = false;passing0 = [passing0, 1];}else {if(_valid0){valid3 = true;passing0 = 1;}}if(!valid3){const err10 = {instancePath:instancePath+"/active",schemaPath:"#/properties/active/oneOf",keyword:"oneOf",params:{passingSchemas: passing0},message:"must match exactly one schema in oneOf"};if(vErrors === null){vErrors = [err10];}else {vErrors.push(err10);}errors++;validate100.errors = vErrors;return false;}else {errors = _errs23;if(vErrors !== null){if(_errs23){vErrors.length = _errs23;}else {vErrors = null;}}}var valid0 = _errs22 === errors;}else {var valid0 = true;}if(valid0){if(data.hot !== undefined){let data12 = data.hot;const _errs35 = errors;const _errs36 = errors;let valid7 = false;let passing2 = null;const _errs37 = errors;if(typeof data12 !== "boolean"){const err11 = {instancePath:instancePath+"/hot",schemaPath:"#/properties/hot/oneOf/0/type",keyword:"type",params:{type: "boolean"},message:"must be boolean"};if(vErrors === null){vErrors = [err11];}else {vErrors.push(err11);}errors++;}var _valid2 = _errs37 === errors;if(_valid2){valid7 = true;passing2 = 0;}const _errs39 = errors;if(errors === _errs39){if(Array.isArray(data12)){if(data12.length > 20){const err12 = {instancePath:instancePath+"/hot",schemaPath:"#/properties/hot/oneOf/1/maxItems",keyword:"maxItems",params:{limit: 20},message:"must NOT have more than 20 items"};if(vErrors === null){vErrors = [err12];}else {vErrors.push(err12);}errors++;}else {var valid8 = true;const len1 = data12.length;for(let i1=0; i1 15 || isNaN(data13)){const err14 = {instancePath:instancePath+"/hot/" + i1,schemaPath:"#/properties/hot/oneOf/1/items/oneOf/0/maximum",keyword:"maximum",params:{comparison: "<=", limit: 15},message:"must be <= 15"};if(vErrors === null){vErrors = [err14];}else {vErrors.push(err14);}errors++;}else {if(data13 < 0 || isNaN(data13)){const err15 = {instancePath:instancePath+"/hot/" + i1,schemaPath:"#/properties/hot/oneOf/1/items/oneOf/0/minimum",keyword:"minimum",params:{comparison: ">=", limit: 0},message:"must be >= 0"};if(vErrors === null){vErrors = [err15];}else {vErrors.push(err15);}errors++;}}}}var _valid3 = _errs43 === errors;if(_valid3){valid9 = true;passing3 = 0;}const _errs45 = errors;const _errs46 = errors;if(errors === _errs46){if(typeof data13 === "string"){if(func2(data13) > 64){const err16 = {instancePath:instancePath+"/hot/" + i1,schemaPath:"../common.schema.json#/$defs/id/maxLength",keyword:"maxLength",params:{limit: 64},message:"must NOT have more than 64 characters"};if(vErrors === null){vErrors = [err16];}else {vErrors.push(err16);}errors++;}else {if(!pattern18.test(data13)){const err17 = {instancePath:instancePath+"/hot/" + i1,schemaPath:"../common.schema.json#/$defs/id/pattern",keyword:"pattern",params:{pattern: "^[a-z0-9]+(?:-[a-z0-9]+)*$"},message:"must match pattern \""+"^[a-z0-9]+(?:-[a-z0-9]+)*$"+"\""};if(vErrors === null){vErrors = [err17];}else {vErrors.push(err17);}errors++;}}}else {const err18 = {instancePath:instancePath+"/hot/" + i1,schemaPath:"../common.schema.json#/$defs/id/type",keyword:"type",params:{type: "string"},message:"must be string"};if(vErrors === null){vErrors = [err18];}else {vErrors.push(err18);}errors++;}}var _valid3 = _errs45 === errors;if(_valid3 && valid9){valid9 = false;passing3 = [passing3, 1];}else {if(_valid3){valid9 = true;passing3 = 1;}}if(!valid9){const err19 = {instancePath:instancePath+"/hot/" + i1,schemaPath:"#/properties/hot/oneOf/1/items/oneOf",keyword:"oneOf",params:{passingSchemas: passing3},message:"must match exactly one schema in oneOf"};if(vErrors === null){vErrors = [err19];}else {vErrors.push(err19);}errors++;}else {errors = _errs42;if(vErrors !== null){if(_errs42){vErrors.length = _errs42;}else {vErrors = null;}}}var valid8 = _errs41 === errors;if(!valid8){break;}}}}else {const err20 = {instancePath:instancePath+"/hot",schemaPath:"#/properties/hot/oneOf/1/type",keyword:"type",params:{type: "array"},message:"must be array"};if(vErrors === null){vErrors = [err20];}else {vErrors.push(err20);}errors++;}}var _valid2 = _errs39 === errors;if(_valid2 && valid7){valid7 = false;passing2 = [passing2, 1];}else {if(_valid2){valid7 = true;passing2 = 1;}}if(!valid7){const err21 = {instancePath:instancePath+"/hot",schemaPath:"#/properties/hot/oneOf",keyword:"oneOf",params:{passingSchemas: passing2},message:"must match exactly one schema in oneOf"};if(vErrors === null){vErrors = [err21];}else {vErrors.push(err21);}errors++;validate100.errors = vErrors;return false;}else {errors = _errs36;if(vErrors !== null){if(_errs36){vErrors.length = _errs36;}else {vErrors = null;}}}var valid0 = _errs35 === errors;}else {var valid0 = true;}if(valid0){if(data.dim !== undefined){const _errs48 = errors;if(typeof data.dim !== "boolean"){validate100.errors = [{instancePath:instancePath+"/dim",schemaPath:"#/properties/dim/type",keyword:"type",params:{type: "boolean"},message:"must be boolean"}];return false;}var valid0 = _errs48 === errors;}else {var valid0 = true;}if(valid0){if(data.visible !== undefined){let data15 = data.visible;const _errs50 = errors;if(errors === _errs50){if(Array.isArray(data15)){if(data15.length > 20){validate100.errors = [{instancePath:instancePath+"/visible",schemaPath:"#/properties/visible/maxItems",keyword:"maxItems",params:{limit: 20},message:"must NOT have more than 20 items"}];return false;}else {var valid11 = true;const len2 = data15.length;for(let i2=0; i2 15 || isNaN(data16)){const err23 = {instancePath:instancePath+"/visible/" + i2,schemaPath:"#/properties/visible/items/oneOf/0/maximum",keyword:"maximum",params:{comparison: "<=", limit: 15},message:"must be <= 15"};if(vErrors === null){vErrors = [err23];}else {vErrors.push(err23);}errors++;}else {if(data16 < 0 || isNaN(data16)){const err24 = {instancePath:instancePath+"/visible/" + i2,schemaPath:"#/properties/visible/items/oneOf/0/minimum",keyword:"minimum",params:{comparison: ">=", limit: 0},message:"must be >= 0"};if(vErrors === null){vErrors = [err24];}else {vErrors.push(err24);}errors++;}}}}var _valid4 = _errs54 === errors;if(_valid4){valid12 = true;passing4 = 0;}const _errs56 = errors;const _errs57 = errors;if(errors === _errs57){if(typeof data16 === "string"){if(func2(data16) > 64){const err25 = {instancePath:instancePath+"/visible/" + i2,schemaPath:"../common.schema.json#/$defs/id/maxLength",keyword:"maxLength",params:{limit: 64},message:"must NOT have more than 64 characters"};if(vErrors === null){vErrors = [err25];}else {vErrors.push(err25);}errors++;}else {if(!pattern18.test(data16)){const err26 = {instancePath:instancePath+"/visible/" + i2,schemaPath:"../common.schema.json#/$defs/id/pattern",keyword:"pattern",params:{pattern: "^[a-z0-9]+(?:-[a-z0-9]+)*$"},message:"must match pattern \""+"^[a-z0-9]+(?:-[a-z0-9]+)*$"+"\""};if(vErrors === null){vErrors = [err26];}else {vErrors.push(err26);}errors++;}}}else {const err27 = {instancePath:instancePath+"/visible/" + i2,schemaPath:"../common.schema.json#/$defs/id/type",keyword:"type",params:{type: "string"},message:"must be string"};if(vErrors === null){vErrors = [err27];}else {vErrors.push(err27);}errors++;}}var _valid4 = _errs56 === errors;if(_valid4 && valid12){valid12 = false;passing4 = [passing4, 1];}else {if(_valid4){valid12 = true;passing4 = 1;}}if(!valid12){const err28 = {instancePath:instancePath+"/visible/" + i2,schemaPath:"#/properties/visible/items/oneOf",keyword:"oneOf",params:{passingSchemas: passing4},message:"must match exactly one schema in oneOf"};if(vErrors === null){vErrors = [err28];}else {vErrors.push(err28);}errors++;validate100.errors = vErrors;return false;}else {errors = _errs53;if(vErrors !== null){if(_errs53){vErrors.length = _errs53;}else {vErrors = null;}}}var valid11 = _errs52 === errors;if(!valid11){break;}}}}else {validate100.errors = [{instancePath:instancePath+"/visible",schemaPath:"#/properties/visible/type",keyword:"type",params:{type: "array"},message:"must be array"}];return false;}}var valid0 = _errs50 === errors;}else {var valid0 = true;}if(valid0){if(data.overrides !== undefined){let data17 = data.overrides;const _errs59 = errors;if(errors === _errs59){if(data17 && typeof data17 == "object" && !Array.isArray(data17)){if(Object.keys(data17).length > 6){validate100.errors = [{instancePath:instancePath+"/overrides",schemaPath:"#/properties/overrides/maxProperties",keyword:"maxProperties",params:{limit: 6},message:"must NOT have more than 6 properties"}];return false;}else {for(const key1 in data17){const _errs61 = errors;if(!(((((((key1 === "title") || (key1 === "sub")) || (key1 === "label")) || (key1 === "text")) || (key1 === "n")) || (key1 === "dashed")) || (key1 === "muted"))){const err29 = {instancePath:instancePath+"/overrides",schemaPath:"#/properties/overrides/propertyNames/enum",keyword:"enum",params:{allowedValues: schema157.properties.overrides.propertyNames.enum},message:"must be equal to one of the allowed values",propertyName:key1};if(vErrors === null){vErrors = [err29];}else {vErrors.push(err29);}errors++;}var valid14 = _errs61 === errors;if(!valid14){const err30 = {instancePath:instancePath+"/overrides",schemaPath:"#/properties/overrides/propertyNames",keyword:"propertyNames",params:{propertyName: key1},message:"property name must be valid"};if(vErrors === null){vErrors = [err30];}else {vErrors.push(err30);}errors++;validate100.errors = vErrors;return false;break;}}if(valid14){for(const key2 in data17){let data18 = data17[key2];const _errs63 = errors;if(errors === _errs63){if(data18 && typeof data18 == "object" && !Array.isArray(data18)){if(Object.keys(data18).length > 20){validate100.errors = [{instancePath:instancePath+"/overrides/" + key2.replace(/~/g, "~0").replace(/\//g, "~1"),schemaPath:"#/properties/overrides/additionalProperties/maxProperties",keyword:"maxProperties",params:{limit: 20},message:"must NOT have more than 20 properties"}];return false;}else {for(const key3 in data18){let data19 = data18[key3];const _errs66 = errors;const _errs67 = errors;let valid17 = false;let passing5 = null;const _errs68 = errors;if(errors === _errs68){if(typeof data19 === "string"){if(func2(data19) > 300){const err31 = {instancePath:instancePath+"/overrides/" + key2.replace(/~/g, "~0").replace(/\//g, "~1")+"/" + key3.replace(/~/g, "~0").replace(/\//g, "~1"),schemaPath:"#/properties/overrides/additionalProperties/additionalProperties/oneOf/0/maxLength",keyword:"maxLength",params:{limit: 300},message:"must NOT have more than 300 characters"};if(vErrors === null){vErrors = [err31];}else {vErrors.push(err31);}errors++;}else {if(func2(data19) < 0){const err32 = {instancePath:instancePath+"/overrides/" + key2.replace(/~/g, "~0").replace(/\//g, "~1")+"/" + key3.replace(/~/g, "~0").replace(/\//g, "~1"),schemaPath:"#/properties/overrides/additionalProperties/additionalProperties/oneOf/0/minLength",keyword:"minLength",params:{limit: 0},message:"must NOT have fewer than 0 characters"};if(vErrors === null){vErrors = [err32];}else {vErrors.push(err32);}errors++;}}}else {const err33 = {instancePath:instancePath+"/overrides/" + key2.replace(/~/g, "~0").replace(/\//g, "~1")+"/" + key3.replace(/~/g, "~0").replace(/\//g, "~1"),schemaPath:"#/properties/overrides/additionalProperties/additionalProperties/oneOf/0/type",keyword:"type",params:{type: "string"},message:"must be string"};if(vErrors === null){vErrors = [err33];}else {vErrors.push(err33);}errors++;}}var _valid5 = _errs68 === errors;if(_valid5){valid17 = true;passing5 = 0;}const _errs70 = errors;if(!((typeof data19 == "number") && (isFinite(data19)))){const err34 = {instancePath:instancePath+"/overrides/" + key2.replace(/~/g, "~0").replace(/\//g, "~1")+"/" + key3.replace(/~/g, "~0").replace(/\//g, "~1"),schemaPath:"#/properties/overrides/additionalProperties/additionalProperties/oneOf/1/type",keyword:"type",params:{type: "number"},message:"must be number"};if(vErrors === null){vErrors = [err34];}else {vErrors.push(err34);}errors++;}var _valid5 = _errs70 === errors;if(_valid5 && valid17){valid17 = false;passing5 = [passing5, 1];}else {if(_valid5){valid17 = true;passing5 = 1;}const _errs72 = errors;if(typeof data19 !== "boolean"){const err35 = {instancePath:instancePath+"/overrides/" + key2.replace(/~/g, "~0").replace(/\//g, "~1")+"/" + key3.replace(/~/g, "~0").replace(/\//g, "~1"),schemaPath:"#/properties/overrides/additionalProperties/additionalProperties/oneOf/2/type",keyword:"type",params:{type: "boolean"},message:"must be boolean"};if(vErrors === null){vErrors = [err35];}else {vErrors.push(err35);}errors++;}var _valid5 = _errs72 === errors;if(_valid5 && valid17){valid17 = false;passing5 = [passing5, 2];}else {if(_valid5){valid17 = true;passing5 = 2;}const _errs74 = errors;if(errors === _errs74){if(Array.isArray(data19)){if(data19.length > 3){const err36 = {instancePath:instancePath+"/overrides/" + key2.replace(/~/g, "~0").replace(/\//g, "~1")+"/" + key3.replace(/~/g, "~0").replace(/\//g, "~1"),schemaPath:"#/properties/overrides/additionalProperties/additionalProperties/oneOf/3/maxItems",keyword:"maxItems",params:{limit: 3},message:"must NOT have more than 3 items"};if(vErrors === null){vErrors = [err36];}else {vErrors.push(err36);}errors++;}else {var valid18 = true;const len3 = data19.length;for(let i3=0; i3 300){const err37 = {instancePath:instancePath+"/overrides/" + key2.replace(/~/g, "~0").replace(/\//g, "~1")+"/" + key3.replace(/~/g, "~0").replace(/\//g, "~1")+"/" + i3,schemaPath:"#/properties/overrides/additionalProperties/additionalProperties/oneOf/3/items/maxLength",keyword:"maxLength",params:{limit: 300},message:"must NOT have more than 300 characters"};if(vErrors === null){vErrors = [err37];}else {vErrors.push(err37);}errors++;}else {if(func2(data20) < 0){const err38 = {instancePath:instancePath+"/overrides/" + key2.replace(/~/g, "~0").replace(/\//g, "~1")+"/" + key3.replace(/~/g, "~0").replace(/\//g, "~1")+"/" + i3,schemaPath:"#/properties/overrides/additionalProperties/additionalProperties/oneOf/3/items/minLength",keyword:"minLength",params:{limit: 0},message:"must NOT have fewer than 0 characters"};if(vErrors === null){vErrors = [err38];}else {vErrors.push(err38);}errors++;}}}else {const err39 = {instancePath:instancePath+"/overrides/" + key2.replace(/~/g, "~0").replace(/\//g, "~1")+"/" + key3.replace(/~/g, "~0").replace(/\//g, "~1")+"/" + i3,schemaPath:"#/properties/overrides/additionalProperties/additionalProperties/oneOf/3/items/type",keyword:"type",params:{type: "string"},message:"must be string"};if(vErrors === null){vErrors = [err39];}else {vErrors.push(err39);}errors++;}}var valid18 = _errs76 === errors;if(!valid18){break;}}}}else {const err40 = {instancePath:instancePath+"/overrides/" + key2.replace(/~/g, "~0").replace(/\//g, "~1")+"/" + key3.replace(/~/g, "~0").replace(/\//g, "~1"),schemaPath:"#/properties/overrides/additionalProperties/additionalProperties/oneOf/3/type",keyword:"type",params:{type: "array"},message:"must be array"};if(vErrors === null){vErrors = [err40];}else {vErrors.push(err40);}errors++;}}var _valid5 = _errs74 === errors;if(_valid5 && valid17){valid17 = false;passing5 = [passing5, 3];}else {if(_valid5){valid17 = true;passing5 = 3;}}}}if(!valid17){const err41 = {instancePath:instancePath+"/overrides/" + key2.replace(/~/g, "~0").replace(/\//g, "~1")+"/" + key3.replace(/~/g, "~0").replace(/\//g, "~1"),schemaPath:"#/properties/overrides/additionalProperties/additionalProperties/oneOf",keyword:"oneOf",params:{passingSchemas: passing5},message:"must match exactly one schema in oneOf"};if(vErrors === null){vErrors = [err41];}else {vErrors.push(err41);}errors++;validate100.errors = vErrors;return false;}else {errors = _errs67;if(vErrors !== null){if(_errs67){vErrors.length = _errs67;}else {vErrors = null;}}}var valid16 = _errs66 === errors;if(!valid16){break;}}}}else {validate100.errors = [{instancePath:instancePath+"/overrides/" + key2.replace(/~/g, "~0").replace(/\//g, "~1"),schemaPath:"#/properties/overrides/additionalProperties/type",keyword:"type",params:{type: "object"},message:"must be object"}];return false;}}var valid15 = _errs63 === errors;if(!valid15){break;}}}}}else {validate100.errors = [{instancePath:instancePath+"/overrides",schemaPath:"#/properties/overrides/type",keyword:"type",params:{type: "object"},message:"must be object"}];return false;}}var valid0 = _errs59 === errors;}else {var valid0 = true;}}}}}}}}}}}}}}}}}}else {validate100.errors = [{instancePath,schemaPath:"#/type",keyword:"type",params:{type: "object"},message:"must be object"}];return false;}}validate100.errors = vErrors;return errors === 0;}validate100.evaluated = {"props":true,"dynamicProps":false,"dynamicItems":false};const schema163 = {"type":"object","additionalProperties":false,"required":["kind","points"],"properties":{"kind":{"const":"arrow"},"points":{"type":"array","minItems":2,"maxItems":12,"items":{"type":"array","minItems":2,"maxItems":2,"items":{"type":"number","minimum":-400,"maximum":2400}}},"label":{"type":"string","minLength":0,"maxLength":120},"labelAt":{"type":"number","minimum":0,"maximum":1},"labelDx":{"type":"number","minimum":-400,"maximum":2400},"labelDy":{"type":"number","minimum":-400,"maximum":2400},"dashed":{"type":"boolean"},"both":{"type":"boolean"},"variant":{"enum":["ok","bad"]},"labelAnchor":{"enum":["start","middle","end"]},"active":{"oneOf":[{"type":"boolean"},{"type":"array","maxItems":20,"items":{"oneOf":[{"type":"integer","minimum":0,"maximum":15},{"$ref":"../common.schema.json#/$defs/id"}]}}]},"hot":{"oneOf":[{"type":"boolean"},{"type":"array","maxItems":20,"items":{"oneOf":[{"type":"integer","minimum":0,"maximum":15},{"$ref":"../common.schema.json#/$defs/id"}]}}]},"dim":{"type":"boolean"},"visible":{"type":"array","maxItems":20,"items":{"oneOf":[{"type":"integer","minimum":0,"maximum":15},{"$ref":"../common.schema.json#/$defs/id"}]}},"overrides":{"type":"object","maxProperties":6,"propertyNames":{"enum":["title","sub","label","text","n","dashed","muted"]},"additionalProperties":{"type":"object","maxProperties":20,"additionalProperties":{"oneOf":[{"type":"string","minLength":0,"maxLength":300},{"type":"number"},{"type":"boolean"},{"type":"array","maxItems":3,"items":{"type":"string","minLength":0,"maxLength":300}}]}}}}};function validate102(data, {instancePath="", parentData, parentDataProperty, rootData=data, dynamicAnchors={}}={}){let vErrors = null;let errors = 0;const evaluated0 = validate102.evaluated;if(evaluated0.dynamicProps){evaluated0.props = undefined;}if(evaluated0.dynamicItems){evaluated0.items = undefined;}if(errors === 0){if(data && typeof data == "object" && !Array.isArray(data)){let missing0;if(((data.kind === undefined) && (missing0 = "kind")) || ((data.points === undefined) && (missing0 = "points"))){validate102.errors = [{instancePath,schemaPath:"#/required",keyword:"required",params:{missingProperty: missing0},message:"must have required property '"+missing0+"'"}];return false;}else {const _errs1 = errors;for(const key0 in data){if(!(func1.call(schema163.properties, key0))){validate102.errors = [{instancePath,schemaPath:"#/additionalProperties",keyword:"additionalProperties",params:{additionalProperty: key0},message:"must NOT have additional properties"}];return false;break;}}if(_errs1 === errors){if(data.kind !== undefined){const _errs2 = errors;if("arrow" !== data.kind){validate102.errors = [{instancePath:instancePath+"/kind",schemaPath:"#/properties/kind/const",keyword:"const",params:{allowedValue: "arrow"},message:"must be equal to constant"}];return false;}var valid0 = _errs2 === errors;}else {var valid0 = true;}if(valid0){if(data.points !== undefined){let data1 = data.points;const _errs3 = errors;if(errors === _errs3){if(Array.isArray(data1)){if(data1.length > 12){validate102.errors = [{instancePath:instancePath+"/points",schemaPath:"#/properties/points/maxItems",keyword:"maxItems",params:{limit: 12},message:"must NOT have more than 12 items"}];return false;}else {if(data1.length < 2){validate102.errors = [{instancePath:instancePath+"/points",schemaPath:"#/properties/points/minItems",keyword:"minItems",params:{limit: 2},message:"must NOT have fewer than 2 items"}];return false;}else {var valid1 = true;const len0 = data1.length;for(let i0=0; i0 2){validate102.errors = [{instancePath:instancePath+"/points/" + i0,schemaPath:"#/properties/points/items/maxItems",keyword:"maxItems",params:{limit: 2},message:"must NOT have more than 2 items"}];return false;}else {if(data2.length < 2){validate102.errors = [{instancePath:instancePath+"/points/" + i0,schemaPath:"#/properties/points/items/minItems",keyword:"minItems",params:{limit: 2},message:"must NOT have fewer than 2 items"}];return false;}else {var valid2 = true;const len1 = data2.length;for(let i1=0; i1 2400 || isNaN(data3)){validate102.errors = [{instancePath:instancePath+"/points/" + i0+"/" + i1,schemaPath:"#/properties/points/items/items/maximum",keyword:"maximum",params:{comparison: "<=", limit: 2400},message:"must be <= 2400"}];return false;}else {if(data3 < -400 || isNaN(data3)){validate102.errors = [{instancePath:instancePath+"/points/" + i0+"/" + i1,schemaPath:"#/properties/points/items/items/minimum",keyword:"minimum",params:{comparison: ">=", limit: -400},message:"must be >= -400"}];return false;}}}else {validate102.errors = [{instancePath:instancePath+"/points/" + i0+"/" + i1,schemaPath:"#/properties/points/items/items/type",keyword:"type",params:{type: "number"},message:"must be number"}];return false;}}var valid2 = _errs7 === errors;if(!valid2){break;}}}}}else {validate102.errors = [{instancePath:instancePath+"/points/" + i0,schemaPath:"#/properties/points/items/type",keyword:"type",params:{type: "array"},message:"must be array"}];return false;}}var valid1 = _errs5 === errors;if(!valid1){break;}}}}}else {validate102.errors = [{instancePath:instancePath+"/points",schemaPath:"#/properties/points/type",keyword:"type",params:{type: "array"},message:"must be array"}];return false;}}var valid0 = _errs3 === errors;}else {var valid0 = true;}if(valid0){if(data.label !== undefined){let data4 = data.label;const _errs9 = errors;if(errors === _errs9){if(typeof data4 === "string"){if(func2(data4) > 120){validate102.errors = [{instancePath:instancePath+"/label",schemaPath:"#/properties/label/maxLength",keyword:"maxLength",params:{limit: 120},message:"must NOT have more than 120 characters"}];return false;}else {if(func2(data4) < 0){validate102.errors = [{instancePath:instancePath+"/label",schemaPath:"#/properties/label/minLength",keyword:"minLength",params:{limit: 0},message:"must NOT have fewer than 0 characters"}];return false;}}}else {validate102.errors = [{instancePath:instancePath+"/label",schemaPath:"#/properties/label/type",keyword:"type",params:{type: "string"},message:"must be string"}];return false;}}var valid0 = _errs9 === errors;}else {var valid0 = true;}if(valid0){if(data.labelAt !== undefined){let data5 = data.labelAt;const _errs11 = errors;if(errors === _errs11){if((typeof data5 == "number") && (isFinite(data5))){if(data5 > 1 || isNaN(data5)){validate102.errors = [{instancePath:instancePath+"/labelAt",schemaPath:"#/properties/labelAt/maximum",keyword:"maximum",params:{comparison: "<=", limit: 1},message:"must be <= 1"}];return false;}else {if(data5 < 0 || isNaN(data5)){validate102.errors = [{instancePath:instancePath+"/labelAt",schemaPath:"#/properties/labelAt/minimum",keyword:"minimum",params:{comparison: ">=", limit: 0},message:"must be >= 0"}];return false;}}}else {validate102.errors = [{instancePath:instancePath+"/labelAt",schemaPath:"#/properties/labelAt/type",keyword:"type",params:{type: "number"},message:"must be number"}];return false;}}var valid0 = _errs11 === errors;}else {var valid0 = true;}if(valid0){if(data.labelDx !== undefined){let data6 = data.labelDx;const _errs13 = errors;if(errors === _errs13){if((typeof data6 == "number") && (isFinite(data6))){if(data6 > 2400 || isNaN(data6)){validate102.errors = [{instancePath:instancePath+"/labelDx",schemaPath:"#/properties/labelDx/maximum",keyword:"maximum",params:{comparison: "<=", limit: 2400},message:"must be <= 2400"}];return false;}else {if(data6 < -400 || isNaN(data6)){validate102.errors = [{instancePath:instancePath+"/labelDx",schemaPath:"#/properties/labelDx/minimum",keyword:"minimum",params:{comparison: ">=", limit: -400},message:"must be >= -400"}];return false;}}}else {validate102.errors = [{instancePath:instancePath+"/labelDx",schemaPath:"#/properties/labelDx/type",keyword:"type",params:{type: "number"},message:"must be number"}];return false;}}var valid0 = _errs13 === errors;}else {var valid0 = true;}if(valid0){if(data.labelDy !== undefined){let data7 = data.labelDy;const _errs15 = errors;if(errors === _errs15){if((typeof data7 == "number") && (isFinite(data7))){if(data7 > 2400 || isNaN(data7)){validate102.errors = [{instancePath:instancePath+"/labelDy",schemaPath:"#/properties/labelDy/maximum",keyword:"maximum",params:{comparison: "<=", limit: 2400},message:"must be <= 2400"}];return false;}else {if(data7 < -400 || isNaN(data7)){validate102.errors = [{instancePath:instancePath+"/labelDy",schemaPath:"#/properties/labelDy/minimum",keyword:"minimum",params:{comparison: ">=", limit: -400},message:"must be >= -400"}];return false;}}}else {validate102.errors = [{instancePath:instancePath+"/labelDy",schemaPath:"#/properties/labelDy/type",keyword:"type",params:{type: "number"},message:"must be number"}];return false;}}var valid0 = _errs15 === errors;}else {var valid0 = true;}if(valid0){if(data.dashed !== undefined){const _errs17 = errors;if(typeof data.dashed !== "boolean"){validate102.errors = [{instancePath:instancePath+"/dashed",schemaPath:"#/properties/dashed/type",keyword:"type",params:{type: "boolean"},message:"must be boolean"}];return false;}var valid0 = _errs17 === errors;}else {var valid0 = true;}if(valid0){if(data.both !== undefined){const _errs19 = errors;if(typeof data.both !== "boolean"){validate102.errors = [{instancePath:instancePath+"/both",schemaPath:"#/properties/both/type",keyword:"type",params:{type: "boolean"},message:"must be boolean"}];return false;}var valid0 = _errs19 === errors;}else {var valid0 = true;}if(valid0){if(data.variant !== undefined){let data10 = data.variant;const _errs21 = errors;if(!((data10 === "ok") || (data10 === "bad"))){validate102.errors = [{instancePath:instancePath+"/variant",schemaPath:"#/properties/variant/enum",keyword:"enum",params:{allowedValues: schema163.properties.variant.enum},message:"must be equal to one of the allowed values"}];return false;}var valid0 = _errs21 === errors;}else {var valid0 = true;}if(valid0){if(data.labelAnchor !== undefined){let data11 = data.labelAnchor;const _errs22 = errors;if(!(((data11 === "start") || (data11 === "middle")) || (data11 === "end"))){validate102.errors = [{instancePath:instancePath+"/labelAnchor",schemaPath:"#/properties/labelAnchor/enum",keyword:"enum",params:{allowedValues: schema163.properties.labelAnchor.enum},message:"must be equal to one of the allowed values"}];return false;}var valid0 = _errs22 === errors;}else {var valid0 = true;}if(valid0){if(data.active !== undefined){let data12 = data.active;const _errs23 = errors;const _errs24 = errors;let valid3 = false;let passing0 = null;const _errs25 = errors;if(typeof data12 !== "boolean"){const err0 = {instancePath:instancePath+"/active",schemaPath:"#/properties/active/oneOf/0/type",keyword:"type",params:{type: "boolean"},message:"must be boolean"};if(vErrors === null){vErrors = [err0];}else {vErrors.push(err0);}errors++;}var _valid0 = _errs25 === errors;if(_valid0){valid3 = true;passing0 = 0;}const _errs27 = errors;if(errors === _errs27){if(Array.isArray(data12)){if(data12.length > 20){const err1 = {instancePath:instancePath+"/active",schemaPath:"#/properties/active/oneOf/1/maxItems",keyword:"maxItems",params:{limit: 20},message:"must NOT have more than 20 items"};if(vErrors === null){vErrors = [err1];}else {vErrors.push(err1);}errors++;}else {var valid4 = true;const len2 = data12.length;for(let i2=0; i2 15 || isNaN(data13)){const err3 = {instancePath:instancePath+"/active/" + i2,schemaPath:"#/properties/active/oneOf/1/items/oneOf/0/maximum",keyword:"maximum",params:{comparison: "<=", limit: 15},message:"must be <= 15"};if(vErrors === null){vErrors = [err3];}else {vErrors.push(err3);}errors++;}else {if(data13 < 0 || isNaN(data13)){const err4 = {instancePath:instancePath+"/active/" + i2,schemaPath:"#/properties/active/oneOf/1/items/oneOf/0/minimum",keyword:"minimum",params:{comparison: ">=", limit: 0},message:"must be >= 0"};if(vErrors === null){vErrors = [err4];}else {vErrors.push(err4);}errors++;}}}}var _valid1 = _errs31 === errors;if(_valid1){valid5 = true;passing1 = 0;}const _errs33 = errors;const _errs34 = errors;if(errors === _errs34){if(typeof data13 === "string"){if(func2(data13) > 64){const err5 = {instancePath:instancePath+"/active/" + i2,schemaPath:"../common.schema.json#/$defs/id/maxLength",keyword:"maxLength",params:{limit: 64},message:"must NOT have more than 64 characters"};if(vErrors === null){vErrors = [err5];}else {vErrors.push(err5);}errors++;}else {if(!pattern18.test(data13)){const err6 = {instancePath:instancePath+"/active/" + i2,schemaPath:"../common.schema.json#/$defs/id/pattern",keyword:"pattern",params:{pattern: "^[a-z0-9]+(?:-[a-z0-9]+)*$"},message:"must match pattern \""+"^[a-z0-9]+(?:-[a-z0-9]+)*$"+"\""};if(vErrors === null){vErrors = [err6];}else {vErrors.push(err6);}errors++;}}}else {const err7 = {instancePath:instancePath+"/active/" + i2,schemaPath:"../common.schema.json#/$defs/id/type",keyword:"type",params:{type: "string"},message:"must be string"};if(vErrors === null){vErrors = [err7];}else {vErrors.push(err7);}errors++;}}var _valid1 = _errs33 === errors;if(_valid1 && valid5){valid5 = false;passing1 = [passing1, 1];}else {if(_valid1){valid5 = true;passing1 = 1;}}if(!valid5){const err8 = {instancePath:instancePath+"/active/" + i2,schemaPath:"#/properties/active/oneOf/1/items/oneOf",keyword:"oneOf",params:{passingSchemas: passing1},message:"must match exactly one schema in oneOf"};if(vErrors === null){vErrors = [err8];}else {vErrors.push(err8);}errors++;}else {errors = _errs30;if(vErrors !== null){if(_errs30){vErrors.length = _errs30;}else {vErrors = null;}}}var valid4 = _errs29 === errors;if(!valid4){break;}}}}else {const err9 = {instancePath:instancePath+"/active",schemaPath:"#/properties/active/oneOf/1/type",keyword:"type",params:{type: "array"},message:"must be array"};if(vErrors === null){vErrors = [err9];}else {vErrors.push(err9);}errors++;}}var _valid0 = _errs27 === errors;if(_valid0 && valid3){valid3 = false;passing0 = [passing0, 1];}else {if(_valid0){valid3 = true;passing0 = 1;}}if(!valid3){const err10 = {instancePath:instancePath+"/active",schemaPath:"#/properties/active/oneOf",keyword:"oneOf",params:{passingSchemas: passing0},message:"must match exactly one schema in oneOf"};if(vErrors === null){vErrors = [err10];}else {vErrors.push(err10);}errors++;validate102.errors = vErrors;return false;}else {errors = _errs24;if(vErrors !== null){if(_errs24){vErrors.length = _errs24;}else {vErrors = null;}}}var valid0 = _errs23 === errors;}else {var valid0 = true;}if(valid0){if(data.hot !== undefined){let data14 = data.hot;const _errs36 = errors;const _errs37 = errors;let valid7 = false;let passing2 = null;const _errs38 = errors;if(typeof data14 !== "boolean"){const err11 = {instancePath:instancePath+"/hot",schemaPath:"#/properties/hot/oneOf/0/type",keyword:"type",params:{type: "boolean"},message:"must be boolean"};if(vErrors === null){vErrors = [err11];}else {vErrors.push(err11);}errors++;}var _valid2 = _errs38 === errors;if(_valid2){valid7 = true;passing2 = 0;}const _errs40 = errors;if(errors === _errs40){if(Array.isArray(data14)){if(data14.length > 20){const err12 = {instancePath:instancePath+"/hot",schemaPath:"#/properties/hot/oneOf/1/maxItems",keyword:"maxItems",params:{limit: 20},message:"must NOT have more than 20 items"};if(vErrors === null){vErrors = [err12];}else {vErrors.push(err12);}errors++;}else {var valid8 = true;const len3 = data14.length;for(let i3=0; i3 15 || isNaN(data15)){const err14 = {instancePath:instancePath+"/hot/" + i3,schemaPath:"#/properties/hot/oneOf/1/items/oneOf/0/maximum",keyword:"maximum",params:{comparison: "<=", limit: 15},message:"must be <= 15"};if(vErrors === null){vErrors = [err14];}else {vErrors.push(err14);}errors++;}else {if(data15 < 0 || isNaN(data15)){const err15 = {instancePath:instancePath+"/hot/" + i3,schemaPath:"#/properties/hot/oneOf/1/items/oneOf/0/minimum",keyword:"minimum",params:{comparison: ">=", limit: 0},message:"must be >= 0"};if(vErrors === null){vErrors = [err15];}else {vErrors.push(err15);}errors++;}}}}var _valid3 = _errs44 === errors;if(_valid3){valid9 = true;passing3 = 0;}const _errs46 = errors;const _errs47 = errors;if(errors === _errs47){if(typeof data15 === "string"){if(func2(data15) > 64){const err16 = {instancePath:instancePath+"/hot/" + i3,schemaPath:"../common.schema.json#/$defs/id/maxLength",keyword:"maxLength",params:{limit: 64},message:"must NOT have more than 64 characters"};if(vErrors === null){vErrors = [err16];}else {vErrors.push(err16);}errors++;}else {if(!pattern18.test(data15)){const err17 = {instancePath:instancePath+"/hot/" + i3,schemaPath:"../common.schema.json#/$defs/id/pattern",keyword:"pattern",params:{pattern: "^[a-z0-9]+(?:-[a-z0-9]+)*$"},message:"must match pattern \""+"^[a-z0-9]+(?:-[a-z0-9]+)*$"+"\""};if(vErrors === null){vErrors = [err17];}else {vErrors.push(err17);}errors++;}}}else {const err18 = {instancePath:instancePath+"/hot/" + i3,schemaPath:"../common.schema.json#/$defs/id/type",keyword:"type",params:{type: "string"},message:"must be string"};if(vErrors === null){vErrors = [err18];}else {vErrors.push(err18);}errors++;}}var _valid3 = _errs46 === errors;if(_valid3 && valid9){valid9 = false;passing3 = [passing3, 1];}else {if(_valid3){valid9 = true;passing3 = 1;}}if(!valid9){const err19 = {instancePath:instancePath+"/hot/" + i3,schemaPath:"#/properties/hot/oneOf/1/items/oneOf",keyword:"oneOf",params:{passingSchemas: passing3},message:"must match exactly one schema in oneOf"};if(vErrors === null){vErrors = [err19];}else {vErrors.push(err19);}errors++;}else {errors = _errs43;if(vErrors !== null){if(_errs43){vErrors.length = _errs43;}else {vErrors = null;}}}var valid8 = _errs42 === errors;if(!valid8){break;}}}}else {const err20 = {instancePath:instancePath+"/hot",schemaPath:"#/properties/hot/oneOf/1/type",keyword:"type",params:{type: "array"},message:"must be array"};if(vErrors === null){vErrors = [err20];}else {vErrors.push(err20);}errors++;}}var _valid2 = _errs40 === errors;if(_valid2 && valid7){valid7 = false;passing2 = [passing2, 1];}else {if(_valid2){valid7 = true;passing2 = 1;}}if(!valid7){const err21 = {instancePath:instancePath+"/hot",schemaPath:"#/properties/hot/oneOf",keyword:"oneOf",params:{passingSchemas: passing2},message:"must match exactly one schema in oneOf"};if(vErrors === null){vErrors = [err21];}else {vErrors.push(err21);}errors++;validate102.errors = vErrors;return false;}else {errors = _errs37;if(vErrors !== null){if(_errs37){vErrors.length = _errs37;}else {vErrors = null;}}}var valid0 = _errs36 === errors;}else {var valid0 = true;}if(valid0){if(data.dim !== undefined){const _errs49 = errors;if(typeof data.dim !== "boolean"){validate102.errors = [{instancePath:instancePath+"/dim",schemaPath:"#/properties/dim/type",keyword:"type",params:{type: "boolean"},message:"must be boolean"}];return false;}var valid0 = _errs49 === errors;}else {var valid0 = true;}if(valid0){if(data.visible !== undefined){let data17 = data.visible;const _errs51 = errors;if(errors === _errs51){if(Array.isArray(data17)){if(data17.length > 20){validate102.errors = [{instancePath:instancePath+"/visible",schemaPath:"#/properties/visible/maxItems",keyword:"maxItems",params:{limit: 20},message:"must NOT have more than 20 items"}];return false;}else {var valid11 = true;const len4 = data17.length;for(let i4=0; i4 15 || isNaN(data18)){const err23 = {instancePath:instancePath+"/visible/" + i4,schemaPath:"#/properties/visible/items/oneOf/0/maximum",keyword:"maximum",params:{comparison: "<=", limit: 15},message:"must be <= 15"};if(vErrors === null){vErrors = [err23];}else {vErrors.push(err23);}errors++;}else {if(data18 < 0 || isNaN(data18)){const err24 = {instancePath:instancePath+"/visible/" + i4,schemaPath:"#/properties/visible/items/oneOf/0/minimum",keyword:"minimum",params:{comparison: ">=", limit: 0},message:"must be >= 0"};if(vErrors === null){vErrors = [err24];}else {vErrors.push(err24);}errors++;}}}}var _valid4 = _errs55 === errors;if(_valid4){valid12 = true;passing4 = 0;}const _errs57 = errors;const _errs58 = errors;if(errors === _errs58){if(typeof data18 === "string"){if(func2(data18) > 64){const err25 = {instancePath:instancePath+"/visible/" + i4,schemaPath:"../common.schema.json#/$defs/id/maxLength",keyword:"maxLength",params:{limit: 64},message:"must NOT have more than 64 characters"};if(vErrors === null){vErrors = [err25];}else {vErrors.push(err25);}errors++;}else {if(!pattern18.test(data18)){const err26 = {instancePath:instancePath+"/visible/" + i4,schemaPath:"../common.schema.json#/$defs/id/pattern",keyword:"pattern",params:{pattern: "^[a-z0-9]+(?:-[a-z0-9]+)*$"},message:"must match pattern \""+"^[a-z0-9]+(?:-[a-z0-9]+)*$"+"\""};if(vErrors === null){vErrors = [err26];}else {vErrors.push(err26);}errors++;}}}else {const err27 = {instancePath:instancePath+"/visible/" + i4,schemaPath:"../common.schema.json#/$defs/id/type",keyword:"type",params:{type: "string"},message:"must be string"};if(vErrors === null){vErrors = [err27];}else {vErrors.push(err27);}errors++;}}var _valid4 = _errs57 === errors;if(_valid4 && valid12){valid12 = false;passing4 = [passing4, 1];}else {if(_valid4){valid12 = true;passing4 = 1;}}if(!valid12){const err28 = {instancePath:instancePath+"/visible/" + i4,schemaPath:"#/properties/visible/items/oneOf",keyword:"oneOf",params:{passingSchemas: passing4},message:"must match exactly one schema in oneOf"};if(vErrors === null){vErrors = [err28];}else {vErrors.push(err28);}errors++;validate102.errors = vErrors;return false;}else {errors = _errs54;if(vErrors !== null){if(_errs54){vErrors.length = _errs54;}else {vErrors = null;}}}var valid11 = _errs53 === errors;if(!valid11){break;}}}}else {validate102.errors = [{instancePath:instancePath+"/visible",schemaPath:"#/properties/visible/type",keyword:"type",params:{type: "array"},message:"must be array"}];return false;}}var valid0 = _errs51 === errors;}else {var valid0 = true;}if(valid0){if(data.overrides !== undefined){let data19 = data.overrides;const _errs60 = errors;if(errors === _errs60){if(data19 && typeof data19 == "object" && !Array.isArray(data19)){if(Object.keys(data19).length > 6){validate102.errors = [{instancePath:instancePath+"/overrides",schemaPath:"#/properties/overrides/maxProperties",keyword:"maxProperties",params:{limit: 6},message:"must NOT have more than 6 properties"}];return false;}else {for(const key1 in data19){const _errs62 = errors;if(!(((((((key1 === "title") || (key1 === "sub")) || (key1 === "label")) || (key1 === "text")) || (key1 === "n")) || (key1 === "dashed")) || (key1 === "muted"))){const err29 = {instancePath:instancePath+"/overrides",schemaPath:"#/properties/overrides/propertyNames/enum",keyword:"enum",params:{allowedValues: schema163.properties.overrides.propertyNames.enum},message:"must be equal to one of the allowed values",propertyName:key1};if(vErrors === null){vErrors = [err29];}else {vErrors.push(err29);}errors++;}var valid14 = _errs62 === errors;if(!valid14){const err30 = {instancePath:instancePath+"/overrides",schemaPath:"#/properties/overrides/propertyNames",keyword:"propertyNames",params:{propertyName: key1},message:"property name must be valid"};if(vErrors === null){vErrors = [err30];}else {vErrors.push(err30);}errors++;validate102.errors = vErrors;return false;break;}}if(valid14){for(const key2 in data19){let data20 = data19[key2];const _errs64 = errors;if(errors === _errs64){if(data20 && typeof data20 == "object" && !Array.isArray(data20)){if(Object.keys(data20).length > 20){validate102.errors = [{instancePath:instancePath+"/overrides/" + key2.replace(/~/g, "~0").replace(/\//g, "~1"),schemaPath:"#/properties/overrides/additionalProperties/maxProperties",keyword:"maxProperties",params:{limit: 20},message:"must NOT have more than 20 properties"}];return false;}else {for(const key3 in data20){let data21 = data20[key3];const _errs67 = errors;const _errs68 = errors;let valid17 = false;let passing5 = null;const _errs69 = errors;if(errors === _errs69){if(typeof data21 === "string"){if(func2(data21) > 300){const err31 = {instancePath:instancePath+"/overrides/" + key2.replace(/~/g, "~0").replace(/\//g, "~1")+"/" + key3.replace(/~/g, "~0").replace(/\//g, "~1"),schemaPath:"#/properties/overrides/additionalProperties/additionalProperties/oneOf/0/maxLength",keyword:"maxLength",params:{limit: 300},message:"must NOT have more than 300 characters"};if(vErrors === null){vErrors = [err31];}else {vErrors.push(err31);}errors++;}else {if(func2(data21) < 0){const err32 = {instancePath:instancePath+"/overrides/" + key2.replace(/~/g, "~0").replace(/\//g, "~1")+"/" + key3.replace(/~/g, "~0").replace(/\//g, "~1"),schemaPath:"#/properties/overrides/additionalProperties/additionalProperties/oneOf/0/minLength",keyword:"minLength",params:{limit: 0},message:"must NOT have fewer than 0 characters"};if(vErrors === null){vErrors = [err32];}else {vErrors.push(err32);}errors++;}}}else {const err33 = {instancePath:instancePath+"/overrides/" + key2.replace(/~/g, "~0").replace(/\//g, "~1")+"/" + key3.replace(/~/g, "~0").replace(/\//g, "~1"),schemaPath:"#/properties/overrides/additionalProperties/additionalProperties/oneOf/0/type",keyword:"type",params:{type: "string"},message:"must be string"};if(vErrors === null){vErrors = [err33];}else {vErrors.push(err33);}errors++;}}var _valid5 = _errs69 === errors;if(_valid5){valid17 = true;passing5 = 0;}const _errs71 = errors;if(!((typeof data21 == "number") && (isFinite(data21)))){const err34 = {instancePath:instancePath+"/overrides/" + key2.replace(/~/g, "~0").replace(/\//g, "~1")+"/" + key3.replace(/~/g, "~0").replace(/\//g, "~1"),schemaPath:"#/properties/overrides/additionalProperties/additionalProperties/oneOf/1/type",keyword:"type",params:{type: "number"},message:"must be number"};if(vErrors === null){vErrors = [err34];}else {vErrors.push(err34);}errors++;}var _valid5 = _errs71 === errors;if(_valid5 && valid17){valid17 = false;passing5 = [passing5, 1];}else {if(_valid5){valid17 = true;passing5 = 1;}const _errs73 = errors;if(typeof data21 !== "boolean"){const err35 = {instancePath:instancePath+"/overrides/" + key2.replace(/~/g, "~0").replace(/\//g, "~1")+"/" + key3.replace(/~/g, "~0").replace(/\//g, "~1"),schemaPath:"#/properties/overrides/additionalProperties/additionalProperties/oneOf/2/type",keyword:"type",params:{type: "boolean"},message:"must be boolean"};if(vErrors === null){vErrors = [err35];}else {vErrors.push(err35);}errors++;}var _valid5 = _errs73 === errors;if(_valid5 && valid17){valid17 = false;passing5 = [passing5, 2];}else {if(_valid5){valid17 = true;passing5 = 2;}const _errs75 = errors;if(errors === _errs75){if(Array.isArray(data21)){if(data21.length > 3){const err36 = {instancePath:instancePath+"/overrides/" + key2.replace(/~/g, "~0").replace(/\//g, "~1")+"/" + key3.replace(/~/g, "~0").replace(/\//g, "~1"),schemaPath:"#/properties/overrides/additionalProperties/additionalProperties/oneOf/3/maxItems",keyword:"maxItems",params:{limit: 3},message:"must NOT have more than 3 items"};if(vErrors === null){vErrors = [err36];}else {vErrors.push(err36);}errors++;}else {var valid18 = true;const len5 = data21.length;for(let i5=0; i5 300){const err37 = {instancePath:instancePath+"/overrides/" + key2.replace(/~/g, "~0").replace(/\//g, "~1")+"/" + key3.replace(/~/g, "~0").replace(/\//g, "~1")+"/" + i5,schemaPath:"#/properties/overrides/additionalProperties/additionalProperties/oneOf/3/items/maxLength",keyword:"maxLength",params:{limit: 300},message:"must NOT have more than 300 characters"};if(vErrors === null){vErrors = [err37];}else {vErrors.push(err37);}errors++;}else {if(func2(data22) < 0){const err38 = {instancePath:instancePath+"/overrides/" + key2.replace(/~/g, "~0").replace(/\//g, "~1")+"/" + key3.replace(/~/g, "~0").replace(/\//g, "~1")+"/" + i5,schemaPath:"#/properties/overrides/additionalProperties/additionalProperties/oneOf/3/items/minLength",keyword:"minLength",params:{limit: 0},message:"must NOT have fewer than 0 characters"};if(vErrors === null){vErrors = [err38];}else {vErrors.push(err38);}errors++;}}}else {const err39 = {instancePath:instancePath+"/overrides/" + key2.replace(/~/g, "~0").replace(/\//g, "~1")+"/" + key3.replace(/~/g, "~0").replace(/\//g, "~1")+"/" + i5,schemaPath:"#/properties/overrides/additionalProperties/additionalProperties/oneOf/3/items/type",keyword:"type",params:{type: "string"},message:"must be string"};if(vErrors === null){vErrors = [err39];}else {vErrors.push(err39);}errors++;}}var valid18 = _errs77 === errors;if(!valid18){break;}}}}else {const err40 = {instancePath:instancePath+"/overrides/" + key2.replace(/~/g, "~0").replace(/\//g, "~1")+"/" + key3.replace(/~/g, "~0").replace(/\//g, "~1"),schemaPath:"#/properties/overrides/additionalProperties/additionalProperties/oneOf/3/type",keyword:"type",params:{type: "array"},message:"must be array"};if(vErrors === null){vErrors = [err40];}else {vErrors.push(err40);}errors++;}}var _valid5 = _errs75 === errors;if(_valid5 && valid17){valid17 = false;passing5 = [passing5, 3];}else {if(_valid5){valid17 = true;passing5 = 3;}}}}if(!valid17){const err41 = {instancePath:instancePath+"/overrides/" + key2.replace(/~/g, "~0").replace(/\//g, "~1")+"/" + key3.replace(/~/g, "~0").replace(/\//g, "~1"),schemaPath:"#/properties/overrides/additionalProperties/additionalProperties/oneOf",keyword:"oneOf",params:{passingSchemas: passing5},message:"must match exactly one schema in oneOf"};if(vErrors === null){vErrors = [err41];}else {vErrors.push(err41);}errors++;validate102.errors = vErrors;return false;}else {errors = _errs68;if(vErrors !== null){if(_errs68){vErrors.length = _errs68;}else {vErrors = null;}}}var valid16 = _errs67 === errors;if(!valid16){break;}}}}else {validate102.errors = [{instancePath:instancePath+"/overrides/" + key2.replace(/~/g, "~0").replace(/\//g, "~1"),schemaPath:"#/properties/overrides/additionalProperties/type",keyword:"type",params:{type: "object"},message:"must be object"}];return false;}}var valid15 = _errs64 === errors;if(!valid15){break;}}}}}else {validate102.errors = [{instancePath:instancePath+"/overrides",schemaPath:"#/properties/overrides/type",keyword:"type",params:{type: "object"},message:"must be object"}];return false;}}var valid0 = _errs60 === errors;}else {var valid0 = true;}}}}}}}}}}}}}}}}}}else {validate102.errors = [{instancePath,schemaPath:"#/type",keyword:"type",params:{type: "object"},message:"must be object"}];return false;}}validate102.errors = vErrors;return errors === 0;}validate102.evaluated = {"props":true,"dynamicProps":false,"dynamicItems":false};const schema167 = {"type":"object","additionalProperties":false,"required":["kind","x","y","text"],"properties":{"kind":{"const":"label"},"x":{"type":"number","minimum":-400,"maximum":2400},"y":{"type":"number","minimum":-400,"maximum":2400},"text":{"type":"string","minLength":0,"maxLength":200},"anchor":{"enum":["start","middle","end"]},"size":{"type":"number","minimum":8,"maximum":32},"muted":{"type":"boolean"},"weight":{"type":"number","minimum":100,"maximum":900},"mono":{"type":"boolean"},"active":{"oneOf":[{"type":"boolean"},{"type":"array","maxItems":20,"items":{"oneOf":[{"type":"integer","minimum":0,"maximum":15},{"$ref":"../common.schema.json#/$defs/id"}]}}]},"hot":{"oneOf":[{"type":"boolean"},{"type":"array","maxItems":20,"items":{"oneOf":[{"type":"integer","minimum":0,"maximum":15},{"$ref":"../common.schema.json#/$defs/id"}]}}]},"dim":{"type":"boolean"},"visible":{"type":"array","maxItems":20,"items":{"oneOf":[{"type":"integer","minimum":0,"maximum":15},{"$ref":"../common.schema.json#/$defs/id"}]}},"overrides":{"type":"object","maxProperties":6,"propertyNames":{"enum":["title","sub","label","text","n","dashed","muted"]},"additionalProperties":{"type":"object","maxProperties":20,"additionalProperties":{"oneOf":[{"type":"string","minLength":0,"maxLength":300},{"type":"number"},{"type":"boolean"},{"type":"array","maxItems":3,"items":{"type":"string","minLength":0,"maxLength":300}}]}}}}};function validate104(data, {instancePath="", parentData, parentDataProperty, rootData=data, dynamicAnchors={}}={}){let vErrors = null;let errors = 0;const evaluated0 = validate104.evaluated;if(evaluated0.dynamicProps){evaluated0.props = undefined;}if(evaluated0.dynamicItems){evaluated0.items = undefined;}if(errors === 0){if(data && typeof data == "object" && !Array.isArray(data)){let missing0;if(((((data.kind === undefined) && (missing0 = "kind")) || ((data.x === undefined) && (missing0 = "x"))) || ((data.y === undefined) && (missing0 = "y"))) || ((data.text === undefined) && (missing0 = "text"))){validate104.errors = [{instancePath,schemaPath:"#/required",keyword:"required",params:{missingProperty: missing0},message:"must have required property '"+missing0+"'"}];return false;}else {const _errs1 = errors;for(const key0 in data){if(!(func1.call(schema167.properties, key0))){validate104.errors = [{instancePath,schemaPath:"#/additionalProperties",keyword:"additionalProperties",params:{additionalProperty: key0},message:"must NOT have additional properties"}];return false;break;}}if(_errs1 === errors){if(data.kind !== undefined){const _errs2 = errors;if("label" !== data.kind){validate104.errors = [{instancePath:instancePath+"/kind",schemaPath:"#/properties/kind/const",keyword:"const",params:{allowedValue: "label"},message:"must be equal to constant"}];return false;}var valid0 = _errs2 === errors;}else {var valid0 = true;}if(valid0){if(data.x !== undefined){let data1 = data.x;const _errs3 = errors;if(errors === _errs3){if((typeof data1 == "number") && (isFinite(data1))){if(data1 > 2400 || isNaN(data1)){validate104.errors = [{instancePath:instancePath+"/x",schemaPath:"#/properties/x/maximum",keyword:"maximum",params:{comparison: "<=", limit: 2400},message:"must be <= 2400"}];return false;}else {if(data1 < -400 || isNaN(data1)){validate104.errors = [{instancePath:instancePath+"/x",schemaPath:"#/properties/x/minimum",keyword:"minimum",params:{comparison: ">=", limit: -400},message:"must be >= -400"}];return false;}}}else {validate104.errors = [{instancePath:instancePath+"/x",schemaPath:"#/properties/x/type",keyword:"type",params:{type: "number"},message:"must be number"}];return false;}}var valid0 = _errs3 === errors;}else {var valid0 = true;}if(valid0){if(data.y !== undefined){let data2 = data.y;const _errs5 = errors;if(errors === _errs5){if((typeof data2 == "number") && (isFinite(data2))){if(data2 > 2400 || isNaN(data2)){validate104.errors = [{instancePath:instancePath+"/y",schemaPath:"#/properties/y/maximum",keyword:"maximum",params:{comparison: "<=", limit: 2400},message:"must be <= 2400"}];return false;}else {if(data2 < -400 || isNaN(data2)){validate104.errors = [{instancePath:instancePath+"/y",schemaPath:"#/properties/y/minimum",keyword:"minimum",params:{comparison: ">=", limit: -400},message:"must be >= -400"}];return false;}}}else {validate104.errors = [{instancePath:instancePath+"/y",schemaPath:"#/properties/y/type",keyword:"type",params:{type: "number"},message:"must be number"}];return false;}}var valid0 = _errs5 === errors;}else {var valid0 = true;}if(valid0){if(data.text !== undefined){let data3 = data.text;const _errs7 = errors;if(errors === _errs7){if(typeof data3 === "string"){if(func2(data3) > 200){validate104.errors = [{instancePath:instancePath+"/text",schemaPath:"#/properties/text/maxLength",keyword:"maxLength",params:{limit: 200},message:"must NOT have more than 200 characters"}];return false;}else {if(func2(data3) < 0){validate104.errors = [{instancePath:instancePath+"/text",schemaPath:"#/properties/text/minLength",keyword:"minLength",params:{limit: 0},message:"must NOT have fewer than 0 characters"}];return false;}}}else {validate104.errors = [{instancePath:instancePath+"/text",schemaPath:"#/properties/text/type",keyword:"type",params:{type: "string"},message:"must be string"}];return false;}}var valid0 = _errs7 === errors;}else {var valid0 = true;}if(valid0){if(data.anchor !== undefined){let data4 = data.anchor;const _errs9 = errors;if(!(((data4 === "start") || (data4 === "middle")) || (data4 === "end"))){validate104.errors = [{instancePath:instancePath+"/anchor",schemaPath:"#/properties/anchor/enum",keyword:"enum",params:{allowedValues: schema167.properties.anchor.enum},message:"must be equal to one of the allowed values"}];return false;}var valid0 = _errs9 === errors;}else {var valid0 = true;}if(valid0){if(data.size !== undefined){let data5 = data.size;const _errs10 = errors;if(errors === _errs10){if((typeof data5 == "number") && (isFinite(data5))){if(data5 > 32 || isNaN(data5)){validate104.errors = [{instancePath:instancePath+"/size",schemaPath:"#/properties/size/maximum",keyword:"maximum",params:{comparison: "<=", limit: 32},message:"must be <= 32"}];return false;}else {if(data5 < 8 || isNaN(data5)){validate104.errors = [{instancePath:instancePath+"/size",schemaPath:"#/properties/size/minimum",keyword:"minimum",params:{comparison: ">=", limit: 8},message:"must be >= 8"}];return false;}}}else {validate104.errors = [{instancePath:instancePath+"/size",schemaPath:"#/properties/size/type",keyword:"type",params:{type: "number"},message:"must be number"}];return false;}}var valid0 = _errs10 === errors;}else {var valid0 = true;}if(valid0){if(data.muted !== undefined){const _errs12 = errors;if(typeof data.muted !== "boolean"){validate104.errors = [{instancePath:instancePath+"/muted",schemaPath:"#/properties/muted/type",keyword:"type",params:{type: "boolean"},message:"must be boolean"}];return false;}var valid0 = _errs12 === errors;}else {var valid0 = true;}if(valid0){if(data.weight !== undefined){let data7 = data.weight;const _errs14 = errors;if(errors === _errs14){if((typeof data7 == "number") && (isFinite(data7))){if(data7 > 900 || isNaN(data7)){validate104.errors = [{instancePath:instancePath+"/weight",schemaPath:"#/properties/weight/maximum",keyword:"maximum",params:{comparison: "<=", limit: 900},message:"must be <= 900"}];return false;}else {if(data7 < 100 || isNaN(data7)){validate104.errors = [{instancePath:instancePath+"/weight",schemaPath:"#/properties/weight/minimum",keyword:"minimum",params:{comparison: ">=", limit: 100},message:"must be >= 100"}];return false;}}}else {validate104.errors = [{instancePath:instancePath+"/weight",schemaPath:"#/properties/weight/type",keyword:"type",params:{type: "number"},message:"must be number"}];return false;}}var valid0 = _errs14 === errors;}else {var valid0 = true;}if(valid0){if(data.mono !== undefined){const _errs16 = errors;if(typeof data.mono !== "boolean"){validate104.errors = [{instancePath:instancePath+"/mono",schemaPath:"#/properties/mono/type",keyword:"type",params:{type: "boolean"},message:"must be boolean"}];return false;}var valid0 = _errs16 === errors;}else {var valid0 = true;}if(valid0){if(data.active !== undefined){let data9 = data.active;const _errs18 = errors;const _errs19 = errors;let valid1 = false;let passing0 = null;const _errs20 = errors;if(typeof data9 !== "boolean"){const err0 = {instancePath:instancePath+"/active",schemaPath:"#/properties/active/oneOf/0/type",keyword:"type",params:{type: "boolean"},message:"must be boolean"};if(vErrors === null){vErrors = [err0];}else {vErrors.push(err0);}errors++;}var _valid0 = _errs20 === errors;if(_valid0){valid1 = true;passing0 = 0;}const _errs22 = errors;if(errors === _errs22){if(Array.isArray(data9)){if(data9.length > 20){const err1 = {instancePath:instancePath+"/active",schemaPath:"#/properties/active/oneOf/1/maxItems",keyword:"maxItems",params:{limit: 20},message:"must NOT have more than 20 items"};if(vErrors === null){vErrors = [err1];}else {vErrors.push(err1);}errors++;}else {var valid2 = true;const len0 = data9.length;for(let i0=0; i0 15 || isNaN(data10)){const err3 = {instancePath:instancePath+"/active/" + i0,schemaPath:"#/properties/active/oneOf/1/items/oneOf/0/maximum",keyword:"maximum",params:{comparison: "<=", limit: 15},message:"must be <= 15"};if(vErrors === null){vErrors = [err3];}else {vErrors.push(err3);}errors++;}else {if(data10 < 0 || isNaN(data10)){const err4 = {instancePath:instancePath+"/active/" + i0,schemaPath:"#/properties/active/oneOf/1/items/oneOf/0/minimum",keyword:"minimum",params:{comparison: ">=", limit: 0},message:"must be >= 0"};if(vErrors === null){vErrors = [err4];}else {vErrors.push(err4);}errors++;}}}}var _valid1 = _errs26 === errors;if(_valid1){valid3 = true;passing1 = 0;}const _errs28 = errors;const _errs29 = errors;if(errors === _errs29){if(typeof data10 === "string"){if(func2(data10) > 64){const err5 = {instancePath:instancePath+"/active/" + i0,schemaPath:"../common.schema.json#/$defs/id/maxLength",keyword:"maxLength",params:{limit: 64},message:"must NOT have more than 64 characters"};if(vErrors === null){vErrors = [err5];}else {vErrors.push(err5);}errors++;}else {if(!pattern18.test(data10)){const err6 = {instancePath:instancePath+"/active/" + i0,schemaPath:"../common.schema.json#/$defs/id/pattern",keyword:"pattern",params:{pattern: "^[a-z0-9]+(?:-[a-z0-9]+)*$"},message:"must match pattern \""+"^[a-z0-9]+(?:-[a-z0-9]+)*$"+"\""};if(vErrors === null){vErrors = [err6];}else {vErrors.push(err6);}errors++;}}}else {const err7 = {instancePath:instancePath+"/active/" + i0,schemaPath:"../common.schema.json#/$defs/id/type",keyword:"type",params:{type: "string"},message:"must be string"};if(vErrors === null){vErrors = [err7];}else {vErrors.push(err7);}errors++;}}var _valid1 = _errs28 === errors;if(_valid1 && valid3){valid3 = false;passing1 = [passing1, 1];}else {if(_valid1){valid3 = true;passing1 = 1;}}if(!valid3){const err8 = {instancePath:instancePath+"/active/" + i0,schemaPath:"#/properties/active/oneOf/1/items/oneOf",keyword:"oneOf",params:{passingSchemas: passing1},message:"must match exactly one schema in oneOf"};if(vErrors === null){vErrors = [err8];}else {vErrors.push(err8);}errors++;}else {errors = _errs25;if(vErrors !== null){if(_errs25){vErrors.length = _errs25;}else {vErrors = null;}}}var valid2 = _errs24 === errors;if(!valid2){break;}}}}else {const err9 = {instancePath:instancePath+"/active",schemaPath:"#/properties/active/oneOf/1/type",keyword:"type",params:{type: "array"},message:"must be array"};if(vErrors === null){vErrors = [err9];}else {vErrors.push(err9);}errors++;}}var _valid0 = _errs22 === errors;if(_valid0 && valid1){valid1 = false;passing0 = [passing0, 1];}else {if(_valid0){valid1 = true;passing0 = 1;}}if(!valid1){const err10 = {instancePath:instancePath+"/active",schemaPath:"#/properties/active/oneOf",keyword:"oneOf",params:{passingSchemas: passing0},message:"must match exactly one schema in oneOf"};if(vErrors === null){vErrors = [err10];}else {vErrors.push(err10);}errors++;validate104.errors = vErrors;return false;}else {errors = _errs19;if(vErrors !== null){if(_errs19){vErrors.length = _errs19;}else {vErrors = null;}}}var valid0 = _errs18 === errors;}else {var valid0 = true;}if(valid0){if(data.hot !== undefined){let data11 = data.hot;const _errs31 = errors;const _errs32 = errors;let valid5 = false;let passing2 = null;const _errs33 = errors;if(typeof data11 !== "boolean"){const err11 = {instancePath:instancePath+"/hot",schemaPath:"#/properties/hot/oneOf/0/type",keyword:"type",params:{type: "boolean"},message:"must be boolean"};if(vErrors === null){vErrors = [err11];}else {vErrors.push(err11);}errors++;}var _valid2 = _errs33 === errors;if(_valid2){valid5 = true;passing2 = 0;}const _errs35 = errors;if(errors === _errs35){if(Array.isArray(data11)){if(data11.length > 20){const err12 = {instancePath:instancePath+"/hot",schemaPath:"#/properties/hot/oneOf/1/maxItems",keyword:"maxItems",params:{limit: 20},message:"must NOT have more than 20 items"};if(vErrors === null){vErrors = [err12];}else {vErrors.push(err12);}errors++;}else {var valid6 = true;const len1 = data11.length;for(let i1=0; i1 15 || isNaN(data12)){const err14 = {instancePath:instancePath+"/hot/" + i1,schemaPath:"#/properties/hot/oneOf/1/items/oneOf/0/maximum",keyword:"maximum",params:{comparison: "<=", limit: 15},message:"must be <= 15"};if(vErrors === null){vErrors = [err14];}else {vErrors.push(err14);}errors++;}else {if(data12 < 0 || isNaN(data12)){const err15 = {instancePath:instancePath+"/hot/" + i1,schemaPath:"#/properties/hot/oneOf/1/items/oneOf/0/minimum",keyword:"minimum",params:{comparison: ">=", limit: 0},message:"must be >= 0"};if(vErrors === null){vErrors = [err15];}else {vErrors.push(err15);}errors++;}}}}var _valid3 = _errs39 === errors;if(_valid3){valid7 = true;passing3 = 0;}const _errs41 = errors;const _errs42 = errors;if(errors === _errs42){if(typeof data12 === "string"){if(func2(data12) > 64){const err16 = {instancePath:instancePath+"/hot/" + i1,schemaPath:"../common.schema.json#/$defs/id/maxLength",keyword:"maxLength",params:{limit: 64},message:"must NOT have more than 64 characters"};if(vErrors === null){vErrors = [err16];}else {vErrors.push(err16);}errors++;}else {if(!pattern18.test(data12)){const err17 = {instancePath:instancePath+"/hot/" + i1,schemaPath:"../common.schema.json#/$defs/id/pattern",keyword:"pattern",params:{pattern: "^[a-z0-9]+(?:-[a-z0-9]+)*$"},message:"must match pattern \""+"^[a-z0-9]+(?:-[a-z0-9]+)*$"+"\""};if(vErrors === null){vErrors = [err17];}else {vErrors.push(err17);}errors++;}}}else {const err18 = {instancePath:instancePath+"/hot/" + i1,schemaPath:"../common.schema.json#/$defs/id/type",keyword:"type",params:{type: "string"},message:"must be string"};if(vErrors === null){vErrors = [err18];}else {vErrors.push(err18);}errors++;}}var _valid3 = _errs41 === errors;if(_valid3 && valid7){valid7 = false;passing3 = [passing3, 1];}else {if(_valid3){valid7 = true;passing3 = 1;}}if(!valid7){const err19 = {instancePath:instancePath+"/hot/" + i1,schemaPath:"#/properties/hot/oneOf/1/items/oneOf",keyword:"oneOf",params:{passingSchemas: passing3},message:"must match exactly one schema in oneOf"};if(vErrors === null){vErrors = [err19];}else {vErrors.push(err19);}errors++;}else {errors = _errs38;if(vErrors !== null){if(_errs38){vErrors.length = _errs38;}else {vErrors = null;}}}var valid6 = _errs37 === errors;if(!valid6){break;}}}}else {const err20 = {instancePath:instancePath+"/hot",schemaPath:"#/properties/hot/oneOf/1/type",keyword:"type",params:{type: "array"},message:"must be array"};if(vErrors === null){vErrors = [err20];}else {vErrors.push(err20);}errors++;}}var _valid2 = _errs35 === errors;if(_valid2 && valid5){valid5 = false;passing2 = [passing2, 1];}else {if(_valid2){valid5 = true;passing2 = 1;}}if(!valid5){const err21 = {instancePath:instancePath+"/hot",schemaPath:"#/properties/hot/oneOf",keyword:"oneOf",params:{passingSchemas: passing2},message:"must match exactly one schema in oneOf"};if(vErrors === null){vErrors = [err21];}else {vErrors.push(err21);}errors++;validate104.errors = vErrors;return false;}else {errors = _errs32;if(vErrors !== null){if(_errs32){vErrors.length = _errs32;}else {vErrors = null;}}}var valid0 = _errs31 === errors;}else {var valid0 = true;}if(valid0){if(data.dim !== undefined){const _errs44 = errors;if(typeof data.dim !== "boolean"){validate104.errors = [{instancePath:instancePath+"/dim",schemaPath:"#/properties/dim/type",keyword:"type",params:{type: "boolean"},message:"must be boolean"}];return false;}var valid0 = _errs44 === errors;}else {var valid0 = true;}if(valid0){if(data.visible !== undefined){let data14 = data.visible;const _errs46 = errors;if(errors === _errs46){if(Array.isArray(data14)){if(data14.length > 20){validate104.errors = [{instancePath:instancePath+"/visible",schemaPath:"#/properties/visible/maxItems",keyword:"maxItems",params:{limit: 20},message:"must NOT have more than 20 items"}];return false;}else {var valid9 = true;const len2 = data14.length;for(let i2=0; i2 15 || isNaN(data15)){const err23 = {instancePath:instancePath+"/visible/" + i2,schemaPath:"#/properties/visible/items/oneOf/0/maximum",keyword:"maximum",params:{comparison: "<=", limit: 15},message:"must be <= 15"};if(vErrors === null){vErrors = [err23];}else {vErrors.push(err23);}errors++;}else {if(data15 < 0 || isNaN(data15)){const err24 = {instancePath:instancePath+"/visible/" + i2,schemaPath:"#/properties/visible/items/oneOf/0/minimum",keyword:"minimum",params:{comparison: ">=", limit: 0},message:"must be >= 0"};if(vErrors === null){vErrors = [err24];}else {vErrors.push(err24);}errors++;}}}}var _valid4 = _errs50 === errors;if(_valid4){valid10 = true;passing4 = 0;}const _errs52 = errors;const _errs53 = errors;if(errors === _errs53){if(typeof data15 === "string"){if(func2(data15) > 64){const err25 = {instancePath:instancePath+"/visible/" + i2,schemaPath:"../common.schema.json#/$defs/id/maxLength",keyword:"maxLength",params:{limit: 64},message:"must NOT have more than 64 characters"};if(vErrors === null){vErrors = [err25];}else {vErrors.push(err25);}errors++;}else {if(!pattern18.test(data15)){const err26 = {instancePath:instancePath+"/visible/" + i2,schemaPath:"../common.schema.json#/$defs/id/pattern",keyword:"pattern",params:{pattern: "^[a-z0-9]+(?:-[a-z0-9]+)*$"},message:"must match pattern \""+"^[a-z0-9]+(?:-[a-z0-9]+)*$"+"\""};if(vErrors === null){vErrors = [err26];}else {vErrors.push(err26);}errors++;}}}else {const err27 = {instancePath:instancePath+"/visible/" + i2,schemaPath:"../common.schema.json#/$defs/id/type",keyword:"type",params:{type: "string"},message:"must be string"};if(vErrors === null){vErrors = [err27];}else {vErrors.push(err27);}errors++;}}var _valid4 = _errs52 === errors;if(_valid4 && valid10){valid10 = false;passing4 = [passing4, 1];}else {if(_valid4){valid10 = true;passing4 = 1;}}if(!valid10){const err28 = {instancePath:instancePath+"/visible/" + i2,schemaPath:"#/properties/visible/items/oneOf",keyword:"oneOf",params:{passingSchemas: passing4},message:"must match exactly one schema in oneOf"};if(vErrors === null){vErrors = [err28];}else {vErrors.push(err28);}errors++;validate104.errors = vErrors;return false;}else {errors = _errs49;if(vErrors !== null){if(_errs49){vErrors.length = _errs49;}else {vErrors = null;}}}var valid9 = _errs48 === errors;if(!valid9){break;}}}}else {validate104.errors = [{instancePath:instancePath+"/visible",schemaPath:"#/properties/visible/type",keyword:"type",params:{type: "array"},message:"must be array"}];return false;}}var valid0 = _errs46 === errors;}else {var valid0 = true;}if(valid0){if(data.overrides !== undefined){let data16 = data.overrides;const _errs55 = errors;if(errors === _errs55){if(data16 && typeof data16 == "object" && !Array.isArray(data16)){if(Object.keys(data16).length > 6){validate104.errors = [{instancePath:instancePath+"/overrides",schemaPath:"#/properties/overrides/maxProperties",keyword:"maxProperties",params:{limit: 6},message:"must NOT have more than 6 properties"}];return false;}else {for(const key1 in data16){const _errs57 = errors;if(!(((((((key1 === "title") || (key1 === "sub")) || (key1 === "label")) || (key1 === "text")) || (key1 === "n")) || (key1 === "dashed")) || (key1 === "muted"))){const err29 = {instancePath:instancePath+"/overrides",schemaPath:"#/properties/overrides/propertyNames/enum",keyword:"enum",params:{allowedValues: schema167.properties.overrides.propertyNames.enum},message:"must be equal to one of the allowed values",propertyName:key1};if(vErrors === null){vErrors = [err29];}else {vErrors.push(err29);}errors++;}var valid12 = _errs57 === errors;if(!valid12){const err30 = {instancePath:instancePath+"/overrides",schemaPath:"#/properties/overrides/propertyNames",keyword:"propertyNames",params:{propertyName: key1},message:"property name must be valid"};if(vErrors === null){vErrors = [err30];}else {vErrors.push(err30);}errors++;validate104.errors = vErrors;return false;break;}}if(valid12){for(const key2 in data16){let data17 = data16[key2];const _errs59 = errors;if(errors === _errs59){if(data17 && typeof data17 == "object" && !Array.isArray(data17)){if(Object.keys(data17).length > 20){validate104.errors = [{instancePath:instancePath+"/overrides/" + key2.replace(/~/g, "~0").replace(/\//g, "~1"),schemaPath:"#/properties/overrides/additionalProperties/maxProperties",keyword:"maxProperties",params:{limit: 20},message:"must NOT have more than 20 properties"}];return false;}else {for(const key3 in data17){let data18 = data17[key3];const _errs62 = errors;const _errs63 = errors;let valid15 = false;let passing5 = null;const _errs64 = errors;if(errors === _errs64){if(typeof data18 === "string"){if(func2(data18) > 300){const err31 = {instancePath:instancePath+"/overrides/" + key2.replace(/~/g, "~0").replace(/\//g, "~1")+"/" + key3.replace(/~/g, "~0").replace(/\//g, "~1"),schemaPath:"#/properties/overrides/additionalProperties/additionalProperties/oneOf/0/maxLength",keyword:"maxLength",params:{limit: 300},message:"must NOT have more than 300 characters"};if(vErrors === null){vErrors = [err31];}else {vErrors.push(err31);}errors++;}else {if(func2(data18) < 0){const err32 = {instancePath:instancePath+"/overrides/" + key2.replace(/~/g, "~0").replace(/\//g, "~1")+"/" + key3.replace(/~/g, "~0").replace(/\//g, "~1"),schemaPath:"#/properties/overrides/additionalProperties/additionalProperties/oneOf/0/minLength",keyword:"minLength",params:{limit: 0},message:"must NOT have fewer than 0 characters"};if(vErrors === null){vErrors = [err32];}else {vErrors.push(err32);}errors++;}}}else {const err33 = {instancePath:instancePath+"/overrides/" + key2.replace(/~/g, "~0").replace(/\//g, "~1")+"/" + key3.replace(/~/g, "~0").replace(/\//g, "~1"),schemaPath:"#/properties/overrides/additionalProperties/additionalProperties/oneOf/0/type",keyword:"type",params:{type: "string"},message:"must be string"};if(vErrors === null){vErrors = [err33];}else {vErrors.push(err33);}errors++;}}var _valid5 = _errs64 === errors;if(_valid5){valid15 = true;passing5 = 0;}const _errs66 = errors;if(!((typeof data18 == "number") && (isFinite(data18)))){const err34 = {instancePath:instancePath+"/overrides/" + key2.replace(/~/g, "~0").replace(/\//g, "~1")+"/" + key3.replace(/~/g, "~0").replace(/\//g, "~1"),schemaPath:"#/properties/overrides/additionalProperties/additionalProperties/oneOf/1/type",keyword:"type",params:{type: "number"},message:"must be number"};if(vErrors === null){vErrors = [err34];}else {vErrors.push(err34);}errors++;}var _valid5 = _errs66 === errors;if(_valid5 && valid15){valid15 = false;passing5 = [passing5, 1];}else {if(_valid5){valid15 = true;passing5 = 1;}const _errs68 = errors;if(typeof data18 !== "boolean"){const err35 = {instancePath:instancePath+"/overrides/" + key2.replace(/~/g, "~0").replace(/\//g, "~1")+"/" + key3.replace(/~/g, "~0").replace(/\//g, "~1"),schemaPath:"#/properties/overrides/additionalProperties/additionalProperties/oneOf/2/type",keyword:"type",params:{type: "boolean"},message:"must be boolean"};if(vErrors === null){vErrors = [err35];}else {vErrors.push(err35);}errors++;}var _valid5 = _errs68 === errors;if(_valid5 && valid15){valid15 = false;passing5 = [passing5, 2];}else {if(_valid5){valid15 = true;passing5 = 2;}const _errs70 = errors;if(errors === _errs70){if(Array.isArray(data18)){if(data18.length > 3){const err36 = {instancePath:instancePath+"/overrides/" + key2.replace(/~/g, "~0").replace(/\//g, "~1")+"/" + key3.replace(/~/g, "~0").replace(/\//g, "~1"),schemaPath:"#/properties/overrides/additionalProperties/additionalProperties/oneOf/3/maxItems",keyword:"maxItems",params:{limit: 3},message:"must NOT have more than 3 items"};if(vErrors === null){vErrors = [err36];}else {vErrors.push(err36);}errors++;}else {var valid16 = true;const len3 = data18.length;for(let i3=0; i3 300){const err37 = {instancePath:instancePath+"/overrides/" + key2.replace(/~/g, "~0").replace(/\//g, "~1")+"/" + key3.replace(/~/g, "~0").replace(/\//g, "~1")+"/" + i3,schemaPath:"#/properties/overrides/additionalProperties/additionalProperties/oneOf/3/items/maxLength",keyword:"maxLength",params:{limit: 300},message:"must NOT have more than 300 characters"};if(vErrors === null){vErrors = [err37];}else {vErrors.push(err37);}errors++;}else {if(func2(data19) < 0){const err38 = {instancePath:instancePath+"/overrides/" + key2.replace(/~/g, "~0").replace(/\//g, "~1")+"/" + key3.replace(/~/g, "~0").replace(/\//g, "~1")+"/" + i3,schemaPath:"#/properties/overrides/additionalProperties/additionalProperties/oneOf/3/items/minLength",keyword:"minLength",params:{limit: 0},message:"must NOT have fewer than 0 characters"};if(vErrors === null){vErrors = [err38];}else {vErrors.push(err38);}errors++;}}}else {const err39 = {instancePath:instancePath+"/overrides/" + key2.replace(/~/g, "~0").replace(/\//g, "~1")+"/" + key3.replace(/~/g, "~0").replace(/\//g, "~1")+"/" + i3,schemaPath:"#/properties/overrides/additionalProperties/additionalProperties/oneOf/3/items/type",keyword:"type",params:{type: "string"},message:"must be string"};if(vErrors === null){vErrors = [err39];}else {vErrors.push(err39);}errors++;}}var valid16 = _errs72 === errors;if(!valid16){break;}}}}else {const err40 = {instancePath:instancePath+"/overrides/" + key2.replace(/~/g, "~0").replace(/\//g, "~1")+"/" + key3.replace(/~/g, "~0").replace(/\//g, "~1"),schemaPath:"#/properties/overrides/additionalProperties/additionalProperties/oneOf/3/type",keyword:"type",params:{type: "array"},message:"must be array"};if(vErrors === null){vErrors = [err40];}else {vErrors.push(err40);}errors++;}}var _valid5 = _errs70 === errors;if(_valid5 && valid15){valid15 = false;passing5 = [passing5, 3];}else {if(_valid5){valid15 = true;passing5 = 3;}}}}if(!valid15){const err41 = {instancePath:instancePath+"/overrides/" + key2.replace(/~/g, "~0").replace(/\//g, "~1")+"/" + key3.replace(/~/g, "~0").replace(/\//g, "~1"),schemaPath:"#/properties/overrides/additionalProperties/additionalProperties/oneOf",keyword:"oneOf",params:{passingSchemas: passing5},message:"must match exactly one schema in oneOf"};if(vErrors === null){vErrors = [err41];}else {vErrors.push(err41);}errors++;validate104.errors = vErrors;return false;}else {errors = _errs63;if(vErrors !== null){if(_errs63){vErrors.length = _errs63;}else {vErrors = null;}}}var valid14 = _errs62 === errors;if(!valid14){break;}}}}else {validate104.errors = [{instancePath:instancePath+"/overrides/" + key2.replace(/~/g, "~0").replace(/\//g, "~1"),schemaPath:"#/properties/overrides/additionalProperties/type",keyword:"type",params:{type: "object"},message:"must be object"}];return false;}}var valid13 = _errs59 === errors;if(!valid13){break;}}}}}else {validate104.errors = [{instancePath:instancePath+"/overrides",schemaPath:"#/properties/overrides/type",keyword:"type",params:{type: "object"},message:"must be object"}];return false;}}var valid0 = _errs55 === errors;}else {var valid0 = true;}}}}}}}}}}}}}}}}}else {validate104.errors = [{instancePath,schemaPath:"#/type",keyword:"type",params:{type: "object"},message:"must be object"}];return false;}}validate104.errors = vErrors;return errors === 0;}validate104.evaluated = {"props":true,"dynamicProps":false,"dynamicItems":false};const schema171 = {"type":"object","additionalProperties":false,"required":["kind","x","y"],"properties":{"kind":{"const":"badge"},"x":{"type":"number","minimum":-400,"maximum":2400},"y":{"type":"number","minimum":-400,"maximum":2400},"n":{"type":"integer","minimum":0,"maximum":99},"active":{"oneOf":[{"type":"boolean"},{"type":"array","maxItems":20,"items":{"oneOf":[{"type":"integer","minimum":0,"maximum":15},{"$ref":"../common.schema.json#/$defs/id"}]}}]},"hot":{"oneOf":[{"type":"boolean"},{"type":"array","maxItems":20,"items":{"oneOf":[{"type":"integer","minimum":0,"maximum":15},{"$ref":"../common.schema.json#/$defs/id"}]}}]},"dim":{"type":"boolean"},"visible":{"type":"array","maxItems":20,"items":{"oneOf":[{"type":"integer","minimum":0,"maximum":15},{"$ref":"../common.schema.json#/$defs/id"}]}},"overrides":{"type":"object","maxProperties":6,"propertyNames":{"enum":["title","sub","label","text","n","dashed","muted"]},"additionalProperties":{"type":"object","maxProperties":20,"additionalProperties":{"oneOf":[{"type":"string","minLength":0,"maxLength":300},{"type":"number"},{"type":"boolean"},{"type":"array","maxItems":3,"items":{"type":"string","minLength":0,"maxLength":300}}]}}}}};function validate106(data, {instancePath="", parentData, parentDataProperty, rootData=data, dynamicAnchors={}}={}){let vErrors = null;let errors = 0;const evaluated0 = validate106.evaluated;if(evaluated0.dynamicProps){evaluated0.props = undefined;}if(evaluated0.dynamicItems){evaluated0.items = undefined;}if(errors === 0){if(data && typeof data == "object" && !Array.isArray(data)){let missing0;if((((data.kind === undefined) && (missing0 = "kind")) || ((data.x === undefined) && (missing0 = "x"))) || ((data.y === undefined) && (missing0 = "y"))){validate106.errors = [{instancePath,schemaPath:"#/required",keyword:"required",params:{missingProperty: missing0},message:"must have required property '"+missing0+"'"}];return false;}else {const _errs1 = errors;for(const key0 in data){if(!(func1.call(schema171.properties, key0))){validate106.errors = [{instancePath,schemaPath:"#/additionalProperties",keyword:"additionalProperties",params:{additionalProperty: key0},message:"must NOT have additional properties"}];return false;break;}}if(_errs1 === errors){if(data.kind !== undefined){const _errs2 = errors;if("badge" !== data.kind){validate106.errors = [{instancePath:instancePath+"/kind",schemaPath:"#/properties/kind/const",keyword:"const",params:{allowedValue: "badge"},message:"must be equal to constant"}];return false;}var valid0 = _errs2 === errors;}else {var valid0 = true;}if(valid0){if(data.x !== undefined){let data1 = data.x;const _errs3 = errors;if(errors === _errs3){if((typeof data1 == "number") && (isFinite(data1))){if(data1 > 2400 || isNaN(data1)){validate106.errors = [{instancePath:instancePath+"/x",schemaPath:"#/properties/x/maximum",keyword:"maximum",params:{comparison: "<=", limit: 2400},message:"must be <= 2400"}];return false;}else {if(data1 < -400 || isNaN(data1)){validate106.errors = [{instancePath:instancePath+"/x",schemaPath:"#/properties/x/minimum",keyword:"minimum",params:{comparison: ">=", limit: -400},message:"must be >= -400"}];return false;}}}else {validate106.errors = [{instancePath:instancePath+"/x",schemaPath:"#/properties/x/type",keyword:"type",params:{type: "number"},message:"must be number"}];return false;}}var valid0 = _errs3 === errors;}else {var valid0 = true;}if(valid0){if(data.y !== undefined){let data2 = data.y;const _errs5 = errors;if(errors === _errs5){if((typeof data2 == "number") && (isFinite(data2))){if(data2 > 2400 || isNaN(data2)){validate106.errors = [{instancePath:instancePath+"/y",schemaPath:"#/properties/y/maximum",keyword:"maximum",params:{comparison: "<=", limit: 2400},message:"must be <= 2400"}];return false;}else {if(data2 < -400 || isNaN(data2)){validate106.errors = [{instancePath:instancePath+"/y",schemaPath:"#/properties/y/minimum",keyword:"minimum",params:{comparison: ">=", limit: -400},message:"must be >= -400"}];return false;}}}else {validate106.errors = [{instancePath:instancePath+"/y",schemaPath:"#/properties/y/type",keyword:"type",params:{type: "number"},message:"must be number"}];return false;}}var valid0 = _errs5 === errors;}else {var valid0 = true;}if(valid0){if(data.n !== undefined){let data3 = data.n;const _errs7 = errors;if(!(((typeof data3 == "number") && (!(data3 % 1) && !isNaN(data3))) && (isFinite(data3)))){validate106.errors = [{instancePath:instancePath+"/n",schemaPath:"#/properties/n/type",keyword:"type",params:{type: "integer"},message:"must be integer"}];return false;}if(errors === _errs7){if((typeof data3 == "number") && (isFinite(data3))){if(data3 > 99 || isNaN(data3)){validate106.errors = [{instancePath:instancePath+"/n",schemaPath:"#/properties/n/maximum",keyword:"maximum",params:{comparison: "<=", limit: 99},message:"must be <= 99"}];return false;}else {if(data3 < 0 || isNaN(data3)){validate106.errors = [{instancePath:instancePath+"/n",schemaPath:"#/properties/n/minimum",keyword:"minimum",params:{comparison: ">=", limit: 0},message:"must be >= 0"}];return false;}}}}var valid0 = _errs7 === errors;}else {var valid0 = true;}if(valid0){if(data.active !== undefined){let data4 = data.active;const _errs9 = errors;const _errs10 = errors;let valid1 = false;let passing0 = null;const _errs11 = errors;if(typeof data4 !== "boolean"){const err0 = {instancePath:instancePath+"/active",schemaPath:"#/properties/active/oneOf/0/type",keyword:"type",params:{type: "boolean"},message:"must be boolean"};if(vErrors === null){vErrors = [err0];}else {vErrors.push(err0);}errors++;}var _valid0 = _errs11 === errors;if(_valid0){valid1 = true;passing0 = 0;}const _errs13 = errors;if(errors === _errs13){if(Array.isArray(data4)){if(data4.length > 20){const err1 = {instancePath:instancePath+"/active",schemaPath:"#/properties/active/oneOf/1/maxItems",keyword:"maxItems",params:{limit: 20},message:"must NOT have more than 20 items"};if(vErrors === null){vErrors = [err1];}else {vErrors.push(err1);}errors++;}else {var valid2 = true;const len0 = data4.length;for(let i0=0; i0 15 || isNaN(data5)){const err3 = {instancePath:instancePath+"/active/" + i0,schemaPath:"#/properties/active/oneOf/1/items/oneOf/0/maximum",keyword:"maximum",params:{comparison: "<=", limit: 15},message:"must be <= 15"};if(vErrors === null){vErrors = [err3];}else {vErrors.push(err3);}errors++;}else {if(data5 < 0 || isNaN(data5)){const err4 = {instancePath:instancePath+"/active/" + i0,schemaPath:"#/properties/active/oneOf/1/items/oneOf/0/minimum",keyword:"minimum",params:{comparison: ">=", limit: 0},message:"must be >= 0"};if(vErrors === null){vErrors = [err4];}else {vErrors.push(err4);}errors++;}}}}var _valid1 = _errs17 === errors;if(_valid1){valid3 = true;passing1 = 0;}const _errs19 = errors;const _errs20 = errors;if(errors === _errs20){if(typeof data5 === "string"){if(func2(data5) > 64){const err5 = {instancePath:instancePath+"/active/" + i0,schemaPath:"../common.schema.json#/$defs/id/maxLength",keyword:"maxLength",params:{limit: 64},message:"must NOT have more than 64 characters"};if(vErrors === null){vErrors = [err5];}else {vErrors.push(err5);}errors++;}else {if(!pattern18.test(data5)){const err6 = {instancePath:instancePath+"/active/" + i0,schemaPath:"../common.schema.json#/$defs/id/pattern",keyword:"pattern",params:{pattern: "^[a-z0-9]+(?:-[a-z0-9]+)*$"},message:"must match pattern \""+"^[a-z0-9]+(?:-[a-z0-9]+)*$"+"\""};if(vErrors === null){vErrors = [err6];}else {vErrors.push(err6);}errors++;}}}else {const err7 = {instancePath:instancePath+"/active/" + i0,schemaPath:"../common.schema.json#/$defs/id/type",keyword:"type",params:{type: "string"},message:"must be string"};if(vErrors === null){vErrors = [err7];}else {vErrors.push(err7);}errors++;}}var _valid1 = _errs19 === errors;if(_valid1 && valid3){valid3 = false;passing1 = [passing1, 1];}else {if(_valid1){valid3 = true;passing1 = 1;}}if(!valid3){const err8 = {instancePath:instancePath+"/active/" + i0,schemaPath:"#/properties/active/oneOf/1/items/oneOf",keyword:"oneOf",params:{passingSchemas: passing1},message:"must match exactly one schema in oneOf"};if(vErrors === null){vErrors = [err8];}else {vErrors.push(err8);}errors++;}else {errors = _errs16;if(vErrors !== null){if(_errs16){vErrors.length = _errs16;}else {vErrors = null;}}}var valid2 = _errs15 === errors;if(!valid2){break;}}}}else {const err9 = {instancePath:instancePath+"/active",schemaPath:"#/properties/active/oneOf/1/type",keyword:"type",params:{type: "array"},message:"must be array"};if(vErrors === null){vErrors = [err9];}else {vErrors.push(err9);}errors++;}}var _valid0 = _errs13 === errors;if(_valid0 && valid1){valid1 = false;passing0 = [passing0, 1];}else {if(_valid0){valid1 = true;passing0 = 1;}}if(!valid1){const err10 = {instancePath:instancePath+"/active",schemaPath:"#/properties/active/oneOf",keyword:"oneOf",params:{passingSchemas: passing0},message:"must match exactly one schema in oneOf"};if(vErrors === null){vErrors = [err10];}else {vErrors.push(err10);}errors++;validate106.errors = vErrors;return false;}else {errors = _errs10;if(vErrors !== null){if(_errs10){vErrors.length = _errs10;}else {vErrors = null;}}}var valid0 = _errs9 === errors;}else {var valid0 = true;}if(valid0){if(data.hot !== undefined){let data6 = data.hot;const _errs22 = errors;const _errs23 = errors;let valid5 = false;let passing2 = null;const _errs24 = errors;if(typeof data6 !== "boolean"){const err11 = {instancePath:instancePath+"/hot",schemaPath:"#/properties/hot/oneOf/0/type",keyword:"type",params:{type: "boolean"},message:"must be boolean"};if(vErrors === null){vErrors = [err11];}else {vErrors.push(err11);}errors++;}var _valid2 = _errs24 === errors;if(_valid2){valid5 = true;passing2 = 0;}const _errs26 = errors;if(errors === _errs26){if(Array.isArray(data6)){if(data6.length > 20){const err12 = {instancePath:instancePath+"/hot",schemaPath:"#/properties/hot/oneOf/1/maxItems",keyword:"maxItems",params:{limit: 20},message:"must NOT have more than 20 items"};if(vErrors === null){vErrors = [err12];}else {vErrors.push(err12);}errors++;}else {var valid6 = true;const len1 = data6.length;for(let i1=0; i1 15 || isNaN(data7)){const err14 = {instancePath:instancePath+"/hot/" + i1,schemaPath:"#/properties/hot/oneOf/1/items/oneOf/0/maximum",keyword:"maximum",params:{comparison: "<=", limit: 15},message:"must be <= 15"};if(vErrors === null){vErrors = [err14];}else {vErrors.push(err14);}errors++;}else {if(data7 < 0 || isNaN(data7)){const err15 = {instancePath:instancePath+"/hot/" + i1,schemaPath:"#/properties/hot/oneOf/1/items/oneOf/0/minimum",keyword:"minimum",params:{comparison: ">=", limit: 0},message:"must be >= 0"};if(vErrors === null){vErrors = [err15];}else {vErrors.push(err15);}errors++;}}}}var _valid3 = _errs30 === errors;if(_valid3){valid7 = true;passing3 = 0;}const _errs32 = errors;const _errs33 = errors;if(errors === _errs33){if(typeof data7 === "string"){if(func2(data7) > 64){const err16 = {instancePath:instancePath+"/hot/" + i1,schemaPath:"../common.schema.json#/$defs/id/maxLength",keyword:"maxLength",params:{limit: 64},message:"must NOT have more than 64 characters"};if(vErrors === null){vErrors = [err16];}else {vErrors.push(err16);}errors++;}else {if(!pattern18.test(data7)){const err17 = {instancePath:instancePath+"/hot/" + i1,schemaPath:"../common.schema.json#/$defs/id/pattern",keyword:"pattern",params:{pattern: "^[a-z0-9]+(?:-[a-z0-9]+)*$"},message:"must match pattern \""+"^[a-z0-9]+(?:-[a-z0-9]+)*$"+"\""};if(vErrors === null){vErrors = [err17];}else {vErrors.push(err17);}errors++;}}}else {const err18 = {instancePath:instancePath+"/hot/" + i1,schemaPath:"../common.schema.json#/$defs/id/type",keyword:"type",params:{type: "string"},message:"must be string"};if(vErrors === null){vErrors = [err18];}else {vErrors.push(err18);}errors++;}}var _valid3 = _errs32 === errors;if(_valid3 && valid7){valid7 = false;passing3 = [passing3, 1];}else {if(_valid3){valid7 = true;passing3 = 1;}}if(!valid7){const err19 = {instancePath:instancePath+"/hot/" + i1,schemaPath:"#/properties/hot/oneOf/1/items/oneOf",keyword:"oneOf",params:{passingSchemas: passing3},message:"must match exactly one schema in oneOf"};if(vErrors === null){vErrors = [err19];}else {vErrors.push(err19);}errors++;}else {errors = _errs29;if(vErrors !== null){if(_errs29){vErrors.length = _errs29;}else {vErrors = null;}}}var valid6 = _errs28 === errors;if(!valid6){break;}}}}else {const err20 = {instancePath:instancePath+"/hot",schemaPath:"#/properties/hot/oneOf/1/type",keyword:"type",params:{type: "array"},message:"must be array"};if(vErrors === null){vErrors = [err20];}else {vErrors.push(err20);}errors++;}}var _valid2 = _errs26 === errors;if(_valid2 && valid5){valid5 = false;passing2 = [passing2, 1];}else {if(_valid2){valid5 = true;passing2 = 1;}}if(!valid5){const err21 = {instancePath:instancePath+"/hot",schemaPath:"#/properties/hot/oneOf",keyword:"oneOf",params:{passingSchemas: passing2},message:"must match exactly one schema in oneOf"};if(vErrors === null){vErrors = [err21];}else {vErrors.push(err21);}errors++;validate106.errors = vErrors;return false;}else {errors = _errs23;if(vErrors !== null){if(_errs23){vErrors.length = _errs23;}else {vErrors = null;}}}var valid0 = _errs22 === errors;}else {var valid0 = true;}if(valid0){if(data.dim !== undefined){const _errs35 = errors;if(typeof data.dim !== "boolean"){validate106.errors = [{instancePath:instancePath+"/dim",schemaPath:"#/properties/dim/type",keyword:"type",params:{type: "boolean"},message:"must be boolean"}];return false;}var valid0 = _errs35 === errors;}else {var valid0 = true;}if(valid0){if(data.visible !== undefined){let data9 = data.visible;const _errs37 = errors;if(errors === _errs37){if(Array.isArray(data9)){if(data9.length > 20){validate106.errors = [{instancePath:instancePath+"/visible",schemaPath:"#/properties/visible/maxItems",keyword:"maxItems",params:{limit: 20},message:"must NOT have more than 20 items"}];return false;}else {var valid9 = true;const len2 = data9.length;for(let i2=0; i2 15 || isNaN(data10)){const err23 = {instancePath:instancePath+"/visible/" + i2,schemaPath:"#/properties/visible/items/oneOf/0/maximum",keyword:"maximum",params:{comparison: "<=", limit: 15},message:"must be <= 15"};if(vErrors === null){vErrors = [err23];}else {vErrors.push(err23);}errors++;}else {if(data10 < 0 || isNaN(data10)){const err24 = {instancePath:instancePath+"/visible/" + i2,schemaPath:"#/properties/visible/items/oneOf/0/minimum",keyword:"minimum",params:{comparison: ">=", limit: 0},message:"must be >= 0"};if(vErrors === null){vErrors = [err24];}else {vErrors.push(err24);}errors++;}}}}var _valid4 = _errs41 === errors;if(_valid4){valid10 = true;passing4 = 0;}const _errs43 = errors;const _errs44 = errors;if(errors === _errs44){if(typeof data10 === "string"){if(func2(data10) > 64){const err25 = {instancePath:instancePath+"/visible/" + i2,schemaPath:"../common.schema.json#/$defs/id/maxLength",keyword:"maxLength",params:{limit: 64},message:"must NOT have more than 64 characters"};if(vErrors === null){vErrors = [err25];}else {vErrors.push(err25);}errors++;}else {if(!pattern18.test(data10)){const err26 = {instancePath:instancePath+"/visible/" + i2,schemaPath:"../common.schema.json#/$defs/id/pattern",keyword:"pattern",params:{pattern: "^[a-z0-9]+(?:-[a-z0-9]+)*$"},message:"must match pattern \""+"^[a-z0-9]+(?:-[a-z0-9]+)*$"+"\""};if(vErrors === null){vErrors = [err26];}else {vErrors.push(err26);}errors++;}}}else {const err27 = {instancePath:instancePath+"/visible/" + i2,schemaPath:"../common.schema.json#/$defs/id/type",keyword:"type",params:{type: "string"},message:"must be string"};if(vErrors === null){vErrors = [err27];}else {vErrors.push(err27);}errors++;}}var _valid4 = _errs43 === errors;if(_valid4 && valid10){valid10 = false;passing4 = [passing4, 1];}else {if(_valid4){valid10 = true;passing4 = 1;}}if(!valid10){const err28 = {instancePath:instancePath+"/visible/" + i2,schemaPath:"#/properties/visible/items/oneOf",keyword:"oneOf",params:{passingSchemas: passing4},message:"must match exactly one schema in oneOf"};if(vErrors === null){vErrors = [err28];}else {vErrors.push(err28);}errors++;validate106.errors = vErrors;return false;}else {errors = _errs40;if(vErrors !== null){if(_errs40){vErrors.length = _errs40;}else {vErrors = null;}}}var valid9 = _errs39 === errors;if(!valid9){break;}}}}else {validate106.errors = [{instancePath:instancePath+"/visible",schemaPath:"#/properties/visible/type",keyword:"type",params:{type: "array"},message:"must be array"}];return false;}}var valid0 = _errs37 === errors;}else {var valid0 = true;}if(valid0){if(data.overrides !== undefined){let data11 = data.overrides;const _errs46 = errors;if(errors === _errs46){if(data11 && typeof data11 == "object" && !Array.isArray(data11)){if(Object.keys(data11).length > 6){validate106.errors = [{instancePath:instancePath+"/overrides",schemaPath:"#/properties/overrides/maxProperties",keyword:"maxProperties",params:{limit: 6},message:"must NOT have more than 6 properties"}];return false;}else {for(const key1 in data11){const _errs48 = errors;if(!(((((((key1 === "title") || (key1 === "sub")) || (key1 === "label")) || (key1 === "text")) || (key1 === "n")) || (key1 === "dashed")) || (key1 === "muted"))){const err29 = {instancePath:instancePath+"/overrides",schemaPath:"#/properties/overrides/propertyNames/enum",keyword:"enum",params:{allowedValues: schema171.properties.overrides.propertyNames.enum},message:"must be equal to one of the allowed values",propertyName:key1};if(vErrors === null){vErrors = [err29];}else {vErrors.push(err29);}errors++;}var valid12 = _errs48 === errors;if(!valid12){const err30 = {instancePath:instancePath+"/overrides",schemaPath:"#/properties/overrides/propertyNames",keyword:"propertyNames",params:{propertyName: key1},message:"property name must be valid"};if(vErrors === null){vErrors = [err30];}else {vErrors.push(err30);}errors++;validate106.errors = vErrors;return false;break;}}if(valid12){for(const key2 in data11){let data12 = data11[key2];const _errs50 = errors;if(errors === _errs50){if(data12 && typeof data12 == "object" && !Array.isArray(data12)){if(Object.keys(data12).length > 20){validate106.errors = [{instancePath:instancePath+"/overrides/" + key2.replace(/~/g, "~0").replace(/\//g, "~1"),schemaPath:"#/properties/overrides/additionalProperties/maxProperties",keyword:"maxProperties",params:{limit: 20},message:"must NOT have more than 20 properties"}];return false;}else {for(const key3 in data12){let data13 = data12[key3];const _errs53 = errors;const _errs54 = errors;let valid15 = false;let passing5 = null;const _errs55 = errors;if(errors === _errs55){if(typeof data13 === "string"){if(func2(data13) > 300){const err31 = {instancePath:instancePath+"/overrides/" + key2.replace(/~/g, "~0").replace(/\//g, "~1")+"/" + key3.replace(/~/g, "~0").replace(/\//g, "~1"),schemaPath:"#/properties/overrides/additionalProperties/additionalProperties/oneOf/0/maxLength",keyword:"maxLength",params:{limit: 300},message:"must NOT have more than 300 characters"};if(vErrors === null){vErrors = [err31];}else {vErrors.push(err31);}errors++;}else {if(func2(data13) < 0){const err32 = {instancePath:instancePath+"/overrides/" + key2.replace(/~/g, "~0").replace(/\//g, "~1")+"/" + key3.replace(/~/g, "~0").replace(/\//g, "~1"),schemaPath:"#/properties/overrides/additionalProperties/additionalProperties/oneOf/0/minLength",keyword:"minLength",params:{limit: 0},message:"must NOT have fewer than 0 characters"};if(vErrors === null){vErrors = [err32];}else {vErrors.push(err32);}errors++;}}}else {const err33 = {instancePath:instancePath+"/overrides/" + key2.replace(/~/g, "~0").replace(/\//g, "~1")+"/" + key3.replace(/~/g, "~0").replace(/\//g, "~1"),schemaPath:"#/properties/overrides/additionalProperties/additionalProperties/oneOf/0/type",keyword:"type",params:{type: "string"},message:"must be string"};if(vErrors === null){vErrors = [err33];}else {vErrors.push(err33);}errors++;}}var _valid5 = _errs55 === errors;if(_valid5){valid15 = true;passing5 = 0;}const _errs57 = errors;if(!((typeof data13 == "number") && (isFinite(data13)))){const err34 = {instancePath:instancePath+"/overrides/" + key2.replace(/~/g, "~0").replace(/\//g, "~1")+"/" + key3.replace(/~/g, "~0").replace(/\//g, "~1"),schemaPath:"#/properties/overrides/additionalProperties/additionalProperties/oneOf/1/type",keyword:"type",params:{type: "number"},message:"must be number"};if(vErrors === null){vErrors = [err34];}else {vErrors.push(err34);}errors++;}var _valid5 = _errs57 === errors;if(_valid5 && valid15){valid15 = false;passing5 = [passing5, 1];}else {if(_valid5){valid15 = true;passing5 = 1;}const _errs59 = errors;if(typeof data13 !== "boolean"){const err35 = {instancePath:instancePath+"/overrides/" + key2.replace(/~/g, "~0").replace(/\//g, "~1")+"/" + key3.replace(/~/g, "~0").replace(/\//g, "~1"),schemaPath:"#/properties/overrides/additionalProperties/additionalProperties/oneOf/2/type",keyword:"type",params:{type: "boolean"},message:"must be boolean"};if(vErrors === null){vErrors = [err35];}else {vErrors.push(err35);}errors++;}var _valid5 = _errs59 === errors;if(_valid5 && valid15){valid15 = false;passing5 = [passing5, 2];}else {if(_valid5){valid15 = true;passing5 = 2;}const _errs61 = errors;if(errors === _errs61){if(Array.isArray(data13)){if(data13.length > 3){const err36 = {instancePath:instancePath+"/overrides/" + key2.replace(/~/g, "~0").replace(/\//g, "~1")+"/" + key3.replace(/~/g, "~0").replace(/\//g, "~1"),schemaPath:"#/properties/overrides/additionalProperties/additionalProperties/oneOf/3/maxItems",keyword:"maxItems",params:{limit: 3},message:"must NOT have more than 3 items"};if(vErrors === null){vErrors = [err36];}else {vErrors.push(err36);}errors++;}else {var valid16 = true;const len3 = data13.length;for(let i3=0; i3 300){const err37 = {instancePath:instancePath+"/overrides/" + key2.replace(/~/g, "~0").replace(/\//g, "~1")+"/" + key3.replace(/~/g, "~0").replace(/\//g, "~1")+"/" + i3,schemaPath:"#/properties/overrides/additionalProperties/additionalProperties/oneOf/3/items/maxLength",keyword:"maxLength",params:{limit: 300},message:"must NOT have more than 300 characters"};if(vErrors === null){vErrors = [err37];}else {vErrors.push(err37);}errors++;}else {if(func2(data14) < 0){const err38 = {instancePath:instancePath+"/overrides/" + key2.replace(/~/g, "~0").replace(/\//g, "~1")+"/" + key3.replace(/~/g, "~0").replace(/\//g, "~1")+"/" + i3,schemaPath:"#/properties/overrides/additionalProperties/additionalProperties/oneOf/3/items/minLength",keyword:"minLength",params:{limit: 0},message:"must NOT have fewer than 0 characters"};if(vErrors === null){vErrors = [err38];}else {vErrors.push(err38);}errors++;}}}else {const err39 = {instancePath:instancePath+"/overrides/" + key2.replace(/~/g, "~0").replace(/\//g, "~1")+"/" + key3.replace(/~/g, "~0").replace(/\//g, "~1")+"/" + i3,schemaPath:"#/properties/overrides/additionalProperties/additionalProperties/oneOf/3/items/type",keyword:"type",params:{type: "string"},message:"must be string"};if(vErrors === null){vErrors = [err39];}else {vErrors.push(err39);}errors++;}}var valid16 = _errs63 === errors;if(!valid16){break;}}}}else {const err40 = {instancePath:instancePath+"/overrides/" + key2.replace(/~/g, "~0").replace(/\//g, "~1")+"/" + key3.replace(/~/g, "~0").replace(/\//g, "~1"),schemaPath:"#/properties/overrides/additionalProperties/additionalProperties/oneOf/3/type",keyword:"type",params:{type: "array"},message:"must be array"};if(vErrors === null){vErrors = [err40];}else {vErrors.push(err40);}errors++;}}var _valid5 = _errs61 === errors;if(_valid5 && valid15){valid15 = false;passing5 = [passing5, 3];}else {if(_valid5){valid15 = true;passing5 = 3;}}}}if(!valid15){const err41 = {instancePath:instancePath+"/overrides/" + key2.replace(/~/g, "~0").replace(/\//g, "~1")+"/" + key3.replace(/~/g, "~0").replace(/\//g, "~1"),schemaPath:"#/properties/overrides/additionalProperties/additionalProperties/oneOf",keyword:"oneOf",params:{passingSchemas: passing5},message:"must match exactly one schema in oneOf"};if(vErrors === null){vErrors = [err41];}else {vErrors.push(err41);}errors++;validate106.errors = vErrors;return false;}else {errors = _errs54;if(vErrors !== null){if(_errs54){vErrors.length = _errs54;}else {vErrors = null;}}}var valid14 = _errs53 === errors;if(!valid14){break;}}}}else {validate106.errors = [{instancePath:instancePath+"/overrides/" + key2.replace(/~/g, "~0").replace(/\//g, "~1"),schemaPath:"#/properties/overrides/additionalProperties/type",keyword:"type",params:{type: "object"},message:"must be object"}];return false;}}var valid13 = _errs50 === errors;if(!valid13){break;}}}}}else {validate106.errors = [{instancePath:instancePath+"/overrides",schemaPath:"#/properties/overrides/type",keyword:"type",params:{type: "object"},message:"must be object"}];return false;}}var valid0 = _errs46 === errors;}else {var valid0 = true;}}}}}}}}}}}}else {validate106.errors = [{instancePath,schemaPath:"#/type",keyword:"type",params:{type: "object"},message:"must be object"}];return false;}}validate106.errors = vErrors;return errors === 0;}validate106.evaluated = {"props":true,"dynamicProps":false,"dynamicItems":false};function validate97(data, {instancePath="", parentData, parentDataProperty, rootData=data, dynamicAnchors={}}={}){/*# sourceURL="https://kernelpath.dev/schema/v1/data/diagram.schema.json" */;let vErrors = null;let errors = 0;const evaluated0 = validate97.evaluated;if(evaluated0.dynamicProps){evaluated0.props = undefined;}if(evaluated0.dynamicItems){evaluated0.items = undefined;}if(errors === 0){if(data && typeof data == "object" && !Array.isArray(data)){let missing0;if((((data.title === undefined) && (missing0 = "title")) || ((data.height === undefined) && (missing0 = "height"))) || ((data.elements === undefined) && (missing0 = "elements"))){validate97.errors = [{instancePath,schemaPath:"#/required",keyword:"required",params:{missingProperty: missing0},message:"must have required property '"+missing0+"'"}];return false;}else {const _errs1 = errors;for(const key0 in data){if(!(func1.call(schema149.properties, key0))){validate97.errors = [{instancePath,schemaPath:"#/additionalProperties",keyword:"additionalProperties",params:{additionalProperty: key0},message:"must NOT have additional properties"}];return false;break;}}if(_errs1 === errors){if(data.title !== undefined){let data0 = data.title;const _errs2 = errors;if(errors === _errs2){if(typeof data0 === "string"){if(func2(data0) > 200){validate97.errors = [{instancePath:instancePath+"/title",schemaPath:"#/properties/title/maxLength",keyword:"maxLength",params:{limit: 200},message:"must NOT have more than 200 characters"}];return false;}else {if(func2(data0) < 1){validate97.errors = [{instancePath:instancePath+"/title",schemaPath:"#/properties/title/minLength",keyword:"minLength",params:{limit: 1},message:"must NOT have fewer than 1 characters"}];return false;}}}else {validate97.errors = [{instancePath:instancePath+"/title",schemaPath:"#/properties/title/type",keyword:"type",params:{type: "string"},message:"must be string"}];return false;}}var valid0 = _errs2 === errors;}else {var valid0 = true;}if(valid0){if(data.caption !== undefined){let data1 = data.caption;const _errs4 = errors;if(errors === _errs4){if(typeof data1 === "string"){if(func2(data1) > 600){validate97.errors = [{instancePath:instancePath+"/caption",schemaPath:"#/properties/caption/maxLength",keyword:"maxLength",params:{limit: 600},message:"must NOT have more than 600 characters"}];return false;}else {if(func2(data1) < 0){validate97.errors = [{instancePath:instancePath+"/caption",schemaPath:"#/properties/caption/minLength",keyword:"minLength",params:{limit: 0},message:"must NOT have fewer than 0 characters"}];return false;}}}else {validate97.errors = [{instancePath:instancePath+"/caption",schemaPath:"#/properties/caption/type",keyword:"type",params:{type: "string"},message:"must be string"}];return false;}}var valid0 = _errs4 === errors;}else {var valid0 = true;}if(valid0){if(data.height !== undefined){let data2 = data.height;const _errs6 = errors;if(!(((typeof data2 == "number") && (!(data2 % 1) && !isNaN(data2))) && (isFinite(data2)))){validate97.errors = [{instancePath:instancePath+"/height",schemaPath:"#/properties/height/type",keyword:"type",params:{type: "integer"},message:"must be integer"}];return false;}if(errors === _errs6){if((typeof data2 == "number") && (isFinite(data2))){if(data2 > 1200 || isNaN(data2)){validate97.errors = [{instancePath:instancePath+"/height",schemaPath:"#/properties/height/maximum",keyword:"maximum",params:{comparison: "<=", limit: 1200},message:"must be <= 1200"}];return false;}else {if(data2 < 40 || isNaN(data2)){validate97.errors = [{instancePath:instancePath+"/height",schemaPath:"#/properties/height/minimum",keyword:"minimum",params:{comparison: ">=", limit: 40},message:"must be >= 40"}];return false;}}}}var valid0 = _errs6 === errors;}else {var valid0 = true;}if(valid0){if(data.width !== undefined){let data3 = data.width;const _errs8 = errors;if(!(((typeof data3 == "number") && (!(data3 % 1) && !isNaN(data3))) && (isFinite(data3)))){validate97.errors = [{instancePath:instancePath+"/width",schemaPath:"#/properties/width/type",keyword:"type",params:{type: "integer"},message:"must be integer"}];return false;}if(errors === _errs8){if((typeof data3 == "number") && (isFinite(data3))){if(data3 > 1600 || isNaN(data3)){validate97.errors = [{instancePath:instancePath+"/width",schemaPath:"#/properties/width/maximum",keyword:"maximum",params:{comparison: "<=", limit: 1600},message:"must be <= 1600"}];return false;}else {if(data3 < 200 || isNaN(data3)){validate97.errors = [{instancePath:instancePath+"/width",schemaPath:"#/properties/width/minimum",keyword:"minimum",params:{comparison: ">=", limit: 200},message:"must be >= 200"}];return false;}}}}var valid0 = _errs8 === errors;}else {var valid0 = true;}if(valid0){if(data.expandable !== undefined){const _errs10 = errors;if(typeof data.expandable !== "boolean"){validate97.errors = [{instancePath:instancePath+"/expandable",schemaPath:"#/properties/expandable/type",keyword:"type",params:{type: "boolean"},message:"must be boolean"}];return false;}var valid0 = _errs10 === errors;}else {var valid0 = true;}if(valid0){if(data.mode !== undefined){let data5 = data.mode;const _errs12 = errors;if(!(((data5 === "static") || (data5 === "select")) || (data5 === "steps"))){validate97.errors = [{instancePath:instancePath+"/mode",schemaPath:"#/properties/mode/enum",keyword:"enum",params:{allowedValues: schema149.properties.mode.enum},message:"must be equal to one of the allowed values"}];return false;}var valid0 = _errs12 === errors;}else {var valid0 = true;}if(valid0){if(data.initial !== undefined){let data6 = data.initial;const _errs13 = errors;const _errs14 = errors;if(errors === _errs14){if(typeof data6 === "string"){if(func2(data6) > 64){validate97.errors = [{instancePath:instancePath+"/initial",schemaPath:"#/$defs/viewKey/maxLength",keyword:"maxLength",params:{limit: 64},message:"must NOT have more than 64 characters"}];return false;}else {if(!pattern18.test(data6)){validate97.errors = [{instancePath:instancePath+"/initial",schemaPath:"#/$defs/viewKey/pattern",keyword:"pattern",params:{pattern: "^[a-z0-9]+(?:-[a-z0-9]+)*$"},message:"must match pattern \""+"^[a-z0-9]+(?:-[a-z0-9]+)*$"+"\""}];return false;}}}else {validate97.errors = [{instancePath:instancePath+"/initial",schemaPath:"#/$defs/viewKey/type",keyword:"type",params:{type: "string"},message:"must be string"}];return false;}}var valid0 = _errs13 === errors;}else {var valid0 = true;}if(valid0){if(data.hint !== undefined){let data7 = data.hint;const _errs16 = errors;if(errors === _errs16){if(typeof data7 === "string"){if(func2(data7) > 300){validate97.errors = [{instancePath:instancePath+"/hint",schemaPath:"#/properties/hint/maxLength",keyword:"maxLength",params:{limit: 300},message:"must NOT have more than 300 characters"}];return false;}else {if(func2(data7) < 0){validate97.errors = [{instancePath:instancePath+"/hint",schemaPath:"#/properties/hint/minLength",keyword:"minLength",params:{limit: 0},message:"must NOT have fewer than 0 characters"}];return false;}}}else {validate97.errors = [{instancePath:instancePath+"/hint",schemaPath:"#/properties/hint/type",keyword:"type",params:{type: "string"},message:"must be string"}];return false;}}var valid0 = _errs16 === errors;}else {var valid0 = true;}if(valid0){if(data.elements !== undefined){let data8 = data.elements;const _errs18 = errors;if(errors === _errs18){if(Array.isArray(data8)){if(data8.length > 120){validate97.errors = [{instancePath:instancePath+"/elements",schemaPath:"#/properties/elements/maxItems",keyword:"maxItems",params:{limit: 120},message:"must NOT have more than 120 items"}];return false;}else {if(data8.length < 1){validate97.errors = [{instancePath:instancePath+"/elements",schemaPath:"#/properties/elements/minItems",keyword:"minItems",params:{limit: 1},message:"must NOT have fewer than 1 items"}];return false;}else {var valid2 = true;const len0 = data8.length;for(let i0=0; i0 30){validate97.errors = [{instancePath:instancePath+"/info",schemaPath:"#/properties/info/maxProperties",keyword:"maxProperties",params:{limit: 30},message:"must NOT have more than 30 properties"}];return false;}else {for(const key1 in data16){const _errs50 = errors;const _errs51 = errors;if(errors === _errs51){if(typeof key1 === "string"){if(func2(key1) > 64){const err15 = {instancePath:instancePath+"/info",schemaPath:"../common.schema.json#/$defs/id/maxLength",keyword:"maxLength",params:{limit: 64},message:"must NOT have more than 64 characters",propertyName:key1};if(vErrors === null){vErrors = [err15];}else {vErrors.push(err15);}errors++;}else {if(!pattern18.test(key1)){const err16 = {instancePath:instancePath+"/info",schemaPath:"../common.schema.json#/$defs/id/pattern",keyword:"pattern",params:{pattern: "^[a-z0-9]+(?:-[a-z0-9]+)*$"},message:"must match pattern \""+"^[a-z0-9]+(?:-[a-z0-9]+)*$"+"\"",propertyName:key1};if(vErrors === null){vErrors = [err16];}else {vErrors.push(err16);}errors++;}}}else {const err17 = {instancePath:instancePath+"/info",schemaPath:"../common.schema.json#/$defs/id/type",keyword:"type",params:{type: "string"},message:"must be string",propertyName:key1};if(vErrors === null){vErrors = [err17];}else {vErrors.push(err17);}errors++;}}var valid15 = _errs50 === errors;if(!valid15){const err18 = {instancePath:instancePath+"/info",schemaPath:"#/properties/info/propertyNames",keyword:"propertyNames",params:{propertyName: key1},message:"property name must be valid"};if(vErrors === null){vErrors = [err18];}else {vErrors.push(err18);}errors++;validate97.errors = vErrors;return false;break;}}if(valid15){for(const key2 in data16){let data17 = data16[key2];const _errs54 = errors;if(errors === _errs54){if(data17 && typeof data17 == "object" && !Array.isArray(data17)){let missing7;if(((data17.title === undefined) && (missing7 = "title")) || ((data17.text === undefined) && (missing7 = "text"))){validate97.errors = [{instancePath:instancePath+"/info/" + key2.replace(/~/g, "~0").replace(/\//g, "~1"),schemaPath:"#/properties/info/additionalProperties/required",keyword:"required",params:{missingProperty: missing7},message:"must have required property '"+missing7+"'"}];return false;}else {const _errs56 = errors;for(const key3 in data17){if(!((((key3 === "title") || (key3 === "text")) || (key3 === "tone")) || (key3 === "code"))){validate97.errors = [{instancePath:instancePath+"/info/" + key2.replace(/~/g, "~0").replace(/\//g, "~1"),schemaPath:"#/properties/info/additionalProperties/additionalProperties",keyword:"additionalProperties",params:{additionalProperty: key3},message:"must NOT have additional properties"}];return false;break;}}if(_errs56 === errors){if(data17.title !== undefined){let data18 = data17.title;const _errs57 = errors;if(errors === _errs57){if(typeof data18 === "string"){if(func2(data18) > 200){validate97.errors = [{instancePath:instancePath+"/info/" + key2.replace(/~/g, "~0").replace(/\//g, "~1")+"/title",schemaPath:"#/properties/info/additionalProperties/properties/title/maxLength",keyword:"maxLength",params:{limit: 200},message:"must NOT have more than 200 characters"}];return false;}else {if(func2(data18) < 1){validate97.errors = [{instancePath:instancePath+"/info/" + key2.replace(/~/g, "~0").replace(/\//g, "~1")+"/title",schemaPath:"#/properties/info/additionalProperties/properties/title/minLength",keyword:"minLength",params:{limit: 1},message:"must NOT have fewer than 1 characters"}];return false;}}}else {validate97.errors = [{instancePath:instancePath+"/info/" + key2.replace(/~/g, "~0").replace(/\//g, "~1")+"/title",schemaPath:"#/properties/info/additionalProperties/properties/title/type",keyword:"type",params:{type: "string"},message:"must be string"}];return false;}}var valid18 = _errs57 === errors;}else {var valid18 = true;}if(valid18){if(data17.text !== undefined){let data19 = data17.text;const _errs59 = errors;if(errors === _errs59){if(typeof data19 === "string"){if(func2(data19) > 1500){validate97.errors = [{instancePath:instancePath+"/info/" + key2.replace(/~/g, "~0").replace(/\//g, "~1")+"/text",schemaPath:"#/properties/info/additionalProperties/properties/text/maxLength",keyword:"maxLength",params:{limit: 1500},message:"must NOT have more than 1500 characters"}];return false;}else {if(func2(data19) < 1){validate97.errors = [{instancePath:instancePath+"/info/" + key2.replace(/~/g, "~0").replace(/\//g, "~1")+"/text",schemaPath:"#/properties/info/additionalProperties/properties/text/minLength",keyword:"minLength",params:{limit: 1},message:"must NOT have fewer than 1 characters"}];return false;}}}else {validate97.errors = [{instancePath:instancePath+"/info/" + key2.replace(/~/g, "~0").replace(/\//g, "~1")+"/text",schemaPath:"#/properties/info/additionalProperties/properties/text/type",keyword:"type",params:{type: "string"},message:"must be string"}];return false;}}var valid18 = _errs59 === errors;}else {var valid18 = true;}if(valid18){if(data17.tone !== undefined){let data20 = data17.tone;const _errs61 = errors;if(!(((((((((data20 === "gray") || (data20 === "purple")) || (data20 === "teal")) || (data20 === "coral")) || (data20 === "pink")) || (data20 === "blue")) || (data20 === "green")) || (data20 === "amber")) || (data20 === "red"))){validate97.errors = [{instancePath:instancePath+"/info/" + key2.replace(/~/g, "~0").replace(/\//g, "~1")+"/tone",schemaPath:"../common.schema.json#/$defs/tone/enum",keyword:"enum",params:{allowedValues: schema148.enum},message:"must be equal to one of the allowed values"}];return false;}var valid18 = _errs61 === errors;}else {var valid18 = true;}if(valid18){if(data17.code !== undefined){let data21 = data17.code;const _errs63 = errors;if(errors === _errs63){if(typeof data21 === "string"){if(func2(data21) > 2000){validate97.errors = [{instancePath:instancePath+"/info/" + key2.replace(/~/g, "~0").replace(/\//g, "~1")+"/code",schemaPath:"#/properties/info/additionalProperties/properties/code/maxLength",keyword:"maxLength",params:{limit: 2000},message:"must NOT have more than 2000 characters"}];return false;}else {if(func2(data21) < 0){validate97.errors = [{instancePath:instancePath+"/info/" + key2.replace(/~/g, "~0").replace(/\//g, "~1")+"/code",schemaPath:"#/properties/info/additionalProperties/properties/code/minLength",keyword:"minLength",params:{limit: 0},message:"must NOT have fewer than 0 characters"}];return false;}}}else {validate97.errors = [{instancePath:instancePath+"/info/" + key2.replace(/~/g, "~0").replace(/\//g, "~1")+"/code",schemaPath:"#/properties/info/additionalProperties/properties/code/type",keyword:"type",params:{type: "string"},message:"must be string"}];return false;}}var valid18 = _errs63 === errors;}else {var valid18 = true;}}}}}}}else {validate97.errors = [{instancePath:instancePath+"/info/" + key2.replace(/~/g, "~0").replace(/\//g, "~1"),schemaPath:"#/properties/info/additionalProperties/type",keyword:"type",params:{type: "object"},message:"must be object"}];return false;}}var valid17 = _errs54 === errors;if(!valid17){break;}}}}}else {validate97.errors = [{instancePath:instancePath+"/info",schemaPath:"#/properties/info/type",keyword:"type",params:{type: "object"},message:"must be object"}];return false;}}var valid0 = _errs48 === errors;}else {var valid0 = true;}if(valid0){if(data.steps !== undefined){let data22 = data.steps;const _errs65 = errors;if(errors === _errs65){if(Array.isArray(data22)){if(data22.length > 12){validate97.errors = [{instancePath:instancePath+"/steps",schemaPath:"#/properties/steps/maxItems",keyword:"maxItems",params:{limit: 12},message:"must NOT have more than 12 items"}];return false;}else {if(data22.length < 2){validate97.errors = [{instancePath:instancePath+"/steps",schemaPath:"#/properties/steps/minItems",keyword:"minItems",params:{limit: 2},message:"must NOT have fewer than 2 items"}];return false;}else {var valid20 = true;const len1 = data22.length;for(let i1=0; i1 200){validate97.errors = [{instancePath:instancePath+"/steps/" + i1+"/title",schemaPath:"#/properties/steps/items/properties/title/maxLength",keyword:"maxLength",params:{limit: 200},message:"must NOT have more than 200 characters"}];return false;}else {if(func2(data24) < 1){validate97.errors = [{instancePath:instancePath+"/steps/" + i1+"/title",schemaPath:"#/properties/steps/items/properties/title/minLength",keyword:"minLength",params:{limit: 1},message:"must NOT have fewer than 1 characters"}];return false;}}}else {validate97.errors = [{instancePath:instancePath+"/steps/" + i1+"/title",schemaPath:"#/properties/steps/items/properties/title/type",keyword:"type",params:{type: "string"},message:"must be string"}];return false;}}var valid21 = _errs70 === errors;}else {var valid21 = true;}if(valid21){if(data23.text !== undefined){let data25 = data23.text;const _errs72 = errors;if(errors === _errs72){if(typeof data25 === "string"){if(func2(data25) > 600){validate97.errors = [{instancePath:instancePath+"/steps/" + i1+"/text",schemaPath:"#/properties/steps/items/properties/text/maxLength",keyword:"maxLength",params:{limit: 600},message:"must NOT have more than 600 characters"}];return false;}else {if(func2(data25) < 0){validate97.errors = [{instancePath:instancePath+"/steps/" + i1+"/text",schemaPath:"#/properties/steps/items/properties/text/minLength",keyword:"minLength",params:{limit: 0},message:"must NOT have fewer than 0 characters"}];return false;}}}else {validate97.errors = [{instancePath:instancePath+"/steps/" + i1+"/text",schemaPath:"#/properties/steps/items/properties/text/type",keyword:"type",params:{type: "string"},message:"must be string"}];return false;}}var valid21 = _errs72 === errors;}else {var valid21 = true;}}}}}else {validate97.errors = [{instancePath:instancePath+"/steps/" + i1,schemaPath:"#/properties/steps/items/type",keyword:"type",params:{type: "object"},message:"must be object"}];return false;}}var valid20 = _errs67 === errors;if(!valid20){break;}}}}}else {validate97.errors = [{instancePath:instancePath+"/steps",schemaPath:"#/properties/steps/type",keyword:"type",params:{type: "array"},message:"must be array"}];return false;}}var valid0 = _errs65 === errors;}else {var valid0 = true;}}}}}}}}}}}}}}else {validate97.errors = [{instancePath,schemaPath:"#/type",keyword:"type",params:{type: "object"},message:"must be object"}];return false;}}validate97.errors = vErrors;return errors === 0;}validate97.evaluated = {"props":true,"dynamicProps":false,"dynamicItems":false};export const validateFeatureGridData = validate108;const schema177 = {"$schema":"https://json-schema.org/draft/2020-12/schema","$id":"https://kernelpath.dev/schema/v1/data/feature-grid.schema.json","type":"object","additionalProperties":false,"required":["items"],"properties":{"items":{"type":"array","minItems":1,"maxItems":8,"items":{"type":"object","additionalProperties":false,"required":["icon","title","text"],"properties":{"icon":{"enum":["book","flask","chart","shield","layers","terminal","rotate","save","laptop","eye","check","git"]},"title":{"$ref":"../common.schema.json#/$defs/shortText"},"text":{"$ref":"../common.schema.json#/$defs/text","maxLength":300,"type":"string"},"tone":{"enum":["purple","teal","coral","pink","blue","green","amber","gray"]}}}}}};function validate109(data, {instancePath="", parentData, parentDataProperty, rootData=data, dynamicAnchors={}}={}){let vErrors = null;let errors = 0;const evaluated0 = validate109.evaluated;if(evaluated0.dynamicProps){evaluated0.props = undefined;}if(evaluated0.dynamicItems){evaluated0.items = undefined;}const _errs0 = errors;const _errs2 = errors;const _errs3 = errors;if(typeof data === "string"){if(!pattern5.test(data)){const err0 = {};if(vErrors === null){vErrors = [err0];}else {vErrors.push(err0);}errors++;}}var valid1 = _errs3 === errors;if(valid1){validate109.errors = [{instancePath,schemaPath:"#/$defs/text/not",keyword:"not",params:{},message:"must NOT be valid"}];return false;}else {errors = _errs2;if(vErrors !== null){if(_errs2){vErrors.length = _errs2;}else {vErrors = null;}}}if(errors === _errs0){if(typeof data === "string"){if(func2(data) > 2000){validate109.errors = [{instancePath,schemaPath:"#/$defs/text/maxLength",keyword:"maxLength",params:{limit: 2000},message:"must NOT have more than 2000 characters"}];return false;}else {if(func2(data) < 1){validate109.errors = [{instancePath,schemaPath:"#/$defs/text/minLength",keyword:"minLength",params:{limit: 1},message:"must NOT have fewer than 1 characters"}];return false;}}}else {validate109.errors = [{instancePath,schemaPath:"#/$defs/text/type",keyword:"type",params:{type: "string"},message:"must be string"}];return false;}}if(errors === 0){if(typeof data === "string"){if(func2(data) > 160){validate109.errors = [{instancePath,schemaPath:"#/maxLength",keyword:"maxLength",params:{limit: 160},message:"must NOT have more than 160 characters"}];return false;}}else {validate109.errors = [{instancePath,schemaPath:"#/type",keyword:"type",params:{type: "string"},message:"must be string"}];return false;}}validate109.errors = vErrors;return errors === 0;}validate109.evaluated = {"dynamicProps":false,"dynamicItems":false};function validate108(data, {instancePath="", parentData, parentDataProperty, rootData=data, dynamicAnchors={}}={}){/*# sourceURL="https://kernelpath.dev/schema/v1/data/feature-grid.schema.json" */;let vErrors = null;let errors = 0;const evaluated0 = validate108.evaluated;if(evaluated0.dynamicProps){evaluated0.props = undefined;}if(evaluated0.dynamicItems){evaluated0.items = undefined;}if(errors === 0){if(data && typeof data == "object" && !Array.isArray(data)){let missing0;if((data.items === undefined) && (missing0 = "items")){validate108.errors = [{instancePath,schemaPath:"#/required",keyword:"required",params:{missingProperty: missing0},message:"must have required property '"+missing0+"'"}];return false;}else {const _errs1 = errors;for(const key0 in data){if(!(key0 === "items")){validate108.errors = [{instancePath,schemaPath:"#/additionalProperties",keyword:"additionalProperties",params:{additionalProperty: key0},message:"must NOT have additional properties"}];return false;break;}}if(_errs1 === errors){if(data.items !== undefined){let data0 = data.items;const _errs2 = errors;if(errors === _errs2){if(Array.isArray(data0)){if(data0.length > 8){validate108.errors = [{instancePath:instancePath+"/items",schemaPath:"#/properties/items/maxItems",keyword:"maxItems",params:{limit: 8},message:"must NOT have more than 8 items"}];return false;}else {if(data0.length < 1){validate108.errors = [{instancePath:instancePath+"/items",schemaPath:"#/properties/items/minItems",keyword:"minItems",params:{limit: 1},message:"must NOT have fewer than 1 items"}];return false;}else {var valid1 = true;const len0 = data0.length;for(let i0=0; i0 2000){validate108.errors = [{instancePath:instancePath+"/items/" + i0+"/text",schemaPath:"../common.schema.json#/$defs/text/maxLength",keyword:"maxLength",params:{limit: 2000},message:"must NOT have more than 2000 characters"}];return false;}else {if(func2(data4) < 1){validate108.errors = [{instancePath:instancePath+"/items/" + i0+"/text",schemaPath:"../common.schema.json#/$defs/text/minLength",keyword:"minLength",params:{limit: 1},message:"must NOT have fewer than 1 characters"}];return false;}}}else {validate108.errors = [{instancePath:instancePath+"/items/" + i0+"/text",schemaPath:"../common.schema.json#/$defs/text/type",keyword:"type",params:{type: "string"},message:"must be string"}];return false;}}if(errors === _errs9){if(typeof data4 === "string"){if(func2(data4) > 300){validate108.errors = [{instancePath:instancePath+"/items/" + i0+"/text",schemaPath:"#/properties/items/items/properties/text/maxLength",keyword:"maxLength",params:{limit: 300},message:"must NOT have more than 300 characters"}];return false;}}else {validate108.errors = [{instancePath:instancePath+"/items/" + i0+"/text",schemaPath:"#/properties/items/items/properties/text/type",keyword:"type",params:{type: "string"},message:"must be string"}];return false;}}var valid2 = _errs9 === errors;}else {var valid2 = true;}if(valid2){if(data1.tone !== undefined){let data5 = data1.tone;const _errs15 = errors;if(!((((((((data5 === "purple") || (data5 === "teal")) || (data5 === "coral")) || (data5 === "pink")) || (data5 === "blue")) || (data5 === "green")) || (data5 === "amber")) || (data5 === "gray"))){validate108.errors = [{instancePath:instancePath+"/items/" + i0+"/tone",schemaPath:"#/properties/items/items/properties/tone/enum",keyword:"enum",params:{allowedValues: schema177.properties.items.items.properties.tone.enum},message:"must be equal to one of the allowed values"}];return false;}var valid2 = _errs15 === errors;}else {var valid2 = true;}}}}}}}else {validate108.errors = [{instancePath:instancePath+"/items/" + i0,schemaPath:"#/properties/items/items/type",keyword:"type",params:{type: "object"},message:"must be object"}];return false;}}var valid1 = _errs4 === errors;if(!valid1){break;}}}}}else {validate108.errors = [{instancePath:instancePath+"/items",schemaPath:"#/properties/items/type",keyword:"type",params:{type: "array"},message:"must be array"}];return false;}}}}}}else {validate108.errors = [{instancePath,schemaPath:"#/type",keyword:"type",params:{type: "object"},message:"must be object"}];return false;}}validate108.errors = vErrors;return errors === 0;}validate108.evaluated = {"props":true,"dynamicProps":false,"dynamicItems":false};export const validateTerminalDemoData = validate111;const schema181 = {"$schema":"https://json-schema.org/draft/2020-12/schema","$id":"https://kernelpath.dev/schema/v1/data/terminal-demo.schema.json","type":"object","additionalProperties":false,"required":["title","lines"],"properties":{"title":{"$ref":"../common.schema.json#/$defs/shortText"},"lines":{"type":"array","minItems":1,"maxItems":14,"items":{"type":"object","additionalProperties":false,"required":["kind","text"],"properties":{"kind":{"enum":["command","output","ok","changed","comment"]},"text":{"type":"string","maxLength":120}}}}}};function validate112(data, {instancePath="", parentData, parentDataProperty, rootData=data, dynamicAnchors={}}={}){let vErrors = null;let errors = 0;const evaluated0 = validate112.evaluated;if(evaluated0.dynamicProps){evaluated0.props = undefined;}if(evaluated0.dynamicItems){evaluated0.items = undefined;}const _errs0 = errors;const _errs2 = errors;const _errs3 = errors;if(typeof data === "string"){if(!pattern5.test(data)){const err0 = {};if(vErrors === null){vErrors = [err0];}else {vErrors.push(err0);}errors++;}}var valid1 = _errs3 === errors;if(valid1){validate112.errors = [{instancePath,schemaPath:"#/$defs/text/not",keyword:"not",params:{},message:"must NOT be valid"}];return false;}else {errors = _errs2;if(vErrors !== null){if(_errs2){vErrors.length = _errs2;}else {vErrors = null;}}}if(errors === _errs0){if(typeof data === "string"){if(func2(data) > 2000){validate112.errors = [{instancePath,schemaPath:"#/$defs/text/maxLength",keyword:"maxLength",params:{limit: 2000},message:"must NOT have more than 2000 characters"}];return false;}else {if(func2(data) < 1){validate112.errors = [{instancePath,schemaPath:"#/$defs/text/minLength",keyword:"minLength",params:{limit: 1},message:"must NOT have fewer than 1 characters"}];return false;}}}else {validate112.errors = [{instancePath,schemaPath:"#/$defs/text/type",keyword:"type",params:{type: "string"},message:"must be string"}];return false;}}if(errors === 0){if(typeof data === "string"){if(func2(data) > 160){validate112.errors = [{instancePath,schemaPath:"#/maxLength",keyword:"maxLength",params:{limit: 160},message:"must NOT have more than 160 characters"}];return false;}}else {validate112.errors = [{instancePath,schemaPath:"#/type",keyword:"type",params:{type: "string"},message:"must be string"}];return false;}}validate112.errors = vErrors;return errors === 0;}validate112.evaluated = {"dynamicProps":false,"dynamicItems":false};function validate111(data, {instancePath="", parentData, parentDataProperty, rootData=data, dynamicAnchors={}}={}){/*# sourceURL="https://kernelpath.dev/schema/v1/data/terminal-demo.schema.json" */;let vErrors = null;let errors = 0;const evaluated0 = validate111.evaluated;if(evaluated0.dynamicProps){evaluated0.props = undefined;}if(evaluated0.dynamicItems){evaluated0.items = undefined;}if(errors === 0){if(data && typeof data == "object" && !Array.isArray(data)){let missing0;if(((data.title === undefined) && (missing0 = "title")) || ((data.lines === undefined) && (missing0 = "lines"))){validate111.errors = [{instancePath,schemaPath:"#/required",keyword:"required",params:{missingProperty: missing0},message:"must have required property '"+missing0+"'"}];return false;}else {const _errs1 = errors;for(const key0 in data){if(!((key0 === "title") || (key0 === "lines"))){validate111.errors = [{instancePath,schemaPath:"#/additionalProperties",keyword:"additionalProperties",params:{additionalProperty: key0},message:"must NOT have additional properties"}];return false;break;}}if(_errs1 === errors){if(data.title !== undefined){const _errs2 = errors;if(!(validate112(data.title, {instancePath:instancePath+"/title",parentData:data,parentDataProperty:"title",rootData,dynamicAnchors}))){vErrors = vErrors === null ? validate112.errors : vErrors.concat(validate112.errors);errors = vErrors.length;}var valid0 = _errs2 === errors;}else {var valid0 = true;}if(valid0){if(data.lines !== undefined){let data1 = data.lines;const _errs3 = errors;if(errors === _errs3){if(Array.isArray(data1)){if(data1.length > 14){validate111.errors = [{instancePath:instancePath+"/lines",schemaPath:"#/properties/lines/maxItems",keyword:"maxItems",params:{limit: 14},message:"must NOT have more than 14 items"}];return false;}else {if(data1.length < 1){validate111.errors = [{instancePath:instancePath+"/lines",schemaPath:"#/properties/lines/minItems",keyword:"minItems",params:{limit: 1},message:"must NOT have fewer than 1 items"}];return false;}else {var valid1 = true;const len0 = data1.length;for(let i0=0; i0 120){validate111.errors = [{instancePath:instancePath+"/lines/" + i0+"/text",schemaPath:"#/properties/lines/items/properties/text/maxLength",keyword:"maxLength",params:{limit: 120},message:"must NOT have more than 120 characters"}];return false;}}else {validate111.errors = [{instancePath:instancePath+"/lines/" + i0+"/text",schemaPath:"#/properties/lines/items/properties/text/type",keyword:"type",params:{type: "string"},message:"must be string"}];return false;}}var valid2 = _errs9 === errors;}else {var valid2 = true;}}}}}else {validate111.errors = [{instancePath:instancePath+"/lines/" + i0,schemaPath:"#/properties/lines/items/type",keyword:"type",params:{type: "object"},message:"must be object"}];return false;}}var valid1 = _errs5 === errors;if(!valid1){break;}}}}}else {validate111.errors = [{instancePath:instancePath+"/lines",schemaPath:"#/properties/lines/type",keyword:"type",params:{type: "array"},message:"must be array"}];return false;}}var valid0 = _errs3 === errors;}else {var valid0 = true;}}}}}else {validate111.errors = [{instancePath,schemaPath:"#/type",keyword:"type",params:{type: "object"},message:"must be object"}];return false;}}validate111.errors = vErrors;return errors === 0;}validate111.evaluated = {"props":true,"dynamicProps":false,"dynamicItems":false}; // What each tag may carry, from the component catalog: checked in the browser before a tag is rendered. -export const TAGS = {"admin-module-finder":{"required":["ref"],"kit":true,"attributes":{"ref":{"type":"string"}}},"annotated-yaml":{"required":["ref"],"kit":true,"attributes":{"ref":{"type":"string"}}},"assessment-timer":{"required":["id"],"attributes":{"id":{"type":"string"},"minutes":{"type":"integer","minimum":5,"maximum":480}}},"block-flow":{"required":["ref"],"kit":true,"attributes":{"ref":{"type":"string"}}},"build-roadmap":{"required":["ref"],"kit":true,"attributes":{"ref":{"type":"string"}}},"callout":{"required":[],"attributes":{"type":{"type":"string","enum":["note","tip","important","warning","exam"]},"title":{"type":"string"}}},"card":{"required":["title"],"attributes":{"title":{"type":"string"},"kicker":{"type":"string"},"tone":{"type":"string","enum":["gray","purple","teal","coral","pink","blue","green","amber","red"]}}},"cards":{"required":[],"attributes":{"cols":{"type":"integer","enum":[2,3]}}},"check-mode-sim":{"required":["ref"],"kit":true,"attributes":{"ref":{"type":"string"}}},"column":{"required":[],"attributes":{"title":{"type":"string"},"tone":{"type":"string","enum":["gray","purple","teal","coral","pink","blue","green","amber","red"]}}},"columns":{"required":[],"attributes":{}},"command-vs-module":{"required":["ref"],"kit":true,"attributes":{"ref":{"type":"string"}}},"condition-playground":{"required":["ref"],"kit":true,"attributes":{"ref":{"type":"string"}}},"cron-builder":{"required":["ref"],"kit":true,"attributes":{"ref":{"type":"string"}}},"custom-fact-builder":{"required":["ref"],"kit":true,"attributes":{"ref":{"type":"string"}}},"data-explorer":{"required":["ref"],"kit":true,"attributes":{"ref":{"type":"string"}}},"diagram":{"required":["ref"],"attributes":{"ref":{"type":"string"}}},"error-decoder":{"required":["ref"],"kit":true,"attributes":{"ref":{"type":"string"}}},"fact-gathering":{"required":["ref"],"kit":true,"attributes":{"ref":{"type":"string"}}},"facts-explorer":{"required":["ref"],"kit":true,"attributes":{"ref":{"type":"string"}}},"feature-grid":{"required":["ref"],"attributes":{"ref":{"type":"string"}}},"fetch-layout":{"required":["ref"],"kit":true,"attributes":{"ref":{"type":"string"}}},"file-edit-simulator":{"required":["ref"],"kit":true,"attributes":{"ref":{"type":"string"}}},"file-module-chooser":{"required":["ref"],"kit":true,"attributes":{"ref":{"type":"string"}}},"flashcards":{"required":["ref"],"attributes":{"ref":{"type":"string"},"title":{"type":"string"}}},"flow-map":{"required":["ref"],"attributes":{"ref":{"type":"string"},"title":{"type":"string"},"caption":{"type":"string"}}},"fqcn-explorer":{"required":["ref"],"kit":true,"attributes":{"ref":{"type":"string"}}},"glob-tester":{"required":["ref"],"kit":true,"attributes":{"ref":{"type":"string"}}},"glossary":{"required":[],"attributes":{}},"grep-tester":{"required":["ref"],"kit":true,"attributes":{"ref":{"type":"string"}}},"group-vars-resolver":{"required":["ref"],"kit":true,"attributes":{"ref":{"type":"string"}}},"handler-timeline":{"required":["ref"],"kit":true,"attributes":{"ref":{"type":"string"}}},"hero":{"required":["title"],"attributes":{"eyebrow":{"type":"string"},"title":{"type":"string"},"art":{"type":"string","enum":["lab"]},"primary":{"type":"string"},"primaryLabel":{"type":"string"},"secondaryLabel":{"type":"string"}}},"host-pattern-tester":{"required":["ref"],"kit":true,"attributes":{"ref":{"type":"string"}}},"idempotency-demo":{"required":["ref"],"kit":true,"attributes":{"ref":{"type":"string"}}},"inventory-explorer":{"required":["ref"],"kit":true,"attributes":{"ref":{"type":"string"}}},"kbd":{"required":[],"attributes":{}},"lab":{"required":["id","title"],"attributes":{"id":{"type":"string"},"title":{"type":"string"},"exercise":{"type":"string"},"ownExercise":{"type":"boolean"},"starter":{"type":"boolean"},"hosts":{"type":"string[]","maxItems":12},"outcomes":{"type":"string[]","maxItems":12},"objectives":{"type":"string[]","maxItems":12}}},"lab-challenge":{"required":[],"attributes":{}},"lab-finish":{"required":["exercise"],"attributes":{"exercise":{"type":"string"},"grade":{"type":"boolean"}}},"lab-network-map":{"required":["ref"],"kit":true,"attributes":{"ref":{"type":"string"}}},"lab-notes":{"required":[],"attributes":{}},"lab-setup":{"required":["variant"],"attributes":{"variant":{"type":"string"}}},"lead":{"required":[],"attributes":{}},"loop-unroller":{"required":["ref"],"kit":true,"attributes":{"ref":{"type":"string"}}},"magic-variables":{"required":["ref"],"kit":true,"attributes":{"ref":{"type":"string"}}},"mode-calculator":{"required":["ref"],"kit":true,"attributes":{"ref":{"type":"string"}}},"module-explorer":{"required":["ref"],"kit":true,"attributes":{"ref":{"type":"string"}}},"objectives":{"required":[],"attributes":{}},"play-order":{"required":["ref"],"kit":true,"attributes":{"ref":{"type":"string"}}},"play-recap":{"required":["ref"],"kit":true,"attributes":{"ref":{"type":"string"}}},"practice":{"required":["ref"],"attributes":{"ref":{"type":"string"}}},"precedence-resolver":{"required":["ref"],"kit":true,"attributes":{"ref":{"type":"string"}}},"program-cards":{"required":[],"attributes":{}},"project-tree":{"required":["ref"],"kit":true,"attributes":{"ref":{"type":"string"}}},"quiz":{"required":["ref"],"attributes":{"id":{"type":"string"},"ref":{"type":"string"},"title":{"type":"string"},"objectives":{"type":"string[]","maxItems":12}}},"range-expander":{"required":["ref"],"kit":true,"attributes":{"ref":{"type":"string"}}},"reader-variables":{"required":[],"attributes":{}},"readiness-checklist":{"required":["ref"],"kit":true,"attributes":{"ref":{"type":"string"}}},"reuse-simulator":{"required":["ref"],"kit":true,"attributes":{"ref":{"type":"string"}}},"reveal":{"required":[],"attributes":{"title":{"type":"string"}}},"role-anatomy":{"required":["ref"],"kit":true,"attributes":{"ref":{"type":"string"}}},"role-var-resolver":{"required":["ref"],"kit":true,"attributes":{"ref":{"type":"string"}}},"run-stages":{"required":["ref"],"kit":true,"attributes":{"ref":{"type":"string"}}},"shell-practice":{"required":["ref"],"kit":true,"attributes":{"ref":{"type":"string"}}},"snapshot-chain":{"required":["ref"],"kit":true,"attributes":{"ref":{"type":"string"}}},"starter-files":{"required":["exercise"],"attributes":{"exercise":{"type":"string"}}},"stat-explorer":{"required":["ref"],"kit":true,"attributes":{"ref":{"type":"string"}}},"step":{"required":[],"attributes":{"title":{"type":"string"}}},"steps":{"required":[],"attributes":{}},"tab":{"required":["label"],"attributes":{"label":{"type":"string"}}},"tabs":{"required":[],"attributes":{}},"task":{"required":["id","title"],"attributes":{"id":{"type":"string"},"title":{"type":"string"},"legacyIndex":{"type":"integer","minimum":1}}},"task-outcome":{"required":["ref"],"kit":true,"attributes":{"ref":{"type":"string"}}},"template-playground":{"required":["ref"],"kit":true,"attributes":{"ref":{"type":"string"}}},"term":{"required":["name"],"attributes":{"name":{"type":"string"}}},"terminal-demo":{"required":["ref"],"attributes":{"ref":{"type":"string"}}},"variable-name-checker":{"required":["ref"],"kit":true,"attributes":{"ref":{"type":"string"}}},"variable-precedence":{"required":["ref"],"kit":true,"attributes":{"ref":{"type":"string"}}},"variable-substitution":{"required":["ref"],"kit":true,"attributes":{"ref":{"type":"string"}}},"variant":{"required":["name"],"attributes":{"name":{"type":"string"},"title":{"type":"string"}}},"variant-group":{"required":[],"attributes":{}},"variant-switch":{"required":[],"attributes":{"label":{"type":"string"}}},"vault-commands":{"required":["ref"],"kit":true,"attributes":{"ref":{"type":"string"}}},"verbosity":{"required":["ref"],"kit":true,"attributes":{"ref":{"type":"string"}}},"yaml-multiline":{"required":["ref"],"kit":true,"attributes":{"ref":{"type":"string"}}}}; +export const TAGS = {"admin-module-finder":{"required":["ref"],"kit":true,"attributes":{"ref":{"type":"string"}}},"annotated-yaml":{"required":["ref"],"kit":true,"attributes":{"ref":{"type":"string"}}},"assessment-timer":{"required":["id"],"attributes":{"id":{"type":"string"},"minutes":{"type":"integer","minimum":5,"maximum":480}}},"block-flow":{"required":["ref"],"kit":true,"attributes":{"ref":{"type":"string"}}},"build-roadmap":{"required":["ref"],"kit":true,"attributes":{"ref":{"type":"string"}}},"callout":{"required":[],"attributes":{"type":{"type":"string","enum":["note","tip","important","warning","exam"]},"title":{"type":"string"}}},"card":{"required":["title"],"attributes":{"title":{"type":"string"},"kicker":{"type":"string"},"tone":{"type":"string","enum":["gray","purple","teal","coral","pink","blue","green","amber","red"]}}},"cards":{"required":[],"attributes":{"cols":{"type":"integer","enum":[2,3]}}},"check-mode-sim":{"required":["ref"],"kit":true,"attributes":{"ref":{"type":"string"}}},"column":{"required":[],"attributes":{"title":{"type":"string"},"tone":{"type":"string","enum":["gray","purple","teal","coral","pink","blue","green","amber","red"]}}},"columns":{"required":[],"attributes":{}},"command-vs-module":{"required":["ref"],"kit":true,"attributes":{"ref":{"type":"string"}}},"condition-playground":{"required":["ref"],"kit":true,"attributes":{"ref":{"type":"string"}}},"cron-builder":{"required":["ref"],"kit":true,"attributes":{"ref":{"type":"string"}}},"custom-fact-builder":{"required":["ref"],"kit":true,"attributes":{"ref":{"type":"string"}}},"data-explorer":{"required":["ref"],"kit":true,"attributes":{"ref":{"type":"string"}}},"diagram":{"required":["ref"],"attributes":{"ref":{"type":"string"}}},"error-decoder":{"required":["ref"],"kit":true,"attributes":{"ref":{"type":"string"}}},"fact-gathering":{"required":["ref"],"kit":true,"attributes":{"ref":{"type":"string"}}},"facts-explorer":{"required":["ref"],"kit":true,"attributes":{"ref":{"type":"string"}}},"feature-grid":{"required":["ref"],"attributes":{"ref":{"type":"string"}}},"fetch-layout":{"required":["ref"],"kit":true,"attributes":{"ref":{"type":"string"}}},"file-edit-simulator":{"required":["ref"],"kit":true,"attributes":{"ref":{"type":"string"}}},"file-module-chooser":{"required":["ref"],"kit":true,"attributes":{"ref":{"type":"string"}}},"flashcards":{"required":["ref"],"attributes":{"ref":{"type":"string"},"title":{"type":"string"}}},"flow-map":{"required":["ref"],"attributes":{"ref":{"type":"string"},"title":{"type":"string"},"caption":{"type":"string"}}},"fqcn-explorer":{"required":["ref"],"kit":true,"attributes":{"ref":{"type":"string"}}},"glob-tester":{"required":["ref"],"kit":true,"attributes":{"ref":{"type":"string"}}},"glossary":{"required":[],"attributes":{}},"grep-tester":{"required":["ref"],"kit":true,"attributes":{"ref":{"type":"string"}}},"group-vars-resolver":{"required":["ref"],"kit":true,"attributes":{"ref":{"type":"string"}}},"handler-timeline":{"required":["ref"],"kit":true,"attributes":{"ref":{"type":"string"}}},"hero":{"required":["title"],"attributes":{"eyebrow":{"type":"string"},"title":{"type":"string"},"art":{"type":"string","enum":["lab"]},"primary":{"type":"string"},"primaryLabel":{"type":"string"},"secondaryLabel":{"type":"string"}}},"host-pattern-tester":{"required":["ref"],"kit":true,"attributes":{"ref":{"type":"string"}}},"idempotency-demo":{"required":["ref"],"kit":true,"attributes":{"ref":{"type":"string"}}},"inventory-explorer":{"required":["ref"],"kit":true,"attributes":{"ref":{"type":"string"}}},"kbd":{"required":[],"attributes":{}},"lab":{"required":["id","title"],"attributes":{"id":{"type":"string"},"title":{"type":"string"},"exercise":{"type":"string"},"ownExercise":{"type":"boolean"},"starter":{"type":"boolean"},"hosts":{"type":"string[]","maxItems":12},"outcomes":{"type":"string[]","maxItems":12},"objectives":{"type":"string[]","maxItems":12},"guided":{"type":"boolean"}}},"lab-challenge":{"required":[],"attributes":{}},"lab-finish":{"required":["exercise"],"attributes":{"exercise":{"type":"string"},"grade":{"type":"boolean"},"servers":{"type":"boolean"}}},"lab-network-map":{"required":["ref"],"kit":true,"attributes":{"ref":{"type":"string"}}},"lab-notes":{"required":[],"attributes":{}},"lab-setup":{"required":["variant"],"attributes":{"variant":{"type":"string"}}},"lead":{"required":[],"attributes":{}},"loop-unroller":{"required":["ref"],"kit":true,"attributes":{"ref":{"type":"string"}}},"magic-variables":{"required":["ref"],"kit":true,"attributes":{"ref":{"type":"string"}}},"mode-calculator":{"required":["ref"],"kit":true,"attributes":{"ref":{"type":"string"}}},"module-explorer":{"required":["ref"],"kit":true,"attributes":{"ref":{"type":"string"}}},"objectives":{"required":[],"attributes":{}},"play-order":{"required":["ref"],"kit":true,"attributes":{"ref":{"type":"string"}}},"play-recap":{"required":["ref"],"kit":true,"attributes":{"ref":{"type":"string"}}},"practice":{"required":["ref"],"attributes":{"ref":{"type":"string"}}},"precedence-resolver":{"required":["ref"],"kit":true,"attributes":{"ref":{"type":"string"}}},"program-cards":{"required":[],"attributes":{}},"project-tree":{"required":["ref"],"kit":true,"attributes":{"ref":{"type":"string"}}},"quiz":{"required":["ref"],"attributes":{"id":{"type":"string"},"ref":{"type":"string"},"title":{"type":"string"},"objectives":{"type":"string[]","maxItems":12}}},"range-expander":{"required":["ref"],"kit":true,"attributes":{"ref":{"type":"string"}}},"reader-variables":{"required":[],"attributes":{}},"readiness-checklist":{"required":["ref"],"kit":true,"attributes":{"ref":{"type":"string"}}},"reuse-simulator":{"required":["ref"],"kit":true,"attributes":{"ref":{"type":"string"}}},"reveal":{"required":[],"attributes":{"title":{"type":"string"}}},"role-anatomy":{"required":["ref"],"kit":true,"attributes":{"ref":{"type":"string"}}},"role-var-resolver":{"required":["ref"],"kit":true,"attributes":{"ref":{"type":"string"}}},"run-stages":{"required":["ref"],"kit":true,"attributes":{"ref":{"type":"string"}}},"shell-practice":{"required":["ref"],"kit":true,"attributes":{"ref":{"type":"string"}}},"snapshot-chain":{"required":["ref"],"kit":true,"attributes":{"ref":{"type":"string"}}},"starter-files":{"required":["exercise"],"attributes":{"exercise":{"type":"string"}}},"stat-explorer":{"required":["ref"],"kit":true,"attributes":{"ref":{"type":"string"}}},"step":{"required":[],"attributes":{"title":{"type":"string"}}},"steps":{"required":[],"attributes":{}},"tab":{"required":["label"],"attributes":{"label":{"type":"string"}}},"tabs":{"required":[],"attributes":{}},"task":{"required":["id","title"],"attributes":{"id":{"type":"string"},"title":{"type":"string"},"legacyIndex":{"type":"integer","minimum":1}}},"task-outcome":{"required":["ref"],"kit":true,"attributes":{"ref":{"type":"string"}}},"template-playground":{"required":["ref"],"kit":true,"attributes":{"ref":{"type":"string"}}},"term":{"required":["name"],"attributes":{"name":{"type":"string"}}},"terminal-demo":{"required":["ref"],"attributes":{"ref":{"type":"string"}}},"variable-name-checker":{"required":["ref"],"kit":true,"attributes":{"ref":{"type":"string"}}},"variable-precedence":{"required":["ref"],"kit":true,"attributes":{"ref":{"type":"string"}}},"variable-substitution":{"required":["ref"],"kit":true,"attributes":{"ref":{"type":"string"}}},"variant":{"required":["name"],"attributes":{"name":{"type":"string"},"title":{"type":"string"}}},"variant-group":{"required":[],"attributes":{}},"variant-switch":{"required":[],"attributes":{"label":{"type":"string"}}},"vault-commands":{"required":["ref"],"kit":true,"attributes":{"ref":{"type":"string"}}},"verbosity":{"required":["ref"],"kit":true,"attributes":{"ref":{"type":"string"}}},"yaml-multiline":{"required":["ref"],"kit":true,"attributes":{"ref":{"type":"string"}}}}; diff --git a/packages/schema/schemas/source/lab.schema.json b/packages/schema/schemas/source/lab.schema.json index 6926745..87ebc89 100644 --- a/packages/schema/schemas/source/lab.schema.json +++ b/packages/schema/schemas/source/lab.schema.json @@ -46,7 +46,7 @@ "setup": { "description": "Platform actions run after the starter files are in place, in order.", "type": "array", - "maxItems": 20, + "maxItems": 60, "items": { "$ref": "#/$defs/action" } @@ -71,6 +71,14 @@ "note": { "description": "A short remark shown beside the title in the exercise list, for example that the project starts empty.", "$ref": "../common.schema.json#/$defs/shortText" + }, + "finish": { + "description": "Host cleanup actions that `lab finish` runs, in order, when the exercise is put away. Each is best effort: one that fails is reported and the rest still run.", + "type": "array", + "maxItems": 60, + "items": { + "$ref": "#/$defs/action" + } } }, "$defs": { @@ -95,7 +103,7 @@ "absolutePath": { "description": "An absolute path on a managed host. Placeholders {host} and {hostShort} are allowed.", "type": "string", - "pattern": "^/(?!.*(?:^|/)\\.\\.(?:/|$))[A-Za-z0-9._@+{}-]*(?:/[A-Za-z0-9._@+{}-]+)*$", + "pattern": "^/(?!.*(?:^|/)\\.\\.(?:/|$))[A-Za-z0-9._@+{} -]*(?:/[A-Za-z0-9._@+{} -]+)*$", "maxLength": 240 }, "literal": { @@ -606,6 +614,524 @@ } } } + }, + { + "type": "object", + "additionalProperties": false, + "required": [ + "action", + "hosts", + "names" + ], + "properties": { + "action": { + "const": "package" + }, + "hosts": { + "$ref": "#/$defs/serverList" + }, + "names": { + "type": "array", + "minItems": 1, + "maxItems": 20, + "items": { + "$ref": "#/$defs/packageName" + } + }, + "state": { + "enum": [ + "present", + "absent" + ] + } + } + }, + { + "type": "object", + "additionalProperties": false, + "required": [ + "action", + "hosts", + "names" + ], + "properties": { + "action": { + "const": "service" + }, + "hosts": { + "$ref": "#/$defs/serverList" + }, + "names": { + "type": "array", + "minItems": 1, + "maxItems": 10, + "items": { + "$ref": "#/$defs/unitName" + } + }, + "state": { + "enum": [ + "started", + "stopped", + "restarted" + ] + }, + "enabled": { + "type": "boolean" + }, + "user": { + "description": "Manage the units of this user's systemd instead of the system's.", + "$ref": "#/$defs/unixName" + } + } + }, + { + "type": "object", + "additionalProperties": false, + "required": [ + "action", + "hosts", + "name" + ], + "properties": { + "action": { + "const": "group" + }, + "hosts": { + "$ref": "#/$defs/serverList" + }, + "name": { + "$ref": "#/$defs/unixName" + }, + "gid": { + "type": "integer", + "minimum": 1000, + "maximum": 60000 + }, + "state": { + "enum": [ + "present", + "absent" + ] + } + } + }, + { + "type": "object", + "additionalProperties": false, + "required": [ + "action", + "hosts", + "name" + ], + "properties": { + "action": { + "const": "user" + }, + "hosts": { + "$ref": "#/$defs/serverList" + }, + "name": { + "$ref": "#/$defs/unixName" + }, + "uid": { + "type": "integer", + "minimum": 1000, + "maximum": 60000 + }, + "group": { + "$ref": "#/$defs/unixName" + }, + "groups": { + "type": "array", + "maxItems": 10, + "items": { + "$ref": "#/$defs/unixName" + } + }, + "shell": { + "type": "string", + "pattern": "^/(?:usr/)?s?bin/[a-z]+$" + }, + "home": { + "type": "boolean" + }, + "password": { + "type": "string", + "pattern": "^[A-Za-z0-9._-]{4,40}$" + }, + "state": { + "enum": [ + "present", + "absent" + ] + } + } + }, + { + "type": "object", + "additionalProperties": false, + "required": [ + "action", + "hosts", + "path" + ], + "properties": { + "action": { + "const": "directory" + }, + "hosts": { + "$ref": "#/$defs/serverList" + }, + "path": { + "$ref": "#/$defs/hostPath" + }, + "owner": { + "$ref": "#/$defs/unixName" + }, + "group": { + "$ref": "#/$defs/unixName" + }, + "mode": { + "type": "string", + "pattern": "^[0-7]{3,4}$" + }, + "state": { + "enum": [ + "present", + "absent" + ] + } + } + }, + { + "type": "object", + "additionalProperties": false, + "required": [ + "action", + "hosts", + "path" + ], + "properties": { + "action": { + "const": "file" + }, + "hosts": { + "$ref": "#/$defs/serverList" + }, + "path": { + "$ref": "#/$defs/hostPath" + }, + "content": { + "type": "string", + "maxLength": 4000 + }, + "owner": { + "$ref": "#/$defs/unixName" + }, + "group": { + "$ref": "#/$defs/unixName" + }, + "mode": { + "type": "string", + "pattern": "^[0-7]{3,4}$" + }, + "state": { + "enum": [ + "present", + "absent" + ] + } + } + }, + { + "type": "object", + "additionalProperties": false, + "required": [ + "action", + "hosts", + "path", + "matching" + ], + "properties": { + "action": { + "const": "remove-lines" + }, + "hosts": { + "$ref": "#/$defs/serverList" + }, + "path": { + "type": "string", + "enum": [ + "/etc/fstab", + "/etc/exports", + "/etc/hosts", + "/etc/chrony.conf" + ] + }, + "matching": { + "description": "Lines matching this extended regular expression are deleted.", + "type": "string", + "pattern": "^[A-Za-z0-9 _./:,@=^$*+?|()\\[\\]-]{1,120}$" + } + } + }, + { + "type": "object", + "additionalProperties": false, + "required": [ + "action", + "hosts" + ], + "properties": { + "action": { + "const": "firewall" + }, + "hosts": { + "$ref": "#/$defs/serverList" + }, + "zone": { + "$ref": "#/$defs/unixName" + }, + "service": { + "$ref": "#/$defs/unitName" + }, + "port": { + "type": "string", + "pattern": "^[0-9]{1,5}/(?:tcp|udp)$" + }, + "source": { + "type": "string", + "pattern": "^[0-9a-fA-F:.]{2,45}(?:/[0-9]{1,3})?$" + }, + "forwardPort": { + "type": "string", + "pattern": "^port=[0-9]{1,5}:proto=(?:tcp|udp):toport=[0-9]{1,5}$" + }, + "deleteZone": { + "type": "boolean", + "description": "Remove the zone (state absent) or create it (state present)." + }, + "deleteService": { + "type": "boolean" + }, + "state": { + "enum": [ + "present", + "absent" + ] + } + } + }, + { + "type": "object", + "additionalProperties": false, + "required": [ + "action", + "hosts", + "kind" + ], + "properties": { + "action": { + "const": "selinux" + }, + "hosts": { + "$ref": "#/$defs/serverList" + }, + "kind": { + "enum": [ + "port", + "fcontext", + "boolean" + ] + }, + "type": { + "type": "string", + "pattern": "^[a-z_]+_t$" + }, + "proto": { + "enum": [ + "tcp", + "udp" + ] + }, + "port": { + "type": "integer", + "minimum": 1, + "maximum": 65535 + }, + "path": { + "type": "string", + "pattern": "^/[A-Za-z0-9_./()*+?|\\\\-]{1,100}$" + }, + "name": { + "type": "string", + "pattern": "^[a-z_]{3,60}$" + }, + "value": { + "type": "boolean" + }, + "state": { + "enum": [ + "present", + "absent" + ] + } + } + }, + { + "type": "object", + "additionalProperties": false, + "required": [ + "action", + "hosts", + "device" + ], + "properties": { + "action": { + "const": "wipe-disk" + }, + "hosts": { + "$ref": "#/$defs/serverList" + }, + "device": { + "type": "string", + "pattern": "^/dev/(?:sd|vd)[b-z]$" + } + } + }, + { + "type": "object", + "additionalProperties": false, + "required": [ + "action", + "hosts" + ], + "properties": { + "action": { + "const": "systemd" + }, + "hosts": { + "$ref": "#/$defs/serverList" + }, + "reload": { + "type": "boolean" + }, + "daemonReexec": { + "type": "boolean" + } + } + }, + { + "type": "object", + "additionalProperties": false, + "required": [ + "action", + "hosts", + "user" + ], + "properties": { + "action": { + "const": "linger" + }, + "hosts": { + "$ref": "#/$defs/serverList" + }, + "user": { + "$ref": "#/$defs/unixName" + }, + "state": { + "enum": [ + "present", + "absent" + ] + } + } + }, + { + "type": "object", + "additionalProperties": false, + "required": [ + "action", + "hosts", + "user" + ], + "properties": { + "action": { + "const": "container-reset" + }, + "hosts": { + "$ref": "#/$defs/serverList" + }, + "user": { + "$ref": "#/$defs/unixName" + } + } + }, + { + "type": "object", + "additionalProperties": false, + "required": [ + "action", + "hosts", + "user", + "tool" + ], + "properties": { + "action": { + "const": "run-as" + }, + "hosts": { + "$ref": "#/$defs/serverList" + }, + "user": { + "$ref": "#/$defs/unixName" + }, + "tool": { + "type": "string", + "enum": [ + "podman-pull", + "podman-rmi" + ] + }, + "image": { + "type": "string", + "pattern": "^[a-z0-9][a-z0-9._/:@-]{0,199}$" + } + } + }, + { + "type": "object", + "additionalProperties": false, + "required": [ + "action", + "hosts", + "user" + ], + "properties": { + "action": { + "const": "restore-skel" + }, + "hosts": { + "$ref": "#/$defs/serverList" + }, + "user": { + "$ref": "#/$defs/unixName" + }, + "files": { + "type": "array", + "minItems": 1, + "maxItems": 6, + "items": { + "type": "string", + "enum": [ + ".bashrc", + ".bash_profile", + ".bash_logout" + ] + } + } + } } ] }, @@ -819,7 +1345,9 @@ "pattern": "^[a-z0-9_]+_t$" }, "symlinkTo": { - "$ref": "#/$defs/absolutePath" + "description": "Where the link must point, exactly as readlink prints it.", + "type": "string", + "pattern": "^[A-Za-z0-9._@+/{} -]{1,240}$" }, "lines": { "description": "Whole lines that must all be present.", @@ -1734,6 +2262,32 @@ } }, "type": "object" + }, + "hostPath": { + "description": "A path a setup or cleanup action may create or remove on a managed host: only under directories meant for local data and drop-in configuration.", + "type": "string", + "maxLength": 200, + "pattern": "^/(?:srv|opt|mnt|tmp|root|home|var/www|var/tmp|usr/local|etc/(?:httpd/conf\\.d|systemd|auto\\.master\\.d|auto\\.[a-z0-9_-]+|cron\\.d|exports\\.d|sudoers\\.d|profile\\.d|ssh/sshd_config\\.d|chrony\\.d|yum\\.repos\\.d|logrotate\\.d|rsyslog\\.d|security/limits\\.d|sysctl\\.d|NetworkManager/system-connections|containers|httpd/conf\\.d|firewalld/(?:services|zones)))(?:/[A-Za-z0-9_.@%+=:, -]+)*$", + "not": { + "pattern": "(?:^|/)\\.\\.(?:/|$)" + } + }, + "serverName": { + "type": "string", + "pattern": "^server[a-d]$" + }, + "serverList": { + "type": "array", + "minItems": 1, + "maxItems": 4, + "uniqueItems": true, + "items": { + "$ref": "#/$defs/serverName" + } + }, + "unixName": { + "type": "string", + "pattern": "^[a-z_][a-z0-9_-]{0,31}$" } } } diff --git a/scripts/validate-content.mjs b/scripts/validate-content.mjs index 6b73d8e..8ccf601 100644 --- a/scripts/validate-content.mjs +++ b/scripts/validate-content.mjs @@ -100,8 +100,9 @@ for (const program of initial.site.programs) { } if (node.name === 'practice') for (const q of page.data[a.ref].questions) headings.add(`challenge-${q.id}`); if (node.name === 'lab' && a.exercise) { - assert.equal(tags(node.c, 'lab-challenge').length, 1, `${route}: graded lab needs one authored challenge brief`); - assert.equal(tags(node.c, 'lab-notes').length, 1, `${route}: graded lab needs authored prerequisites and verification`); + const expected = a.guided ? 0 : 1; + assert.equal(tags(node.c, 'lab-challenge').length, expected, `${route}: graded lab needs one authored challenge brief (a guided exercise has none)`); + assert.equal(tags(node.c, 'lab-notes').length, expected, `${route}: graded lab needs authored prerequisites and verification (a guided exercise has none)`); labs.add(a.exercise); assert(lab.exercises[a.exercise], `${a.exercise}: no exercise definition`); } From f7c1454d1994a05be9486b3ac9f28a4d6ba286bc Mon Sep 17 00:00:00 2001 From: Ayesh Rodrigo Date: Sun, 4 Oct 2026 11:05:09 +1300 Subject: [PATCH 2/9] Guided exercise labs for chapters 6-9 --- .../03-exercise-superuser.md | 18 +++ .../05-exercise-accounts.md | 25 ++-- .../07-exercise-passwords.md | 22 ++- .../02-exercise-reading.md | 43 ++---- .../04-exercise-changing.md | 40 ++--- .../06-exercise-shared.md | 36 ++--- .../08-exercise-acls.md | 39 ++--- .../ch08-processes/06-exercise-signals.md | 36 ++--- .../ch08-processes/08-exercise-monitoring.md | 22 ++- .../02-exercise-services.md | 18 +++ .../04-exercise-unit-files.md | 37 ++--- .../ch09-systemd-and-boot/06-exercise-boot.md | 18 +++ .../08-exercise-fstab.md | 18 +++ .../rhel9-sysadmin/lab/sa-accounts.yml | 84 +++++++++++ .../programs/rhel9-sysadmin/lab/sa-acls.yml | 125 ++++++++++++++++ .../programs/rhel9-sysadmin/lab/sa-boot.yml | 31 ++++ .../rhel9-sysadmin/lab/sa-changing.yml | 132 +++++++++++++++++ .../programs/rhel9-sysadmin/lab/sa-fstab.yml | 78 ++++++++++ .../rhel9-sysadmin/lab/sa-monitoring.yml | 66 +++++++++ .../rhel9-sysadmin/lab/sa-passwords.yml | 42 ++++++ .../rhel9-sysadmin/lab/sa-reading.yml | 118 +++++++++++++++ .../rhel9-sysadmin/lab/sa-services.yml | 57 +++++++ .../programs/rhel9-sysadmin/lab/sa-shared.yml | 140 ++++++++++++++++++ .../rhel9-sysadmin/lab/sa-signals.yml | 100 +++++++++++++ .../programs/rhel9-sysadmin/lab/sa-sudo.yml | 52 +++++++ .../rhel9-sysadmin/lab/sa-unit-files.yml | 126 ++++++++++++++++ packages/compiler/src/lab.js | 2 +- packages/lab-tools/prepare.py | 25 +++- .../schema/schemas/source/lab.schema.json | 35 ++++- 29 files changed, 1393 insertions(+), 192 deletions(-) create mode 100644 content/programs/rhel9-sysadmin/lab/sa-accounts.yml create mode 100644 content/programs/rhel9-sysadmin/lab/sa-acls.yml create mode 100644 content/programs/rhel9-sysadmin/lab/sa-boot.yml create mode 100644 content/programs/rhel9-sysadmin/lab/sa-changing.yml create mode 100644 content/programs/rhel9-sysadmin/lab/sa-fstab.yml create mode 100644 content/programs/rhel9-sysadmin/lab/sa-monitoring.yml create mode 100644 content/programs/rhel9-sysadmin/lab/sa-passwords.yml create mode 100644 content/programs/rhel9-sysadmin/lab/sa-reading.yml create mode 100644 content/programs/rhel9-sysadmin/lab/sa-services.yml create mode 100644 content/programs/rhel9-sysadmin/lab/sa-shared.yml create mode 100644 content/programs/rhel9-sysadmin/lab/sa-signals.yml create mode 100644 content/programs/rhel9-sysadmin/lab/sa-sudo.yml create mode 100644 content/programs/rhel9-sysadmin/lab/sa-unit-files.yml diff --git a/content/programs/rhel9-sysadmin/chapters/ch06-users-and-groups/03-exercise-superuser.md b/content/programs/rhel9-sysadmin/chapters/ch06-users-and-groups/03-exercise-superuser.md index 31d93ac..512c5ff 100644 --- a/content/programs/rhel9-sysadmin/chapters/ch06-users-and-groups/03-exercise-superuser.md +++ b/content/programs/rhel9-sysadmin/chapters/ch06-users-and-groups/03-exercise-superuser.md @@ -12,9 +12,23 @@ On servera, compare su and sudo, create a user who may not use sudo, then give t objectives=["ch06.superuser"] id="superuser" title="Grant limited root access" + exercise="sa-sudo" + ownExercise=true + guided=true + starter=false hosts=["workstation","servera"] outcomes=["Switch to root with su - and sudo -i.","Write, check and test a narrow sudo rule.","Find sudo's record of allowed and refused commands."] %} + {% task id="task-fa89e230c528" title="Start the exercise" %} + On workstation, start the exercise. It removes a tester account and sudo rule left by an earlier run from servera. + +```console +[student@workstation ~]$ lab start sa-sudo +``` + + Later tasks assume that you are logged in to the server they name: `ssh student@servera` from workstation, then `sudo -i` for a root shell. + {% /task %} + {% task id="task-9afd16d38260" title="Become root with su -, then with sudo -i" %} On servera, as student. root's password in the lab is `redhat`; student's is `student`. @@ -117,4 +131,8 @@ Sorry, user tester is not allowed to execute '/bin/systemctl restart sshd' as ro Each line shows who, from which directory, as whom and what, and whether sudo allowed it. {% /task %} + + {% task id="task-049f8d2e2c27" title="Grade and finish" %} + {% lab-finish exercise="sa-sudo" grade=true servers=true /%} + {% /task %} {% /lab %} diff --git a/content/programs/rhel9-sysadmin/chapters/ch06-users-and-groups/05-exercise-accounts.md b/content/programs/rhel9-sysadmin/chapters/ch06-users-and-groups/05-exercise-accounts.md index 6fb73af..52b16ff 100644 --- a/content/programs/rhel9-sysadmin/chapters/ch06-users-and-groups/05-exercise-accounts.md +++ b/content/programs/rhel9-sysadmin/chapters/ch06-users-and-groups/05-exercise-accounts.md @@ -12,9 +12,23 @@ On servera, create a group for a web team, two people in it, and a service accou objectives=["ch06.accounts","ch06.concepts"] id="accounts" title="Set up a web team" + exercise="sa-accounts" + ownExercise=true + guided=true + starter=false hosts=["workstation","servera"] outcomes=["Create a group with a chosen GID and users with supplementary groups.","Create a service account with nologin.","Recover from replacing a user's groups by mistake."] %} + {% task id="task-4067a94bdade" title="Start the exercise" %} + On workstation, start the exercise. It removes the accounts and the group of an earlier run from servera. + +```console +[student@workstation ~]$ lab start sa-accounts +``` + + Later tasks assume that you are logged in to the server they name: `ssh student@servera` from workstation, then `sudo -i` for a root shell. + {% /task %} + {% task id="task-e9566e8670be" title="Create the group" %} On servera, open a root shell with `sudo -i`, and create the group `webteam` with GID 40000. @@ -95,14 +109,7 @@ drwx------. 2 webapp webteam 4096 Oct 3 10:36 /home/webapp Mode `700` (`drwx------`): each owner, and nobody else, may enter their home. `/home/webapp` belongs to the group `webteam` because that is webapp's primary group. {% /task %} - {% task id="task-4d710a1ff0e6" title="Clean up" %} - Delete the three accounts with their home directories, then the group. If `userdel` reports that a user *is currently used by process* N, a process still runs as that account (for example from an earlier `su`): wait a moment and try again, or reset servera. - -```console -[root@servera ~]# userdel -r alice; userdel -r bob; userdel -r webapp -[root@servera ~]# groupdel webteam -[root@servera ~]# exit -[student@servera ~]$ exit -``` + {% task id="task-16d7cf000ea3" title="Grade and finish" %} + {% lab-finish exercise="sa-accounts" grade=true servers=true /%} {% /task %} {% /lab %} diff --git a/content/programs/rhel9-sysadmin/chapters/ch06-users-and-groups/07-exercise-passwords.md b/content/programs/rhel9-sysadmin/chapters/ch06-users-and-groups/07-exercise-passwords.md index 39ee6fe..6e61aea 100644 --- a/content/programs/rhel9-sysadmin/chapters/ch06-users-and-groups/07-exercise-passwords.md +++ b/content/programs/rhel9-sysadmin/chapters/ch06-users-and-groups/07-exercise-passwords.md @@ -12,9 +12,23 @@ An intern starts on servera today. Give them a temporary password they must chan objectives=["ch06.passwords"] id="passwords" title="Onboard and offboard an intern" + exercise="sa-passwords" + ownExercise=true + guided=true + starter=false hosts=["workstation","servera"] outcomes=["Set a temporary password and force a change at the next login.","Apply password ageing with chage.","See the difference between locking and expiring an account."] %} + {% task id="task-05f7cae6c3c5" title="Start the exercise" %} + On workstation, start the exercise. It removes an intern account left by an earlier run from servera. + +```console +[student@workstation ~]$ lab start sa-passwords +``` + + Later tasks assume that you are logged in to the server they name: `ssh student@servera` from workstation, then `sudo -i` for a root shell. + {% /task %} + {% task id="task-f7644ce3f4a5" title="Create the account with a temporary password" %} On servera as root (`sudo -i`), create `intern` ("Summer intern"), set the password `Welcome-2026`, and force a change at the next login. @@ -100,11 +114,7 @@ su: User account has expired Unlike a lock, expiry blocks every kind of login, including SSH keys. The account and its files remain, for the handover. {% /task %} - {% task id="task-8faa4ecab9f2" title="Clean up" %} - -```console -[student@servera ~]$ sudo userdel -r intern -[student@servera ~]$ exit -``` + {% task id="task-a28b7e5c2b0f" title="Grade and finish" %} + {% lab-finish exercise="sa-passwords" grade=true servers=true /%} {% /task %} {% /lab %} diff --git a/content/programs/rhel9-sysadmin/chapters/ch07-permissions-and-acls/02-exercise-reading.md b/content/programs/rhel9-sysadmin/chapters/ch07-permissions-and-acls/02-exercise-reading.md index 6c9d3fd..ceeb1b0 100644 --- a/content/programs/rhel9-sysadmin/chapters/ch07-permissions-and-acls/02-exercise-reading.md +++ b/content/programs/rhel9-sysadmin/chapters/ch07-permissions-and-acls/02-exercise-reading.md @@ -12,30 +12,27 @@ Predict, then test. You will set up three users and a report file, work out for objectives=["ch07.reading"] id="reading" title="Who can read this?" + exercise="sa-reading" + ownExercise=true + guided=true + starter=false hosts=["workstation","servera"] outcomes=["Read owner, group and other permissions and predict access.","See that directory permissions decide who can list, enter and delete.","Use namei -l to find a blocked path."] %} - {% task id="task-39ebad7f9c3a" title="Create the users and the file" %} - On servera, open a root shell with `sudo -i`. Create the group `project` (GID 4500) and users `maria` and `john`, both in `project`, and `priya`, who is not. Then create `/srv/lab7/report.txt` containing a line of text, owned by `maria:project`, mode 640. + {% task id="task-8f0c9c3dd818" title="Start the exercise" %} + On workstation, start the exercise. It creates the group project (4500), the users maria, john and priya, and `/srv/lab7/report.txt` (owned by maria, group project, mode 640). - {% reveal title="Show solution" %} +```console +[student@workstation ~]$ lab start sa-reading +``` + + Later tasks assume that you are logged in to the server they name: `ssh student@servera` from workstation, then `sudo -i` for a root shell. + + Then, as root on servera, change to the exercise directory: ```console -[student@workstation ~]$ ssh student@servera -[student@servera ~]$ sudo -i -[root@servera ~]# groupadd -g 4500 project -[root@servera ~]# useradd -m -G project maria -[root@servera ~]# useradd -m -G project john -[root@servera ~]# useradd -m priya -[root@servera ~]# mkdir /srv/lab7 [root@servera ~]# cd /srv/lab7 -[root@servera lab7]# echo "Q3 figures" > report.txt -[root@servera lab7]# chown maria:project report.txt -[root@servera lab7]# chmod 640 report.txt -[root@servera lab7]# ls -l report.txt --rw-r-----. 1 maria project 11 Oct 3 16:11 report.txt ``` - {% /reveal %} {% /task %} {% task id="task-6a8712ce4075" title="Predict and test" %} @@ -132,17 +129,7 @@ dr-xr-xr-x maria root box {% /reveal %} {% /task %} - {% task id="task-b71d77a9b856" title="Clean up" %} - - If `userdel` says a user is still in use, a login session from `su` has not closed yet: wait a few seconds and run it again. - -```console -[root@servera lab7]# cd -[root@servera ~]# rm -rf /srv/lab7 -[root@servera ~]# userdel -r maria; userdel -r john; userdel -r priya -[root@servera ~]# groupdel project -[root@servera ~]# exit -[student@servera ~]$ exit -``` + {% task id="task-547ea3f80e70" title="Grade and finish" %} + {% lab-finish exercise="sa-reading" grade=true servers=true /%} {% /task %} {% /lab %} diff --git a/content/programs/rhel9-sysadmin/chapters/ch07-permissions-and-acls/04-exercise-changing.md b/content/programs/rhel9-sysadmin/chapters/ch07-permissions-and-acls/04-exercise-changing.md index 658611b..fb26c20 100644 --- a/content/programs/rhel9-sysadmin/chapters/ch07-permissions-and-acls/04-exercise-changing.md +++ b/content/programs/rhel9-sysadmin/chapters/ch07-permissions-and-acls/04-exercise-changing.md @@ -12,32 +12,21 @@ A small website lives in `/srv/site`. Its owner maria and the group `webteam` ne objectives=["ch07.changing"] id="changing" title="Secure a web folder" + exercise="sa-changing" + ownExercise=true + guided=true + starter=false hosts=["workstation","servera"] outcomes=["Change owner and group recursively with chown -R.","Set modes with symbols, octal numbers and the capital X.","Verify access by acting as different users."] %} - {% task id="task-d9cf5dfc07d5" title="Build the tree" %} - On servera, open a root shell with `sudo -i`. Create the group `webteam` (GID 4600), the users `maria` (in `webteam`) and `john` (not in it), and the tree `/srv/site/html/index.html` plus `/srv/site/bin/deploy.sh`, using `mkdir -p` and `touch`. List it with `ls -lR /srv/site`. - - {% reveal title="Show solution" %} + {% task id="task-c43425e35dd2" title="Start the exercise" %} + On workstation, start the exercise. It creates the group webteam (4600), the users maria and john, and the tree `/srv/site` with an empty `index.html` and `deploy.sh`, all owned by root. ```console -[student@workstation ~]$ ssh student@servera -[student@servera ~]$ sudo -i -[root@servera ~]# groupadd -g 4600 webteam -[root@servera ~]# useradd -m -G webteam maria -[root@servera ~]# useradd -m john -[root@servera ~]# mkdir -p /srv/site/html /srv/site/bin -[root@servera ~]# touch /srv/site/html/index.html /srv/site/bin/deploy.sh -[root@servera ~]# ls -lR /srv/site -/srv/site: -total 8 -drwxr-xr-x. 2 root root 4096 Oct 3 16:12 bin -drwxr-xr-x. 2 root root 4096 Oct 3 16:12 html -...output omitted... +[student@workstation ~]$ lab start sa-changing ``` - Everything belongs to root and is readable by everyone. That is what we will change. - {% /reveal %} + Later tasks assume that you are logged in to the server they name: `ssh student@servera` from workstation, then `sudo -i` for a root shell. {% /task %} {% task id="task-0bd717699754" title="Give it to maria and webteam" %} @@ -133,16 +122,7 @@ ok {% /reveal %} {% /task %} - {% task id="task-844e07e9a91b" title="Clean up" %} - - If `userdel` says a user is still in use, a login session from `su` has not closed yet: wait a few seconds and run it again. - -```console -[root@servera ~]# rm -rf /srv/site -[root@servera ~]# userdel -r maria; userdel -r john -[root@servera ~]# groupdel webteam -[root@servera ~]# exit -[student@servera ~]$ exit -``` + {% task id="task-31b287046207" title="Grade and finish" %} + {% lab-finish exercise="sa-changing" grade=true servers=true /%} {% /task %} {% /lab %} diff --git a/content/programs/rhel9-sysadmin/chapters/ch07-permissions-and-acls/06-exercise-shared.md b/content/programs/rhel9-sysadmin/chapters/ch07-permissions-and-acls/06-exercise-shared.md index 32caf34..a2b8fbe 100644 --- a/content/programs/rhel9-sysadmin/chapters/ch07-permissions-and-acls/06-exercise-shared.md +++ b/content/programs/rhel9-sysadmin/chapters/ch07-permissions-and-acls/06-exercise-shared.md @@ -12,28 +12,21 @@ Two shared spaces on servera: a team directory where `maria` and `john` work on objectives=["ch07.defaults"] id="shared" title="Team directory and drop box" + exercise="sa-shared" + ownExercise=true + guided=true + starter=false hosts=["workstation","servera"] outcomes=["Create a setgid directory so new files inherit the team group.","Use the umask so team members can write each other's files.","Protect a public directory with the sticky bit."] %} - {% task id="task-5339c3f48094" title="Create the people and the team directory" %} - On servera as root (`sudo -i`), create the group `team` (GID 4700); users `maria` and `john` in `team`; and `priya`, who is not. Then create `/srv/team` owned by `root:team` with mode 2770, in a single command. - - {% reveal title="Show solution" %} + {% task id="task-213d49a6a335" title="Start the exercise" %} + On workstation, start the exercise. It creates the group team (4700), the users maria, john and priya, and the directory `/srv/team` (root:team, mode 2770). ```console -[student@workstation ~]$ ssh student@servera -[student@servera ~]$ sudo -i -[root@servera ~]# groupadd -g 4700 team -[root@servera ~]# useradd -m -G team maria -[root@servera ~]# useradd -m -G team john -[root@servera ~]# useradd -m priya -[root@servera ~]# install -d -o root -g team -m 2770 /srv/team -[root@servera ~]# ls -ld /srv/team -drwxrws---. 2 root team 4096 Oct 3 16:13 /srv/team +[student@workstation ~]$ lab start sa-shared ``` - `install -d` creates a directory and sets its owner, group and mode together. - {% /reveal %} + Later tasks assume that you are logged in to the server they name: `ssh student@servera` from workstation, then `sudo -i` for a root shell. {% /task %} {% task id="task-b56b62e149ac" title="See the group inherited, but not group write" %} @@ -114,16 +107,7 @@ priya.txt {% /reveal %} {% /task %} - {% task id="task-89d73f7b02cf" title="Clean up" %} - - If `userdel` says a user is still in use, a login session from `su` has not closed yet: wait a few seconds and run it again. - -```console -[root@servera ~]# rm -rf /srv/team /srv/dropbox -[root@servera ~]# userdel -r maria; userdel -r john; userdel -r priya -[root@servera ~]# groupdel team -[root@servera ~]# exit -[student@servera ~]$ exit -``` + {% task id="task-771aac8cd855" title="Grade and finish" %} + {% lab-finish exercise="sa-shared" grade=true servers=true /%} {% /task %} {% /lab %} diff --git a/content/programs/rhel9-sysadmin/chapters/ch07-permissions-and-acls/08-exercise-acls.md b/content/programs/rhel9-sysadmin/chapters/ch07-permissions-and-acls/08-exercise-acls.md index 0c5a0e4..5e2219d 100644 --- a/content/programs/rhel9-sysadmin/chapters/ch07-permissions-and-acls/08-exercise-acls.md +++ b/content/programs/rhel9-sysadmin/chapters/ch07-permissions-and-acls/08-exercise-acls.md @@ -12,29 +12,21 @@ maria's quarterly report is private. The auditors must be able to read it, one n objectives=["ch07.acls"] id="acls" title="Give the auditors read access" + exercise="sa-acls" + ownExercise=true + guided=true + starter=false hosts=["workstation","servera"] outcomes=["Grant a group read access to one file with setfacl.","Explain an effective permission cut by the mask.","Set a default ACL so new files inherit access."] %} - {% task id="task-bbcafb964453" title="Create the private report" %} - On servera as root (`sudo -i`), create the group `auditors` (GID 4800), the users `priya` (in `auditors`), `maria` and `john`, and the files `/srv/reports/q3/summary.txt` owned by maria. Make the whole tree private to maria and confirm that priya is refused. - - {% reveal title="Show solution" %} + {% task id="task-fd5a39e536f0" title="Start the exercise" %} + On workstation, start the exercise. It creates the group auditors (4800), the users priya (an auditor), maria and john, and the private report `/srv/reports/q3/summary.txt` that only maria can read. ```console -[student@workstation ~]$ ssh student@servera -[student@servera ~]$ sudo -i -[root@servera ~]# groupadd -g 4800 auditors -[root@servera ~]# useradd -m -G auditors priya -[root@servera ~]# useradd -m maria -[root@servera ~]# useradd -m john -[root@servera ~]# mkdir -p /srv/reports/q3 -[root@servera ~]# echo figures > /srv/reports/q3/summary.txt -[root@servera ~]# chown -R maria:maria /srv/reports -[root@servera ~]# chmod -R u=rwX,g=,o= /srv/reports -[root@servera ~]# su - priya -c 'cat /srv/reports/q3/summary.txt' -cat: /srv/reports/q3/summary.txt: Permission denied +[student@workstation ~]$ lab start sa-acls ``` - {% /reveal %} + + Later tasks assume that you are logged in to the server they name: `ssh student@servera` from workstation, then `sudo -i` for a root shell. {% /task %} {% task id="task-ea7177a7713e" title="Grant the group read access" %} @@ -152,16 +144,7 @@ drwx------. 2 maria maria 4096 Oct 3 16:14 /srv/reports/q3 {% /reveal %} {% /task %} - {% task id="task-5226ce8b142c" title="Clean up" %} - - If `userdel` says a user is still in use, a login session from `su` has not closed yet: wait a few seconds and run it again. - -```console -[root@servera ~]# rm -rf /srv/reports -[root@servera ~]# userdel -r priya; userdel -r maria; userdel -r john -[root@servera ~]# groupdel auditors -[root@servera ~]# exit -[student@servera ~]$ exit -``` + {% task id="task-c35a2b6e4c9d" title="Grade and finish" %} + {% lab-finish exercise="sa-acls" grade=true servers=true /%} {% /task %} {% /lab %} diff --git a/content/programs/rhel9-sysadmin/chapters/ch08-processes/06-exercise-signals.md b/content/programs/rhel9-sysadmin/chapters/ch08-processes/06-exercise-signals.md index 0d4c7a3..e3132fb 100644 --- a/content/programs/rhel9-sysadmin/chapters/ch08-processes/06-exercise-signals.md +++ b/content/programs/rhel9-sysadmin/chapters/ch08-processes/06-exercise-signals.md @@ -12,27 +12,21 @@ Compare a clean shutdown with a forced one, pause and resume a CPU hog, and end objectives=["ch08.signals"] id="signals" title="Stop a runaway process the right way" + exercise="sa-signals" + ownExercise=true + guided=true + starter=false hosts=["workstation","servera"] outcomes=["Send SIGTERM, SIGKILL, SIGSTOP and SIGCONT and see the difference.","Preview matches with pgrep before using pkill.","End all of one user's processes."] %} - {% task id="task-ad7d36659974" title="Write a program that cleans up" %} - On servera, create `~/cleanup.sh` with the content below, and make it executable. It creates a lock file and removes it when it receives SIGTERM. - -```bash -#!/bin/bash -trap 'echo "cleaning up"; rm -f /tmp/work.lock; exit 0' TERM -touch /tmp/work.lock -while true; do sleep 1; done -``` - - {% reveal title="Show solution" %} + {% task id="task-3c38194bb33c" title="Start the exercise" %} + On workstation, start the exercise. It puts the script `~/cleanup.sh` (a loop that creates `/tmp/work.lock` and removes it when it receives SIGTERM) in student's home on servera. ```console -[student@workstation ~]$ ssh student@servera -[student@servera ~]$ vim cleanup.sh -[student@servera ~]$ chmod +x cleanup.sh +[student@workstation ~]$ lab start sa-signals ``` - {% /reveal %} + + Later tasks assume that you are logged in to the server they name: `ssh student@servera` from workstation, then `sudo -i` for a root shell. {% /task %} {% task id="task-8dac31281941" title="Terminate politely" %} @@ -122,15 +116,7 @@ ls: cannot access '/tmp/work.lock': No such file or directory {% /reveal %} {% /task %} - {% task id="task-01dbf4b6e00c" title="Clean up" %} - -```console -[root@servera ~]# userdel -r bob -[root@servera ~]# exit -[student@servera ~]$ rm -f cleanup.sh -[student@servera ~]$ exit -``` - - If `userdel` says bob is still in use, wait a moment and run it again. + {% task id="task-0f55f19ffce0" title="Grade and finish" %} + {% lab-finish exercise="sa-signals" grade=true servers=true /%} {% /task %} {% /lab %} diff --git a/content/programs/rhel9-sysadmin/chapters/ch08-processes/08-exercise-monitoring.md b/content/programs/rhel9-sysadmin/chapters/ch08-processes/08-exercise-monitoring.md index 6478eff..cc0dc58 100644 --- a/content/programs/rhel9-sysadmin/chapters/ch08-processes/08-exercise-monitoring.md +++ b/content/programs/rhel9-sysadmin/chapters/ch08-processes/08-exercise-monitoring.md @@ -12,9 +12,23 @@ Two CPU-hungry processes are fighting for servera's CPU. Find them, watch how `n objectives=["ch08.monitoring"] id="monitoring" title="Find the slowdown and tune the server" + exercise="sa-monitoring" + ownExercise=true + guided=true + starter=false hosts=["workstation","servera"] outcomes=["Read load average, top and free.","Compare a normal and a nice process.","Install tuned and switch profiles."] %} + {% task id="task-4c53c1c672e9" title="Start the exercise" %} + On workstation, start the exercise. It makes sure the tuned package is not installed yet on servera. + +```console +[student@workstation ~]$ lab start sa-monitoring +``` + + Later tasks assume that you are logged in to the server they name: `ssh student@servera` from workstation, then `sudo -i` for a root shell. + {% /task %} + {% task id="task-e65f5036b47e" title="Take the baseline" %} On servera, record the load average, the number of CPUs and the memory figures before you start any load. @@ -123,11 +137,7 @@ Current active profile: virtual-guest {% /reveal %} {% /task %} - {% task id="task-e5d7824a63af" title="Clean up" %} - -```console -[student@servera ~]$ sudo dnf remove -y tuned -[student@servera ~]$ exit -``` + {% task id="task-784c0dd399df" title="Grade and finish" %} + {% lab-finish exercise="sa-monitoring" grade=true servers=true /%} {% /task %} {% /lab %} diff --git a/content/programs/rhel9-sysadmin/chapters/ch09-systemd-and-boot/02-exercise-services.md b/content/programs/rhel9-sysadmin/chapters/ch09-systemd-and-boot/02-exercise-services.md index 07da9f6..84b1a6a 100644 --- a/content/programs/rhel9-sysadmin/chapters/ch09-systemd-and-boot/02-exercise-services.md +++ b/content/programs/rhel9-sysadmin/chapters/ch09-systemd-and-boot/02-exercise-services.md @@ -12,9 +12,23 @@ Take the scheduler service `crond` through all of its states, and see exactly wh objectives=["ch09.services"] id="services" title="Control a service" + exercise="sa-services" + ownExercise=true + guided=true + starter=false hosts=["workstation","servera"] outcomes=["Read systemctl status and the is-active and is-enabled answers.","Separate start/stop from enable/disable.","Mask and unmask a service."] %} + {% task id="task-d20cf68d9eb5" title="Start the exercise" %} + On workstation, start the exercise. It makes sure crond runs and is enabled, and that bluetooth is not masked. + +```console +[student@workstation ~]$ lab start sa-services +``` + + Later tasks assume that you are logged in to the server they name: `ssh student@servera` from workstation, then `sudo -i` for a root shell. + {% /task %} + {% task id="task-44a476ebef43" title="Read the status" %} On servera, open a root shell with `sudo -i`. Show the status of `crond`. What are its main PID, its state, and its enabled state? Then ask the two short questions. @@ -126,4 +140,8 @@ Removed "/etc/systemd/system/bluetooth.service". ``` {% /reveal %} {% /task %} + + {% task id="task-a2c0be26893e" title="Grade and finish" %} + {% lab-finish exercise="sa-services" grade=true servers=true /%} + {% /task %} {% /lab %} diff --git a/content/programs/rhel9-sysadmin/chapters/ch09-systemd-and-boot/04-exercise-unit-files.md b/content/programs/rhel9-sysadmin/chapters/ch09-systemd-and-boot/04-exercise-unit-files.md index a733ae5..3ba4426 100644 --- a/content/programs/rhel9-sysadmin/chapters/ch09-systemd-and-boot/04-exercise-unit-files.md +++ b/content/programs/rhel9-sysadmin/chapters/ch09-systemd-and-boot/04-exercise-unit-files.md @@ -12,28 +12,21 @@ Turn a small shell script into a proper service: write the unit, start it, watch objectives=["ch09.units"] id="units" title="Write your own service" + exercise="sa-unit-files" + ownExercise=true + guided=true + starter=false hosts=["workstation","servera"] outcomes=["Write a service unit and enable it.","See Restart=on-failure bring a killed service back.","Override a setting with a drop-in and diagnose status 203."] %} - {% task id="task-0a43b748f06a" title="Create the program" %} - On servera as root (`sudo -i`), create `/usr/local/bin/reporter.sh` that appends `HH:MM:SS reporter alive` to `/var/log/reporter.log` every `${INTERVAL:-10}` seconds, and make it executable. - - {% reveal title="Show solution" %} + {% task id="task-fdd776e1cca3" title="Start the exercise" %} + On workstation, start the exercise. It installs the program `/usr/local/bin/reporter.sh` on servera: a loop that appends a heartbeat line to `/var/log/reporter.log` every `$INTERVAL` seconds (default 10). ```console -[student@workstation ~]$ ssh student@servera -[student@servera ~]$ sudo -i -[root@servera ~]# vim /usr/local/bin/reporter.sh -[root@servera ~]# cat /usr/local/bin/reporter.sh -#!/bin/bash -# Append a timestamp to the report every few seconds. -while true; do - echo "$(date +%T) reporter alive" >> /var/log/reporter.log - sleep "${INTERVAL:-10}" -done -[root@servera ~]# chmod 755 /usr/local/bin/reporter.sh +[student@workstation ~]$ lab start sa-unit-files ``` - {% /reveal %} + + Later tasks assume that you are logged in to the server they name: `ssh student@servera` from workstation, then `sudo -i` for a root shell. {% /task %} {% task id="task-6b3bbe70e5ef" title="Write the unit and start it" %} @@ -142,15 +135,7 @@ active {% /reveal %} {% /task %} - {% task id="task-0f4394e6e68d" title="Clean up" %} - -```console -[root@servera ~]# systemctl disable --now reporter -Removed "/etc/systemd/system/multi-user.target.wants/reporter.service". -[root@servera ~]# rm -rf /etc/systemd/system/reporter.service /etc/systemd/system/reporter.service.d /usr/local/bin/reporter.sh /var/log/reporter.log -[root@servera ~]# systemctl daemon-reload -[root@servera ~]# exit -[student@servera ~]$ exit -``` + {% task id="task-afe68587c470" title="Grade and finish" %} + {% lab-finish exercise="sa-unit-files" grade=true servers=true /%} {% /task %} {% /lab %} diff --git a/content/programs/rhel9-sysadmin/chapters/ch09-systemd-and-boot/06-exercise-boot.md b/content/programs/rhel9-sysadmin/chapters/ch09-systemd-and-boot/06-exercise-boot.md index 0dee097..bcb061e 100644 --- a/content/programs/rhel9-sysadmin/chapters/ch09-systemd-and-boot/06-exercise-boot.md +++ b/content/programs/rhel9-sysadmin/chapters/ch09-systemd-and-boot/06-exercise-boot.md @@ -12,9 +12,23 @@ Make servera boot to the text-mode target, add a kernel argument, reboot, and pr objectives=["ch09.boot"] id="boot" title="Change how the server boots" + exercise="sa-boot" + ownExercise=true + guided=true + starter=false hosts=["workstation","servera"] outcomes=["Set the default target and check it after a reboot.","Add and remove a kernel argument with grubby.","Read boot timing with systemd-analyze."] %} + {% task id="task-7fe20de7b383" title="Start the exercise" %} + On workstation, start the exercise. It makes sure servera starts from the default target graphical.target without the extra kernel argument. + +```console +[student@workstation ~]$ lab start sa-boot +``` + + Later tasks assume that you are logged in to the server they name: `ssh student@servera` from workstation, then `sudo -i` for a root shell. + {% /task %} + {% task id="task-05d504378a68" title="Record the starting point" %} On servera as root (`sudo -i`), show the default target, the kernel command line and the boot time. @@ -107,4 +121,8 @@ args="console=tty1 console=ttyS0 ro" ``` {% /reveal %} {% /task %} + + {% task id="task-e50569b8f094" title="Grade and finish" %} + {% lab-finish exercise="sa-boot" grade=true servers=true /%} + {% /task %} {% /lab %} diff --git a/content/programs/rhel9-sysadmin/chapters/ch09-systemd-and-boot/08-exercise-fstab.md b/content/programs/rhel9-sysadmin/chapters/ch09-systemd-and-boot/08-exercise-fstab.md index 9998056..ce2e4c1 100644 --- a/content/programs/rhel9-sysadmin/chapters/ch09-systemd-and-boot/08-exercise-fstab.md +++ b/content/programs/rhel9-sysadmin/chapters/ch09-systemd-and-boot/08-exercise-fstab.md @@ -12,9 +12,23 @@ You add a mount for a disk that is not there yet. Learn to catch the mistake wit objectives=["ch09.recovery"] id="fstab" title="Check /etc/fstab before you reboot" + exercise="sa-fstab" + ownExercise=true + guided=true + starter=false hosts=["workstation","servera"] outcomes=["Verify an fstab file without rebooting.","Use mount -a to test entries.","Understand what nofail changes."] %} + {% task id="task-8cee2fe9aef7" title="Start the exercise" %} + On workstation, start the exercise. It removes a test mount from `/etc/fstab` if an earlier run left one. + +```console +[student@workstation ~]$ lab start sa-fstab +``` + + Later tasks assume that you are logged in to the server they name: `ssh student@servera` from workstation, then `sudo -i` for a root shell. + {% /task %} + {% task id="task-bc9d2b67d349" title="Back up and look" %} On servera as root, copy `/etc/fstab` to `/root/fstab.bak` and show its entries without comments. @@ -104,4 +118,8 @@ status=0 [student@servera ~]$ exit ``` {% /task %} + + {% task id="task-eaa4e2a36bd1" title="Grade and finish" %} + {% lab-finish exercise="sa-fstab" grade=true servers=true /%} + {% /task %} {% /lab %} diff --git a/content/programs/rhel9-sysadmin/lab/sa-accounts.yml b/content/programs/rhel9-sysadmin/lab/sa-accounts.yml new file mode 100644 index 0000000..32510e4 --- /dev/null +++ b/content/programs/rhel9-sysadmin/lab/sa-accounts.yml @@ -0,0 +1,84 @@ +name: sa-accounts +title: Set up a web team +page: ch06/exercise-accounts +transport: ssh +version: 1 +starter: [] +setup: +- action: user + hosts: + - servera + name: alice + state: absent +- action: user + hosts: + - servera + name: bob + state: absent +- action: user + hosts: + - servera + name: webapp + state: absent +- action: group + hosts: + - servera + name: webteam + state: absent +checkpoints: + final: + files: [] + checks: + - id: group + kind: file + 'on': servera.lab.example.com + targets: + - servera.lab.example.com + message: the group webteam has GID 40000 + paths: + - /etc/group + contains: + - 'webteam:x:40000:' + - id: members + kind: user + 'on': servera.lab.example.com + targets: + - servera.lab.example.com + message: alice and bob are members of webteam + names: + - alice + - bob + groups: + - webteam + - id: service + kind: user + 'on': servera.lab.example.com + targets: + - servera.lab.example.com + message: webapp is a login-less service account in webteam + names: + - webapp + uid: 4000 + primaryGroup: webteam + shell: /sbin/nologin +finish: +- action: user + hosts: + - servera + name: alice + state: absent +- action: user + hosts: + - servera + name: bob + state: absent +- action: user + hosts: + - servera + name: webapp + state: absent +- action: group + hosts: + - servera + name: webteam + state: absent diff --git a/content/programs/rhel9-sysadmin/lab/sa-acls.yml b/content/programs/rhel9-sysadmin/lab/sa-acls.yml new file mode 100644 index 0000000..c345cbb --- /dev/null +++ b/content/programs/rhel9-sysadmin/lab/sa-acls.yml @@ -0,0 +1,125 @@ +name: sa-acls +title: Give the auditors read access +page: ch07/exercise-acls +transport: ssh +version: 1 +starter: [] +setup: +- action: group + hosts: + - servera + name: auditors + state: present + gid: 4800 +- action: user + hosts: + - servera + name: priya + state: present + groups: + - auditors +- action: user + hosts: + - servera + name: maria + state: present +- action: user + hosts: + - servera + name: john + state: present +- action: directory + hosts: + - servera + path: /srv/reports + state: present + owner: maria + group: maria + mode: '700' +- action: directory + hosts: + - servera + path: /srv/reports/q3 + state: present + owner: maria + group: maria + mode: '700' +- action: file + hosts: + - servera + path: /srv/reports/q3/summary.txt + state: present + content: 'figures + + ' + owner: maria + group: maria + mode: '600' +checkpoints: + final: + files: [] + checks: + - id: summary + kind: file + 'on': servera.lab.example.com + targets: + - servera.lab.example.com + message: the auditors group can read summary.txt through an ACL + paths: + - /srv/reports/q3/summary.txt + acl: + - group:auditors:r-- + - id: reports + kind: file + 'on': servera.lab.example.com + targets: + - servera.lab.example.com + message: auditors can traverse /srv/reports + paths: + - /srv/reports + acl: + - group:auditors:r-x + - id: after + kind: file + 'on': servera.lab.example.com + targets: + - servera.lab.example.com + message: after.txt was created by maria + paths: + - /srv/reports/q3/after.txt + owner: maria + - id: before + kind: file + 'on': servera.lab.example.com + targets: + - servera.lab.example.com + message: before.txt was created by maria + paths: + - /srv/reports/q3/before.txt + owner: maria +finish: +- action: directory + hosts: + - servera + path: /srv/reports + state: absent +- action: user + hosts: + - servera + name: priya + state: absent +- action: user + hosts: + - servera + name: maria + state: absent +- action: user + hosts: + - servera + name: john + state: absent +- action: group + hosts: + - servera + name: auditors + state: absent diff --git a/content/programs/rhel9-sysadmin/lab/sa-boot.yml b/content/programs/rhel9-sysadmin/lab/sa-boot.yml new file mode 100644 index 0000000..ea35392 --- /dev/null +++ b/content/programs/rhel9-sysadmin/lab/sa-boot.yml @@ -0,0 +1,31 @@ +name: sa-boot +title: Change how the server boots +page: ch09/exercise-boot +transport: ssh +version: 1 +starter: [] +setup: +- action: boot + hosts: + - servera + target: graphical.target + removeKernelArgs: + - loglevel=5 +checkpoints: + final: + files: [] + checks: + - id: target + kind: boot-target + 'on': servera.lab.example.com + targets: + - servera.lab.example.com + message: the default target is graphical.target again + target: graphical.target +finish: +- action: boot + hosts: + - servera + target: graphical.target + removeKernelArgs: + - loglevel=5 diff --git a/content/programs/rhel9-sysadmin/lab/sa-changing.yml b/content/programs/rhel9-sysadmin/lab/sa-changing.yml new file mode 100644 index 0000000..5a7a711 --- /dev/null +++ b/content/programs/rhel9-sysadmin/lab/sa-changing.yml @@ -0,0 +1,132 @@ +name: sa-changing +title: Secure a web folder +page: ch07/exercise-changing +transport: ssh +version: 1 +starter: [] +setup: +- action: group + hosts: + - servera + name: webteam + state: present + gid: 4600 +- action: user + hosts: + - servera + name: maria + state: present + groups: + - webteam +- action: user + hosts: + - servera + name: john + state: present +- action: directory + hosts: + - servera + path: /srv/site/html + state: present +- action: directory + hosts: + - servera + path: /srv/site/bin + state: present +- action: file + hosts: + - servera + path: /srv/site/html/index.html + state: present + content: '' +- action: file + hosts: + - servera + path: /srv/site/bin/deploy.sh + state: present + content: '' +checkpoints: + final: + files: [] + checks: + - id: owner + kind: file + 'on': servera.lab.example.com + targets: + - servera.lab.example.com + message: the tree belongs to maria:webteam + paths: + - /srv/site/html/index.html + owner: maria + group: webteam + - id: page + kind: file + 'on': servera.lab.example.com + targets: + - servera.lab.example.com + message: 'index.html is 664: the group may edit, others read' + paths: + - /srv/site/html/index.html + mode: '0664' + - id: script + kind: file + 'on': servera.lab.example.com + targets: + - servera.lab.example.com + message: deploy.sh is mode 750 and holds the command + paths: + - /srv/site/bin/deploy.sh + mode: '0750' + contains: + - echo deployed + - id: dirs + kind: file + 'on': servera.lab.example.com + targets: + - servera.lab.example.com + message: site and html are traversable by others (755) + paths: + - /srv/site + fileType: directory + mode: '0755' + - id: html + kind: file + 'on': servera.lab.example.com + targets: + - servera.lab.example.com + message: html is traversable by others (755) + paths: + - /srv/site/html + fileType: directory + mode: '0755' + - id: bin + kind: file + 'on': servera.lab.example.com + targets: + - servera.lab.example.com + message: bin stays closed to others (750) + paths: + - /srv/site/bin + fileType: directory + mode: '0750' +finish: +- action: directory + hosts: + - servera + path: /srv/site + state: absent +- action: user + hosts: + - servera + name: maria + state: absent +- action: user + hosts: + - servera + name: john + state: absent +- action: group + hosts: + - servera + name: webteam + state: absent diff --git a/content/programs/rhel9-sysadmin/lab/sa-fstab.yml b/content/programs/rhel9-sysadmin/lab/sa-fstab.yml new file mode 100644 index 0000000..42b9af3 --- /dev/null +++ b/content/programs/rhel9-sysadmin/lab/sa-fstab.yml @@ -0,0 +1,78 @@ +name: sa-fstab +title: Check /etc/fstab before you reboot +page: ch09/exercise-fstab +transport: ssh +version: 1 +starter: [] +setup: +- action: remove-lines + hosts: + - servera + path: /etc/fstab + matching: ^/dev/sdb1 +- action: directory + hosts: + - servera + path: /data + state: absent +- action: file + hosts: + - servera + path: /root/fstab.bak + state: absent +- action: systemd + hosts: + - servera + reload: true +checkpoints: + final: + files: [] + checks: + - id: fstab + kind: file + 'on': servera.lab.example.com + targets: + - servera.lab.example.com + message: /etc/fstab has no /dev/sdb1 line any more + paths: + - /etc/fstab + lacks: + - /dev/sdb1 + - id: mountpoint + kind: file + 'on': servera.lab.example.com + targets: + - servera.lab.example.com + message: /data is gone again + paths: + - /data + exists: false + - id: backup + kind: file + 'on': servera.lab.example.com + targets: + - servera.lab.example.com + message: the backup /root/fstab.bak was removed + paths: + - /root/fstab.bak + exists: false +finish: +- action: remove-lines + hosts: + - servera + path: /etc/fstab + matching: ^/dev/sdb1 +- action: directory + hosts: + - servera + path: /data + state: absent +- action: file + hosts: + - servera + path: /root/fstab.bak + state: absent +- action: systemd + hosts: + - servera + reload: true diff --git a/content/programs/rhel9-sysadmin/lab/sa-monitoring.yml b/content/programs/rhel9-sysadmin/lab/sa-monitoring.yml new file mode 100644 index 0000000..7c357f6 --- /dev/null +++ b/content/programs/rhel9-sysadmin/lab/sa-monitoring.yml @@ -0,0 +1,66 @@ +name: sa-monitoring +title: Find the slowdown and tune the server +page: ch08/exercise-monitoring +transport: ssh +version: 1 +starter: [] +setup: +- action: package + hosts: + - servera + names: + - tuned + state: absent +checkpoints: + final: + files: [] + checks: + - id: tuned + kind: package + 'on': servera.lab.example.com + targets: + - servera.lab.example.com + message: tuned is installed + names: + - tuned + - id: service + kind: service + 'on': servera.lab.example.com + targets: + - servera.lab.example.com + message: tuned is running and enabled + names: + - tuned.service + active: true + enabled: true + - id: profile + kind: file + 'on': servera.lab.example.com + targets: + - servera.lab.example.com + message: a tuned profile is active + paths: + - /etc/tuned/active_profile + matches: ^[a-z-]+$ + - id: no-hog + kind: process + 'on': servera.lab.example.com + targets: + - servera.lab.example.com + message: the sha256sum hogs were ended + pattern: ^sha256sum /dev/zero + running: false +finish: +- action: service + hosts: + - servera + names: + - tuned + state: stopped + enabled: false +- action: package + hosts: + - servera + names: + - tuned + state: absent diff --git a/content/programs/rhel9-sysadmin/lab/sa-passwords.yml b/content/programs/rhel9-sysadmin/lab/sa-passwords.yml new file mode 100644 index 0000000..2e9e85f --- /dev/null +++ b/content/programs/rhel9-sysadmin/lab/sa-passwords.yml @@ -0,0 +1,42 @@ +name: sa-passwords +title: Onboard and offboard an intern +page: ch06/exercise-passwords +transport: ssh +version: 1 +starter: [] +setup: +- action: user + hosts: + - servera + name: intern + state: absent +checkpoints: + final: + files: [] + checks: + - id: intern + kind: user + 'on': servera.lab.example.com + targets: + - servera.lab.example.com + message: intern exists with the aging policy 90/1/7 + names: + - intern + maxDays: 90 + minDays: 1 + warnDays: 7 + - id: expired + kind: user + 'on': servera.lab.example.com + targets: + - servera.lab.example.com + message: the account is expired + names: + - intern + expires: '1970-01-01' +finish: +- action: user + hosts: + - servera + name: intern + state: absent diff --git a/content/programs/rhel9-sysadmin/lab/sa-reading.yml b/content/programs/rhel9-sysadmin/lab/sa-reading.yml new file mode 100644 index 0000000..7fafcf0 --- /dev/null +++ b/content/programs/rhel9-sysadmin/lab/sa-reading.yml @@ -0,0 +1,118 @@ +name: sa-reading +title: Who can read this? +page: ch07/exercise-reading +transport: ssh +version: 1 +starter: [] +setup: +- action: group + hosts: + - servera + name: project + state: present + gid: 4500 +- action: user + hosts: + - servera + name: maria + state: present + groups: + - project +- action: user + hosts: + - servera + name: john + state: present + groups: + - project +- action: user + hosts: + - servera + name: priya + state: present +- action: directory + hosts: + - servera + path: /srv/lab7 + state: present +- action: file + hosts: + - servera + path: /srv/lab7/report.txt + state: present + content: 'Q3 figures + + ' + owner: maria + group: project + mode: '640' +checkpoints: + final: + files: [] + checks: + - id: report + kind: file + 'on': servera.lab.example.com + targets: + - servera.lab.example.com + message: report.txt is locked down to mode 070 + paths: + - /srv/lab7/report.txt + owner: maria + group: project + mode: '0070' + - id: box + kind: file + 'on': servera.lab.example.com + targets: + - servera.lab.example.com + message: box is owned by maria with mode 555 + paths: + - /srv/lab7/box + fileType: directory + owner: maria + mode: '0555' + - id: one + kind: file + 'on': servera.lab.example.com + targets: + - servera.lab.example.com + message: maria deleted box/one + paths: + - /srv/lab7/box/one + exists: false + - id: two + kind: file + 'on': servera.lab.example.com + targets: + - servera.lab.example.com + message: box/two is still there + paths: + - /srv/lab7/box/two + fileType: regular +finish: +- action: directory + hosts: + - servera + path: /srv/lab7 + state: absent +- action: user + hosts: + - servera + name: maria + state: absent +- action: user + hosts: + - servera + name: john + state: absent +- action: user + hosts: + - servera + name: priya + state: absent +- action: group + hosts: + - servera + name: project + state: absent diff --git a/content/programs/rhel9-sysadmin/lab/sa-services.yml b/content/programs/rhel9-sysadmin/lab/sa-services.yml new file mode 100644 index 0000000..e8e7567 --- /dev/null +++ b/content/programs/rhel9-sysadmin/lab/sa-services.yml @@ -0,0 +1,57 @@ +name: sa-services +title: Control a service +page: ch09/exercise-services +transport: ssh +version: 1 +starter: [] +setup: +- action: service + hosts: + - servera + names: + - crond + state: started + enabled: true +- action: service + hosts: + - servera + names: + - bluetooth + masked: false +checkpoints: + final: + files: [] + checks: + - id: crond + kind: service + 'on': servera.lab.example.com + targets: + - servera.lab.example.com + message: crond is running and enabled again + names: + - crond.service + active: true + enabled: true + - id: bluetooth + kind: service + 'on': servera.lab.example.com + targets: + - servera.lab.example.com + message: bluetooth is not masked any more + names: + - bluetooth.service + masked: false +finish: +- action: service + hosts: + - servera + names: + - crond + state: started + enabled: true +- action: service + hosts: + - servera + names: + - bluetooth + masked: false diff --git a/content/programs/rhel9-sysadmin/lab/sa-shared.yml b/content/programs/rhel9-sysadmin/lab/sa-shared.yml new file mode 100644 index 0000000..27f6177 --- /dev/null +++ b/content/programs/rhel9-sysadmin/lab/sa-shared.yml @@ -0,0 +1,140 @@ +name: sa-shared +title: Build a team directory and a drop box +page: ch07/exercise-shared +transport: ssh +version: 1 +starter: [] +setup: +- action: group + hosts: + - servera + name: team + state: present + gid: 4700 +- action: user + hosts: + - servera + name: maria + state: present + groups: + - team +- action: user + hosts: + - servera + name: john + state: present + groups: + - team +- action: user + hosts: + - servera + name: priya + state: present +- action: directory + hosts: + - servera + path: /srv/team + state: present + owner: root + group: team + mode: '2770' +checkpoints: + final: + files: [] + checks: + - id: team + kind: file + 'on': servera.lab.example.com + targets: + - servera.lab.example.com + message: /srv/team is root:team with setgid (2770) + paths: + - /srv/team + fileType: directory + owner: root + group: team + mode: '2770' + - id: plan + kind: file + 'on': servera.lab.example.com + targets: + - servera.lab.example.com + message: plan.txt is group-writable and holds both lines + paths: + - /srv/team/plan.txt + owner: maria + group: team + mode: '0660' + lines: + - plan + - john was here + - id: docs + kind: file + 'on': servera.lab.example.com + targets: + - servera.lab.example.com + message: docs inherited group and setgid + paths: + - /srv/team/docs + fileType: directory + group: team + mode: '2770' + - id: dropbox + kind: file + 'on': servera.lab.example.com + targets: + - servera.lab.example.com + message: /srv/dropbox exists without the sticky bit now (777) + paths: + - /srv/dropbox + fileType: directory + mode: '0777' + - id: priya + kind: file + 'on': servera.lab.example.com + targets: + - servera.lab.example.com + message: priya.txt is in the drop box + paths: + - /srv/dropbox/priya.txt + owner: priya + - id: maria-gone + kind: file + 'on': servera.lab.example.com + targets: + - servera.lab.example.com + message: maria.txt was deleted once the sticky bit was off + paths: + - /srv/dropbox/maria.txt + exists: false +finish: +- action: directory + hosts: + - servera + path: /srv/team + state: absent +- action: directory + hosts: + - servera + path: /srv/dropbox + state: absent +- action: user + hosts: + - servera + name: maria + state: absent +- action: user + hosts: + - servera + name: john + state: absent +- action: user + hosts: + - servera + name: priya + state: absent +- action: group + hosts: + - servera + name: team + state: absent diff --git a/content/programs/rhel9-sysadmin/lab/sa-signals.yml b/content/programs/rhel9-sysadmin/lab/sa-signals.yml new file mode 100644 index 0000000..b2410ec --- /dev/null +++ b/content/programs/rhel9-sysadmin/lab/sa-signals.yml @@ -0,0 +1,100 @@ +name: sa-signals +title: Stop a runaway process the right way +page: ch08/exercise-signals +transport: ssh +version: 1 +starter: [] +setup: +- action: user + hosts: + - servera + name: bob + state: absent +- action: file + hosts: + - servera + path: /tmp/work.lock + state: absent +- action: file + hosts: + - servera + path: /home/student/cleanup.sh + state: present + content: '#!/bin/bash + + trap ''echo "cleaning up"; rm -f /tmp/work.lock; exit 0'' TERM + + touch /tmp/work.lock + + while true; do sleep 1; done + + ' + owner: student + group: student + mode: '755' +checkpoints: + final: + files: [] + checks: + - id: script + kind: file + 'on': servera.lab.example.com + targets: + - servera.lab.example.com + message: ~/cleanup.sh is an executable script with a TERM trap + paths: + - /home/student/cleanup.sh + mode: '0755' + owner: student + contains: + - trap + - id: lock + kind: file + 'on': servera.lab.example.com + targets: + - servera.lab.example.com + message: the lock file was removed (the polite kill ran the trap, and you removed the rest) + paths: + - /tmp/work.lock + exists: false + - id: no-hog + kind: process + 'on': servera.lab.example.com + targets: + - servera.lab.example.com + message: no sha256sum hog is left running + pattern: ^sha256sum /dev/zero + running: false + - id: no-bob + kind: process + 'on': servera.lab.example.com + targets: + - servera.lab.example.com + message: no sleep of user bob is left + pattern: ^sleep 90[01]$ + user: bob + running: false + - id: bob + kind: user + 'on': servera.lab.example.com + targets: + - servera.lab.example.com + message: bob exists (the exercise removes him in the clean-up step) + names: + - bob +finish: +- action: user + hosts: + - servera + name: bob + state: absent +- action: file + hosts: + - servera + path: /home/student/cleanup.sh + state: absent +- action: file + hosts: + - servera + path: /tmp/work.lock + state: absent diff --git a/content/programs/rhel9-sysadmin/lab/sa-sudo.yml b/content/programs/rhel9-sysadmin/lab/sa-sudo.yml new file mode 100644 index 0000000..02dcff6 --- /dev/null +++ b/content/programs/rhel9-sysadmin/lab/sa-sudo.yml @@ -0,0 +1,52 @@ +name: sa-sudo +title: Grant limited root access +page: ch06/exercise-superuser +transport: ssh +version: 1 +starter: [] +setup: +- action: user + hosts: + - servera + name: tester + state: absent +- action: file + hosts: + - servera + path: /etc/sudoers.d/tester + state: absent +checkpoints: + final: + files: [] + checks: + - id: tester + kind: user + 'on': servera.lab.example.com + targets: + - servera.lab.example.com + message: the account tester exists + names: + - tester + - id: rule + kind: sudoers + 'on': servera.lab.example.com + targets: + - servera.lab.example.com + message: tester may only run systemctl status + path: /etc/sudoers.d/tester + mode: '0440' + owner: root + valid: true + rules: + - tester ALL=(ALL) /usr/bin/systemctl status * +finish: +- action: user + hosts: + - servera + name: tester + state: absent +- action: file + hosts: + - servera + path: /etc/sudoers.d/tester + state: absent diff --git a/content/programs/rhel9-sysadmin/lab/sa-unit-files.yml b/content/programs/rhel9-sysadmin/lab/sa-unit-files.yml new file mode 100644 index 0000000..cf9b620 --- /dev/null +++ b/content/programs/rhel9-sysadmin/lab/sa-unit-files.yml @@ -0,0 +1,126 @@ +name: sa-unit-files +title: Write your own service +page: ch09/exercise-unit-files +transport: ssh +version: 1 +starter: [] +setup: +- action: service + hosts: + - servera + names: + - reporter + state: stopped + enabled: false +- action: file + hosts: + - servera + path: /etc/systemd/system/reporter.service + state: absent +- action: file + hosts: + - servera + path: /usr/local/bin/reporter.sh + state: absent +- action: file + hosts: + - servera + path: /var/log/reporter.log + state: absent +- action: directory + hosts: + - servera + path: /etc/systemd/system/reporter.service.d + state: absent +- action: systemd + hosts: + - servera + reload: true +- action: file + hosts: + - servera + path: /usr/local/bin/reporter.sh + state: present + content: "#!/bin/bash\n# Append a timestamp to the report every few seconds.\nwhile true; do\n echo \"$(date +%T) reporter alive\" >> /var/log/reporter.log\n sleep \"${INTERVAL:-10}\"\ndone\n" + owner: root + group: root + mode: '755' +checkpoints: + final: + files: [] + checks: + - id: unit + kind: file + 'on': servera.lab.example.com + targets: + - servera.lab.example.com + message: the unit runs the script, restarts on failure after 2 s and is wanted by multi-user.target + paths: + - /etc/systemd/system/reporter.service + contains: + - ExecStart=/usr/local/bin/reporter.sh + - Restart=on-failure + - RestartSec=2 + - WantedBy=multi-user.target + - id: dropin + kind: file + 'on': servera.lab.example.com + targets: + - servera.lab.example.com + message: a drop-in sets INTERVAL=2 + paths: + - /etc/systemd/system/reporter.service.d/10-interval.conf + contains: + - Environment=INTERVAL=2 + - id: service + kind: service + 'on': servera.lab.example.com + targets: + - servera.lab.example.com + message: reporter is running and enabled + names: + - reporter.service + active: true + enabled: true + - id: log + kind: file + 'on': servera.lab.example.com + targets: + - servera.lab.example.com + message: the report file gets lines + paths: + - /var/log/reporter.log + contains: + - reporter alive +finish: +- action: service + hosts: + - servera + names: + - reporter + state: stopped + enabled: false +- action: file + hosts: + - servera + path: /etc/systemd/system/reporter.service + state: absent +- action: file + hosts: + - servera + path: /usr/local/bin/reporter.sh + state: absent +- action: file + hosts: + - servera + path: /var/log/reporter.log + state: absent +- action: directory + hosts: + - servera + path: /etc/systemd/system/reporter.service.d + state: absent +- action: systemd + hosts: + - servera + reload: true diff --git a/packages/compiler/src/lab.js b/packages/compiler/src/lab.js index d430569..f84efbe 100644 --- a/packages/compiler/src/lab.js +++ b/packages/compiler/src/lab.js @@ -32,7 +32,7 @@ const CONTROL_ONLY = new Set(['git', 'lint']); // Setup and cleanup actions that run as root on the lab servers; the rest work on the workstation. const HOST_ACTIONS = new Set([ 'package', 'service', 'group', 'user', 'directory', 'file', 'remove-lines', 'firewall', 'selinux', 'wipe-disk', 'systemd', 'linger', - 'container-reset', 'run-as', 'restore-skel', + 'container-reset', 'run-as', 'restore-skel', 'boot', ]); /** Every file under `dir` as paths relative to it, sorted. */ diff --git a/packages/lab-tools/prepare.py b/packages/lab-tools/prepare.py index 380dbba..7af8a65 100644 --- a/packages/lab-tools/prepare.py +++ b/packages/lab-tools/prepare.py @@ -200,8 +200,8 @@ def ssh_keypairs(a, ctx): # --------------------------------------------------------------------------------------- # Host actions: fixed scripts, built from validated values, run as root on lab servers. -SAFE_PATH = re.compile(r'^/(?:srv|opt|mnt|tmp|root|home|var/www|var/tmp|usr/local|etc/(?:httpd/conf\.d|systemd|auto\.master\.d|auto\.[a-z0-9_-]+|cron\.d|exports\.d|sudoers\.d|profile\.d|ssh/sshd_config\.d|chrony\.d|yum\.repos\.d|logrotate\.d|rsyslog\.d|security/limits\.d|sysctl\.d|NetworkManager/system-connections|containers|firewalld/(?:services|zones)))(?:/[A-Za-z0-9_.@%+=:, -]+)*$') -PROTECTED = {'/srv', '/opt', '/mnt', '/tmp', '/root', '/home', '/var/www', '/var/tmp', '/usr/local', '/etc/systemd', '/etc/containers'} +SAFE_PATH = re.compile(r'^/(?:srv|opt|mnt|data|backup|tmp|root|home|var/www|var/tmp|var/log|usr/local|etc/(?:httpd/conf\.d|systemd|auto\.master\.d|auto\.[a-z0-9_-]+|cron\.d|exports\.d|sudoers\.d|profile\.d|ssh/sshd_config\.d|chrony\.d|yum\.repos\.d|logrotate\.d|rsyslog\.d|security/limits\.d|sysctl\.d|NetworkManager/system-connections|containers|firewalld/(?:services|zones)))(?:/[A-Za-z0-9_.@%+=:, -]+)*$') +PROTECTED = {'/srv', '/opt', '/mnt', '/tmp', '/root', '/home', '/var/www', '/var/tmp', '/var/log', '/usr/local', '/etc/systemd', '/etc/containers'} NAME = re.compile(r'^[a-z_][a-z0-9_-]{0,31}$') SSH = ['ssh', '-o', 'BatchMode=yes', '-o', 'ConnectTimeout=10', '-o', 'StrictHostKeyChecking=accept-new', '-o', 'LogLevel=ERROR'] @@ -247,12 +247,14 @@ def script_service(a): out.append('%s%s enable --now %s' % (pre, ctl, u)) continue if enabled is False and state == 'stopped': - out.append('%s%s disable --now %s' % (pre, ctl, u)) + out.append('%s%s disable --now %s 2>/dev/null || true' % (pre, ctl, u)) continue if state: - out.append('%s%s %s %s' % (pre, ctl, {'started': 'start', 'stopped': 'stop', 'restarted': 'restart'}[state], u)) + out.append('%s%s %s %s%s' % (pre, ctl, {'started': 'start', 'stopped': 'stop', 'restarted': 'restart'}[state], u, ' 2>/dev/null || true' if state == 'stopped' else '')) if enabled is not None: - out.append('%s%s %s %s' % (pre, ctl, 'enable' if enabled else 'disable', u)) + out.append('%s%s %s %s%s' % (pre, ctl, 'enable' if enabled else 'disable', u, '' if enabled else ' 2>/dev/null || true')) + if a.get('masked') is not None: + out.append('%s%s %s %s%s' % (pre, ctl, 'mask' if a['masked'] else 'unmask', u, '' if a['masked'] else ' 2>/dev/null || true')) return '\n'.join(out) @@ -383,6 +385,17 @@ def script_restore_skel(a): return '\n'.join('install -o %s -g %s -m 644 /etc/skel/%s /home/%s/%s' % (q(user), q(user), q(x), q(user), q(x)) for x in files if re.match(r'^\.bash[a-z_]+$', x)) +def script_boot(a): + out = [] + if 'target' in a: + out.append('systemctl set-default %s' % q(a['target'])) + for arg in a.get('removeKernelArgs', []): + if not re.match(r'^[A-Za-z0-9_.=,-]{1,60}$', arg): + raise SetupError('unsafe kernel argument') + out.append('grubby --update-kernel=ALL --remove-args=%s' % q(arg)) + return '\n'.join(out) or 'true' + + def script_run_as(a): user = q(name(a['user'])) image = q(a['image']) @@ -394,7 +407,7 @@ def script_run_as(a): 'package': script_package, 'service': script_service, 'group': script_group, 'user': script_user, 'directory': script_directory, 'file': script_file, 'remove-lines': script_remove_lines, 'firewall': script_firewall, 'selinux': script_selinux, 'wipe-disk': script_wipe_disk, 'systemd': script_systemd, 'linger': script_linger, 'container-reset': script_container_reset, - 'run-as': script_run_as, 'restore-skel': script_restore_skel, + 'run-as': script_run_as, 'restore-skel': script_restore_skel, 'boot': script_boot, } diff --git a/packages/schema/schemas/source/lab.schema.json b/packages/schema/schemas/source/lab.schema.json index 87ebc89..fda06ba 100644 --- a/packages/schema/schemas/source/lab.schema.json +++ b/packages/schema/schemas/source/lab.schema.json @@ -682,6 +682,9 @@ "user": { "description": "Manage the units of this user's systemd instead of the system's.", "$ref": "#/$defs/unixName" + }, + "masked": { + "type": "boolean" } } }, @@ -1132,6 +1135,36 @@ } } } + }, + { + "type": "object", + "additionalProperties": false, + "required": [ + "action", + "hosts" + ], + "properties": { + "action": { + "const": "boot" + }, + "hosts": { + "$ref": "#/$defs/serverList" + }, + "target": { + "enum": [ + "graphical.target", + "multi-user.target" + ] + }, + "removeKernelArgs": { + "type": "array", + "maxItems": 5, + "items": { + "type": "string", + "pattern": "^[A-Za-z0-9_.=,-]{1,60}$" + } + } + } } ] }, @@ -2267,7 +2300,7 @@ "description": "A path a setup or cleanup action may create or remove on a managed host: only under directories meant for local data and drop-in configuration.", "type": "string", "maxLength": 200, - "pattern": "^/(?:srv|opt|mnt|tmp|root|home|var/www|var/tmp|usr/local|etc/(?:httpd/conf\\.d|systemd|auto\\.master\\.d|auto\\.[a-z0-9_-]+|cron\\.d|exports\\.d|sudoers\\.d|profile\\.d|ssh/sshd_config\\.d|chrony\\.d|yum\\.repos\\.d|logrotate\\.d|rsyslog\\.d|security/limits\\.d|sysctl\\.d|NetworkManager/system-connections|containers|httpd/conf\\.d|firewalld/(?:services|zones)))(?:/[A-Za-z0-9_.@%+=:, -]+)*$", + "pattern": "^/(?:srv|opt|mnt|data|backup|tmp|root|home|var/www|var/tmp|var/log|usr/local|etc/(?:httpd/conf\\.d|systemd|auto\\.master\\.d|auto\\.[a-z0-9_-]+|cron\\.d|exports\\.d|sudoers\\.d|profile\\.d|ssh/sshd_config\\.d|chrony\\.d|yum\\.repos\\.d|logrotate\\.d|rsyslog\\.d|security/limits\\.d|sysctl\\.d|NetworkManager/system-connections|containers|httpd/conf\\.d|firewalld/(?:services|zones)))(?:/[A-Za-z0-9_.@%+=:, -]+)*$", "not": { "pattern": "(?:^|/)\\.\\.(?:/|$)" } From dc60c4d63b25f642fdf5063cff911f42aacce404 Mon Sep 17 00:00:00 2001 From: Ayesh Rodrigo Date: Sun, 4 Oct 2026 11:13:19 +1300 Subject: [PATCH 3/9] Guided exercise labs for chapters 10 and 11 --- .../04-exercise-keys.md | 20 ++- .../06-exercise-transfer.md | 18 +++ .../08-exercise-hardening.md | 30 ++-- .../04-exercise-persistence.md | 26 ++-- .../ch11-logs-and-time/06-exercise-time.md | 18 +++ .../08-exercise-diagnosing.md | 61 ++------ .../rhel9-sysadmin/lab/sa-diagnosing.yml | 117 +++++++++++++++ .../rhel9-sysadmin/lab/sa-hardening.yml | 121 +++++++++++++++ .../programs/rhel9-sysadmin/lab/sa-keys.yml | 72 +++++++++ .../rhel9-sysadmin/lab/sa-persistence.yml | 138 ++++++++++++++++++ .../programs/rhel9-sysadmin/lab/sa-time.yml | 84 +++++++++++ .../rhel9-sysadmin/lab/sa-transfer.yml | 138 ++++++++++++++++++ packages/compiler/src/lab.js | 2 +- packages/lab-tools/prepare.py | 68 ++++++++- .../schema/schemas/source/lab.schema.json | 101 ++++++++++++- 15 files changed, 931 insertions(+), 83 deletions(-) create mode 100644 content/programs/rhel9-sysadmin/lab/sa-diagnosing.yml create mode 100644 content/programs/rhel9-sysadmin/lab/sa-hardening.yml create mode 100644 content/programs/rhel9-sysadmin/lab/sa-keys.yml create mode 100644 content/programs/rhel9-sysadmin/lab/sa-persistence.yml create mode 100644 content/programs/rhel9-sysadmin/lab/sa-time.yml create mode 100644 content/programs/rhel9-sysadmin/lab/sa-transfer.yml diff --git a/content/programs/rhel9-sysadmin/chapters/ch10-ssh-and-remote-access/04-exercise-keys.md b/content/programs/rhel9-sysadmin/chapters/ch10-ssh-and-remote-access/04-exercise-keys.md index 7204c6c..af3ffda 100644 --- a/content/programs/rhel9-sysadmin/chapters/ch10-ssh-and-remote-access/04-exercise-keys.md +++ b/content/programs/rhel9-sysadmin/chapters/ch10-ssh-and-remote-access/04-exercise-keys.md @@ -12,19 +12,21 @@ Set up key-based login from workstation to the `ops` account on servera, check t objectives=["ch10.keys"] id="keys" title="Passwordless access for an account" + exercise="sa-keys" + ownExercise=true + guided=true + starter=false hosts=["workstation","servera"] outcomes=["Create a key pair and install it with ssh-copy-id.","Diagnose a key refused because of permissions.","Use a passphrase with ssh-agent."] %} - {% task id="task-c734fcaadb7f" title="Make sure the account exists" %} - If you reset the servers since the last exercise, create `ops` again on servera, with the password `Ops-Pass-2026`. (Use `ssh root@servera`.) - - {% reveal title="Show solution" %} + {% task id="task-d19e45fd21eb" title="Start the exercise" %} + On workstation, start the exercise. It creates the account `ops` on servera with the password `Ops-Pass-2026` and removes key files of an earlier run from workstation. ```console -[student@workstation ~]$ ssh root@servera "id ops || (useradd -m ops && echo 'Ops-Pass-2026' | passwd --stdin ops)" -uid=1001(ops) gid=1001(ops) groups=1001(ops) +[student@workstation ~]$ lab start sa-keys ``` - {% /reveal %} + + Later tasks assume that you are logged in to the server they name: `ssh student@servera` from workstation, then `sudo -i` for a root shell. {% /task %} {% task id="task-dba8dc306c6b" title="Create the key pair" %} @@ -136,4 +138,8 @@ echo Agent pid 1400 killed; The copy has the same public half, so the server accepted it without any change. Without the agent, ssh would have asked for the passphrase. {% /reveal %} {% /task %} + + {% task id="task-d76dc44caa6f" title="Grade and finish" %} + {% lab-finish exercise="sa-keys" grade=true servers=true /%} + {% /task %} {% /lab %} diff --git a/content/programs/rhel9-sysadmin/chapters/ch10-ssh-and-remote-access/06-exercise-transfer.md b/content/programs/rhel9-sysadmin/chapters/ch10-ssh-and-remote-access/06-exercise-transfer.md index 26ffa33..16f9dac 100644 --- a/content/programs/rhel9-sysadmin/chapters/ch10-ssh-and-remote-access/06-exercise-transfer.md +++ b/content/programs/rhel9-sysadmin/chapters/ch10-ssh-and-remote-access/06-exercise-transfer.md @@ -12,9 +12,23 @@ Define an alias for the `ops` account, then use scp, sftp and rsync to move file objectives=["ch10.transfer"] id="transfer" title="Move files between machines" + exercise="sa-transfer" + ownExercise=true + guided=true + starter=false hosts=["workstation","servera"] outcomes=["Write a Host block in ~/.ssh/config.","Upload and download with scp and sftp.","Mirror a directory with rsync and preview with -n."] %} + {% task id="task-13cc9ad0b055" title="Start the exercise" %} + On workstation, start the exercise. It creates the account `ops` on servera and the key `~/.ssh/id_ops` on workstation, installed for ops (the result of the previous exercise). + +```console +[student@workstation ~]$ lab start sa-transfer +``` + + Later tasks assume that you are logged in to the server they name: `ssh student@servera` from workstation, then `sudo -i` for a root shell. + {% /task %} + {% task id="task-f20aebf82d9f" title="Prepare files and an alias" %} This exercise needs the key from the previous exercise (`~/.ssh/id_ops`, installed for `ops@servera`). Create a working directory `~/sshlab` with `inventory.txt` (any line) and `reports/a.txt`, `reports/b.txt`. Then add a `Host opsa` block to `~/.ssh/config` and test it. @@ -140,4 +154,8 @@ c.txt Always add `-n` first when `--delete` is involved. {% /reveal %} {% /task %} + + {% task id="task-e6b9ebfb2272" title="Grade and finish" %} + {% lab-finish exercise="sa-transfer" grade=true servers=true /%} + {% /task %} {% /lab %} diff --git a/content/programs/rhel9-sysadmin/chapters/ch10-ssh-and-remote-access/08-exercise-hardening.md b/content/programs/rhel9-sysadmin/chapters/ch10-ssh-and-remote-access/08-exercise-hardening.md index 3f5c0ff..a03ae94 100644 --- a/content/programs/rhel9-sysadmin/chapters/ch10-ssh-and-remote-access/08-exercise-hardening.md +++ b/content/programs/rhel9-sysadmin/chapters/ch10-ssh-and-remote-access/08-exercise-hardening.md @@ -12,9 +12,23 @@ Turn off password logins on servera, prove the change with a failed password att objectives=["ch10.hardening"] id="hardening" title="Harden sshd, safely" + exercise="sa-hardening" + ownExercise=true + guided=true + starter=false hosts=["workstation","servera"] outcomes=["Write a sshd drop-in and verify it with sshd -T.","Demonstrate that password logins are refused and key logins still work.","Explain the first-value-wins rule."] %} + {% task id="task-902b9a7b40a6" title="Start the exercise" %} + On workstation, start the exercise. It creates the account `ops` on servera with a key (`~/.ssh/id_ops` on workstation) that you will keep using after passwords are switched off. + +```console +[student@workstation ~]$ lab start sa-hardening +``` + + Later tasks assume that you are logged in to the server they name: `ssh student@servera` from workstation, then `sudo -i` for a root shell. + {% /task %} + {% task id="task-2fc1cb637c22" title="Open the spare session" %} Open **two** terminals on workstation, both logged in to servera as root (`ssh root@servera`). Leave the second one alone: it is your way back in if the change goes wrong. Show the current values of the options you will change. @@ -98,19 +112,7 @@ sshd-session[1049]: pam_unix(sshd:session): session opened for user ops(uid=1001 {% /reveal %} {% /task %} - {% task id="task-255297d55311" title="Put it back" %} - Remove the drop-in, reload, confirm that the defaults are back, and close the spare session. - - {% reveal title="Show solution" %} - -```console -[root@servera ~]# rm /etc/ssh/sshd_config.d/10-hardening.conf -[root@servera ~]# sshd -t && systemctl reload sshd -[root@servera ~]# sshd -T | grep -E '^(passwordauthentication|permitrootlogin) ' -permitrootlogin without-password -passwordauthentication yes -[root@servera ~]# exit -``` - {% /reveal %} + {% task id="task-501bd5eab37d" title="Grade and finish" %} + {% lab-finish exercise="sa-hardening" grade=true servers=true /%} {% /task %} {% /lab %} diff --git a/content/programs/rhel9-sysadmin/chapters/ch11-logs-and-time/04-exercise-persistence.md b/content/programs/rhel9-sysadmin/chapters/ch11-logs-and-time/04-exercise-persistence.md index 58a721d..d81851e 100644 --- a/content/programs/rhel9-sysadmin/chapters/ch11-logs-and-time/04-exercise-persistence.md +++ b/content/programs/rhel9-sysadmin/chapters/ch11-logs-and-time/04-exercise-persistence.md @@ -12,9 +12,23 @@ Make the journal persistent and prove it across a reboot, send a facility to its objectives=["ch11.retention"] id="retention" title="Keep and rotate your logs" + exercise="sa-persistence" + ownExercise=true + guided=true + starter=false hosts=["workstation","servera"] outcomes=["Make the journal persistent and verify it after a reboot.","Add an rsyslog rule for a facility.","Write, test and force a logrotate rule."] %} + {% task id="task-e55c88aec5ed" title="Start the exercise" %} + On workstation, start the exercise. It removes the logging settings of an earlier run from servera. + +```console +[student@workstation ~]$ lab start sa-persistence +``` + + Later tasks assume that you are logged in to the server they name: `ssh student@servera` from workstation, then `sudo -i` for a root shell. + {% /task %} + {% task id="task-0b7282cb49ac" title="Check the journal's lifetime" %} On servera as root, show how many boots the journal holds and how much space it uses. @@ -117,15 +131,7 @@ EOT {% /reveal %} {% /task %} - {% task id="task-b71ceeaceb3f" title="Clean up" %} - -```console -[root@servera ~]# rm -f /etc/rsyslog.d/10-myapp.conf /etc/logrotate.d/myapp /var/log/myapp.log* -[root@servera ~]# systemctl restart rsyslog -[root@servera ~]# exit -[student@servera ~]$ exit -``` - - The persistent journal can stay: it is good practice. + {% task id="task-012ddc2e2403" title="Grade and finish" %} + {% lab-finish exercise="sa-persistence" grade=true servers=true /%} {% /task %} {% /lab %} diff --git a/content/programs/rhel9-sysadmin/chapters/ch11-logs-and-time/06-exercise-time.md b/content/programs/rhel9-sysadmin/chapters/ch11-logs-and-time/06-exercise-time.md index dbc3a5a..81bfaf4 100644 --- a/content/programs/rhel9-sysadmin/chapters/ch11-logs-and-time/06-exercise-time.md +++ b/content/programs/rhel9-sysadmin/chapters/ch11-logs-and-time/06-exercise-time.md @@ -12,9 +12,23 @@ Read the state of servera's clock, move it to another time zone, inspect where i objectives=["ch11.time"] id="time" title="Time zone and time sources" + exercise="sa-time" + ownExercise=true + guided=true + starter=false hosts=["workstation","servera"] outcomes=["Read timedatectl, chronyc sources and chronyc tracking.","Change the time zone.","Add an NTP server and verify it."] %} + {% task id="task-749cd80a2bb6" title="Start the exercise" %} + On workstation, start the exercise. It puts servera in the UTC time zone and removes an extra time source from an earlier run. + +```console +[student@workstation ~]$ lab start sa-time +``` + + Later tasks assume that you are logged in to the server they name: `ssh student@servera` from workstation, then `sudo -i` for a root shell. + {% /task %} + {% task id="task-e8defc8ba9e0" title="Check the clock" %} On servera as root, show the time status, the time sources, and the clock offset. @@ -113,4 +127,8 @@ System clock synchronized: no Right after a restart, `synchronized` may say `no` for a few moments, until chronyd has chosen a source again. It says `yes` again within a minute or two. {% /task %} + + {% task id="task-d05e32874b50" title="Grade and finish" %} + {% lab-finish exercise="sa-time" grade=true servers=true /%} + {% /task %} {% /lab %} diff --git a/content/programs/rhel9-sysadmin/chapters/ch11-logs-and-time/08-exercise-diagnosing.md b/content/programs/rhel9-sysadmin/chapters/ch11-logs-and-time/08-exercise-diagnosing.md index 60ff21d..e7a3a62 100644 --- a/content/programs/rhel9-sysadmin/chapters/ch11-logs-and-time/08-exercise-diagnosing.md +++ b/content/programs/rhel9-sysadmin/chapters/ch11-logs-and-time/08-exercise-diagnosing.md @@ -12,55 +12,21 @@ A colleague has installed a small service called `billing`, and it does not run. objectives=["ch11.diagnosis"] id="diagnosis" title="Find and fix a failing service" + exercise="sa-diagnosing" + ownExercise=true + guided=true + starter=false hosts=["workstation","servera"] outcomes=["Read a failed service's status and journal.","Identify the first error and fix its cause.","Verify the fix and clean up."] %} - {% task id="task-ac12e7a5c79b" title="Install the broken service" %} - As root on servera (`sudo -i`), create the program and unit below. They represent the colleague's work; do not read them closely yet. - -```bash -cat > /usr/local/bin/billing.sh <<'EOT' -#!/bin/bash -CONF=/etc/billing.conf -if [ ! -r "$CONF" ]; then - echo "billing: cannot read $CONF" >&2 - exit 3 -fi -. "$CONF" -echo "billing: started with rate=$RATE" -exec sleep infinity -EOT -chmod 755 /usr/local/bin/billing.sh -cat > /etc/systemd/system/billing.service <<'EOT' -[Unit] -Description=Billing job - -[Service] -ExecStart=/usr/local/bin/billing.sh -Restart=on-failure -RestartSec=1 - -[Install] -WantedBy=multi-user.target -EOT -systemctl daemon-reload -``` - - {% reveal title="Show the commands in a session" %} + {% task id="task-1b2c47fa03e4" title="Start the exercise" %} + On workstation, start the exercise. It installs the broken billing service (a program and a unit, written by a "colleague") on servera and reloads systemd. ```console -[student@workstation ~]$ ssh student@servera -[student@servera ~]$ sudo -i -[root@servera ~]# cat > /usr/local/bin/billing.sh <<'EOT' -... -EOT -[root@servera ~]# chmod 755 /usr/local/bin/billing.sh -[root@servera ~]# cat > /etc/systemd/system/billing.service <<'EOT' -... -EOT -[root@servera ~]# systemctl daemon-reload +[student@workstation ~]$ lab start sa-diagnosing ``` - {% /reveal %} + + Later tasks assume that you are logged in to the server they name: `ssh student@servera` from workstation, then `sudo -i` for a root shell. {% /task %} {% task id="task-e6518c1c69ad" title="State the symptom" %} @@ -131,7 +97,7 @@ Oct 03 17:04:58 servera.lab.example.com systemd[1]: Started Billing job. {% /task %} {% task id="task-999070fbb650" title="Look wider, then clean up" %} - Check that nothing else is failing, and look at the system-wide errors of this boot. Then remove everything you created. + Check that nothing else is failing, and look at the system-wide errors of this boot. {% reveal title="Show solution" %} @@ -140,12 +106,13 @@ Oct 03 17:04:58 servera.lab.example.com systemd[1]: Started Billing job. UNIT LOAD ACTIVE SUB DESCRIPTION 0 loaded units listed. [root@servera ~]# journalctl -p err -b --no-pager | tail -3 -[root@servera ~]# systemctl stop billing -[root@servera ~]# rm -f /etc/systemd/system/billing.service /usr/local/bin/billing.sh /etc/billing.conf -[root@servera ~]# systemctl daemon-reload [root@servera ~]# exit [student@servera ~]$ exit ``` {% /reveal %} {% /task %} + + {% task id="task-dd17bafc70a7" title="Grade and finish" %} + {% lab-finish exercise="sa-diagnosing" grade=true servers=true /%} + {% /task %} {% /lab %} diff --git a/content/programs/rhel9-sysadmin/lab/sa-diagnosing.yml b/content/programs/rhel9-sysadmin/lab/sa-diagnosing.yml new file mode 100644 index 0000000..577a4d0 --- /dev/null +++ b/content/programs/rhel9-sysadmin/lab/sa-diagnosing.yml @@ -0,0 +1,117 @@ +name: sa-diagnosing +title: Find and fix a failing service +page: ch11/exercise-diagnosing +transport: ssh +version: 1 +starter: [] +setup: +- action: service + hosts: + - servera + names: + - billing + state: stopped +- action: file + hosts: + - servera + path: /etc/systemd/system/billing.service + state: absent +- action: file + hosts: + - servera + path: /usr/local/bin/billing.sh + state: absent +- action: file + hosts: + - servera + path: /etc/billing.conf + state: absent +- action: systemd + hosts: + - servera + reload: true +- action: file + hosts: + - servera + path: /usr/local/bin/billing.sh + state: present + content: "#!/bin/bash\nCONF=/etc/billing.conf\nif [ ! -r \"$CONF\" ]; then\n echo \"billing: cannot read $CONF\" >&2\n exit 3\nfi\n. \"$CONF\"\necho \"billing: started with rate=$RATE\"\nexec sleep infinity\n" + owner: root + group: root + mode: '755' +- action: file + hosts: + - servera + path: /etc/systemd/system/billing.service + state: present + content: '[Unit] + + Description=Billing job + + + [Service] + + ExecStart=/usr/local/bin/billing.sh + + Restart=on-failure + + RestartSec=1 + + + [Install] + + WantedBy=multi-user.target + + ' +- action: systemd + hosts: + - servera + reload: true +checkpoints: + final: + files: [] + checks: + - id: service + kind: service + 'on': servera.lab.example.com + targets: + - servera.lab.example.com + message: billing is running + names: + - billing.service + active: true + - id: conf + kind: file + 'on': servera.lab.example.com + targets: + - servera.lab.example.com + message: /etc/billing.conf holds RATE=10 + paths: + - /etc/billing.conf + line: RATE=10 +finish: +- action: service + hosts: + - servera + names: + - billing + state: stopped +- action: file + hosts: + - servera + path: /etc/systemd/system/billing.service + state: absent +- action: file + hosts: + - servera + path: /usr/local/bin/billing.sh + state: absent +- action: file + hosts: + - servera + path: /etc/billing.conf + state: absent +- action: systemd + hosts: + - servera + reload: true diff --git a/content/programs/rhel9-sysadmin/lab/sa-hardening.yml b/content/programs/rhel9-sysadmin/lab/sa-hardening.yml new file mode 100644 index 0000000..0b568fb --- /dev/null +++ b/content/programs/rhel9-sysadmin/lab/sa-hardening.yml @@ -0,0 +1,121 @@ +name: sa-hardening +title: Harden sshd, safely +page: ch10/exercise-hardening +transport: ssh +version: 1 +starter: [] +setup: +- action: user + hosts: + - servera + name: ops + state: absent +- action: user + hosts: + - servera + name: ops + state: present + password: Ops-Pass-2026 +- action: home-remove + paths: + - .ssh/id_ops + - .ssh/id_ops.pub +- action: ssh-identity + name: id_ops + hosts: + - servera + user: ops +- action: file + hosts: + - servera + path: /etc/ssh/sshd_config.d/99-hardening.conf + state: absent +- action: file + hosts: + - servera + path: /etc/ssh/sshd_config.d/10-hardening.conf + state: absent +checkpoints: + final: + files: [] + checks: + - id: file + kind: file + 'on': servera.lab.example.com + targets: + - servera.lab.example.com + message: the drop-in 10-hardening.conf has the three options + paths: + - /etc/ssh/sshd_config.d/10-hardening.conf + contains: + - PasswordAuthentication no + - X11Forwarding no + - MaxAuthTries 3 + - id: no-wrong + kind: file + 'on': servera.lab.example.com + targets: + - servera.lab.example.com + message: the wrongly named 99-hardening.conf is gone + paths: + - /etc/ssh/sshd_config.d/99-hardening.conf + exists: false + - id: password + kind: sshd + 'on': servera.lab.example.com + targets: + - servera.lab.example.com + message: sshd refuses password authentication + setting: passwordauthentication + value: 'no' + - id: x11 + kind: sshd + 'on': servera.lab.example.com + targets: + - servera.lab.example.com + message: sshd refuses X11 forwarding + setting: x11forwarding + value: 'no' + - id: tries + kind: sshd + 'on': servera.lab.example.com + targets: + - servera.lab.example.com + message: sshd allows three authentication tries + setting: maxauthtries + value: '3' + - id: sshd + kind: service + 'on': servera.lab.example.com + targets: + - servera.lab.example.com + message: sshd is running + names: + - sshd.service + active: true +finish: +- action: file + hosts: + - servera + path: /etc/ssh/sshd_config.d/99-hardening.conf + state: absent +- action: file + hosts: + - servera + path: /etc/ssh/sshd_config.d/10-hardening.conf + state: absent +- action: service + hosts: + - servera + names: + - sshd + state: restarted +- action: user + hosts: + - servera + name: ops + state: absent +- action: home-remove + paths: + - .ssh/id_ops + - .ssh/id_ops.pub diff --git a/content/programs/rhel9-sysadmin/lab/sa-keys.yml b/content/programs/rhel9-sysadmin/lab/sa-keys.yml new file mode 100644 index 0000000..b66538f --- /dev/null +++ b/content/programs/rhel9-sysadmin/lab/sa-keys.yml @@ -0,0 +1,72 @@ +name: sa-keys +title: Passwordless access for an account +page: ch10/exercise-keys +transport: ssh +version: 1 +starter: [] +setup: +- action: user + hosts: + - servera + name: ops + state: absent +- action: user + hosts: + - servera + name: ops + state: present + password: Ops-Pass-2026 +- action: home-remove + paths: + - .ssh/id_ops + - .ssh/id_ops.pub + - .ssh/id_pp + - .ssh/id_pp.pub +checkpoints: + final: + files: [] + checks: + - id: key + kind: file + 'on': control + message: the private key ~/.ssh/id_ops exists with mode 600 + paths: + - /home/student/.ssh/id_ops + mode: '0600' + - id: pub + kind: file + 'on': control + message: the public key ~/.ssh/id_ops.pub exists + paths: + - /home/student/.ssh/id_ops.pub + contains: + - student@workstation (ops) + - id: installed + kind: user + 'on': servera.lab.example.com + targets: + - servera.lab.example.com + message: ops has an authorized_keys file with the right owner and mode + names: + - ops + authorizedKeys: true + - id: pp-gone + kind: file + 'on': control + message: the temporary passphrase copy id_pp was removed + paths: + - /home/student/.ssh/id_pp + - /home/student/.ssh/id_pp.pub + exists: false +finish: +- action: user + hosts: + - servera + name: ops + state: absent +- action: home-remove + paths: + - .ssh/id_ops + - .ssh/id_ops.pub + - .ssh/id_pp + - .ssh/id_pp.pub diff --git a/content/programs/rhel9-sysadmin/lab/sa-persistence.yml b/content/programs/rhel9-sysadmin/lab/sa-persistence.yml new file mode 100644 index 0000000..b85fedf --- /dev/null +++ b/content/programs/rhel9-sysadmin/lab/sa-persistence.yml @@ -0,0 +1,138 @@ +name: sa-persistence +title: Keep and rotate your logs +page: ch11/exercise-persistence +transport: ssh +version: 1 +starter: [] +setup: +- action: file + hosts: + - servera + path: /etc/rsyslog.d/10-myapp.conf + state: absent +- action: file + hosts: + - servera + path: /etc/logrotate.d/myapp + state: absent +- action: file + hosts: + - servera + path: /var/log/myapp.log + state: absent +- action: file + hosts: + - servera + path: /var/log/myapp.log.1.gz + state: absent +- action: file + hosts: + - servera + path: /etc/systemd/journald.conf.d/10-retention.conf + state: absent +- action: service + hosts: + - servera + names: + - rsyslog + state: restarted +checkpoints: + final: + files: [] + checks: + - id: journal-dir + kind: file + 'on': servera.lab.example.com + targets: + - servera.lab.example.com + message: the journal directory exists + paths: + - /var/log/journal + fileType: directory + - id: journal-conf + kind: file + 'on': servera.lab.example.com + targets: + - servera.lab.example.com + message: journald is persistent and limited to 200M + paths: + - /etc/systemd/journald.conf.d/10-retention.conf + contains: + - Storage=persistent + - SystemMaxUse=200M + - id: rsyslog + kind: file + 'on': servera.lab.example.com + targets: + - servera.lab.example.com + message: rsyslog sends facility local5 to /var/log/myapp.log + paths: + - /etc/rsyslog.d/10-myapp.conf + matches: ^local5\.\*[[:space:]]+/var/log/myapp\.log[[:space:]]*$ + - id: rotate + kind: file + 'on': servera.lab.example.com + targets: + - servera.lab.example.com + message: logrotate rotates weekly, keeps 4, compresses + paths: + - /etc/logrotate.d/myapp + contains: + - weekly + - rotate 4 + - compress + - missingok + - notifempty + - create 0640 root root + - id: rotated + kind: file + 'on': servera.lab.example.com + targets: + - servera.lab.example.com + message: the first rotation produced myapp.log.1.gz + paths: + - /var/log/myapp.log.1.gz + fileType: regular +finish: +- action: file + hosts: + - servera + path: /etc/rsyslog.d/10-myapp.conf + state: absent +- action: file + hosts: + - servera + path: /etc/logrotate.d/myapp + state: absent +- action: file + hosts: + - servera + path: /var/log/myapp.log + state: absent +- action: file + hosts: + - servera + path: /var/log/myapp.log.1.gz + state: absent +- action: file + hosts: + - servera + path: /etc/systemd/journald.conf.d/10-retention.conf + state: absent +- action: service + hosts: + - servera + names: + - rsyslog + state: restarted +- action: directory + hosts: + - servera + path: /var/log/journal + state: absent +- action: service + hosts: + - servera + names: + - systemd-journald + state: restarted diff --git a/content/programs/rhel9-sysadmin/lab/sa-time.yml b/content/programs/rhel9-sysadmin/lab/sa-time.yml new file mode 100644 index 0000000..50b117f --- /dev/null +++ b/content/programs/rhel9-sysadmin/lab/sa-time.yml @@ -0,0 +1,84 @@ +name: sa-time +title: Time zone and time sources +page: ch11/exercise-time +transport: ssh +version: 1 +starter: [] +setup: +- action: timezone + hosts: + - servera + zone: UTC +- action: remove-lines + hosts: + - servera + path: /etc/chrony.conf + matching: ^server 0[.]pool[.]ntp[.]org +- action: file + hosts: + - servera + path: /root/chrony.conf.bak + state: absent +checkpoints: + final: + files: [] + checks: + - id: zone + kind: file + 'on': servera.lab.example.com + targets: + - servera.lab.example.com + message: the time zone is UTC again + paths: + - /etc/localtime + resolvesTo: /usr/share/zoneinfo/UTC + - id: conf + kind: file + 'on': servera.lab.example.com + targets: + - servera.lab.example.com + message: chrony.conf has no extra pool source any more + paths: + - /etc/chrony.conf + lacks: + - server 0.pool.ntp.org iburst + - id: backup + kind: file + 'on': servera.lab.example.com + targets: + - servera.lab.example.com + message: the backup /root/chrony.conf.bak was removed + paths: + - /root/chrony.conf.bak + exists: false + - id: chronyd + kind: service + 'on': servera.lab.example.com + targets: + - servera.lab.example.com + message: chronyd is running and enabled + names: + - chronyd.service + active: true + enabled: true +finish: +- action: timezone + hosts: + - servera + zone: UTC +- action: remove-lines + hosts: + - servera + path: /etc/chrony.conf + matching: ^server 0[.]pool[.]ntp[.]org +- action: file + hosts: + - servera + path: /root/chrony.conf.bak + state: absent +- action: service + hosts: + - servera + names: + - chronyd + state: restarted diff --git a/content/programs/rhel9-sysadmin/lab/sa-transfer.yml b/content/programs/rhel9-sysadmin/lab/sa-transfer.yml new file mode 100644 index 0000000..1baabf4 --- /dev/null +++ b/content/programs/rhel9-sysadmin/lab/sa-transfer.yml @@ -0,0 +1,138 @@ +name: sa-transfer +title: Move files between machines +page: ch10/exercise-transfer +transport: ssh +version: 1 +starter: [] +setup: +- action: user + hosts: + - servera + name: ops + state: absent +- action: user + hosts: + - servera + name: ops + state: present + password: Ops-Pass-2026 +- action: home-remove + paths: + - sshlab + - .ssh/id_ops + - .ssh/id_ops.pub +- action: ssh-config-block + host: opsa + state: absent +- action: directory + hosts: + - servera + path: /tmp/reports + state: absent +- action: ssh-identity + name: id_ops + hosts: + - servera + user: ops +checkpoints: + final: + files: [] + checks: + - id: inventory-local + kind: file + 'on': control + message: ~/sshlab/inventory.txt exists on workstation + paths: + - /home/student/sshlab/inventory.txt + contains: + - web1 + - id: alias + kind: file + 'on': control + message: ~/.ssh/config defines the alias opsa + paths: + - /home/student/.ssh/config + contains: + - Host opsa + - User ops + - id: downloaded + kind: file + 'on': control + message: servera's host name was downloaded + paths: + - /home/student/sshlab/servera-hostname.txt + contains: + - servera + - id: sftp + kind: file + 'on': control + message: sftp fetched downloaded-a.txt + paths: + - /home/student/sshlab/downloaded-a.txt + contains: + - r1 + - id: uploaded + kind: file + 'on': servera.lab.example.com + targets: + - servera.lab.example.com + message: inventory.txt is in the home of ops + paths: + - /home/ops/inventory.txt + owner: ops + - id: scp-dir + kind: file + 'on': servera.lab.example.com + targets: + - servera.lab.example.com + message: the reports directory was copied to /tmp + paths: + - /tmp/reports/a.txt + contains: + - r1 + - id: rsync-a + kind: file + 'on': servera.lab.example.com + targets: + - servera.lab.example.com + message: rsyncdir holds a.txt + paths: + - /home/ops/rsyncdir/a.txt + owner: ops + - id: rsync-c + kind: file + 'on': servera.lab.example.com + targets: + - servera.lab.example.com + message: rsyncdir holds c.txt + paths: + - /home/ops/rsyncdir/c.txt + owner: ops + - id: rsync-b + kind: file + 'on': servera.lab.example.com + targets: + - servera.lab.example.com + message: the mirror deleted b.txt + paths: + - /home/ops/rsyncdir/b.txt + exists: false +finish: +- action: user + hosts: + - servera + name: ops + state: absent +- action: home-remove + paths: + - sshlab + - .ssh/id_ops + - .ssh/id_ops.pub +- action: ssh-config-block + host: opsa + state: absent +- action: directory + hosts: + - servera + path: /tmp/reports + state: absent diff --git a/packages/compiler/src/lab.js b/packages/compiler/src/lab.js index f84efbe..4f1fac1 100644 --- a/packages/compiler/src/lab.js +++ b/packages/compiler/src/lab.js @@ -32,7 +32,7 @@ const CONTROL_ONLY = new Set(['git', 'lint']); // Setup and cleanup actions that run as root on the lab servers; the rest work on the workstation. const HOST_ACTIONS = new Set([ 'package', 'service', 'group', 'user', 'directory', 'file', 'remove-lines', 'firewall', 'selinux', 'wipe-disk', 'systemd', 'linger', - 'container-reset', 'run-as', 'restore-skel', 'boot', + 'container-reset', 'run-as', 'restore-skel', 'boot', 'timezone', ]); /** Every file under `dir` as paths relative to it, sorted. */ diff --git a/packages/lab-tools/prepare.py b/packages/lab-tools/prepare.py index 7af8a65..65c447d 100644 --- a/packages/lab-tools/prepare.py +++ b/packages/lab-tools/prepare.py @@ -200,7 +200,7 @@ def ssh_keypairs(a, ctx): # --------------------------------------------------------------------------------------- # Host actions: fixed scripts, built from validated values, run as root on lab servers. -SAFE_PATH = re.compile(r'^/(?:srv|opt|mnt|data|backup|tmp|root|home|var/www|var/tmp|var/log|usr/local|etc/(?:httpd/conf\.d|systemd|auto\.master\.d|auto\.[a-z0-9_-]+|cron\.d|exports\.d|sudoers\.d|profile\.d|ssh/sshd_config\.d|chrony\.d|yum\.repos\.d|logrotate\.d|rsyslog\.d|security/limits\.d|sysctl\.d|NetworkManager/system-connections|containers|firewalld/(?:services|zones)))(?:/[A-Za-z0-9_.@%+=:, -]+)*$') +SAFE_PATH = re.compile(r'^/(?:srv|opt|mnt|data|backup|tmp|root|home|var/www|var/tmp|var/log|usr/local|etc/(?:billing\.conf|httpd/conf\.d|systemd|auto\.master\.d|auto\.[a-z0-9_-]+|cron\.d|exports\.d|sudoers\.d|profile\.d|ssh/sshd_config\.d|chrony\.d|yum\.repos\.d|logrotate\.d|rsyslog\.d|security/limits\.d|sysctl\.d|NetworkManager/system-connections|containers|firewalld/(?:services|zones)))(?:/[A-Za-z0-9_.@%+=:, -]+)*$') PROTECTED = {'/srv', '/opt', '/mnt', '/tmp', '/root', '/home', '/var/www', '/var/tmp', '/var/log', '/usr/local', '/etc/systemd', '/etc/containers'} NAME = re.compile(r'^[a-z_][a-z0-9_-]{0,31}$') SSH = ['ssh', '-o', 'BatchMode=yes', '-o', 'ConnectTimeout=10', '-o', 'StrictHostKeyChecking=accept-new', '-o', 'LogLevel=ERROR'] @@ -396,6 +396,12 @@ def script_boot(a): return '\n'.join(out) or 'true' +def script_timezone(a): + if not re.match(r'^[A-Za-z]+(?:/[A-Za-z_+-]+)*$', a['zone']): + raise SetupError('unsafe time zone') + return 'timedatectl set-timezone %s' % q(a['zone']) + + def script_run_as(a): user = q(name(a['user'])) image = q(a['image']) @@ -407,7 +413,7 @@ def script_run_as(a): 'package': script_package, 'service': script_service, 'group': script_group, 'user': script_user, 'directory': script_directory, 'file': script_file, 'remove-lines': script_remove_lines, 'firewall': script_firewall, 'selinux': script_selinux, 'wipe-disk': script_wipe_disk, 'systemd': script_systemd, 'linger': script_linger, 'container-reset': script_container_reset, - 'run-as': script_run_as, 'restore-skel': script_restore_skel, 'boot': script_boot, + 'run-as': script_run_as, 'restore-skel': script_restore_skel, 'boot': script_boot, 'timezone': script_timezone, } @@ -434,13 +440,69 @@ def host_action(a, ctx): say('%s: %s%s' % (host, label.strip(), '' if ok else ' (skipped: nothing to undo)')) +# --------------------------------------------------------------------------------------- +# Actions on workstation outside the project folder: only the learner's own home, with a deny list. + +HOME_PATH = re.compile(r'^[A-Za-z0-9_.-][A-Za-z0-9_. -]*(?:/[A-Za-z0-9_.-][A-Za-z0-9_. -]*)*$') +HOME_DENIED = {'.ssh', '.ssh/authorized_keys', '.ssh/known_hosts', '.ssh/config', '.bashrc', '.bash_profile', '.bash_logout', '.profile', + '.ssh/id_rsa', '.ssh/id_rsa.pub', '.ssh/id_ed25519', '.ssh/id_ed25519.pub', '.ssh/id_ecdsa', '.ssh/id_ecdsa.pub', '.ssh/id_dsa', '.ssh/id_dsa.pub'} + + +def home_dir(): + return Path(os.environ.get('LAB_HOME') or Path.home()) + + +def home_remove(a, ctx): + for relative in a['paths']: + clean = relative.rstrip('/') + if not HOME_PATH.match(clean) or '..' in clean.split('/') or clean in HOME_DENIED or clean.startswith('lab-archive'): + raise SetupError('this version of lab will not remove ~/' + relative) + target = home_dir() / clean + if target.is_dir() and not target.is_symlink(): + shutil.rmtree(target, ignore_errors=True) + elif target.exists() or target.is_symlink(): + target.unlink() + say('removed ' + ', '.join('~/' + p for p in a['paths'])) + + +def ssh_config_block(a, ctx): + path = home_dir() / '.ssh' / 'config' + if not path.exists(): + return + kept, skipping = [], False + for line in path.read_text().splitlines(): + words = line.split() + if words[:1] and words[0].lower() == 'host': + skipping = a['host'] in words[1:] + if not skipping: + kept.append(line) + path.write_text('\n'.join(kept).rstrip('\n') + '\n' if kept else '') + say('removed the ssh alias ' + a['host']) + + +def ssh_identity(a, ctx): + key = home_dir() / '.ssh' / a['name'] + (home_dir() / '.ssh').mkdir(mode=0o700, exist_ok=True) + for old in (key, Path(str(key) + '.pub')): + old.unlink(missing_ok=True) + tool(['ssh-keygen', '-q', '-t', 'ed25519', '-N', '', '-C', 'student@workstation (%s)' % a['user'], '-f', str(key)]) + public = Path(str(key) + '.pub').read_text().strip() + user = name(a['user']) + script = ('install -d -m 700 -o %(u)s -g %(u)s ~%(u)s/.ssh\necho %(k)s >> ~%(u)s/.ssh/authorized_keys\n' + 'chown %(u)s:%(u)s ~%(u)s/.ssh/authorized_keys; chmod 600 ~%(u)s/.ssh/authorized_keys') % {'u': user, 'k': q(public)} + for host in a['hosts']: + run_on_host(host, script, False) + say('created ~/.ssh/%s and installed it for %s on %s' % (a['name'], user, ', '.join(a['hosts']))) + + ACTIONS = { 'self-signed-cert': self_signed_cert, 'htpasswd': htpasswd, 'password-hash-var': password_hash_var, 'vault-encrypt': vault_encrypt, 'pack-installed-collection': pack_installed_collection, 'build-collection': build_collection, 'collection-requirements': collection_requirements, 'git-seed-remote': git_seed_remote, 'ssh-keypairs': ssh_keypairs, } +ACTIONS.update({'home-remove': home_remove, 'ssh-config-block': ssh_config_block, 'ssh-identity': ssh_identity}) ACTIONS.update({kind: host_action for kind in HOST_SCRIPTS}) -CONTROL_ACTIONS = set(ACTIONS) - set(HOST_SCRIPTS) +CONTROL_ACTIONS = set(ACTIONS) - set(HOST_SCRIPTS) - {'home-remove', 'ssh-config-block', 'ssh-identity'} class Context: diff --git a/packages/schema/schemas/source/lab.schema.json b/packages/schema/schemas/source/lab.schema.json index fda06ba..5a91e81 100644 --- a/packages/schema/schemas/source/lab.schema.json +++ b/packages/schema/schemas/source/lab.schema.json @@ -1165,6 +1165,96 @@ } } } + }, + { + "type": "object", + "additionalProperties": false, + "required": [ + "action", + "paths" + ], + "properties": { + "action": { + "const": "home-remove" + }, + "paths": { + "type": "array", + "minItems": 1, + "maxItems": 20, + "items": { + "$ref": "#/$defs/homePath" + } + } + } + }, + { + "type": "object", + "additionalProperties": false, + "required": [ + "action", + "host" + ], + "properties": { + "action": { + "const": "ssh-config-block" + }, + "host": { + "type": "string", + "pattern": "^[a-z][a-z0-9-]{0,30}$" + }, + "state": { + "enum": [ + "absent" + ] + } + } + }, + { + "type": "object", + "additionalProperties": false, + "required": [ + "action", + "name", + "hosts", + "user" + ], + "properties": { + "action": { + "const": "ssh-identity" + }, + "name": { + "type": "string", + "pattern": "^id_[a-z]{2,12}$" + }, + "hosts": { + "$ref": "#/$defs/serverList" + }, + "user": { + "$ref": "#/$defs/unixName" + } + } + }, + { + "type": "object", + "additionalProperties": false, + "required": [ + "action", + "hosts", + "zone" + ], + "properties": { + "action": { + "const": "timezone" + }, + "hosts": { + "$ref": "#/$defs/serverList" + }, + "zone": { + "type": "string", + "pattern": "^[A-Za-z]+(?:/[A-Za-z_+-]+)*$", + "maxLength": 40 + } + } } ] }, @@ -2300,7 +2390,7 @@ "description": "A path a setup or cleanup action may create or remove on a managed host: only under directories meant for local data and drop-in configuration.", "type": "string", "maxLength": 200, - "pattern": "^/(?:srv|opt|mnt|data|backup|tmp|root|home|var/www|var/tmp|var/log|usr/local|etc/(?:httpd/conf\\.d|systemd|auto\\.master\\.d|auto\\.[a-z0-9_-]+|cron\\.d|exports\\.d|sudoers\\.d|profile\\.d|ssh/sshd_config\\.d|chrony\\.d|yum\\.repos\\.d|logrotate\\.d|rsyslog\\.d|security/limits\\.d|sysctl\\.d|NetworkManager/system-connections|containers|httpd/conf\\.d|firewalld/(?:services|zones)))(?:/[A-Za-z0-9_.@%+=:, -]+)*$", + "pattern": "^/(?:srv|opt|mnt|data|backup|tmp|root|home|var/www|var/tmp|var/log|usr/local|etc/(?:billing\\.conf|httpd/conf\\.d|systemd|auto\\.master\\.d|auto\\.[a-z0-9_-]+|cron\\.d|exports\\.d|sudoers\\.d|profile\\.d|ssh/sshd_config\\.d|chrony\\.d|yum\\.repos\\.d|logrotate\\.d|rsyslog\\.d|security/limits\\.d|sysctl\\.d|NetworkManager/system-connections|containers|httpd/conf\\.d|firewalld/(?:services|zones)))(?:/[A-Za-z0-9_.@%+=:, -]+)*$", "not": { "pattern": "(?:^|/)\\.\\.(?:/|$)" } @@ -2321,6 +2411,15 @@ "unixName": { "type": "string", "pattern": "^[a-z_][a-z0-9_-]{0,31}$" + }, + "homePath": { + "description": "A path in the learner's home directory on workstation. Shell start-up files, authorized_keys, known_hosts, ssh config and the standard private keys are refused.", + "type": "string", + "maxLength": 120, + "pattern": "^[A-Za-z0-9_.-][A-Za-z0-9_. -]*(?:/[A-Za-z0-9_.-][A-Za-z0-9_. -]*)*$", + "not": { + "pattern": "(?:^|/)\\.\\.(?:/|$)" + } } } } From bbdcb6ac5160504e8f439b95bec1e453a2abca72 Mon Sep 17 00:00:00 2001 From: Ayesh Rodrigo Date: Sun, 4 Oct 2026 11:25:14 +1300 Subject: [PATCH 4/9] Guided exercise labs for chapters 12 and 13 --- .../ch12-networking/04-exercise-nmcli.md | 18 +++ .../ch12-networking/06-exercise-names.md | 18 +++ .../08-exercise-troubleshooting.md | 43 +++--- .../chapters/ch13-software/02-exercise-rpm.md | 18 +++ .../chapters/ch13-software/04-exercise-dnf.md | 18 +++ .../ch13-software/06-exercise-local-repo.md | 38 +++--- .../ch13-software/08-exercise-modules.md | 18 +++ .../programs/rhel9-sysadmin/lab/sa-dnf.yml | 73 +++++++++++ .../rhel9-sysadmin/lab/sa-local-repo.yml | 82 ++++++++++++ .../rhel9-sysadmin/lab/sa-modules.yml | 32 +++++ .../programs/rhel9-sysadmin/lab/sa-names.yml | 67 ++++++++++ .../programs/rhel9-sysadmin/lab/sa-nmcli.yml | 40 ++++++ .../programs/rhel9-sysadmin/lab/sa-reach.yml | 85 ++++++++++++ .../programs/rhel9-sysadmin/lab/sa-rpm.yml | 33 +++++ packages/compiler/src/lab.js | 2 +- packages/lab-tools/grade.py | 2 +- packages/lab-tools/prepare.py | 37 +++++- .../schema/schemas/source/lab.schema.json | 123 +++++++++++++++++- 18 files changed, 692 insertions(+), 55 deletions(-) create mode 100644 content/programs/rhel9-sysadmin/lab/sa-dnf.yml create mode 100644 content/programs/rhel9-sysadmin/lab/sa-local-repo.yml create mode 100644 content/programs/rhel9-sysadmin/lab/sa-modules.yml create mode 100644 content/programs/rhel9-sysadmin/lab/sa-names.yml create mode 100644 content/programs/rhel9-sysadmin/lab/sa-nmcli.yml create mode 100644 content/programs/rhel9-sysadmin/lab/sa-reach.yml create mode 100644 content/programs/rhel9-sysadmin/lab/sa-rpm.yml diff --git a/content/programs/rhel9-sysadmin/chapters/ch12-networking/04-exercise-nmcli.md b/content/programs/rhel9-sysadmin/chapters/ch12-networking/04-exercise-nmcli.md index fc5ca39..996fc7a 100644 --- a/content/programs/rhel9-sysadmin/chapters/ch12-networking/04-exercise-nmcli.md +++ b/content/programs/rhel9-sysadmin/chapters/ch12-networking/04-exercise-nmcli.md @@ -12,9 +12,23 @@ Build a static network configuration from scratch on a virtual interface: create objectives=["ch12.networkmanager"] id="nmcli" title="Configure a static connection" + exercise="sa-nmcli" + ownExercise=true + guided=true + starter=false hosts=["workstation","servera"] outcomes=["Create a static dummy connection with nmcli.","Add and replace addresses, applying them with connection up.","Verify persistence across a reboot and clean up."] %} + {% task id="task-35822c99c20e" title="Start the exercise" %} + On workstation, start the exercise. It removes a dummy0 connection left by an earlier run from servera. + +```console +[student@workstation ~]$ lab start sa-nmcli +``` + + Later tasks assume that you are logged in to the server they name: `ssh student@servera` from workstation, then `sudo -i` for a root shell. + {% /task %} + {% task id="task-fcdedbe17877" title="Look before you touch" %} On servera as root (`sudo -i`), list the devices and the connections. @@ -157,4 +171,8 @@ nameserver 172.25.250.254 The virtual device and the extra DNS server disappear a moment after the profile is deleted. {% /task %} + + {% task id="task-cac4d7d77de1" title="Grade and finish" %} + {% lab-finish exercise="sa-nmcli" grade=true servers=true /%} + {% /task %} {% /lab %} diff --git a/content/programs/rhel9-sysadmin/chapters/ch12-networking/06-exercise-names.md b/content/programs/rhel9-sysadmin/chapters/ch12-networking/06-exercise-names.md index 3df0e4b..8f7ded7 100644 --- a/content/programs/rhel9-sysadmin/chapters/ch12-networking/06-exercise-names.md +++ b/content/programs/rhel9-sysadmin/chapters/ch12-networking/06-exercise-names.md @@ -12,9 +12,23 @@ Look at how servera knows its own name and how it finds serverb, then plant a wr objectives=["ch12.names"] id="names" title="Names, hosts and DNS" + exercise="sa-names" + ownExercise=true + guided=true + starter=false hosts=["workstation","servera"] outcomes=["Show and change the host name with hostnamectl.","Compare getent with dig.","Use /etc/hosts and see that it wins over DNS."] %} + {% task id="task-538f7e88aa10" title="Start the exercise" %} + On workstation, start the exercise. It removes test entries from `/etc/hosts` of servera and checks its host name. + +```console +[student@workstation ~]$ lab start sa-names +``` + + Later tasks assume that you are logged in to the server they name: `ssh student@servera` from workstation, then `sudo -i` for a root shell. + {% /task %} + {% task id="task-ba878b5cb585" title="Read the name configuration" %} On servera as root (`sudo -i`), show the short and fully qualified names, the file that stores the name, the DNS servers and the search domain, and the order of name sources. @@ -125,4 +139,8 @@ servera.lab.example.com ``` {% /reveal %} {% /task %} + + {% task id="task-e2b9d263c480" title="Grade and finish" %} + {% lab-finish exercise="sa-names" grade=true servers=true /%} + {% /task %} {% /lab %} diff --git a/content/programs/rhel9-sysadmin/chapters/ch12-networking/08-exercise-troubleshooting.md b/content/programs/rhel9-sysadmin/chapters/ch12-networking/08-exercise-troubleshooting.md index 640f09f..d0fcc3e 100644 --- a/content/programs/rhel9-sysadmin/chapters/ch12-networking/08-exercise-troubleshooting.md +++ b/content/programs/rhel9-sysadmin/chapters/ch12-networking/08-exercise-troubleshooting.md @@ -12,28 +12,27 @@ A small web service on serverb answers on its own machine and from nowhere else. objectives=["ch12.troubleshooting"] id="troubleshooting" title="The service nobody can reach" + exercise="sa-reach" + ownExercise=true + guided=true + starter=false hosts=["workstation","servera","serverb"] outcomes=["Use the error message to pick the next layer to check.","Find a loopback-only listener with ss.","Open a port and fix the bind address."] %} - {% task id="task-968f7f9436c8" title="Start the broken service on serverb" %} - On serverb as root (`sudo -i`), create a page and start a web server on port 8080 that is bound to the loopback address only. (Use `nohup` and `&` so it keeps running.) + {% task id="task-aeb68e6f8fc8" title="Start the exercise" %} + On workstation, start the exercise. It starts a small web server on serverb that listens on port 8080 of the loopback address only, with the page `hello from serverb`. - {% reveal title="Show solution" %} +```console +[student@workstation ~]$ lab start sa-reach +``` + + Later tasks assume that you are logged in to the server they name: `ssh student@servera` from workstation, then `sudo -i` for a root shell. + + Then, as root on serverb, change to the exercise directory: ```console -[student@workstation ~]$ ssh student@serverb -[student@serverb ~]$ sudo -i -[root@serverb ~]# mkdir -p /srv/web -[root@serverb ~]# echo "hello from serverb" > /srv/web/index.html [root@serverb ~]# cd /srv/web -[root@serverb web]# nohup python3 -m http.server 8080 --bind 127.0.0.1 > /tmp/http.log 2>&1 & -[1] 679 -[root@serverb web]# curl -s http://127.0.0.1:8080/ -hello from serverb ``` - - It works locally, as expected. Keep this session open. - {% /reveal %} {% /task %} {% task id="task-f3c90437d725" title="Rung 1 to 5: is the machine reachable?" %} @@ -105,19 +104,7 @@ hello from serverb {% /reveal %} {% /task %} - {% task id="task-d032740aa9df" title="Clean up" %} - Stop the server, remove the page and the temporary firewall rule (a reload restores the permanent configuration). - - {% reveal title="Show solution" %} - -```console -[root@serverb web]# pkill -f 'http.server 8080' -[root@serverb web]# firewall-cmd --reload -success -[root@serverb web]# cd; rm -rf /srv/web /tmp/http.log -[root@serverb ~]# exit -[student@serverb ~]$ exit -``` - {% /reveal %} + {% task id="task-e77fdca4e4c7" title="Grade and finish" %} + {% lab-finish exercise="sa-reach" grade=true servers=true /%} {% /task %} {% /lab %} diff --git a/content/programs/rhel9-sysadmin/chapters/ch13-software/02-exercise-rpm.md b/content/programs/rhel9-sysadmin/chapters/ch13-software/02-exercise-rpm.md index 5af5a40..82d24dc 100644 --- a/content/programs/rhel9-sysadmin/chapters/ch13-software/02-exercise-rpm.md +++ b/content/programs/rhel9-sysadmin/chapters/ch13-software/02-exercise-rpm.md @@ -12,9 +12,23 @@ Use `rpm` to learn where files come from, what a package contains, and whether a objectives=["ch13.rpm"] id="rpm" title="Interrogate the installed software" + exercise="sa-rpm" + ownExercise=true + guided=true + starter=false hosts=["workstation","servera"] outcomes=["Query packages with rpm -q, -qi, -ql, -qc, -qd and -qf.","Find the owner of a file.","Detect a modified file with rpm -V and repair it."] %} + {% task id="task-9955f107ff28" title="Start the exercise" %} + On workstation, start the exercise. It makes sure the package `tree` is installed and intact on servera. + +```console +[student@workstation ~]$ lab start sa-rpm +``` + + Later tasks assume that you are logged in to the server they name: `ssh student@servera` from workstation, then `sudo -i` for a root shell. + {% /task %} + {% task id="task-50e42e415190" title="Ask about a package" %} On servera as root (`sudo -i`), show the exact version of the `tree` package, its summary and licence, and the files it installed. @@ -111,4 +125,8 @@ Complete! No output: the file is back to what the package contains. {% /reveal %} {% /task %} + + {% task id="task-b1a6c98506c8" title="Grade and finish" %} + {% lab-finish exercise="sa-rpm" grade=true servers=true /%} + {% /task %} {% /lab %} diff --git a/content/programs/rhel9-sysadmin/chapters/ch13-software/04-exercise-dnf.md b/content/programs/rhel9-sysadmin/chapters/ch13-software/04-exercise-dnf.md index 3a2ddb6..5a6e9ea 100644 --- a/content/programs/rhel9-sysadmin/chapters/ch13-software/04-exercise-dnf.md +++ b/content/programs/rhel9-sysadmin/chapters/ch13-software/04-exercise-dnf.md @@ -12,9 +12,23 @@ A colleague asks for the `zip` command. Find the package, check what it needs, i objectives=["ch13.dnf"] id="dnf" title="Install, inspect and undo" + exercise="sa-dnf" + ownExercise=true + guided=true + starter=false hosts=["workstation","servera"] outcomes=["Search, inspect and install a package with dnf.","Read the transaction summary and the dependencies.","Review and undo a transaction."] %} + {% task id="task-140a1b6e4799" title="Start the exercise" %} + On workstation, start the exercise. It makes sure zip and unzip are not installed on servera. + +```console +[student@workstation ~]$ lab start sa-dnf +``` + + Later tasks assume that you are logged in to the server they name: `ssh student@servera` from workstation, then `sudo -i` for a root shell. + {% /task %} + {% task id="task-f7ebcb15fdf9" title="Find the package" %} On servera as root (`sudo -i`), confirm that `zip` is not installed. Search the repositories for it, and show its description. @@ -153,4 +167,8 @@ package unzip is not installed ``` {% /reveal %} {% /task %} + + {% task id="task-a2dc88f87d6a" title="Grade and finish" %} + {% lab-finish exercise="sa-dnf" grade=true servers=true /%} + {% /task %} {% /lab %} diff --git a/content/programs/rhel9-sysadmin/chapters/ch13-software/06-exercise-local-repo.md b/content/programs/rhel9-sysadmin/chapters/ch13-software/06-exercise-local-repo.md index 1ded582..e323c7b 100644 --- a/content/programs/rhel9-sysadmin/chapters/ch13-software/06-exercise-local-repo.md +++ b/content/programs/rhel9-sysadmin/chapters/ch13-software/06-exercise-local-repo.md @@ -12,9 +12,23 @@ Build a small repository from downloaded packages, define it for dnf with signat objectives=["ch13.repositories"] id="local-repo" title="Serve your own repository" + exercise="sa-local-repo" + ownExercise=true + guided=true + starter=false hosts=["workstation","servera"] outcomes=["Download packages with their dependencies.","Create repository metadata with createrepo_c.","Define a repository, keep gpgcheck on, and install from it only."] %} + {% task id="task-10d8a144bf5a" title="Start the exercise" %} + On workstation, start the exercise. It installs `dnf-plugins-core` and `createrepo_c` on servera, and removes a repository left by an earlier run. + +```console +[student@workstation ~]$ lab start sa-local-repo +``` + + Later tasks assume that you are logged in to the server they name: `ssh student@servera` from workstation, then `sudo -i` for a root shell. + {% /task %} + {% task id="task-feece255f9d1" title="Look at the existing repositories" %} On servera as root (`sudo -i`), list the enabled repositories and find the `gpgkey` line that the distribution's repositories use. @@ -36,19 +50,6 @@ gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-Rocky-9 {% /reveal %} {% /task %} - {% task id="task-6f286b1c5260" title="Install the tools" %} - Install `dnf-plugins-core` (for `dnf download`) and `createrepo_c`. - - {% reveal title="Show solution" %} - -```console -[root@servera ~]# dnf install -y dnf-plugins-core createrepo_c -...output omitted... -Complete! -``` - {% /reveal %} - {% /task %} - {% task id="task-10e2bfc26016" title="Download the packages" %} Create `/srv/localrepo` and download `zip` with its dependencies into it. Check one signature with `rpm -K`. @@ -122,14 +123,7 @@ From repo : localrepo {% /reveal %} {% /task %} - {% task id="task-ccc490605f79" title="Clean up" %} - -```console -[root@servera ~]# dnf remove -y zip -[root@servera ~]# rm -rf /srv/localrepo /etc/yum.repos.d/local.repo -[root@servera ~]# dnf clean all > /dev/null -[root@servera ~]# exit -[student@servera ~]$ exit -``` + {% task id="task-d85d24fd5360" title="Grade and finish" %} + {% lab-finish exercise="sa-local-repo" grade=true servers=true /%} {% /task %} {% /lab %} diff --git a/content/programs/rhel9-sysadmin/chapters/ch13-software/08-exercise-modules.md b/content/programs/rhel9-sysadmin/chapters/ch13-software/08-exercise-modules.md index 3a401b2..291be41 100644 --- a/content/programs/rhel9-sysadmin/chapters/ch13-software/08-exercise-modules.md +++ b/content/programs/rhel9-sysadmin/chapters/ch13-software/08-exercise-modules.md @@ -12,9 +12,23 @@ Pick a version of a program from the module streams, see what that does to the p objectives=["ch13.updates"] id="modules" title="Choose a version and check for updates" + exercise="sa-modules" + ownExercise=true + guided=true + starter=false hosts=["workstation","servera"] outcomes=["List module streams and enable one.","See the effect of a stream on the offered version, and reset it.","Review pending updates and decide about a reboot."] %} + {% task id="task-6270123b483f" title="Start the exercise" %} + On workstation, start the exercise. It resets the nginx module on servera, so no stream is enabled. + +```console +[student@workstation ~]$ lab start sa-modules +``` + + Later tasks assume that you are logged in to the server they name: `ssh student@servera` from workstation, then `sudo -i` for a root shell. + {% /task %} + {% task id="task-f0eefe80162a" title="List the streams" %} On servera as root (`sudo -i`), list the streams of the `nginx` module, and show which version of the `nginx` package dnf offers before any stream is enabled. @@ -127,4 +141,8 @@ Reboot should not be necessary. A kernel update is waiting, but it has not been installed yet; once it is, `uname -r` will still show the old version until a reboot. Do not apply the updates in this exercise. {% /reveal %} {% /task %} + + {% task id="task-a6c18ad03688" title="Grade and finish" %} + {% lab-finish exercise="sa-modules" grade=true servers=true /%} + {% /task %} {% /lab %} diff --git a/content/programs/rhel9-sysadmin/lab/sa-dnf.yml b/content/programs/rhel9-sysadmin/lab/sa-dnf.yml new file mode 100644 index 0000000..1bc698b --- /dev/null +++ b/content/programs/rhel9-sysadmin/lab/sa-dnf.yml @@ -0,0 +1,73 @@ +name: sa-dnf +title: Install, inspect and undo +page: ch13/exercise-dnf +transport: ssh +version: 1 +starter: [] +setup: +- action: package + hosts: + - servera + names: + - zip + - unzip + state: absent +- action: file + hosts: + - servera + path: /tmp/a.txt + state: absent +- action: file + hosts: + - servera + path: /tmp/a.zip + state: absent +checkpoints: + final: + files: [] + checks: + - id: zip + kind: package + 'on': servera.lab.example.com + targets: + - servera.lab.example.com + message: zip is gone again after the undo + names: + - zip + installed: false + - id: unzip + kind: package + 'on': servera.lab.example.com + targets: + - servera.lab.example.com + message: unzip is gone again after the undo + names: + - unzip + installed: false + - id: files + kind: file + 'on': servera.lab.example.com + targets: + - servera.lab.example.com + message: the practice files in /tmp were removed + paths: + - /tmp/a.zip + exists: false +finish: +- action: package + hosts: + - servera + names: + - zip + - unzip + state: absent +- action: file + hosts: + - servera + path: /tmp/a.txt + state: absent +- action: file + hosts: + - servera + path: /tmp/a.zip + state: absent diff --git a/content/programs/rhel9-sysadmin/lab/sa-local-repo.yml b/content/programs/rhel9-sysadmin/lab/sa-local-repo.yml new file mode 100644 index 0000000..1fa7422 --- /dev/null +++ b/content/programs/rhel9-sysadmin/lab/sa-local-repo.yml @@ -0,0 +1,82 @@ +name: sa-local-repo +title: Serve your own repository +page: ch13/exercise-local-repo +transport: ssh +version: 1 +starter: [] +setup: +- action: package + hosts: + - servera + names: + - dnf-plugins-core + - createrepo_c + state: present +- action: package + hosts: + - servera + names: + - zip + - unzip + state: absent +- action: directory + hosts: + - servera + path: /srv/localrepo + state: absent +- action: file + hosts: + - servera + path: /etc/yum.repos.d/local.repo + state: absent +checkpoints: + final: + files: [] + checks: + - id: rpm + kind: file + 'on': servera.lab.example.com + targets: + - servera.lab.example.com + message: /srv/localrepo holds the downloaded zip package + paths: + - /srv/localrepo/repodata/repomd.xml + fileType: regular + - id: repo + kind: file + 'on': servera.lab.example.com + targets: + - servera.lab.example.com + message: a repository definition points at /srv/localrepo + paths: + - /etc/yum.repos.d/local.repo + contains: + - '[localrepo]' + - file:///srv/localrepo + - id: zip + kind: package + 'on': servera.lab.example.com + targets: + - servera.lab.example.com + message: zip was installed from the local repository + names: + - zip +finish: +- action: package + hosts: + - servera + names: + - zip + - unzip + - createrepo_c + state: absent +- action: directory + hosts: + - servera + path: /srv/localrepo + state: absent +- action: file + hosts: + - servera + path: /etc/yum.repos.d/local.repo + state: absent diff --git a/content/programs/rhel9-sysadmin/lab/sa-modules.yml b/content/programs/rhel9-sysadmin/lab/sa-modules.yml new file mode 100644 index 0000000..b364444 --- /dev/null +++ b/content/programs/rhel9-sysadmin/lab/sa-modules.yml @@ -0,0 +1,32 @@ +name: sa-modules +title: Choose a version and check for updates +page: ch13/exercise-modules +transport: ssh +version: 1 +starter: [] +setup: +- action: dnf-module + hosts: + - servera + name: nginx + state: reset +checkpoints: + final: + files: [] + checks: + - id: no-stream + kind: file + 'on': servera.lab.example.com + targets: + - servera.lab.example.com + message: no nginx stream is left enabled + paths: + - /etc/dnf/modules.d/nginx.module + lacks: + - state=enabled +finish: +- action: dnf-module + hosts: + - servera + name: nginx + state: reset diff --git a/content/programs/rhel9-sysadmin/lab/sa-names.yml b/content/programs/rhel9-sysadmin/lab/sa-names.yml new file mode 100644 index 0000000..6c88dd4 --- /dev/null +++ b/content/programs/rhel9-sysadmin/lab/sa-names.yml @@ -0,0 +1,67 @@ +name: sa-names +title: Names, hosts and DNS +page: ch12/exercise-names +transport: ssh +version: 1 +starter: [] +setup: +- action: remove-lines + hosts: + - servera + path: /etc/hosts + matching: ^10[.]1[.]1[.]1 serverb +- action: remove-lines + hosts: + - servera + path: /etc/hosts + matching: ' app[.]lab[.]example[.]com app' +- action: hostname + hosts: + - servera + name: servera.lab.example.com +checkpoints: + final: + files: [] + checks: + - id: hostname + kind: hostname + 'on': servera.lab.example.com + targets: + - servera.lab.example.com + message: the host name is servera again + short: servera + - id: no-wrong + kind: file + 'on': servera.lab.example.com + targets: + - servera.lab.example.com + message: /etc/hosts has no wrong entry for serverb any more + paths: + - /etc/hosts + lacks: + - 10.1.1.1 serverb + - id: no-app + kind: file + 'on': servera.lab.example.com + targets: + - servera.lab.example.com + message: the temporary entry for app was removed + paths: + - /etc/hosts + lacks: + - app.lab.example.com app +finish: +- action: remove-lines + hosts: + - servera + path: /etc/hosts + matching: ^10[.]1[.]1[.]1 serverb +- action: remove-lines + hosts: + - servera + path: /etc/hosts + matching: ' app[.]lab[.]example[.]com app' +- action: hostname + hosts: + - servera + name: servera.lab.example.com diff --git a/content/programs/rhel9-sysadmin/lab/sa-nmcli.yml b/content/programs/rhel9-sysadmin/lab/sa-nmcli.yml new file mode 100644 index 0000000..536e06c --- /dev/null +++ b/content/programs/rhel9-sysadmin/lab/sa-nmcli.yml @@ -0,0 +1,40 @@ +name: sa-nmcli +title: Configure a static connection +page: ch12/exercise-nmcli +transport: ssh +version: 1 +starter: [] +setup: +- action: nm-connection + hosts: + - servera + name: dummy0 + state: absent +checkpoints: + final: + files: [] + checks: + - id: profile + kind: file + 'on': servera.lab.example.com + targets: + - servera.lab.example.com + message: the dummy0 profile was deleted at the end + paths: + - /etc/NetworkManager/system-connections/dummy0.nmconnection + exists: false + - id: device + kind: file + 'on': servera.lab.example.com + targets: + - servera.lab.example.com + message: the dummy0 interface is gone + paths: + - /sys/class/net/dummy0 + exists: false +finish: +- action: nm-connection + hosts: + - servera + name: dummy0 + state: absent diff --git a/content/programs/rhel9-sysadmin/lab/sa-reach.yml b/content/programs/rhel9-sysadmin/lab/sa-reach.yml new file mode 100644 index 0000000..9b254d9 --- /dev/null +++ b/content/programs/rhel9-sysadmin/lab/sa-reach.yml @@ -0,0 +1,85 @@ +name: sa-reach +title: The service nobody can reach +page: ch12/exercise-troubleshooting +transport: ssh +version: 1 +starter: [] +setup: +- action: directory + hosts: + - serverb + path: /srv/web + state: present +- action: file + hosts: + - serverb + path: /srv/web/index.html + state: present + content: 'hello from serverb + + ' +- action: http-server + hosts: + - serverb + port: 8080 + bind: 127.0.0.1 + directory: /srv/web + state: started +- action: firewall + hosts: + - serverb + state: absent + port: 8080/tcp +checkpoints: + final: + files: [] + checks: + - id: listen + kind: http + 'on': serverb.lab.example.com + targets: + - serverb.lab.example.com + message: the web server on serverb answers locally on 8080 + url: http://localhost:8080/ + containsAny: + - hello from serverb + - id: firewall + kind: firewall + 'on': serverb.lab.example.com + targets: + - serverb.lab.example.com + message: 8080/tcp is allowed in the running firewall + port: 8080/tcp + permanent: false + - id: remote + kind: http + 'on': servera.lab.example.com + targets: + - servera.lab.example.com + message: servera reaches serverb on port 8080 + url: http://serverb.lab.example.com:8080/ + containsAny: + - hello from serverb +finish: +- action: http-server + hosts: + - serverb + port: 8080 + bind: 127.0.0.1 + directory: /srv/web + state: stopped +- action: firewall + hosts: + - serverb + state: absent + port: 8080/tcp +- action: directory + hosts: + - serverb + path: /srv/web + state: absent +- action: file + hosts: + - serverb + path: /tmp/http.log + state: absent diff --git a/content/programs/rhel9-sysadmin/lab/sa-rpm.yml b/content/programs/rhel9-sysadmin/lab/sa-rpm.yml new file mode 100644 index 0000000..af90b4a --- /dev/null +++ b/content/programs/rhel9-sysadmin/lab/sa-rpm.yml @@ -0,0 +1,33 @@ +name: sa-rpm +title: Interrogate the installed software +page: ch13/exercise-rpm +transport: ssh +version: 1 +starter: [] +setup: +- action: package + hosts: + - servera + names: + - tree + state: reinstalled +checkpoints: + final: + files: [] + checks: + - id: tree + kind: package + 'on': servera.lab.example.com + targets: + - servera.lab.example.com + message: tree is installed and every file matches the RPM database again + names: + - tree + verify: true +finish: +- action: package + hosts: + - servera + names: + - tree + state: reinstalled diff --git a/packages/compiler/src/lab.js b/packages/compiler/src/lab.js index 4f1fac1..4365647 100644 --- a/packages/compiler/src/lab.js +++ b/packages/compiler/src/lab.js @@ -32,7 +32,7 @@ const CONTROL_ONLY = new Set(['git', 'lint']); // Setup and cleanup actions that run as root on the lab servers; the rest work on the workstation. const HOST_ACTIONS = new Set([ 'package', 'service', 'group', 'user', 'directory', 'file', 'remove-lines', 'firewall', 'selinux', 'wipe-disk', 'systemd', 'linger', - 'container-reset', 'run-as', 'restore-skel', 'boot', 'timezone', + 'container-reset', 'run-as', 'restore-skel', 'boot', 'timezone', 'nm-connection', 'hostname', 'http-server', 'dnf-module', ]); /** Every file under `dir` as paths relative to it, sorted. */ diff --git a/packages/lab-tools/grade.py b/packages/lab-tools/grade.py index 21e38d2..c70a040 100644 --- a/packages/lab-tools/grade.py +++ b/packages/lab-tools/grade.py @@ -163,7 +163,7 @@ def conditions_firewall(c): def conditions_package(c): names = ' '.join(shlex.quote(n) for n in c['names']) if c.get('installed', True): - return ['rpm -q %s >/dev/null' % names] + return ['rpm -q %s >/dev/null' % names] + (['rpm -V %s' % names] if c.get('verify') else []) return ['! rpm -q %s >/dev/null' % shlex.quote(n) for n in c['names']] diff --git a/packages/lab-tools/prepare.py b/packages/lab-tools/prepare.py index 65c447d..472bee0 100644 --- a/packages/lab-tools/prepare.py +++ b/packages/lab-tools/prepare.py @@ -228,6 +228,8 @@ def script_package(a): names = ' '.join(q(n) for n in a['names']) if a.get('state', 'present') == 'absent': return 'dnf remove -y -q %s >/dev/null 2>&1' % names + if a.get('state') == 'reinstalled': + return 'dnf install -y -q %s >/dev/null 2>&1; dnf reinstall -y -q %s >/dev/null 2>&1' % (names, names) return 'dnf install -y -q %s >/dev/null 2>&1' % names @@ -269,7 +271,7 @@ def script_group(a): def script_user(a): n = q(name(a['name'])) if a.get('state', 'present') == 'absent': - return 'if id %s >/dev/null 2>&1; then pkill -KILL -u %s 2>/dev/null; sleep 1; userdel -r -f %s 2>/dev/null || userdel -f %s; fi' % (n, n, n, n) + return 'if id %s >/dev/null 2>&1; then pkill -KILL -u %s 2>/dev/null || true; sleep 1; userdel -r -f %s 2>/dev/null || userdel -f %s; fi' % (n, n, n, n) flags = '' if 'uid' in a: flags += ' -u %d' % a['uid'] @@ -353,7 +355,8 @@ def script_selinux(a): def script_wipe_disk(a): dev = q(a['device']) - return """for s in $(swapon --noheadings --show=NAME 2>/dev/null); do + return """set +e +for s in $(swapon --noheadings --show=NAME 2>/dev/null); do case $s in %(d)s*|/dev/dm-*|/dev/mapper/*) swapoff $s 2>/dev/null;; esac done for m in $(lsblk -nrpo MOUNTPOINTS %(dev)s 2>/dev/null | grep '^/'); do umount -l "$m" 2>/dev/null; done @@ -402,6 +405,35 @@ def script_timezone(a): return 'timedatectl set-timezone %s' % q(a['zone']) +def script_nm_connection(a): + return 'nmcli connection delete %s >/dev/null 2>&1 || true' % q(a['name']) + + +def script_hostname(a): + if not re.match(r'^[a-z][a-z0-9.-]{1,60}$', a['name']): + raise SetupError('unsafe host name') + return 'hostnamectl set-hostname %s' % q(a['name']) + + +def script_http_server(a): + port = int(a['port']) + if a['bind'] not in ('127.0.0.1', '0.0.0.0'): + raise SetupError('unsafe address') + unit = 'lab-http-%d' % port + stop = 'systemctl stop %s 2>/dev/null || true\nsystemctl reset-failed %s 2>/dev/null || true\npkill -f %s 2>/dev/null || true' % (unit, unit, q('http.server %d' % port)) + if a.get('state', 'started') == 'stopped': + return stop + return '%s\nsystemd-run --quiet --unit=%s -p WorkingDirectory=%s python3 -m http.server %d --bind %s\nsleep 1' % ( + stop, unit, q(safe_path(a['directory'])), port, a['bind']) + + +def script_dnf_module(a): + n = q(a['name']) + if a['state'] == 'reset': + return 'dnf module reset -y %s >/dev/null 2>&1' % n + return 'dnf module enable -y %s:%s >/dev/null 2>&1' % (n, q(a['stream'])) + + def script_run_as(a): user = q(name(a['user'])) image = q(a['image']) @@ -414,6 +446,7 @@ def script_run_as(a): 'file': script_file, 'remove-lines': script_remove_lines, 'firewall': script_firewall, 'selinux': script_selinux, 'wipe-disk': script_wipe_disk, 'systemd': script_systemd, 'linger': script_linger, 'container-reset': script_container_reset, 'run-as': script_run_as, 'restore-skel': script_restore_skel, 'boot': script_boot, 'timezone': script_timezone, + 'nm-connection': script_nm_connection, 'hostname': script_hostname, 'http-server': script_http_server, 'dnf-module': script_dnf_module, } diff --git a/packages/schema/schemas/source/lab.schema.json b/packages/schema/schemas/source/lab.schema.json index 5a91e81..863a71c 100644 --- a/packages/schema/schemas/source/lab.schema.json +++ b/packages/schema/schemas/source/lab.schema.json @@ -641,7 +641,8 @@ "state": { "enum": [ "present", - "absent" + "absent", + "reinstalled" ] } } @@ -1255,6 +1256,122 @@ "maxLength": 40 } } + }, + { + "type": "object", + "additionalProperties": false, + "required": [ + "action", + "hosts", + "name" + ], + "properties": { + "action": { + "const": "nm-connection" + }, + "hosts": { + "$ref": "#/$defs/serverList" + }, + "name": { + "type": "string", + "pattern": "^[A-Za-z0-9_-]{1,30}$" + }, + "state": { + "enum": [ + "absent" + ] + } + } + }, + { + "type": "object", + "additionalProperties": false, + "required": [ + "action", + "hosts", + "name" + ], + "properties": { + "action": { + "const": "hostname" + }, + "hosts": { + "$ref": "#/$defs/serverList" + }, + "name": { + "type": "string", + "pattern": "^[a-z][a-z0-9.-]{1,60}$" + } + } + }, + { + "type": "object", + "additionalProperties": false, + "required": [ + "action", + "hosts", + "port", + "bind", + "directory" + ], + "properties": { + "action": { + "const": "http-server" + }, + "hosts": { + "$ref": "#/$defs/serverList" + }, + "port": { + "type": "integer", + "minimum": 1024, + "maximum": 65535 + }, + "bind": { + "type": "string", + "pattern": "^(?:127\\.0\\.0\\.1|0\\.0\\.0\\.0)$" + }, + "directory": { + "$ref": "#/$defs/hostPath" + }, + "state": { + "enum": [ + "started", + "stopped" + ] + } + } + }, + { + "type": "object", + "additionalProperties": false, + "required": [ + "action", + "hosts", + "name", + "state" + ], + "properties": { + "action": { + "const": "dnf-module" + }, + "hosts": { + "$ref": "#/$defs/serverList" + }, + "name": { + "type": "string", + "pattern": "^[a-z][a-z0-9-]{1,30}$" + }, + "stream": { + "type": "string", + "pattern": "^[0-9][A-Za-z0-9._-]{0,15}$" + }, + "state": { + "enum": [ + "reset", + "enabled" + ] + } + } } ] }, @@ -1386,6 +1503,10 @@ "installed": { "type": "boolean", "default": true + }, + "verify": { + "description": "Every file of the package matches the RPM database (rpm -V is clean).", + "type": "boolean" } }, "type": "object" From d28afe5310df3d41073d2f37b0e528f7ac01318a Mon Sep 17 00:00:00 2001 From: Ayesh Rodrigo Date: Sun, 4 Oct 2026 11:38:09 +1300 Subject: [PATCH 5/9] Guided exercise labs for chapters 14 and 15 --- .../02-exercise-tar.md | 23 ++- .../04-exercise-checksums.md | 23 ++- .../06-exercise-backups.md | 23 ++- .../08-exercise-moving.md | 27 ++-- .../02-exercise-first-scripts.md | 22 ++- .../04-exercise-conditions.md | 19 ++- .../06-exercise-cron.md | 30 ++-- .../08-exercise-timers.md | 27 ++-- .../rhel9-sysadmin/lab/sa-backups.yml | 90 ++++++++++++ .../rhel9-sysadmin/lab/sa-checksums.yml | 84 +++++++++++ .../rhel9-sysadmin/lab/sa-conditions.yml | 67 +++++++++ .../programs/rhel9-sysadmin/lab/sa-cron.yml | 95 +++++++++++++ .../rhel9-sysadmin/lab/sa-first-scripts.yml | 64 +++++++++ .../programs/rhel9-sysadmin/lab/sa-moving.yml | 103 ++++++++++++++ .../programs/rhel9-sysadmin/lab/sa-tar.yml | 84 +++++++++++ .../programs/rhel9-sysadmin/lab/sa-timers.yml | 133 ++++++++++++++++++ packages/compiler/src/lab.js | 2 +- packages/lab-tools/grade.py | 2 + packages/lab-tools/prepare.py | 6 +- .../schema/schemas/source/lab.schema.json | 29 ++++ 20 files changed, 882 insertions(+), 71 deletions(-) create mode 100644 content/programs/rhel9-sysadmin/lab/sa-backups.yml create mode 100644 content/programs/rhel9-sysadmin/lab/sa-checksums.yml create mode 100644 content/programs/rhel9-sysadmin/lab/sa-conditions.yml create mode 100644 content/programs/rhel9-sysadmin/lab/sa-cron.yml create mode 100644 content/programs/rhel9-sysadmin/lab/sa-first-scripts.yml create mode 100644 content/programs/rhel9-sysadmin/lab/sa-moving.yml create mode 100644 content/programs/rhel9-sysadmin/lab/sa-tar.yml create mode 100644 content/programs/rhel9-sysadmin/lab/sa-timers.yml diff --git a/content/programs/rhel9-sysadmin/chapters/ch14-archives-and-transfer/02-exercise-tar.md b/content/programs/rhel9-sysadmin/chapters/ch14-archives-and-transfer/02-exercise-tar.md index ac0f348..6c6d453 100644 --- a/content/programs/rhel9-sysadmin/chapters/ch14-archives-and-transfer/02-exercise-tar.md +++ b/content/programs/rhel9-sysadmin/chapters/ch14-archives-and-transfer/02-exercise-tar.md @@ -12,9 +12,23 @@ Build a small project, archive it three ways, restore it into a clean directory, objectives=["ch14.tar"] id="tar" title="Archive a project" + exercise="sa-tar" + ownExercise=true + guided=true + starter=false hosts=["workstation","servera"] outcomes=["Create, list and extract tar archives.","Extract into a chosen directory and extract a single member.","Compare the restored tree with the original."] %} + {% task id="task-61f650ffb6d4" title="Start the exercise" %} + On workstation, start the exercise. It removes `/root/arch` of an earlier run from servera. + +```console +[student@workstation ~]$ lab start sa-tar +``` + + Later tasks assume that you are logged in to the server they name: `ssh student@servera` from workstation, then `sudo -i` for a root shell. + {% /task %} + {% task id="task-cb1b078db942" title="Build the project" %} On servera as root (`sudo -i`), create `/root/arch/project` with `docs/notes.md`, `src/data1.txt` (the numbers 1 to 20000, from `seq`), and an executable script `run.sh`. @@ -128,12 +142,7 @@ root/arch/project/docs/notes.md {% /reveal %} {% /task %} - {% task id="task-fba6ad76cade" title="Clean up" %} - -```console -[root@servera arch]# cd; rm -rf /root/arch -[root@servera ~]# exit -[student@servera ~]$ exit -``` + {% task id="task-13eba9e5f961" title="Grade and finish" %} + {% lab-finish exercise="sa-tar" grade=true servers=true /%} {% /task %} {% /lab %} diff --git a/content/programs/rhel9-sysadmin/chapters/ch14-archives-and-transfer/04-exercise-checksums.md b/content/programs/rhel9-sysadmin/chapters/ch14-archives-and-transfer/04-exercise-checksums.md index df0c9b4..242d193 100644 --- a/content/programs/rhel9-sysadmin/chapters/ch14-archives-and-transfer/04-exercise-checksums.md +++ b/content/programs/rhel9-sysadmin/chapters/ch14-archives-and-transfer/04-exercise-checksums.md @@ -12,9 +12,23 @@ Compress the same data with gzip, bzip2 and xz and compare the cost and the gain objectives=["ch14.checksums"] id="checksums" title="Compress and verify" + exercise="sa-checksums" + ownExercise=true + guided=true + starter=false hosts=["workstation","servera"] outcomes=["Compare compression tools by size and time.","Create and verify a SHA-256 checksum list.","Detect a modified file."] %} + {% task id="task-40ad2a7e9796" title="Start the exercise" %} + On workstation, start the exercise. It installs bzip2, zip and unzip on servera and removes `/root/arch` of an earlier run. + +```console +[student@workstation ~]$ lab start sa-checksums +``` + + Later tasks assume that you are logged in to the server they name: `ssh student@servera` from workstation, then `sudo -i` for a root shell. + {% /task %} + {% task id="task-768cf4925687" title="Make the data" %} On servera as root (`sudo -i`), install `bzip2` and `zip` if needed. Make an uncompressed archive `etc.tar` of `/etc` in `/root/arch` and note its size. @@ -133,12 +147,7 @@ status=1 {% /reveal %} {% /task %} - {% task id="task-4b503a339821" title="Clean up" %} - -```console -[root@servera arch]# cd; rm -rf /root/arch -[root@servera ~]# exit -[student@servera ~]$ exit -``` + {% task id="task-4fecbba60ac0" title="Grade and finish" %} + {% lab-finish exercise="sa-checksums" grade=true servers=true /%} {% /task %} {% /lab %} diff --git a/content/programs/rhel9-sysadmin/chapters/ch14-archives-and-transfer/06-exercise-backups.md b/content/programs/rhel9-sysadmin/chapters/ch14-archives-and-transfer/06-exercise-backups.md index 169cdfc..99ae0a4 100644 --- a/content/programs/rhel9-sysadmin/chapters/ch14-archives-and-transfer/06-exercise-backups.md +++ b/content/programs/rhel9-sysadmin/chapters/ch14-archives-and-transfer/06-exercise-backups.md @@ -12,9 +12,23 @@ Make a directory with special properties (a script, a private file with an ACL), objectives=["ch14.backups"] id="backups" title="Back up with ACLs, then restore" + exercise="sa-backups" + ownExercise=true + guided=true + starter=false hosts=["workstation","servera"] outcomes=["Show that a plain tar loses ACLs and that --acls keeps them.","Make and restore a full and an incremental backup.","Create hard-linked rsync snapshots."] %} + {% task id="task-b1493b0d7765" title="Start the exercise" %} + On workstation, start the exercise. It removes `/root/arch` of an earlier run from servera. + +```console +[student@workstation ~]$ lab start sa-backups +``` + + Later tasks assume that you are logged in to the server they name: `ssh student@servera` from workstation, then `sudo -i` for a root shell. + {% /task %} + {% task id="task-8303f5800ecd" title="Build a directory with an ACL" %} On servera as root (`sudo -i`), create `/root/arch/project/docs/notes.md`, give the user `student` read access to it with an ACL, and show the ACL. @@ -146,12 +160,7 @@ a.txt c.txt d.txt {% /reveal %} {% /task %} - {% task id="task-20ae7ae8d8f9" title="Clean up" %} - -```console -[root@servera arch]# cd; rm -rf /root/arch -[root@servera ~]# exit -[student@servera ~]$ exit -``` + {% task id="task-28fe69e0b6ab" title="Grade and finish" %} + {% lab-finish exercise="sa-backups" grade=true servers=true /%} {% /task %} {% /lab %} diff --git a/content/programs/rhel9-sysadmin/chapters/ch14-archives-and-transfer/08-exercise-moving.md b/content/programs/rhel9-sysadmin/chapters/ch14-archives-and-transfer/08-exercise-moving.md index 229626a..f13bc09 100644 --- a/content/programs/rhel9-sysadmin/chapters/ch14-archives-and-transfer/08-exercise-moving.md +++ b/content/programs/rhel9-sysadmin/chapters/ch14-archives-and-transfer/08-exercise-moving.md @@ -12,21 +12,21 @@ Create data on servera, send it to serverb through a pipe, prove it arrived inta objectives=["ch14.transfer"] id="moving" title="Move a tree between servers" + exercise="sa-moving" + ownExercise=true + guided=true + starter=false hosts=["workstation","servera","serverb"] outcomes=["Stream a tar archive between hosts through ssh.","Verify a transfer with sha256sum.","Mirror with rsync and confirm identity with a dry run."] %} - {% task id="task-13e5da3c7ecd" title="Create data on servera" %} - On workstation, run the commands via `ssh root@servera`: create `/tmp/arch/work/docs` with three small files `a.txt`, `c.txt` and `d.txt`. - - {% reveal title="Show solution" %} + {% task id="task-ed923932f474" title="Start the exercise" %} + On workstation, start the exercise. It creates `/tmp/arch/work/docs` with three small files on servera, and removes leftovers of an earlier run. ```console -[student@workstation ~]$ ssh root@servera 'mkdir -p /tmp/arch/work/docs && cd /tmp/arch/work/docs && echo one > a.txt && echo three > c.txt && echo four > d.txt && ls' -a.txt -c.txt -d.txt +[student@workstation ~]$ lab start sa-moving ``` - {% /reveal %} + + Later tasks assume that you are logged in to the server they name: `ssh student@servera` from workstation, then `sudo -i` for a root shell. {% /task %} {% task id="task-9a52dcc13d7d" title="Stream it to serverb" %} @@ -92,12 +92,7 @@ total size is 23 speedup is 0.11 (DRY RUN) {% /reveal %} {% /task %} - {% task id="task-34d24f741acb" title="Clean up" %} - -```console -[student@workstation ~]$ rm -rf ~/xfer sums.txt -[student@workstation ~]$ ssh root@serverb 'rm -rf /tmp/recv' -[student@workstation ~]$ ssh root@servera 'rm -rf /tmp/arch' -``` + {% task id="task-4031fbe4f4fd" title="Grade and finish" %} + {% lab-finish exercise="sa-moving" grade=true servers=true /%} {% /task %} {% /lab %} diff --git a/content/programs/rhel9-sysadmin/chapters/ch15-scripts-and-scheduling/02-exercise-first-scripts.md b/content/programs/rhel9-sysadmin/chapters/ch15-scripts-and-scheduling/02-exercise-first-scripts.md index 88ba6c8..c8e7ee9 100644 --- a/content/programs/rhel9-sysadmin/chapters/ch15-scripts-and-scheduling/02-exercise-first-scripts.md +++ b/content/programs/rhel9-sysadmin/chapters/ch15-scripts-and-scheduling/02-exercise-first-scripts.md @@ -12,9 +12,23 @@ Write a greeting script, run it every way that works, then pass it arguments and objectives=["ch15.scripts"] id="scripts" title="Write and run your first scripts" + exercise="sa-first-scripts" + ownExercise=true + guided=true + starter=false hosts=["workstation","servera"] outcomes=["Write a script with a shebang and make it executable.","Run it with ./, bash and through PATH.","Use arguments, quoting and exit statuses."] %} + {% task id="task-c3e3d33935eb" title="Start the exercise" %} + On workstation, start the exercise. It removes the scripts of an earlier run from student's home on servera. + +```console +[student@workstation ~]$ lab start sa-first-scripts +``` + + Later tasks assume that you are logged in to the server they name: `ssh student@servera` from workstation, then `sudo -i` for a root shell. + {% /task %} + {% task id="task-fa4171b2c43e" title="Write hello.sh" %} On servera as `student`, create `~/scripts/hello.sh` (use `vim`) with the content below. Try to run it before doing anything else. @@ -136,11 +150,7 @@ not found {% /reveal %} {% /task %} - {% task id="task-64ceed0d2588" title="Clean up" %} - -```console -[student@servera scripts]$ cd; rm -rf ~/scripts ~/bin/hello.sh -[student@servera ~]$ exit -``` + {% task id="task-0fafec186b58" title="Grade and finish" %} + {% lab-finish exercise="sa-first-scripts" grade=true servers=true /%} {% /task %} {% /lab %} diff --git a/content/programs/rhel9-sysadmin/chapters/ch15-scripts-and-scheduling/04-exercise-conditions.md b/content/programs/rhel9-sysadmin/chapters/ch15-scripts-and-scheduling/04-exercise-conditions.md index 94977fc..f3fbc65 100644 --- a/content/programs/rhel9-sysadmin/chapters/ch15-scripts-and-scheduling/04-exercise-conditions.md +++ b/content/programs/rhel9-sysadmin/chapters/ch15-scripts-and-scheduling/04-exercise-conditions.md @@ -12,9 +12,23 @@ Write a script that checks its input and chooses what to do, one that loops over objectives=["ch15.logic"] id="logic" title="Scripts that decide and repeat" + exercise="sa-conditions" + ownExercise=true + guided=true + starter=false hosts=["workstation","servera"] outcomes=["Validate arguments and exit with distinct statuses.","Use if/elif/else, case, for and while.","Parse /etc/passwd line by line."] %} + {% task id="task-769f4bc2a0b2" title="Start the exercise" %} + On workstation, start the exercise. It removes the scripts of an earlier run from student's home on servera. + +```console +[student@workstation ~]$ lab start sa-conditions +``` + + Later tasks assume that you are logged in to the server they name: `ssh student@servera` from workstation, then `sudo -i` for a root shell. + {% /task %} + {% task id="task-eeb0172f1a47" title="check.sh: validate and decide" %} On servera as `student`, create `~/scripts/check.sh`: with exactly one argument, it reports whether it is a directory (with the number of entries), a file (with its size in bytes) or missing. With no argument or too many it prints `Usage: SCRIPT PATH` to standard error and exits with status 2; a missing path exits with status 1. @@ -167,11 +181,14 @@ student /bin/bash ++ stat -c %s /etc/hostname + echo '/etc/hostname is a file of 24 bytes' /etc/hostname is a file of 24 bytes -[student@servera scripts]$ cd; rm -rf ~/scripts [student@servera ~]$ exit ``` Each `+` line is a command as bash ran it, after variables were expanded. This is the first tool to reach for when a script misbehaves. {% /reveal %} {% /task %} + + {% task id="task-07b6845fc0ce" title="Grade and finish" %} + {% lab-finish exercise="sa-conditions" grade=true servers=true /%} + {% /task %} {% /lab %} diff --git a/content/programs/rhel9-sysadmin/chapters/ch15-scripts-and-scheduling/06-exercise-cron.md b/content/programs/rhel9-sysadmin/chapters/ch15-scripts-and-scheduling/06-exercise-cron.md index 14ea8fa..ef4ebe5 100644 --- a/content/programs/rhel9-sysadmin/chapters/ch15-scripts-and-scheduling/06-exercise-cron.md +++ b/content/programs/rhel9-sysadmin/chapters/ch15-scripts-and-scheduling/06-exercise-cron.md @@ -12,9 +12,23 @@ Put a job in a personal crontab, watch it fail because of an unescaped `%`, fix objectives=["ch15.cron"] id="cron" title="Schedule a job" + exercise="sa-cron" + ownExercise=true + guided=true + starter=false hosts=["workstation","servera"] outcomes=["Create a personal crontab entry and read the cron log.","Diagnose and fix the percent-sign problem.","Create a system job in /etc/cron.d."] %} + {% task id="task-e01c5e932d9e" title="Start the exercise" %} + On workstation, start the exercise. It removes cron jobs and output files of an earlier run from servera and makes sure crond runs. + +```console +[student@workstation ~]$ lab start sa-cron +``` + + Later tasks assume that you are logged in to the server they name: `ssh student@servera` from workstation, then `sudo -i` for a root shell. + {% /task %} + {% task id="task-d99ea3db39cb" title="Look at what is scheduled" %} On servera as root (`sudo -i`), list the system cron files, show `0hourly`, and check whether `student` has a crontab. @@ -93,19 +107,7 @@ Oct 3 17:54:01 servera CROND[1010]: (root) CMD (/usr/bin/logger -t cron-demo he {% /reveal %} {% /task %} - {% task id="task-972ba6bc7dbc" title="Clean up" %} - Remove student's crontab and the demo job, and the output file. - - {% reveal title="Show solution" %} - -```console -[root@servera ~]# crontab -u student -r -[root@servera ~]# rm -f /etc/cron.d/demo /home/student/cron.out /tmp/ct -[root@servera ~]# crontab -u student -l -no crontab for student -[root@servera ~]# exit -[student@servera ~]$ exit -``` - {% /reveal %} + {% task id="task-c4f416e79bc4" title="Grade and finish" %} + {% lab-finish exercise="sa-cron" grade=true servers=true /%} {% /task %} {% /lab %} diff --git a/content/programs/rhel9-sysadmin/chapters/ch15-scripts-and-scheduling/08-exercise-timers.md b/content/programs/rhel9-sysadmin/chapters/ch15-scripts-and-scheduling/08-exercise-timers.md index bb4b07e..fe55063 100644 --- a/content/programs/rhel9-sysadmin/chapters/ch15-scripts-and-scheduling/08-exercise-timers.md +++ b/content/programs/rhel9-sysadmin/chapters/ch15-scripts-and-scheduling/08-exercise-timers.md @@ -12,9 +12,23 @@ Build a service and timer pair, test the service by hand, watch the timer fire a objectives=["ch15.timers"] id="timers" title="Timers and one-off jobs" + exercise="sa-timers" + ownExercise=true + guided=true + starter=false hosts=["workstation","servera"] outcomes=["Write a oneshot service and a timer and enable the timer.","Check calendar expressions with systemd-analyze.","Schedule and remove an at job."] %} + {% task id="task-1912509ccaac" title="Start the exercise" %} + On workstation, start the exercise. It removes the hello units and the at package of an earlier run from servera. + +```console +[student@workstation ~]$ lab start sa-timers +``` + + Later tasks assume that you are logged in to the server they name: `ssh student@servera` from workstation, then `sudo -i` for a root shell. + {% /task %} + {% task id="task-a8c581357ef5" title="The service" %} On servera as root (`sudo -i`), create `/etc/systemd/system/hello.service`: a oneshot service that logs `hello from the timer` with the tag `hello-timer` (use `logger`). Start it by hand and look in the journal. @@ -134,16 +148,7 @@ job 2 at Sat Oct 3 23:59:00 2026 {% /reveal %} {% /task %} - {% task id="task-30bce7bfd9f5" title="Clean up" %} - -```console -[root@servera ~]# systemctl disable --now hello.timer -Removed "/etc/systemd/system/timers.target.wants/hello.timer". -[root@servera ~]# rm -f /etc/systemd/system/hello.service /etc/systemd/system/hello.timer /home/student/at.out -[root@servera ~]# systemctl daemon-reload -[root@servera ~]# dnf remove -y at > /dev/null -[root@servera ~]# exit -[student@servera ~]$ exit -``` + {% task id="task-b5cbf9f9e6ea" title="Grade and finish" %} + {% lab-finish exercise="sa-timers" grade=true servers=true /%} {% /task %} {% /lab %} diff --git a/content/programs/rhel9-sysadmin/lab/sa-backups.yml b/content/programs/rhel9-sysadmin/lab/sa-backups.yml new file mode 100644 index 0000000..2f599c0 --- /dev/null +++ b/content/programs/rhel9-sysadmin/lab/sa-backups.yml @@ -0,0 +1,90 @@ +name: sa-backups +title: Back up with ACLs, then restore +page: ch14/exercise-backups +transport: ssh +version: 1 +starter: [] +setup: +- action: directory + hosts: + - servera + path: /root/arch + state: absent +checkpoints: + final: + files: [] + checks: + - id: acl-tar + kind: archive + 'on': servera.lab.example.com + targets: + - servera.lab.example.com + message: acl.tar.gz holds the project + path: /root/arch/acl.tar.gz + format: tar.gz + members: + - project/docs/notes.md + - id: acl + kind: file + 'on': servera.lab.example.com + targets: + - servera.lab.example.com + message: the careful restore kept the ACL entry for student + paths: + - /root/arch/out2/project/docs/notes.md + acl: + - user:student:r-- + - id: full + kind: archive + 'on': servera.lab.example.com + targets: + - servera.lab.example.com + message: the full backup holds both files + path: /root/arch/bk/full.tar.gz + format: tar.gz + members: + - work/docs/a.txt + - work/docs/b.txt + - id: incr + kind: archive + 'on': servera.lab.example.com + targets: + - servera.lab.example.com + message: the incremental backup holds the new file + path: /root/arch/bk/incr1.tar.gz + format: tar.gz + members: + - work/docs/c.txt + - id: restored + kind: file + 'on': servera.lab.example.com + targets: + - servera.lab.example.com + message: the restore in order has c.txt + paths: + - /root/arch/restore/work/docs/c.txt + fileType: regular + - id: b-gone + kind: file + 'on': servera.lab.example.com + targets: + - servera.lab.example.com + message: the restore has no b.txt (deleted before the increment) + paths: + - /root/arch/restore/work/docs/b.txt + exists: false + - id: snap + kind: file + 'on': servera.lab.example.com + targets: + - servera.lab.example.com + message: unchanged files in snap2 are hard links to snap1 + paths: + - /root/arch/bk/snap2/docs/a.txt + sameFileAs: /root/arch/bk/snap1/docs/a.txt +finish: +- action: directory + hosts: + - servera + path: /root/arch + state: absent diff --git a/content/programs/rhel9-sysadmin/lab/sa-checksums.yml b/content/programs/rhel9-sysadmin/lab/sa-checksums.yml new file mode 100644 index 0000000..ebe9fc9 --- /dev/null +++ b/content/programs/rhel9-sysadmin/lab/sa-checksums.yml @@ -0,0 +1,84 @@ +name: sa-checksums +title: Compress and verify +page: ch14/exercise-checksums +transport: ssh +version: 1 +starter: [] +setup: +- action: package + hosts: + - servera + names: + - bzip2 + - zip + - unzip + state: present +- action: directory + hosts: + - servera + path: /root/arch + state: absent +checkpoints: + final: + files: [] + checks: + - id: gz + kind: file + 'on': servera.lab.example.com + targets: + - servera.lab.example.com + message: etc.tar.gz exists + paths: + - /root/arch/etc.tar.gz + fileType: regular + - id: bz2 + kind: file + 'on': servera.lab.example.com + targets: + - servera.lab.example.com + message: etc.tar.bz2 exists + paths: + - /root/arch/etc.tar.bz2 + fileType: regular + - id: xz + kind: file + 'on': servera.lab.example.com + targets: + - servera.lab.example.com + message: etc.tar.xz exists + paths: + - /root/arch/etc.tar.xz + fileType: regular + - id: zip + kind: archive + 'on': servera.lab.example.com + targets: + - servera.lab.example.com + message: ssh.zip is a readable zip file + path: /root/arch/ssh.zip + format: zip + - id: sums + kind: file + 'on': servera.lab.example.com + targets: + - servera.lab.example.com + message: SHA256SUMS lists the three archives + paths: + - /root/arch/SHA256SUMS + contains: + - etc.tar.gz + - etc.tar.bz2 + - etc.tar.xz +finish: +- action: directory + hosts: + - servera + path: /root/arch + state: absent +- action: package + hosts: + - servera + names: + - zip + - unzip + state: absent diff --git a/content/programs/rhel9-sysadmin/lab/sa-conditions.yml b/content/programs/rhel9-sysadmin/lab/sa-conditions.yml new file mode 100644 index 0000000..34c7c5d --- /dev/null +++ b/content/programs/rhel9-sysadmin/lab/sa-conditions.yml @@ -0,0 +1,67 @@ +name: sa-conditions +title: Scripts that decide and repeat +page: ch15/exercise-conditions +transport: ssh +version: 1 +starter: [] +setup: +- action: directory + hosts: + - servera + path: /home/student/scripts + state: absent +checkpoints: + final: + files: [] + checks: + - id: check + kind: file + 'on': servera.lab.example.com + targets: + - servera.lab.example.com + message: check.sh is executable + paths: + - /home/student/scripts/check.sh + executable: true + contains: + - exit 2 + - exit 1 + - id: loops + kind: file + 'on': servera.lab.example.com + targets: + - servera.lab.example.com + message: loops.sh is executable + paths: + - /home/student/scripts/loops.sh + executable: true + contains: + - 'for ' + - id: svc + kind: file + 'on': servera.lab.example.com + targets: + - servera.lab.example.com + message: svc.sh uses case + paths: + - /home/student/scripts/svc.sh + executable: true + contains: + - 'case ' + - id: users + kind: file + 'on': servera.lab.example.com + targets: + - servera.lab.example.com + message: users.sh reads a file line by line + paths: + - /home/student/scripts/users.sh + executable: true + contains: + - while +finish: +- action: directory + hosts: + - servera + path: /home/student/scripts + state: absent diff --git a/content/programs/rhel9-sysadmin/lab/sa-cron.yml b/content/programs/rhel9-sysadmin/lab/sa-cron.yml new file mode 100644 index 0000000..96ed91a --- /dev/null +++ b/content/programs/rhel9-sysadmin/lab/sa-cron.yml @@ -0,0 +1,95 @@ +name: sa-cron +title: Schedule a job +page: ch15/exercise-cron +transport: ssh +version: 1 +starter: [] +setup: +- action: crontab + hosts: + - servera + user: student + state: absent +- action: file + hosts: + - servera + path: /etc/cron.d/demo + state: absent +- action: file + hosts: + - servera + path: /home/student/cron.out + state: absent +- action: file + hosts: + - servera + path: /tmp/ct + state: absent +- action: service + hosts: + - servera + names: + - crond + state: started + enabled: true +checkpoints: + final: + files: [] + checks: + - id: crontab + kind: file + 'on': servera.lab.example.com + targets: + - servera.lab.example.com + message: student has a job with an escaped percent sign + paths: + - /var/spool/cron/student + matches: date \+\\%T + - id: out + kind: file + 'on': servera.lab.example.com + targets: + - servera.lab.example.com + message: the job has written its output file + paths: + - /home/student/cron.out + contains: + - cron ran at + - id: demo + kind: cron + 'on': servera.lab.example.com + targets: + - servera.lab.example.com + message: a system job in /etc/cron.d/demo runs logger every two minutes + file: demo + matches: ^\*/2 \* \* \* \* root /usr/bin/logger -t cron-demo hello + - id: crond + kind: service + 'on': servera.lab.example.com + targets: + - servera.lab.example.com + message: crond is running + names: + - crond.service + active: true +finish: +- action: crontab + hosts: + - servera + user: student + state: absent +- action: file + hosts: + - servera + path: /etc/cron.d/demo + state: absent +- action: file + hosts: + - servera + path: /home/student/cron.out + state: absent +- action: file + hosts: + - servera + path: /tmp/ct + state: absent diff --git a/content/programs/rhel9-sysadmin/lab/sa-first-scripts.yml b/content/programs/rhel9-sysadmin/lab/sa-first-scripts.yml new file mode 100644 index 0000000..6d6a4cb --- /dev/null +++ b/content/programs/rhel9-sysadmin/lab/sa-first-scripts.yml @@ -0,0 +1,64 @@ +name: sa-first-scripts +title: Write and run your first scripts +page: ch15/exercise-first-scripts +transport: ssh +version: 1 +starter: [] +setup: +- action: directory + hosts: + - servera + path: /home/student/scripts + state: absent +- action: file + hosts: + - servera + path: /home/student/bin/hello.sh + state: absent +checkpoints: + final: + files: [] + checks: + - id: hello + kind: file + 'on': servera.lab.example.com + targets: + - servera.lab.example.com + message: hello.sh is an executable script with a default name + paths: + - /home/student/scripts/hello.sh + executable: true + owner: student + contains: + - name=${1:-$USER} + - id: args + kind: file + 'on': servera.lab.example.com + targets: + - servera.lab.example.com + message: args.sh loops over "$@" + paths: + - /home/student/scripts/args.sh + executable: true + contains: + - for a in "$@" + - id: path + kind: file + 'on': servera.lab.example.com + targets: + - servera.lab.example.com + message: a copy of hello.sh is in ~/bin (on the PATH) + paths: + - /home/student/bin/hello.sh + executable: true +finish: +- action: directory + hosts: + - servera + path: /home/student/scripts + state: absent +- action: file + hosts: + - servera + path: /home/student/bin/hello.sh + state: absent diff --git a/content/programs/rhel9-sysadmin/lab/sa-moving.yml b/content/programs/rhel9-sysadmin/lab/sa-moving.yml new file mode 100644 index 0000000..8cf31ea --- /dev/null +++ b/content/programs/rhel9-sysadmin/lab/sa-moving.yml @@ -0,0 +1,103 @@ +name: sa-moving +title: Move a tree between servers +page: ch14/exercise-moving +transport: ssh +version: 1 +starter: [] +setup: +- action: directory + hosts: + - servera + path: /tmp/arch + state: absent +- action: directory + hosts: + - serverb + path: /tmp/recv + state: absent +- action: directory + hosts: + - servera + path: /tmp/arch/work/docs + state: present +- action: file + hosts: + - servera + path: /tmp/arch/work/docs/a.txt + state: present + content: 'one + + ' +- action: file + hosts: + - servera + path: /tmp/arch/work/docs/c.txt + state: present + content: 'three + + ' +- action: file + hosts: + - servera + path: /tmp/arch/work/docs/d.txt + state: present + content: 'four + + ' +- action: home-remove + paths: + - xfer + - sums.txt +checkpoints: + final: + files: [] + checks: + - id: received + kind: file + 'on': serverb.lab.example.com + targets: + - serverb.lab.example.com + message: the tree arrived on serverb + paths: + - /tmp/recv/work/docs/a.txt + line: one + - id: damaged + kind: file + 'on': serverb.lab.example.com + targets: + - serverb.lab.example.com + message: c.txt on serverb was damaged on purpose + paths: + - /tmp/recv/work/docs/c.txt + contains: + - oops + - id: sums + kind: file + 'on': control + message: the checksum list is on workstation + paths: + - /home/student/sums.txt + contains: + - docs/a.txt + - id: pulled + kind: file + 'on': control + message: the rsync copy is in ~/xfer/work + paths: + - /home/student/xfer/work/docs/d.txt + line: four +finish: +- action: directory + hosts: + - servera + path: /tmp/arch + state: absent +- action: directory + hosts: + - serverb + path: /tmp/recv + state: absent +- action: home-remove + paths: + - xfer + - sums.txt diff --git a/content/programs/rhel9-sysadmin/lab/sa-tar.yml b/content/programs/rhel9-sysadmin/lab/sa-tar.yml new file mode 100644 index 0000000..c201b9b --- /dev/null +++ b/content/programs/rhel9-sysadmin/lab/sa-tar.yml @@ -0,0 +1,84 @@ +name: sa-tar +title: Archive a project +page: ch14/exercise-tar +transport: ssh +version: 1 +starter: [] +setup: +- action: directory + hosts: + - servera + path: /root/arch + state: absent +checkpoints: + final: + files: [] + checks: + - id: tar + kind: archive + 'on': servera.lab.example.com + targets: + - servera.lab.example.com + message: project.tar lists the project + path: /root/arch/project.tar + format: tar + members: + - project/run.sh + - project/docs/notes.md + - id: tgz + kind: archive + 'on': servera.lab.example.com + targets: + - servera.lab.example.com + message: project.tar.gz is a readable archive of the project + path: /root/arch/project.tar.gz + format: tar.gz + members: + - project/run.sh + - project/src/data1.txt + - id: notxt + kind: archive + 'on': servera.lab.example.com + targets: + - servera.lab.example.com + message: notxt.tar.gz leaves out the .txt files + path: /root/arch/notxt.tar.gz + format: tar.gz + members: + - project/docs/notes.md + absentMembers: + - project/src/data1.txt + - id: abs + kind: archive + 'on': servera.lab.example.com + targets: + - servera.lab.example.com + message: abs.tar stores the path without the leading slash + path: /root/arch/abs.tar + format: tar + members: + - root/arch/project/docs/notes.md + - id: restore + kind: file + 'on': servera.lab.example.com + targets: + - servera.lab.example.com + message: the full restore holds the script with its mode + paths: + - /root/arch/restore/project/run.sh + mode: '0750' + - id: one + kind: file + 'on': servera.lab.example.com + targets: + - servera.lab.example.com + message: only run.sh was extracted into one/ + paths: + - /root/arch/one/project/run.sh + fileType: regular +finish: +- action: directory + hosts: + - servera + path: /root/arch + state: absent diff --git a/content/programs/rhel9-sysadmin/lab/sa-timers.yml b/content/programs/rhel9-sysadmin/lab/sa-timers.yml new file mode 100644 index 0000000..6eaa30b --- /dev/null +++ b/content/programs/rhel9-sysadmin/lab/sa-timers.yml @@ -0,0 +1,133 @@ +name: sa-timers +title: Timers and one-off jobs +page: ch15/exercise-timers +transport: ssh +version: 1 +starter: [] +setup: +- action: service + hosts: + - servera + names: + - hello.timer + state: stopped + enabled: false +- action: file + hosts: + - servera + path: /etc/systemd/system/hello.service + state: absent +- action: file + hosts: + - servera + path: /etc/systemd/system/hello.timer + state: absent +- action: file + hosts: + - servera + path: /home/student/at.out + state: absent +- action: systemd + hosts: + - servera + reload: true +- action: package + hosts: + - servera + names: + - at + state: absent +checkpoints: + final: + files: [] + checks: + - id: service + kind: file + 'on': servera.lab.example.com + targets: + - servera.lab.example.com + message: hello.service is a oneshot service that logs with the tag hello-timer + paths: + - /etc/systemd/system/hello.service + contains: + - Type=oneshot + - hello-timer + - id: timer-file + kind: file + 'on': servera.lab.example.com + targets: + - servera.lab.example.com + message: hello.timer fires every minute and catches up + paths: + - /etc/systemd/system/hello.timer + contains: + - Persistent=true + - WantedBy=timers.target + - id: timer + kind: service + 'on': servera.lab.example.com + targets: + - servera.lab.example.com + message: hello.timer is enabled and running + names: + - hello.timer + active: true + enabled: true + - id: atd + kind: service + 'on': servera.lab.example.com + targets: + - servera.lab.example.com + message: atd is running + names: + - atd.service + active: true + - id: at-out + kind: file + 'on': servera.lab.example.com + targets: + - servera.lab.example.com + message: the at job ran + paths: + - /home/student/at.out + line: at job ran +finish: +- action: service + hosts: + - servera + names: + - hello.timer + state: stopped + enabled: false +- action: file + hosts: + - servera + path: /etc/systemd/system/hello.service + state: absent +- action: file + hosts: + - servera + path: /etc/systemd/system/hello.timer + state: absent +- action: file + hosts: + - servera + path: /home/student/at.out + state: absent +- action: systemd + hosts: + - servera + reload: true +- action: service + hosts: + - servera + names: + - atd + state: stopped + enabled: false +- action: package + hosts: + - servera + names: + - at + state: absent diff --git a/packages/compiler/src/lab.js b/packages/compiler/src/lab.js index 4365647..95f2867 100644 --- a/packages/compiler/src/lab.js +++ b/packages/compiler/src/lab.js @@ -32,7 +32,7 @@ const CONTROL_ONLY = new Set(['git', 'lint']); // Setup and cleanup actions that run as root on the lab servers; the rest work on the workstation. const HOST_ACTIONS = new Set([ 'package', 'service', 'group', 'user', 'directory', 'file', 'remove-lines', 'firewall', 'selinux', 'wipe-disk', 'systemd', 'linger', - 'container-reset', 'run-as', 'restore-skel', 'boot', 'timezone', 'nm-connection', 'hostname', 'http-server', 'dnf-module', + 'container-reset', 'run-as', 'restore-skel', 'boot', 'timezone', 'nm-connection', 'hostname', 'http-server', 'dnf-module', 'crontab', ]); /** Every file under `dir` as paths relative to it, sorted. */ diff --git a/packages/lab-tools/grade.py b/packages/lab-tools/grade.py index c70a040..6826b55 100644 --- a/packages/lab-tools/grade.py +++ b/packages/lab-tools/grade.py @@ -91,6 +91,8 @@ def file_conditions(c, path): out.append('[ "$(cat %s)" = "$(%s)" ]' % (p, fact)) if 'mode' in c: out.append(stat_is(p, '%a', octal(c['mode']))) + if c.get('executable'): + out.append('[ -x %s ]' % p) if 'owner' in c: out.append(stat_is(p, '%U', c['owner'])) if 'group' in c: diff --git a/packages/lab-tools/prepare.py b/packages/lab-tools/prepare.py index 472bee0..ad2ada2 100644 --- a/packages/lab-tools/prepare.py +++ b/packages/lab-tools/prepare.py @@ -434,6 +434,10 @@ def script_dnf_module(a): return 'dnf module enable -y %s:%s >/dev/null 2>&1' % (n, q(a['stream'])) +def script_crontab(a): + return 'crontab -r -u %s 2>/dev/null || true' % q(name(a['user'])) + + def script_run_as(a): user = q(name(a['user'])) image = q(a['image']) @@ -446,7 +450,7 @@ def script_run_as(a): 'file': script_file, 'remove-lines': script_remove_lines, 'firewall': script_firewall, 'selinux': script_selinux, 'wipe-disk': script_wipe_disk, 'systemd': script_systemd, 'linger': script_linger, 'container-reset': script_container_reset, 'run-as': script_run_as, 'restore-skel': script_restore_skel, 'boot': script_boot, 'timezone': script_timezone, - 'nm-connection': script_nm_connection, 'hostname': script_hostname, 'http-server': script_http_server, 'dnf-module': script_dnf_module, + 'nm-connection': script_nm_connection, 'hostname': script_hostname, 'http-server': script_http_server, 'dnf-module': script_dnf_module, 'crontab': script_crontab, } diff --git a/packages/schema/schemas/source/lab.schema.json b/packages/schema/schemas/source/lab.schema.json index 863a71c..8743d3b 100644 --- a/packages/schema/schemas/source/lab.schema.json +++ b/packages/schema/schemas/source/lab.schema.json @@ -1372,6 +1372,31 @@ ] } } + }, + { + "type": "object", + "additionalProperties": false, + "required": [ + "action", + "hosts", + "user" + ], + "properties": { + "action": { + "const": "crontab" + }, + "hosts": { + "$ref": "#/$defs/serverList" + }, + "user": { + "$ref": "#/$defs/unixName" + }, + "state": { + "enum": [ + "absent" + ] + } + } } ] }, @@ -1678,6 +1703,10 @@ "$ref": "#/$defs/relativePath" } ] + }, + "executable": { + "description": "The file has its execute permission for the owner.", + "type": "boolean" } }, "type": "object" From 1e20eb4b063a5abeab1f57194f8fc151899a3131 Mon Sep 17 00:00:00 2001 From: Ayesh Rodrigo Date: Sun, 4 Oct 2026 11:50:22 +1300 Subject: [PATCH 6/9] Guided exercise labs for chapters 16 and 17 --- .../ch16-selinux/04-exercise-fix-labels.md | 39 +--- .../06-exercise-booleans-ports.md | 39 +--- .../08-exercise-troubleshooting.md | 45 ++-- .../02-exercise-partitions.md | 18 ++ .../04-exercise-filesystems.md | 26 +-- .../06-exercise-fstab.md | 18 ++ .../08-exercise-space.md | 32 +-- .../rhel9-sysadmin/lab/sa-booleans-ports.yml | 153 +++++++++++++ .../rhel9-sysadmin/lab/sa-filesystems.yml | 109 ++++++++++ .../rhel9-sysadmin/lab/sa-fix-labels.yml | 144 ++++++++++++ .../rhel9-sysadmin/lab/sa-fstab-storage.yml | 155 +++++++++++++ .../rhel9-sysadmin/lab/sa-partitions.yml | 58 +++++ .../programs/rhel9-sysadmin/lab/sa-space.yml | 159 ++++++++++++++ .../rhel9-sysadmin/lab/sa-two-faults.yml | 135 ++++++++++++ packages/compiler/src/lab.js | 2 +- packages/lab-tools/grade.py | 21 +- packages/lab-tools/prepare.py | 54 ++++- .../schema/schemas/source/lab.schema.json | 205 +++++++++++++++++- 18 files changed, 1289 insertions(+), 123 deletions(-) create mode 100644 content/programs/rhel9-sysadmin/lab/sa-booleans-ports.yml create mode 100644 content/programs/rhel9-sysadmin/lab/sa-filesystems.yml create mode 100644 content/programs/rhel9-sysadmin/lab/sa-fix-labels.yml create mode 100644 content/programs/rhel9-sysadmin/lab/sa-fstab-storage.yml create mode 100644 content/programs/rhel9-sysadmin/lab/sa-partitions.yml create mode 100644 content/programs/rhel9-sysadmin/lab/sa-space.yml create mode 100644 content/programs/rhel9-sysadmin/lab/sa-two-faults.yml diff --git a/content/programs/rhel9-sysadmin/chapters/ch16-selinux/04-exercise-fix-labels.md b/content/programs/rhel9-sysadmin/chapters/ch16-selinux/04-exercise-fix-labels.md index b1649e3..1d931ef 100644 --- a/content/programs/rhel9-sysadmin/chapters/ch16-selinux/04-exercise-fix-labels.md +++ b/content/programs/rhel9-sysadmin/chapters/ch16-selinux/04-exercise-fix-labels.md @@ -12,31 +12,21 @@ Serve a page from a new directory with Apache, watch it fail, prove that SELinux objectives=["ch16.contexts"] id="fix-labels" title="Fix a 403 caused by a label" + exercise="sa-fix-labels" + ownExercise=true + guided=true + starter=false hosts=["workstation","servera"] outcomes=["Find that a wrong type blocks httpd.","Use semanage fcontext and restorecon for a permanent fix.","Compare cp, mv and chcon."] %} - {% task id="task-89e066f27a14" title="Install and configure httpd" %} - On servera as root (`sudo -i`), install `httpd` and the audit tools (`audit`, `setroubleshoot-server`), start `auditd`, create `/srv/web/index.html` containing `Hello from /srv/web`, and tell Apache to serve `/srv/web` with a file `/etc/httpd/conf.d/web.conf`. Start httpd. - - {% reveal title="Show solution" %} + {% task id="task-66a6edd47ff0" title="Start the exercise" %} + On workstation, start the exercise. It installs httpd and the audit tools on servera, creates `/srv/web/index.html` and the Apache file `/etc/httpd/conf.d/web.conf` that serves it, and starts httpd. ```console -[student@workstation ~]$ ssh student@servera -[student@servera ~]$ sudo -i -[root@servera ~]# dnf install -y httpd audit setroubleshoot-server > /dev/null -[root@servera ~]# systemctl enable --now auditd -[root@servera ~]# mkdir -p /srv/web -[root@servera ~]# echo "Hello from /srv/web" > /srv/web/index.html -[root@servera ~]# cat > /etc/httpd/conf.d/web.conf <<'EOT' -DocumentRoot "/srv/web" - - Require all granted - -EOT -[root@servera ~]# systemctl enable --now httpd -Created symlink /etc/systemd/system/multi-user.target.wants/httpd.service → /usr/lib/systemd/system/httpd.service. +[student@workstation ~]$ lab start sa-fix-labels ``` - {% /reveal %} + + Later tasks assume that you are logged in to the server they name: `ssh student@servera` from workstation, then `sudo -i` for a root shell. {% /task %} {% task id="task-108264cdfa5b" title="See it fail" %} @@ -134,14 +124,7 @@ Relabeled /srv/web/copy.html from unconfined_u:object_r:user_home_t:s0 to unconf {% /reveal %} {% /task %} - {% task id="task-e5747f243852" title="Clean up" %} - -```console -[root@servera ~]# semanage fcontext -d "/srv/web(/.*)?" -[root@servera ~]# systemctl disable --now httpd -[root@servera ~]# rm -rf /srv/web /etc/httpd/conf.d/web.conf /tmp/second.html -[root@servera ~]# exit -[student@servera ~]$ exit -``` + {% task id="task-0534637ecf5b" title="Grade and finish" %} + {% lab-finish exercise="sa-fix-labels" grade=true servers=true /%} {% /task %} {% /lab %} diff --git a/content/programs/rhel9-sysadmin/chapters/ch16-selinux/06-exercise-booleans-ports.md b/content/programs/rhel9-sysadmin/chapters/ch16-selinux/06-exercise-booleans-ports.md index e0e9ae8..ca3a492 100644 --- a/content/programs/rhel9-sysadmin/chapters/ch16-selinux/06-exercise-booleans-ports.md +++ b/content/programs/rhel9-sysadmin/chapters/ch16-selinux/06-exercise-booleans-ports.md @@ -12,29 +12,21 @@ Two more reasons for a service to be refused by SELinux, and their proper fixes: objectives=["ch16.booleans"] id="booleans-ports" title="Flip a switch and label a port" + exercise="sa-booleans-ports" + ownExercise=true + guided=true + starter=false hosts=["workstation","servera"] outcomes=["Find and change an httpd boolean permanently.","Label a port for httpd and start the service on it.","Review local SELinux customisations."] %} - {% task id="task-338fabc53204" title="Prepare httpd and a user page" %} - On servera as root (`sudo -i`), install `httpd`, `audit` and `setroubleshoot-server` if they are not there (`dnf install -y …`), start `auditd`, create the user `bob` with a page `/home/bob/public_html/index.html` (home mode 711, directory 755, file 644), and enable user directories by editing `/etc/httpd/conf.d/userdir.conf`: comment out `UserDir disabled` and add `UserDir public_html`. Start httpd. - - {% reveal title="Show solution" %} + {% task id="task-713375a67e2c" title="Start the exercise" %} + On workstation, start the exercise. It installs httpd and the audit tools on servera, creates the user bob with a page in `~/public_html`, turns on Apache's UserDir feature and starts httpd. ```console -[student@workstation ~]$ ssh student@servera -[student@servera ~]$ sudo -i -[root@servera ~]# dnf install -y httpd audit setroubleshoot-server > /dev/null -[root@servera ~]# systemctl enable --now auditd -[root@servera ~]# useradd -m bob -[root@servera ~]# mkdir -p /home/bob/public_html -[root@servera ~]# echo "bob page" > /home/bob/public_html/index.html -[root@servera ~]# chmod 711 /home/bob; chmod 755 /home/bob/public_html; chmod 644 /home/bob/public_html/index.html -[root@servera ~]# sed -i 's/^\s*UserDir disabled/#UserDir disabled/; s/^\s*#\s*UserDir public_html/ UserDir public_html/' /etc/httpd/conf.d/userdir.conf -[root@servera ~]# grep -n "UserDir public_html" /etc/httpd/conf.d/userdir.conf -24: UserDir public_html -[root@servera ~]# systemctl enable --now httpd +[student@workstation ~]$ lab start sa-booleans-ports ``` - {% /reveal %} + + Later tasks assume that you are logged in to the server they name: `ssh student@servera` from workstation, then `sudo -i` for a root shell. {% /task %} {% task id="task-4860cc481235" title="See it fail" %} @@ -117,16 +109,7 @@ http_port_t tcp 82 {% /reveal %} {% /task %} - {% task id="task-2fccc677c5a9" title="Put everything back" %} - -```console -[root@servera ~]# semanage port -d -t http_port_t -p tcp 82 -[root@servera ~]# setsebool -P httpd_enable_homedirs off -[root@servera ~]# sed -i 's/^Listen 82$/Listen 80/' /etc/httpd/conf/httpd.conf -[root@servera ~]# systemctl disable --now httpd -[root@servera ~]# userdel -r bob -[root@servera ~]# exit -[student@servera ~]$ exit -``` + {% task id="task-dcc7eb5690cd" title="Grade and finish" %} + {% lab-finish exercise="sa-booleans-ports" grade=true servers=true /%} {% /task %} {% /lab %} diff --git a/content/programs/rhel9-sysadmin/chapters/ch16-selinux/08-exercise-troubleshooting.md b/content/programs/rhel9-sysadmin/chapters/ch16-selinux/08-exercise-troubleshooting.md index b487b4e..8df4e22 100644 --- a/content/programs/rhel9-sysadmin/chapters/ch16-selinux/08-exercise-troubleshooting.md +++ b/content/programs/rhel9-sysadmin/chapters/ch16-selinux/08-exercise-troubleshooting.md @@ -12,42 +12,21 @@ A colleague has set up a web site on port 8085 serving `/srv/site`, and it does objectives=["ch16.troubleshooting"] id="troubleshooting" title="Two faults, one symptom" + exercise="sa-two-faults" + ownExercise=true + guided=true + starter=false hosts=["workstation","servera"] outcomes=["Use the journal and the audit log to find a name_bind denial.","Fix the port and then the file label.","Verify the result in enforcing mode."] %} - {% task id="task-d7d349765459" title="Create the broken site" %} - On servera as root (`sudo -i`), install `httpd`, `audit` and `setroubleshoot-server`, start `auditd`, and run the commands below. They build the site the way a hurried colleague might have: the page is moved in from `/tmp`, and Apache is set to a new port. - -```bash -mkdir -p /srv/site -echo "

Site OK

" > /tmp/index.html -mv /tmp/index.html /srv/site/index.html -cat > /etc/httpd/conf.d/site.conf <<'EOT' -Listen 8085 - - DocumentRoot "/srv/site" - - - Require all granted - -EOT -systemctl restart httpd -``` - - {% reveal title="Show solution" %} + {% task id="task-ca0b7c73b438" title="Start the exercise" %} + On workstation, start the exercise. It installs httpd and the audit tools on servera and builds the "hurried colleague's" site: a page moved in from `/tmp`, and Apache set to port 8085. httpd fails to start. ```console -[student@workstation ~]$ ssh student@servera -[student@servera ~]$ sudo -i -[root@servera ~]# dnf install -y httpd audit setroubleshoot-server > /dev/null -[root@servera ~]# systemctl enable --now auditd -[root@servera ~]# mkdir -p /srv/site -...output omitted... -[root@servera ~]# systemctl restart httpd -Job for httpd.service failed because the control process exited with error code. -See "systemctl status httpd.service" and "journalctl -xeu httpd.service" for details. +[student@workstation ~]$ lab start sa-two-faults ``` - {% /reveal %} + + Later tasks assume that you are logged in to the server they name: `ssh student@servera` from workstation, then `sudo -i` for a root shell. {% /task %} {% task id="task-796cb6ad962f" title="Symptom, then the service log" %} @@ -154,8 +133,6 @@ http_port_t tcp 8085 SELinux fcontext type Context /srv/site(/.*)? all files system_u:object_r:httpd_sys_content_t:s0 -[root@servera ~]# semanage fcontext -d "/srv/site(/.*)?"; semanage port -d -t http_port_t -p tcp 8085 -[root@servera ~]# systemctl disable --now httpd; rm -rf /srv/site /etc/httpd/conf.d/site.conf [root@servera ~]# exit [student@servera ~]$ exit ``` @@ -163,4 +140,8 @@ SELinux fcontext type Context (The lab image may list one more rule, for its own management agent: leave that one alone.) The review is the habit that matters: every customisation should be one you can name and explain. {% /reveal %} {% /task %} + + {% task id="task-6c87f915ab2c" title="Grade and finish" %} + {% lab-finish exercise="sa-two-faults" grade=true servers=true /%} + {% /task %} {% /lab %} diff --git a/content/programs/rhel9-sysadmin/chapters/ch17-storage-and-file-systems/02-exercise-partitions.md b/content/programs/rhel9-sysadmin/chapters/ch17-storage-and-file-systems/02-exercise-partitions.md index b38dcd2..b79489a 100644 --- a/content/programs/rhel9-sysadmin/chapters/ch17-storage-and-file-systems/02-exercise-partitions.md +++ b/content/programs/rhel9-sysadmin/chapters/ch17-storage-and-file-systems/02-exercise-partitions.md @@ -12,9 +12,23 @@ servera has an empty 5 GiB disk, `/dev/sdb`. Look at the disks, give it a GPT pa objectives=["ch17.partitions"] id="partitions" title="Partition a new disk" + exercise="sa-partitions" + ownExercise=true + guided=true + starter=false hosts=["workstation","servera"] outcomes=["Identify the system disk and the spare disk.","Create a GPT table and three partitions with parted.","Check the result with lsblk and parted."] %} + {% task id="task-992656087c6b" title="Start the exercise" %} + On workstation, start the exercise. It clears the spare disk `/dev/sdb` of servera (5 GiB). + +```console +[student@workstation ~]$ lab start sa-partitions +``` + + Later tasks assume that you are logged in to the server they name: `ssh student@servera` from workstation, then `sudo -i` for a root shell. + {% /task %} + {% task id="task-a28017581855" title="Which disk is which?" %} On servera as root (`sudo -i`), list the block devices with their file systems. Which disk is the system disk, and which one is empty? @@ -117,4 +131,8 @@ Device Start End Sectors Size Type `Disklabel type: gpt`: fdisk and parted read the same table. Sector 2048 is exactly 1 MiB, the aligned start. {% /reveal %} {% /task %} + + {% task id="task-d8a604eca4d7" title="Grade and finish" %} + {% lab-finish exercise="sa-partitions" grade=true servers=true /%} + {% /task %} {% /lab %} diff --git a/content/programs/rhel9-sysadmin/chapters/ch17-storage-and-file-systems/04-exercise-filesystems.md b/content/programs/rhel9-sysadmin/chapters/ch17-storage-and-file-systems/04-exercise-filesystems.md index b3d00e7..2e9411e 100644 --- a/content/programs/rhel9-sysadmin/chapters/ch17-storage-and-file-systems/04-exercise-filesystems.md +++ b/content/programs/rhel9-sysadmin/chapters/ch17-storage-and-file-systems/04-exercise-filesystems.md @@ -12,27 +12,21 @@ Put XFS on the data partition, ext4 on the logs partition, prepare the swap part objectives=["ch17.filesystems"] id="filesystems" title="Create file systems and mount them" + exercise="sa-filesystems" + ownExercise=true + guided=true + starter=false hosts=["workstation","servera"] outcomes=["Create xfs and ext4 file systems and swap space.","Mount, use and unmount file systems.","Read UUIDs and diagnose a busy mount."] %} - {% task id="task-bb548a7edb8d" title="Check that the partitions exist" %} - On servera as root, show the partitions of `/dev/sdb`. If you reset the servers since the last exercise, recreate them first. - - {% reveal title="Show solution" %} + {% task id="task-69432ff18268" title="Start the exercise" %} + On workstation, start the exercise. It partitions the spare disk `/dev/sdb` of servera into the three partitions of the previous exercise (data 1 GiB, logs 1 GiB, swap 512 MiB). ```console -[student@workstation ~]$ ssh student@servera -[student@servera ~]$ sudo -i -[root@servera ~]# lsblk /dev/sdb -NAME MAJ:MIN RM SIZE RO TYPE MOUNTPOINTS -sdb 8:16 0 5G 0 disk -├─sdb1 8:17 0 1G 0 part -├─sdb2 8:18 0 1G 0 part -└─sdb3 8:19 0 512M 0 part +[student@workstation ~]$ lab start sa-filesystems ``` - To recreate them after a reset: `parted -s /dev/sdb mklabel gpt mkpart data xfs 1MiB 1025MiB mkpart logs ext4 1025MiB 2049MiB mkpart swap linux-swap 2049MiB 2561MiB`. - {% /reveal %} + Later tasks assume that you are logged in to the server they name: `ssh student@servera` from workstation, then `sudo -i` for a root shell. {% /task %} {% task id="task-80688e69edf7" title="Create the file systems and the swap area" %} @@ -129,4 +123,8 @@ Swap: 511Mi 0B 511Mi Leave the file systems mounted for the next exercise, or unmount them with `umount /data /logs` and start from the partitions again. {% /reveal %} {% /task %} + + {% task id="task-94d037b1ee6c" title="Grade and finish" %} + {% lab-finish exercise="sa-filesystems" grade=true servers=true /%} + {% /task %} {% /lab %} diff --git a/content/programs/rhel9-sysadmin/chapters/ch17-storage-and-file-systems/06-exercise-fstab.md b/content/programs/rhel9-sysadmin/chapters/ch17-storage-and-file-systems/06-exercise-fstab.md index 4e775c1..24e8fa9 100644 --- a/content/programs/rhel9-sysadmin/chapters/ch17-storage-and-file-systems/06-exercise-fstab.md +++ b/content/programs/rhel9-sysadmin/chapters/ch17-storage-and-file-systems/06-exercise-fstab.md @@ -12,9 +12,23 @@ Mount the two file systems and the swap area at boot, using UUIDs and sensible o objectives=["ch17.fstab"] id="fstab" title="Make the storage permanent" + exercise="sa-fstab-storage" + ownExercise=true + guided=true + starter=false hosts=["workstation","servera"] outcomes=["Write fstab entries by UUID for xfs, ext4 and swap.","Test with findmnt --verify, mount -a and swapon -a.","Verify the entries after a reboot."] %} + {% task id="task-21fde289a53b" title="Start the exercise" %} + On workstation, start the exercise. It partitions `/dev/sdb` of servera and creates the xfs file system, the ext4 file system and the swap area of the previous exercise. + +```console +[student@workstation ~]$ lab start sa-fstab-storage +``` + + Later tasks assume that you are logged in to the server they name: `ssh student@servera` from workstation, then `sudo -i` for a root shell. + {% /task %} + {% task id="task-d7a6ee2b8619" title="Unmount and collect the UUIDs" %} On servera as root, unmount `/data` and `/logs` if they are still mounted from the previous exercise, create the mount points `/srv/data` and `/srv/archive`, and read the UUIDs of the three partitions. @@ -127,4 +141,8 @@ sdb Everything came back without any manual `mount`: that is a permanent configuration. Keep it for the next exercise. {% /reveal %} {% /task %} + + {% task id="task-b33c9410fa0a" title="Grade and finish" %} + {% lab-finish exercise="sa-fstab-storage" grade=true servers=true /%} + {% /task %} {% /lab %} diff --git a/content/programs/rhel9-sysadmin/chapters/ch17-storage-and-file-systems/08-exercise-space.md b/content/programs/rhel9-sysadmin/chapters/ch17-storage-and-file-systems/08-exercise-space.md index f7541d1..d3b8b58 100644 --- a/content/programs/rhel9-sysadmin/chapters/ch17-storage-and-file-systems/08-exercise-space.md +++ b/content/programs/rhel9-sysadmin/chapters/ch17-storage-and-file-systems/08-exercise-space.md @@ -12,9 +12,23 @@ Use up the space of one file system, find who is responsible, run out of inodes objectives=["ch17.maintenance"] id="space" title="Hunt the missing space" + exercise="sa-space" + ownExercise=true + guided=true + starter=false hosts=["workstation","servera"] outcomes=["Find large files with df and du.","Recognise and demonstrate inode exhaustion.","Find a deleted-but-open file with lsof +L1."] %} + {% task id="task-21b5a96407c1" title="Start the exercise" %} + On workstation, start the exercise. It partitions `/dev/sdb` of servera, creates the file systems and the swap area, adds them to `/etc/fstab` and mounts them on `/srv/data` and `/srv/archive`. + +```console +[student@workstation ~]$ lab start sa-space +``` + + Later tasks assume that you are logged in to the server they name: `ssh student@servera` from workstation, then `sudo -i` for a root shell. + {% /task %} + {% task id="task-ad866e16ee50" title="Check the starting point" %} On servera as root, make sure `/srv/data` (xfs) and `/srv/archive` (ext4) are mounted, and show their blocks and inodes. @@ -124,7 +138,7 @@ tail 1072 root 3r REG 8,17 209715200 0 132 /srv/data/log.big (d {% /task %} {% task id="task-83cf3efe9e5d" title="Maintenance and clean up" %} - Unmount `/srv/data`, check it with `xfs_repair -n`, set the label `datavol` with `xfs_admin -L`, and mount it again. Then undo everything from this chapter: remove the three fstab lines (restore your backup), unmount, turn off swap, wipe the signatures, and check that `sdb` is empty. + Unmount `/srv/data`, check it with `xfs_repair -n`, set the label `datavol` with `xfs_admin -L`, and mount it again. `lab finish` undoes the fstab lines, the mounts, the swap and the partitions for you. {% reveal title="Show solution" %} @@ -138,20 +152,12 @@ writing all SBs new label = "datavol" [root@servera ~]# mount /srv/data && blkid /dev/sdb1 | cut -c1-60 /dev/sdb1: LABEL="datavol" UUID="c64946eb-fc27-4485-8020-db4119b686eb" -[root@servera ~]# umount /srv/data /srv/archive; swapoff -a -[root@servera ~]# cp /root/fstab.bak /etc/fstab; systemctl daemon-reload -[root@servera ~]# wipefs -a /dev/sdb1 /dev/sdb2 /dev/sdb3 /dev/sdb > /dev/null -[root@servera ~]# rmdir /srv/data /srv/archive -[root@servera ~]# lsblk /dev/sdb -NAME MAJ:MIN RM SIZE RO TYPE MOUNTPOINTS -sdb 8:16 0 5G 0 disk -[root@servera ~]# findmnt --verify | tail -1 -0 parse errors, 0 errors, 1 warning -[root@servera ~]# exit -[student@servera ~]$ exit ``` - `wipefs -a` erases the file system and partition-table signatures. You can also simply reset the servers with `rht-vmctl reset servers`. {% /reveal %} {% /task %} + + {% task id="task-00691b48290b" title="Grade and finish" %} + {% lab-finish exercise="sa-space" grade=true servers=true /%} + {% /task %} {% /lab %} diff --git a/content/programs/rhel9-sysadmin/lab/sa-booleans-ports.yml b/content/programs/rhel9-sysadmin/lab/sa-booleans-ports.yml new file mode 100644 index 0000000..a149fec --- /dev/null +++ b/content/programs/rhel9-sysadmin/lab/sa-booleans-ports.yml @@ -0,0 +1,153 @@ +name: sa-booleans-ports +title: Flip a switch and label a port +page: ch16/exercise-booleans-ports +transport: ssh +version: 1 +starter: [] +setup: +- action: package + hosts: + - servera + names: + - httpd + - audit + - setroubleshoot-server + state: present +- action: service + hosts: + - servera + names: + - auditd + state: started + enabled: true + tolerant: true +- action: user + hosts: + - servera + name: bob + state: present +- action: directory + hosts: + - servera + path: /home/bob/public_html + state: present + owner: bob + group: bob + mode: '755' +- action: file + hosts: + - servera + path: /home/bob/public_html/index.html + state: present + content: 'bob page + + ' + owner: bob + group: bob + mode: '644' +- action: directory + hosts: + - servera + path: /home/bob + mode: '711' + state: present +- action: file + hosts: + - servera + path: /etc/httpd/conf.d/userdir.conf + state: present + content: "\n UserDir public_html\n\n\n\n AllowOverride FileInfo AuthConfig Limit Indexes\n Options MultiViews Indexes SymLinksIfOwnerMatch IncludesNoExec\n Require method GET POST OPTIONS\n\n" +- action: service + hosts: + - servera + names: + - httpd + state: started + enabled: true +checkpoints: + final: + files: [] + checks: + - id: boolean + kind: selinux-boolean + 'on': servera.lab.example.com + targets: + - servera.lab.example.com + message: httpd_enable_homedirs is on, now and permanently + name: httpd_enable_homedirs + value: true + - id: port + kind: selinux-port + 'on': servera.lab.example.com + targets: + - servera.lab.example.com + message: TCP port 82 is labelled http_port_t + type: http_port_t + proto: tcp + port: 82 + - id: listen + kind: file + 'on': servera.lab.example.com + targets: + - servera.lab.example.com + message: httpd listens on port 82 + paths: + - /etc/httpd/conf/httpd.conf + line: Listen 82 + - id: svc + kind: service + 'on': servera.lab.example.com + targets: + - servera.lab.example.com + message: httpd is running + names: + - httpd.service + active: true + - id: page82 + kind: http + 'on': servera.lab.example.com + targets: + - servera.lab.example.com + message: the page of bob is served on port 82 + url: http://localhost:82/~bob/ + containsAny: + - bob page +finish: +- action: selinux + hosts: + - servera + kind: boolean + state: present + name: httpd_enable_homedirs + value: false +- action: selinux + hosts: + - servera + kind: port + state: absent + type: http_port_t + proto: tcp + port: 82 +- action: service + hosts: + - servera + names: + - httpd + state: stopped + enabled: false +- action: package + hosts: + - servera + names: + - httpd + state: absent +- action: user + hosts: + - servera + name: bob + state: absent +- action: file + hosts: + - servera + path: /etc/httpd/conf.d/userdir.conf + state: absent diff --git a/content/programs/rhel9-sysadmin/lab/sa-filesystems.yml b/content/programs/rhel9-sysadmin/lab/sa-filesystems.yml new file mode 100644 index 0000000..6e6fd57 --- /dev/null +++ b/content/programs/rhel9-sysadmin/lab/sa-filesystems.yml @@ -0,0 +1,109 @@ +name: sa-filesystems +title: Create file systems and mount them +page: ch17/exercise-filesystems +transport: ssh +version: 1 +starter: [] +setup: +- action: wipe-disk + hosts: + - servera + device: /dev/sdb +- action: partition-disk + hosts: + - servera + device: /dev/sdb + partitions: + - name: data + fs: xfs + startMiB: 1 + endMiB: 1025 + - name: logs + fs: ext4 + startMiB: 1025 + endMiB: 2049 + - name: swap + fs: linux-swap + startMiB: 2049 + endMiB: 2561 +- action: directory + hosts: + - servera + path: /data + state: absent +- action: directory + hosts: + - servera + path: /logs + state: absent +checkpoints: + final: + files: [] + checks: + - id: xfs + kind: mount + 'on': servera.lab.example.com + targets: + - servera.lab.example.com + message: /data is /dev/sdb1 formatted as xfs + path: /data + fstype: xfs + persistent: false + - id: ext4 + kind: mount + 'on': servera.lab.example.com + targets: + - servera.lab.example.com + message: /logs is /dev/sdb2 formatted as ext4 + path: /logs + fstype: ext4 + persistent: false + - id: label + kind: partition + 'on': servera.lab.example.com + targets: + - servera.lab.example.com + message: the ext4 file system is labelled logs + device: /dev/sdb2 + fsLabel: logs + - id: swap + kind: partition + 'on': servera.lab.example.com + targets: + - servera.lab.example.com + message: /dev/sdb3 holds a swap area + device: /dev/sdb3 + fsType: swap + - id: a + kind: file + 'on': servera.lab.example.com + targets: + - servera.lab.example.com + message: /data/a.txt exists + paths: + - /data/a.txt + line: hello + - id: b + kind: file + 'on': servera.lab.example.com + targets: + - servera.lab.example.com + message: /logs/b.txt exists + paths: + - /logs/b.txt + line: world +finish: +- action: wipe-disk + hosts: + - servera + device: /dev/sdb +- action: directory + hosts: + - servera + path: /data + state: absent +- action: directory + hosts: + - servera + path: /logs + state: absent diff --git a/content/programs/rhel9-sysadmin/lab/sa-fix-labels.yml b/content/programs/rhel9-sysadmin/lab/sa-fix-labels.yml new file mode 100644 index 0000000..861e489 --- /dev/null +++ b/content/programs/rhel9-sysadmin/lab/sa-fix-labels.yml @@ -0,0 +1,144 @@ +name: sa-fix-labels +title: Fix a 403 caused by a label +page: ch16/exercise-fix-labels +transport: ssh +version: 1 +starter: [] +setup: +- action: package + hosts: + - servera + names: + - httpd + - audit + - setroubleshoot-server + state: present +- action: service + hosts: + - servera + names: + - auditd + state: started + enabled: true + tolerant: true +- action: directory + hosts: + - servera + path: /srv/web + state: present +- action: file + hosts: + - servera + path: /srv/web/index.html + state: present + content: 'Hello from /srv/web + + ' +- action: file + hosts: + - servera + path: /etc/httpd/conf.d/web.conf + state: present + content: "DocumentRoot \"/srv/web\"\n\n Require all granted\n\n" +- action: service + hosts: + - servera + names: + - httpd + state: started + enabled: true +checkpoints: + final: + files: [] + checks: + - id: fcontext + kind: selinux-fcontext + 'on': servera.lab.example.com + targets: + - servera.lab.example.com + message: a stored mapping gives /srv/web and below the web content type + path: /srv/web(/.*)? + type: httpd_sys_content_t + - id: index + kind: file + 'on': servera.lab.example.com + targets: + - servera.lab.example.com + message: index.html carries the web content type + paths: + - /srv/web/index.html + selinuxType: httpd_sys_content_t + - id: copy + kind: file + 'on': servera.lab.example.com + targets: + - servera.lab.example.com + message: copy.html (restored after chcon) carries the web content type + paths: + - /srv/web/copy.html + selinuxType: httpd_sys_content_t + - id: moved + kind: file + 'on': servera.lab.example.com + targets: + - servera.lab.example.com + message: moved.html (relabelled after the move) carries the web content type + paths: + - /srv/web/moved.html + selinuxType: httpd_sys_content_t + - id: enforcing + kind: selinux + 'on': servera.lab.example.com + targets: + - servera.lab.example.com + message: SELinux is enforcing, now and in the configuration + mode: enforcing + - id: page + kind: http + 'on': servera.lab.example.com + targets: + - servera.lab.example.com + message: the page is served + url: http://localhost/ + containsAny: + - Hello from /srv/web +finish: +- action: selinux + hosts: + - servera + kind: fcontext + state: absent + path: /srv/web(/.*)? +- action: service + hosts: + - servera + names: + - httpd + state: stopped + enabled: false +- action: package + hosts: + - servera + names: + - httpd + state: absent +- action: directory + hosts: + - servera + path: /srv/web + state: absent +- action: file + hosts: + - servera + path: /etc/httpd/conf.d/web.conf + state: absent +- action: file + hosts: + - servera + path: /tmp/second.html + state: absent +- action: file + hosts: + - servera + path: /tmp/moved.html + state: absent diff --git a/content/programs/rhel9-sysadmin/lab/sa-fstab-storage.yml b/content/programs/rhel9-sysadmin/lab/sa-fstab-storage.yml new file mode 100644 index 0000000..a94571b --- /dev/null +++ b/content/programs/rhel9-sysadmin/lab/sa-fstab-storage.yml @@ -0,0 +1,155 @@ +name: sa-fstab-storage +title: Make the storage permanent +page: ch17/exercise-fstab +transport: ssh +version: 1 +starter: [] +setup: +- action: wipe-disk + hosts: + - servera + device: /dev/sdb +- action: remove-lines + hosts: + - servera + path: /etc/fstab + matching: /dev/sdb[123]|^UUID=[0-9a-f-]+[[:space:]]+(/srv/data|/srv/archive|none[[:space:]]+swap) +- action: directory + hosts: + - servera + path: /data + state: absent +- action: directory + hosts: + - servera + path: /logs + state: absent +- action: directory + hosts: + - servera + path: /srv/data + state: absent +- action: directory + hosts: + - servera + path: /srv/archive + state: absent +- action: file + hosts: + - servera + path: /root/fstab.bak + state: absent +- action: systemd + hosts: + - servera + reload: true +- action: partition-disk + hosts: + - servera + device: /dev/sdb + partitions: + - name: data + fs: xfs + startMiB: 1 + endMiB: 1025 + - name: logs + fs: ext4 + startMiB: 1025 + endMiB: 2049 + - name: swap + fs: linux-swap + startMiB: 2049 + endMiB: 2561 +- action: format + hosts: + - servera + device: /dev/sdb1 + type: xfs +- action: format + hosts: + - servera + device: /dev/sdb2 + type: ext4 + label: logs +- action: format + hosts: + - servera + device: /dev/sdb3 + type: swap +checkpoints: + final: + files: [] + checks: + - id: data + kind: mount + 'on': servera.lab.example.com + targets: + - servera.lab.example.com + message: /srv/data is xfs, mounted now and at boot + path: /srv/data + fstype: xfs + persistent: true + - id: archive + kind: mount + 'on': servera.lab.example.com + targets: + - servera.lab.example.com + message: /srv/archive is ext4, mounted now and at boot + path: /srv/archive + fstype: ext4 + persistent: true + - id: noexec + kind: file + 'on': servera.lab.example.com + targets: + - servera.lab.example.com + message: the archive entry has noexec + paths: + - /etc/fstab + matches: /srv/archive[[:space:]]+ext4[[:space:]]+defaults,noexec + - id: swap + kind: swap + 'on': servera.lab.example.com + targets: + - servera.lab.example.com + message: the swap partition is active and in fstab + minSizeMiB: 400 +finish: +- action: wipe-disk + hosts: + - servera + device: /dev/sdb +- action: remove-lines + hosts: + - servera + path: /etc/fstab + matching: /dev/sdb[123]|^UUID=[0-9a-f-]+[[:space:]]+(/srv/data|/srv/archive|none[[:space:]]+swap) +- action: directory + hosts: + - servera + path: /data + state: absent +- action: directory + hosts: + - servera + path: /logs + state: absent +- action: directory + hosts: + - servera + path: /srv/data + state: absent +- action: directory + hosts: + - servera + path: /srv/archive + state: absent +- action: file + hosts: + - servera + path: /root/fstab.bak + state: absent +- action: systemd + hosts: + - servera + reload: true diff --git a/content/programs/rhel9-sysadmin/lab/sa-partitions.yml b/content/programs/rhel9-sysadmin/lab/sa-partitions.yml new file mode 100644 index 0000000..dc52c23 --- /dev/null +++ b/content/programs/rhel9-sysadmin/lab/sa-partitions.yml @@ -0,0 +1,58 @@ +name: sa-partitions +title: Partition a new disk +page: ch17/exercise-partitions +transport: ssh +version: 1 +starter: [] +setup: +- action: wipe-disk + hosts: + - servera + device: /dev/sdb +checkpoints: + final: + files: [] + checks: + - id: table + kind: partition + 'on': servera.lab.example.com + targets: + - servera.lab.example.com + message: /dev/sdb1 is on a GPT disk + device: /dev/sdb1 + tableType: gpt + - id: p1 + kind: partition + 'on': servera.lab.example.com + targets: + - servera.lab.example.com + message: partition 1 is named data and is about 1 GiB + device: /dev/sdb1 + partLabel: data + minSizeMiB: 1000 + maxSizeMiB: 1030 + - id: p2 + kind: partition + 'on': servera.lab.example.com + targets: + - servera.lab.example.com + message: partition 2 is named logs and is about 1 GiB + device: /dev/sdb2 + partLabel: logs + minSizeMiB: 1000 + maxSizeMiB: 1030 + - id: p3 + kind: partition + 'on': servera.lab.example.com + targets: + - servera.lab.example.com + message: partition 3 is named swap and is about 512 MiB + device: /dev/sdb3 + partLabel: swap + minSizeMiB: 500 + maxSizeMiB: 520 +finish: +- action: wipe-disk + hosts: + - servera + device: /dev/sdb diff --git a/content/programs/rhel9-sysadmin/lab/sa-space.yml b/content/programs/rhel9-sysadmin/lab/sa-space.yml new file mode 100644 index 0000000..8383d48 --- /dev/null +++ b/content/programs/rhel9-sysadmin/lab/sa-space.yml @@ -0,0 +1,159 @@ +name: sa-space +title: Hunt the missing space +page: ch17/exercise-space +transport: ssh +version: 1 +starter: [] +setup: +- action: wipe-disk + hosts: + - servera + device: /dev/sdb +- action: partition-disk + hosts: + - servera + device: /dev/sdb + partitions: + - name: data + fs: xfs + startMiB: 1 + endMiB: 1025 + - name: logs + fs: ext4 + startMiB: 1025 + endMiB: 2049 + - name: swap + fs: linux-swap + startMiB: 2049 + endMiB: 2561 +- action: format + hosts: + - servera + device: /dev/sdb1 + type: xfs +- action: format + hosts: + - servera + device: /dev/sdb2 + type: ext4 + label: logs +- action: format + hosts: + - servera + device: /dev/sdb3 + type: swap +- action: append-line + hosts: + - servera + path: /etc/fstab + line: /dev/sdb1 /srv/data xfs defaults 0 0 +- action: append-line + hosts: + - servera + path: /etc/fstab + line: /dev/sdb2 /srv/archive ext4 defaults,noexec 0 0 +- action: append-line + hosts: + - servera + path: /etc/fstab + line: /dev/sdb3 none swap defaults 0 0 +- action: directory + hosts: + - servera + path: /srv/data + state: present +- action: directory + hosts: + - servera + path: /srv/archive + state: present +- action: mount-all + hosts: + - servera +checkpoints: + final: + files: [] + checks: + - id: data + kind: mount + 'on': servera.lab.example.com + targets: + - servera.lab.example.com + message: /srv/data is mounted from /dev/sdb1 + path: /srv/data + fstype: xfs + persistent: true + - id: label + kind: partition + 'on': servera.lab.example.com + targets: + - servera.lab.example.com + message: the xfs file system carries the label datavol + device: /dev/sdb1 + fsLabel: datavol + - id: archive + kind: mount + 'on': servera.lab.example.com + targets: + - servera.lab.example.com + message: /srv/archive is mounted + path: /srv/archive + fstype: ext4 + persistent: true + - id: many + kind: file + 'on': servera.lab.example.com + targets: + - servera.lab.example.com + message: the directory with 65,600 files was removed again + paths: + - /srv/archive/many + exists: false + - id: big + kind: file + 'on': servera.lab.example.com + targets: + - servera.lab.example.com + message: the big log file is gone + paths: + - /srv/data/log.big + exists: false +finish: +- action: wipe-disk + hosts: + - servera + device: /dev/sdb +- action: remove-lines + hosts: + - servera + path: /etc/fstab + matching: /dev/sdb[123]|^UUID=[0-9a-f-]+[[:space:]]+(/srv/data|/srv/archive|none[[:space:]]+swap) +- action: directory + hosts: + - servera + path: /data + state: absent +- action: directory + hosts: + - servera + path: /logs + state: absent +- action: directory + hosts: + - servera + path: /srv/data + state: absent +- action: directory + hosts: + - servera + path: /srv/archive + state: absent +- action: file + hosts: + - servera + path: /root/fstab.bak + state: absent +- action: systemd + hosts: + - servera + reload: true diff --git a/content/programs/rhel9-sysadmin/lab/sa-two-faults.yml b/content/programs/rhel9-sysadmin/lab/sa-two-faults.yml new file mode 100644 index 0000000..720a2ea --- /dev/null +++ b/content/programs/rhel9-sysadmin/lab/sa-two-faults.yml @@ -0,0 +1,135 @@ +name: sa-two-faults +title: Two faults, one symptom +page: ch16/exercise-troubleshooting +transport: ssh +version: 1 +starter: [] +setup: +- action: package + hosts: + - servera + names: + - httpd + - audit + - setroubleshoot-server + state: present +- action: service + hosts: + - servera + names: + - auditd + state: started + enabled: true + tolerant: true +- action: directory + hosts: + - servera + path: /srv/site + state: present +- action: file + hosts: + - servera + path: /srv/site/index.html + state: present + content: '

Site OK

+ + ' + selinuxType: user_tmp_t +- action: file + hosts: + - servera + path: /etc/httpd/conf.d/site.conf + state: present + content: "Listen 8085\n\n DocumentRoot \"/srv/site\"\n\n\n Require all granted\n\n" +- action: service + hosts: + - servera + names: + - httpd + state: restarted + enabled: false + tolerant: true +checkpoints: + final: + files: [] + checks: + - id: port + kind: selinux-port + 'on': servera.lab.example.com + targets: + - servera.lab.example.com + message: TCP port 8085 is labelled http_port_t + type: http_port_t + proto: tcp + port: 8085 + - id: fcontext + kind: selinux-fcontext + 'on': servera.lab.example.com + targets: + - servera.lab.example.com + message: a stored mapping gives /srv/site and below the web content type + path: /srv/site(/.*)? + type: httpd_sys_content_t + - id: label + kind: file + 'on': servera.lab.example.com + targets: + - servera.lab.example.com + message: index.html carries the web content type + paths: + - /srv/site/index.html + selinuxType: httpd_sys_content_t + - id: enforcing + kind: selinux + 'on': servera.lab.example.com + targets: + - servera.lab.example.com + message: SELinux is still enforcing + mode: enforcing + - id: page + kind: http + 'on': servera.lab.example.com + targets: + - servera.lab.example.com + message: the site answers on port 8085 + url: http://localhost:8085/ + containsAny: + - Site OK +finish: +- action: selinux + hosts: + - servera + kind: fcontext + state: absent + path: /srv/site(/.*)? +- action: selinux + hosts: + - servera + kind: port + state: absent + type: http_port_t + proto: tcp + port: 8085 +- action: service + hosts: + - servera + names: + - httpd + state: stopped + enabled: false +- action: package + hosts: + - servera + names: + - httpd + state: absent +- action: directory + hosts: + - servera + path: /srv/site + state: absent +- action: file + hosts: + - servera + path: /etc/httpd/conf.d/site.conf + state: absent diff --git a/packages/compiler/src/lab.js b/packages/compiler/src/lab.js index 95f2867..6cd9569 100644 --- a/packages/compiler/src/lab.js +++ b/packages/compiler/src/lab.js @@ -32,7 +32,7 @@ const CONTROL_ONLY = new Set(['git', 'lint']); // Setup and cleanup actions that run as root on the lab servers; the rest work on the workstation. const HOST_ACTIONS = new Set([ 'package', 'service', 'group', 'user', 'directory', 'file', 'remove-lines', 'firewall', 'selinux', 'wipe-disk', 'systemd', 'linger', - 'container-reset', 'run-as', 'restore-skel', 'boot', 'timezone', 'nm-connection', 'hostname', 'http-server', 'dnf-module', 'crontab', + 'container-reset', 'run-as', 'restore-skel', 'boot', 'timezone', 'nm-connection', 'hostname', 'http-server', 'dnf-module', 'crontab', 'partition-disk', 'format', 'append-line', 'mount-all', ]); /** Every file under `dir` as paths relative to it, sorted. */ diff --git a/packages/lab-tools/grade.py b/packages/lab-tools/grade.py index 6826b55..9b28c0c 100644 --- a/packages/lab-tools/grade.py +++ b/packages/lab-tools/grade.py @@ -380,12 +380,31 @@ def conditions_container(c): return out +def conditions_partition(c): + dev = shlex.quote(c['device']) + out = ['[ -b %s ]' % dev] + if 'minSizeMiB' in c: + out.append('[ $(( $(blockdev --getsize64 %s) / 1048576 )) -ge %d ]' % (dev, c['minSizeMiB'])) + if 'maxSizeMiB' in c: + out.append('[ $(( $(blockdev --getsize64 %s) / 1048576 )) -le %d ]' % (dev, c['maxSizeMiB'])) + if 'partLabel' in c: + out.append('[ "$(lsblk -no PARTLABEL %s | head -1)" = %s ]' % (dev, shlex.quote(c['partLabel']))) + if 'tableType' in c: + disk = re.sub(r'[0-9]+$', '', c['device']) + out.append('parted -s %s print 2>/dev/null | grep -q %s' % (shlex.quote(disk), shlex.quote('Partition Table: ' + c['tableType']))) + if 'fsType' in c: + out.append('[ "$(blkid -s TYPE -o value %s)" = %s ]' % (dev, shlex.quote(c['fsType']))) + if 'fsLabel' in c: + out.append('[ "$(blkid -s LABEL -o value %s)" = %s ]' % (dev, shlex.quote(c['fsLabel']))) + return out + + HOST_CHECKS = { 'service': conditions_service, 'firewall': conditions_firewall, 'package': conditions_package, 'file': conditions_file, 'file-compare': conditions_file_compare, 'archive': conditions_archive, 'user': conditions_user, 'mount': conditions_mount, 'logical-volume': conditions_logical_volume, 'volume-group': conditions_volume_group, 'swap': conditions_swap, 'http': conditions_http, 'selinux': conditions_selinux, 'sudoers': conditions_sudoers, 'sshd': conditions_sshd, 'cron': conditions_cron, 'boot-target': conditions_boot_target, 'selinux-boolean': conditions_selinux_boolean, 'selinux-port': conditions_selinux_port, 'selinux-fcontext': conditions_selinux_fcontext, 'address': conditions_address, - 'hostname': conditions_hostname, 'commands': conditions_commands, 'process': conditions_process, 'container': conditions_container, + 'hostname': conditions_hostname, 'commands': conditions_commands, 'process': conditions_process, 'container': conditions_container, 'partition': conditions_partition, } diff --git a/packages/lab-tools/prepare.py b/packages/lab-tools/prepare.py index ad2ada2..e3ca505 100644 --- a/packages/lab-tools/prepare.py +++ b/packages/lab-tools/prepare.py @@ -200,7 +200,7 @@ def ssh_keypairs(a, ctx): # --------------------------------------------------------------------------------------- # Host actions: fixed scripts, built from validated values, run as root on lab servers. -SAFE_PATH = re.compile(r'^/(?:srv|opt|mnt|data|backup|tmp|root|home|var/www|var/tmp|var/log|usr/local|etc/(?:billing\.conf|httpd/conf\.d|systemd|auto\.master\.d|auto\.[a-z0-9_-]+|cron\.d|exports\.d|sudoers\.d|profile\.d|ssh/sshd_config\.d|chrony\.d|yum\.repos\.d|logrotate\.d|rsyslog\.d|security/limits\.d|sysctl\.d|NetworkManager/system-connections|containers|firewalld/(?:services|zones)))(?:/[A-Za-z0-9_.@%+=:, -]+)*$') +SAFE_PATH = re.compile(r'^/(?:srv|opt|mnt|data|logs|backup|tmp|root|home|var/www|var/tmp|var/log|usr/local|etc/(?:billing\.conf|httpd/conf\.d|systemd|auto\.master\.d|auto\.[a-z0-9_-]+|cron\.d|exports\.d|sudoers\.d|profile\.d|ssh/sshd_config\.d|chrony\.d|yum\.repos\.d|logrotate\.d|rsyslog\.d|security/limits\.d|sysctl\.d|NetworkManager/system-connections|containers|firewalld/(?:services|zones)))(?:/[A-Za-z0-9_.@%+=:, -]+)*$') PROTECTED = {'/srv', '/opt', '/mnt', '/tmp', '/root', '/home', '/var/www', '/var/tmp', '/var/log', '/usr/local', '/etc/systemd', '/etc/containers'} NAME = re.compile(r'^[a-z_][a-z0-9_-]{0,31}$') SSH = ['ssh', '-o', 'BatchMode=yes', '-o', 'ConnectTimeout=10', '-o', 'StrictHostKeyChecking=accept-new', '-o', 'LogLevel=ERROR'] @@ -246,13 +246,13 @@ def script_service(a): state = a.get('state') enabled = a.get('enabled') if enabled is True and state == 'started': - out.append('%s%s enable --now %s' % (pre, ctl, u)) + out.append('%s%s enable --now %s%s' % (pre, ctl, u, ' 2>/dev/null || true' if a.get('tolerant') else '')) continue if enabled is False and state == 'stopped': out.append('%s%s disable --now %s 2>/dev/null || true' % (pre, ctl, u)) continue if state: - out.append('%s%s %s %s%s' % (pre, ctl, {'started': 'start', 'stopped': 'stop', 'restarted': 'restart'}[state], u, ' 2>/dev/null || true' if state == 'stopped' else '')) + out.append('%s%s %s %s%s' % (pre, ctl, {'started': 'start', 'stopped': 'stop', 'restarted': 'restart'}[state], u, ' 2>/dev/null || true' if state == 'stopped' or a.get('tolerant') else '')) if enabled is not None: out.append('%s%s %s %s%s' % (pre, ctl, 'enable' if enabled else 'disable', u, '' if enabled else ' 2>/dev/null || true')) if a.get('masked') is not None: @@ -298,12 +298,20 @@ def owner_args(a, path): return out +def label_args(a, path): + if 'selinuxType' not in a: + return [] + if not re.match(r'^[a-z_]+_t$', a['selinuxType']): + raise SetupError('unsafe SELinux type') + return ['chcon -t %s %s' % (q(a['selinuxType']), path)] + + def script_directory(a): path = safe_path(a['path'], a.get('state') == 'absent') p = q(path) if a.get('state', 'present') == 'absent': return 'rm -rf -- %s' % p - return '\n'.join(['mkdir -p -- %s' % p] + owner_args(a, p)) + return '\n'.join(['mkdir -p -- %s' % p] + owner_args(a, p) + label_args(a, p)) def script_file(a): @@ -314,7 +322,7 @@ def script_file(a): import base64 data = base64.b64encode(a.get('content', '').encode()).decode() lines = ['mkdir -p -- "$(dirname %s)"' % p, 'echo %s | base64 -d > %s' % (data, p)] - return '\n'.join(lines + owner_args(a, p)) + return '\n'.join(lines + owner_args(a, p) + label_args(a, p)) def script_remove_lines(a): @@ -438,6 +446,41 @@ def script_crontab(a): return 'crontab -r -u %s 2>/dev/null || true' % q(name(a['user'])) +def script_partition_disk(a): + dev = a['device'] + if not re.match(r'^/dev/(?:sd|vd)[b-z]$', dev): + raise SetupError('unsafe device') + out = ['parted -s %s mklabel gpt' % q(dev)] + for part in a['partitions']: + if not re.match(r'^[a-z][a-z0-9-]{0,15}$', part['name']) or part['fs'] not in ('xfs', 'ext4', 'linux-swap', 'lvm'): + raise SetupError('unsafe partition') + fs = 'xfs' if part['fs'] == 'lvm' else part['fs'] + out.append('parted -s %s mkpart %s %s %dMiB %dMiB' % (q(dev), q(part['name']), fs, part['startMiB'], part['endMiB'])) + if part['fs'] == 'lvm': + out.append('parted -s %s set %d lvm on' % (q(dev), a['partitions'].index(part) + 1)) + out.append('udevadm settle') + return '\n'.join(out) + + +def script_format(a): + dev = a['device'] + if not re.match(r'^/dev/(?:sd|vd)[b-z][0-9]{1,2}$', dev): + raise SetupError('unsafe device') + label = ' -L %s' % q(a['label']) if a.get('label') else '' + cmd = {'xfs': 'mkfs.xfs -f%s %s', 'ext4': 'mkfs.ext4 -q -F%s %s', 'swap': 'mkswap%s %s'}[a['type']] + return (cmd % (label, q(dev))) + ' >/dev/null' + + +def script_append_line(a): + if a['path'] not in ('/etc/fstab', '/etc/exports', '/etc/hosts') or not re.match(r'^[A-Za-z0-9 _./:,@=()*+-]{1,160}$', a['line']): + raise SetupError('unsafe line') + return 'grep -qxF -- %s %s || echo %s >> %s' % (q(a['line']), a['path'], q(a['line']), a['path']) + + +def script_mount_all(a): + return 'systemctl daemon-reload\nmount -a\nswapon -a' + + def script_run_as(a): user = q(name(a['user'])) image = q(a['image']) @@ -451,6 +494,7 @@ def script_run_as(a): 'wipe-disk': script_wipe_disk, 'systemd': script_systemd, 'linger': script_linger, 'container-reset': script_container_reset, 'run-as': script_run_as, 'restore-skel': script_restore_skel, 'boot': script_boot, 'timezone': script_timezone, 'nm-connection': script_nm_connection, 'hostname': script_hostname, 'http-server': script_http_server, 'dnf-module': script_dnf_module, 'crontab': script_crontab, + 'partition-disk': script_partition_disk, 'format': script_format, 'append-line': script_append_line, 'mount-all': script_mount_all, } diff --git a/packages/schema/schemas/source/lab.schema.json b/packages/schema/schemas/source/lab.schema.json index 8743d3b..3147809 100644 --- a/packages/schema/schemas/source/lab.schema.json +++ b/packages/schema/schemas/source/lab.schema.json @@ -317,6 +317,9 @@ }, { "$ref": "#/$defs/checkContainer" + }, + { + "$ref": "#/$defs/checkPartition" } ] }, @@ -686,6 +689,10 @@ }, "masked": { "type": "boolean" + }, + "tolerant": { + "type": "boolean", + "description": "A failing start is not an error (the exercise starts from a service that fails)." } } }, @@ -805,6 +812,10 @@ "present", "absent" ] + }, + "selinuxType": { + "type": "string", + "pattern": "^[a-z_]+_t$" } } }, @@ -845,6 +856,10 @@ "present", "absent" ] + }, + "selinuxType": { + "type": "string", + "pattern": "^[a-z_]+_t$" } } }, @@ -1397,6 +1412,145 @@ ] } } + }, + { + "type": "object", + "additionalProperties": false, + "required": [ + "action", + "hosts", + "device", + "partitions" + ], + "properties": { + "action": { + "const": "partition-disk" + }, + "hosts": { + "$ref": "#/$defs/serverList" + }, + "device": { + "type": "string", + "pattern": "^/dev/(?:sd|vd)[b-z]$" + }, + "partitions": { + "type": "array", + "minItems": 1, + "maxItems": 8, + "items": { + "type": "object", + "additionalProperties": false, + "required": [ + "name", + "fs", + "startMiB", + "endMiB" + ], + "properties": { + "name": { + "type": "string", + "pattern": "^[a-z][a-z0-9-]{0,15}$" + }, + "fs": { + "enum": [ + "xfs", + "ext4", + "linux-swap", + "lvm" + ] + }, + "startMiB": { + "type": "integer", + "minimum": 1, + "maximum": 100000 + }, + "endMiB": { + "type": "integer", + "minimum": 2, + "maximum": 100000 + } + } + } + } + } + }, + { + "type": "object", + "additionalProperties": false, + "required": [ + "action", + "hosts", + "device", + "type" + ], + "properties": { + "action": { + "const": "format" + }, + "hosts": { + "$ref": "#/$defs/serverList" + }, + "device": { + "type": "string", + "pattern": "^/dev/(?:sd|vd)[b-z][0-9]{1,2}$" + }, + "type": { + "enum": [ + "xfs", + "ext4", + "swap" + ] + }, + "label": { + "type": "string", + "pattern": "^[A-Za-z0-9_-]{1,12}$" + } + } + }, + { + "type": "object", + "additionalProperties": false, + "required": [ + "action", + "hosts", + "path", + "line" + ], + "properties": { + "action": { + "const": "append-line" + }, + "hosts": { + "$ref": "#/$defs/serverList" + }, + "path": { + "enum": [ + "/etc/fstab", + "/etc/exports", + "/etc/hosts" + ] + }, + "line": { + "type": "string", + "pattern": "^[A-Za-z0-9 _./:,@=()*+-]{1,160}$" + } + } + }, + { + "type": "object", + "additionalProperties": false, + "required": [ + "action", + "hosts" + ], + "properties": { + "action": { + "const": "mount-all" + }, + "hosts": { + "$ref": "#/$defs/serverList" + } + } } ] }, @@ -2540,7 +2694,7 @@ "description": "A path a setup or cleanup action may create or remove on a managed host: only under directories meant for local data and drop-in configuration.", "type": "string", "maxLength": 200, - "pattern": "^/(?:srv|opt|mnt|data|backup|tmp|root|home|var/www|var/tmp|var/log|usr/local|etc/(?:billing\\.conf|httpd/conf\\.d|systemd|auto\\.master\\.d|auto\\.[a-z0-9_-]+|cron\\.d|exports\\.d|sudoers\\.d|profile\\.d|ssh/sshd_config\\.d|chrony\\.d|yum\\.repos\\.d|logrotate\\.d|rsyslog\\.d|security/limits\\.d|sysctl\\.d|NetworkManager/system-connections|containers|httpd/conf\\.d|firewalld/(?:services|zones)))(?:/[A-Za-z0-9_.@%+=:, -]+)*$", + "pattern": "^/(?:srv|opt|mnt|data|logs|backup|tmp|root|home|var/www|var/tmp|var/log|usr/local|etc/(?:billing\\.conf|httpd/conf\\.d|systemd|auto\\.master\\.d|auto\\.[a-z0-9_-]+|cron\\.d|exports\\.d|sudoers\\.d|profile\\.d|ssh/sshd_config\\.d|chrony\\.d|yum\\.repos\\.d|logrotate\\.d|rsyslog\\.d|security/limits\\.d|sysctl\\.d|NetworkManager/system-connections|containers|httpd/conf\\.d|firewalld/(?:services|zones)))(?:/[A-Za-z0-9_.@%+=:, -]+)*$", "not": { "pattern": "(?:^|/)\\.\\.(?:/|$)" } @@ -2570,6 +2724,55 @@ "not": { "pattern": "(?:^|/)\\.\\.(?:/|$)" } + }, + "checkPartition": { + "allOf": [ + { + "$ref": "#/$defs/checkBase" + } + ], + "unevaluatedProperties": false, + "required": [ + "device" + ], + "properties": { + "kind": { + "const": "partition" + }, + "device": { + "type": "string", + "pattern": "^/dev/(?:sd|vd)[b-z][0-9]{0,2}$" + }, + "minSizeMiB": { + "type": "integer" + }, + "maxSizeMiB": { + "type": "integer" + }, + "partLabel": { + "type": "string", + "pattern": "^[a-z][a-z0-9-]{0,15}$" + }, + "tableType": { + "enum": [ + "gpt", + "msdos" + ] + }, + "fsType": { + "enum": [ + "xfs", + "ext4", + "swap", + "LVM2_member" + ] + }, + "fsLabel": { + "type": "string", + "pattern": "^[A-Za-z0-9_-]{1,12}$" + } + }, + "type": "object" } } } From 4873c3a98e176d87f3e2f1684bda24b5740469dd Mon Sep 17 00:00:00 2001 From: Ayesh Rodrigo Date: Sun, 4 Oct 2026 12:11:00 +1300 Subject: [PATCH 7/9] Guided exercise labs for chapters 18 and 19 --- .../chapters/ch18-lvm/02-exercise-build.md | 29 +- .../chapters/ch18-lvm/04-exercise-resize.md | 18 + .../ch18-lvm/06-exercise-maintenance.md | 18 + .../ch18-lvm/08-exercise-troubleshooting.md | 25 +- .../ch19-nfs-and-autofs/02-exercise-share.md | 27 +- .../04-exercise-permissions.md | 26 +- .../06-exercise-persistent.md | 22 +- .../ch19-nfs-and-autofs/08-exercise-maps.md | 23 +- .../rhel9-sysadmin/lab/sa-lvm-build.yml | 152 +++++++ .../rhel9-sysadmin/lab/sa-lvm-maintenance.yml | 158 +++++++ .../rhel9-sysadmin/lab/sa-lvm-resize.yml | 151 +++++++ .../lab/sa-lvm-troubleshooting.yml | 144 +++++++ .../rhel9-sysadmin/lab/sa-nfs-maps.yml | 384 ++++++++++++++++++ .../rhel9-sysadmin/lab/sa-nfs-permissions.yml | 384 ++++++++++++++++++ .../rhel9-sysadmin/lab/sa-nfs-persistent.yml | 355 ++++++++++++++++ .../rhel9-sysadmin/lab/sa-nfs-share.yml | 337 +++++++++++++++ packages/compiler/src/lab.js | 2 +- packages/lab-tools/prepare.py | 34 +- .../schema/schemas/source/lab.schema.json | 92 ++++- 19 files changed, 2313 insertions(+), 68 deletions(-) create mode 100644 content/programs/rhel9-sysadmin/lab/sa-lvm-build.yml create mode 100644 content/programs/rhel9-sysadmin/lab/sa-lvm-maintenance.yml create mode 100644 content/programs/rhel9-sysadmin/lab/sa-lvm-resize.yml create mode 100644 content/programs/rhel9-sysadmin/lab/sa-lvm-troubleshooting.yml create mode 100644 content/programs/rhel9-sysadmin/lab/sa-nfs-maps.yml create mode 100644 content/programs/rhel9-sysadmin/lab/sa-nfs-permissions.yml create mode 100644 content/programs/rhel9-sysadmin/lab/sa-nfs-persistent.yml create mode 100644 content/programs/rhel9-sysadmin/lab/sa-nfs-share.yml diff --git a/content/programs/rhel9-sysadmin/chapters/ch18-lvm/02-exercise-build.md b/content/programs/rhel9-sysadmin/chapters/ch18-lvm/02-exercise-build.md index 4b82df0..127e454 100644 --- a/content/programs/rhel9-sysadmin/chapters/ch18-lvm/02-exercise-build.md +++ b/content/programs/rhel9-sysadmin/chapters/ch18-lvm/02-exercise-build.md @@ -12,30 +12,21 @@ Prepare two partitions of the spare disk for LVM, build a volume group on the fi objectives=["ch18.concepts"] id="build" title="Build your first volume group" + exercise="sa-lvm-build" + ownExercise=true + guided=true + starter=false hosts=["workstation","servera"] outcomes=["Create PV, VG and LVs with the lvm tools.","Make and mount file systems on logical volumes.","Read pvs, vgs, lvs and lsblk -f."] %} - {% task id="task-1f0b6cee1e45" title="Prepare the disk" %} - On servera as root (`sudo -i`), check that `/dev/sdb` is empty. If it still has partitions from the previous chapter, clear it with `wipefs -a`. Then create two 1.5 GiB partitions flagged for LVM. - - {% reveal title="Show solution" %} + {% task id="task-934bbfeee5c4" title="Start the exercise" %} + On workstation, start the exercise. It clears the spare disk `/dev/sdb` of servera and creates two 1.5 GiB LVM partitions on it (`sdb1` and `sdb2`). ```console -[student@workstation ~]$ ssh student@servera -[student@servera ~]$ sudo -i -[root@servera ~]# lsblk /dev/sdb -NAME MAJ:MIN RM SIZE RO TYPE MOUNTPOINTS -sdb 8:16 0 5G 0 disk -[root@servera ~]# parted -s /dev/sdb mklabel gpt mkpart pv1 1MiB 1537MiB mkpart pv2 1537MiB 3073MiB set 1 lvm on set 2 lvm on -[root@servera ~]# udevadm settle; lsblk /dev/sdb -NAME MAJ:MIN RM SIZE RO TYPE MOUNTPOINTS -sdb 8:16 0 5G 0 disk -├─sdb1 8:17 0 1.5G 0 part -└─sdb2 8:18 0 1.5G 0 part +[student@workstation ~]$ lab start sa-lvm-build ``` - If `sdb` has leftovers: `wipefs -a /dev/sdb1 /dev/sdb2 /dev/sdb3 /dev/sdb` (this erases them) and look again, or reset the servers. - {% /reveal %} + Later tasks assume that you are logged in to the server they name: `ssh student@servera` from workstation, then `sudo -i` for a root shell. {% /task %} {% task id="task-d6d2124a23eb" title="Physical volume and volume group" %} @@ -130,4 +121,8 @@ lrwxrwxrwx. 1 root root 7 Oct 3 18:33 /dev/vgdata/lvapp -> ../dm-0 Two names, one device (`dm-0`, a "device mapper" volume). Leave everything mounted for the next exercise. {% /reveal %} {% /task %} + + {% task id="task-74c98e500cd0" title="Grade and finish" %} + {% lab-finish exercise="sa-lvm-build" grade=true servers=true /%} + {% /task %} {% /lab %} diff --git a/content/programs/rhel9-sysadmin/chapters/ch18-lvm/04-exercise-resize.md b/content/programs/rhel9-sysadmin/chapters/ch18-lvm/04-exercise-resize.md index 2be3393..ac1c076 100644 --- a/content/programs/rhel9-sysadmin/chapters/ch18-lvm/04-exercise-resize.md +++ b/content/programs/rhel9-sysadmin/chapters/ch18-lvm/04-exercise-resize.md @@ -12,9 +12,23 @@ Run out of space on purpose, add a disk to the volume group, grow a mounted XFS objectives=["ch18.resize"] id="resize" title="Grow and shrink volumes" + exercise="sa-lvm-resize" + ownExercise=true + guided=true + starter=false hosts=["workstation","servera"] outcomes=["Extend an LV and its file system online with lvextend -r.","Add a physical volume to a volume group.","Shrink ext4 and understand why XFS cannot shrink."] %} + {% task id="task-554715ab948f" title="Start the exercise" %} + On workstation, start the exercise. It builds the volume group `vgdata` on `/dev/sdb1` of servera with the volumes `lvapp` (1 GiB, XFS, `/srv/app`) and `lvlog` (ext4, `/srv/log`), the result of the previous exercise. + +```console +[student@workstation ~]$ lab start sa-lvm-resize +``` + + Later tasks assume that you are logged in to the server they name: `ssh student@servera` from workstation, then `sudo -i` for a root shell. + {% /task %} + {% task id="task-576f1da0ffde" title="Start from the previous exercise" %} On servera as root, make sure `vgdata` with `lvapp` (xfs, `/srv/app`) and `lvlog` (ext4, `/srv/log`) exist and are mounted. Write a recognisable file into `/srv/app`. @@ -142,4 +156,8 @@ resize2fs done ext4 shrank (offline); XFS refused. Your exact sizes differ slightly. {% /reveal %} {% /task %} + + {% task id="task-1d1ba5c9c5c5" title="Grade and finish" %} + {% lab-finish exercise="sa-lvm-resize" grade=true servers=true /%} + {% /task %} {% /lab %} diff --git a/content/programs/rhel9-sysadmin/chapters/ch18-lvm/06-exercise-maintenance.md b/content/programs/rhel9-sysadmin/chapters/ch18-lvm/06-exercise-maintenance.md index ef072ff..b83c13e 100644 --- a/content/programs/rhel9-sysadmin/chapters/ch18-lvm/06-exercise-maintenance.md +++ b/content/programs/rhel9-sysadmin/chapters/ch18-lvm/06-exercise-maintenance.md @@ -12,9 +12,23 @@ A volume holds your data on an old disk. Move it to a new disk while it stays mo objectives=["ch18.maintenance"] id="maintenance" title="Move data to a new disk" + exercise="sa-lvm-maintenance" + ownExercise=true + guided=true + starter=false hosts=["workstation","servera"] outcomes=["Replace a PV with pvmove, vgreduce and pvremove.","Rename a volume and update fstab.","Add LVM swap and verify after a reboot."] %} + {% task id="task-817671257cef" title="Start the exercise" %} + On workstation, start the exercise. It builds the volume group `vgdata` from the two partitions of `/dev/sdb` on servera, with the volumes `lvapp` and `lvlog` mounted on `/srv/app` and `/srv/log`. + +```console +[student@workstation ~]$ lab start sa-lvm-maintenance +``` + + Later tasks assume that you are logged in to the server they name: `ssh student@servera` from workstation, then `sudo -i` for a root shell. + {% /task %} + {% task id="task-a249cb7e09bd" title="A fresh start" %} On servera as root, remove the volumes from the previous exercises (unmount them first), keep the volume group `vgdata` with both PVs, and create one volume `lvdata` of 800 MiB that lives only on `/dev/sdb1`. Put XFS on it, mount it on `/srv/data`, and write a file. Note its checksum. Check on which PV it lives. @@ -145,4 +159,8 @@ b2bc7d3f8b652d2e Volumes activate by themselves at boot, and the data is exactly what you wrote. Keep this layout for the troubleshooting exercise. {% /reveal %} {% /task %} + + {% task id="task-04e5d8e441d2" title="Grade and finish" %} + {% lab-finish exercise="sa-lvm-maintenance" grade=true servers=true /%} + {% /task %} {% /lab %} diff --git a/content/programs/rhel9-sysadmin/chapters/ch18-lvm/08-exercise-troubleshooting.md b/content/programs/rhel9-sysadmin/chapters/ch18-lvm/08-exercise-troubleshooting.md index c4ae6d6..4f67e7a 100644 --- a/content/programs/rhel9-sysadmin/chapters/ch18-lvm/08-exercise-troubleshooting.md +++ b/content/programs/rhel9-sysadmin/chapters/ch18-lvm/08-exercise-troubleshooting.md @@ -12,9 +12,23 @@ Three problems, each with a different message. Diagnose each from its message, f objectives=["ch18.troubleshooting"] id="troubleshooting" title="Fix three LVM problems" + exercise="sa-lvm-troubleshooting" + ownExercise=true + guided=true + starter=false hosts=["workstation","servera"] outcomes=["Activate an inactive volume.","Respond to Insufficient free space.","Resolve a group that cannot be deactivated."] %} + {% task id="task-1c291d41ff72" title="Start the exercise" %} + On workstation, start the exercise. It builds the volume group `vgdata` on `/dev/sdb2` of servera with the XFS volume `lvstore` (800 MiB, mounted on `/srv/data`) and the swap volume `lvswap`, both in `/etc/fstab`. + +```console +[student@workstation ~]$ lab start sa-lvm-troubleshooting +``` + + Later tasks assume that you are logged in to the server they name: `ssh student@servera` from workstation, then `sudo -i` for a root shell. + {% /task %} + {% task id="task-733227e1e67f" title="The starting point" %} On servera as root, make sure `vgdata` has the mounted volume `lvstore` on `/srv/data` (as left by the previous exercise). @@ -112,19 +126,12 @@ lvswap ```console [root@servera ~]# vgcfgbackup vgdata Volume group "vgdata" successfully backed up. -[root@servera ~]# swapoff -a; umount /srv/data -[root@servera ~]# cp /root/fstab.bak /etc/fstab; systemctl daemon-reload -[root@servera ~]# lvremove -y vgdata/lvstore vgdata/lvswap Logical volume "lvstore" successfully removed. Logical volume "lvswap" successfully removed. -[root@servera ~]# vgremove vgdata Volume group "vgdata" successfully removed -[root@servera ~]# pvremove /dev/sdb2 Labels on physical volume "/dev/sdb2" successfully wiped. -[root@servera ~]# wipefs -a /dev/sdb1 /dev/sdb2 /dev/sdb > /dev/null 2>&1; lsblk /dev/sdb NAME MAJ:MIN RM SIZE RO TYPE MOUNTPOINTS sdb 8:16 0 5G 0 disk -[root@servera ~]# rmdir /srv/data /srv/app /srv/log 2> /dev/null [root@servera ~]# exit [student@servera ~]$ exit ``` @@ -132,4 +139,8 @@ sdb 8:16 0 5G 0 disk Order matters: volumes, then the group, then the PVs. `rht-vmctl reset servers` is the quick alternative. {% /reveal %} {% /task %} + + {% task id="task-95b4e17972cb" title="Grade and finish" %} + {% lab-finish exercise="sa-lvm-troubleshooting" grade=true servers=true /%} + {% /task %} {% /lab %} diff --git a/content/programs/rhel9-sysadmin/chapters/ch19-nfs-and-autofs/02-exercise-share.md b/content/programs/rhel9-sysadmin/chapters/ch19-nfs-and-autofs/02-exercise-share.md index 083d23b..59b9e81 100644 --- a/content/programs/rhel9-sysadmin/chapters/ch19-nfs-and-autofs/02-exercise-share.md +++ b/content/programs/rhel9-sysadmin/chapters/ch19-nfs-and-autofs/02-exercise-share.md @@ -12,28 +12,21 @@ Make serverb an NFS server for one directory, open the firewall for it, and moun objectives=["ch19.nfs"] id="share" title="Export and mount a directory" + exercise="sa-nfs-share" + ownExercise=true + guided=true + starter=false hosts=["workstation","servera","serverb"] outcomes=["Export a directory with exportfs.","Open NFS in the firewall.","Mount it on a client and check the result."] %} - {% task id="task-6d627eeb3294" title="Prepare the server" %} - On serverb as root (`sudo -i`), make sure `nfs-utils` is installed, create `/srv/shared` with a file `hello.txt` containing `shared file`, and find your lab network (the network of `ip -br addr show enp5s0`). - - {% reveal title="Show solution" %} + {% task id="task-baeb0ca16eaa" title="Start the exercise" %} + On workstation, start the exercise. It installs nfs-utils on servera and serverb and creates `/srv/shared/hello.txt` on serverb. ```console -[student@workstation ~]$ ssh student@serverb -[student@serverb ~]$ sudo -i -[root@serverb ~]# dnf install -y nfs-utils -...output omitted... -Complete! -[root@serverb ~]# mkdir -p /srv/shared -[root@serverb ~]# echo "shared file" > /srv/shared/hello.txt -[root@serverb ~]# ip -br addr show enp5s0 -enp5s0 UP 172.25.250.11/24 fe80::216:3eff:fe04:a5a7/64 +[student@workstation ~]$ lab start sa-nfs-share ``` - The network here is `172.25.250.0/24` (the address with the host part zeroed). Use the network of your own lab in the next task. - {% /reveal %} + Later tasks assume that you are logged in to the server they name: `ssh student@servera` from workstation, then `sudo -i` for a root shell. {% /task %} {% task id="task-043134ef0721" title="Export it" %} @@ -142,4 +135,8 @@ exporting 172.25.250.0/24:/srv/shared "Read-only file system" is the message in both cases; `mount` shows which side decided. Leave the server exporting read-write, and the share unmounted, for the next exercises. {% /reveal %} {% /task %} + + {% task id="task-5e32b427ad3d" title="Grade and finish" %} + {% lab-finish exercise="sa-nfs-share" grade=true servers=true /%} + {% /task %} {% /lab %} diff --git a/content/programs/rhel9-sysadmin/chapters/ch19-nfs-and-autofs/04-exercise-permissions.md b/content/programs/rhel9-sysadmin/chapters/ch19-nfs-and-autofs/04-exercise-permissions.md index c30a3aa..929bfa1 100644 --- a/content/programs/rhel9-sysadmin/chapters/ch19-nfs-and-autofs/04-exercise-permissions.md +++ b/content/programs/rhel9-sysadmin/chapters/ch19-nfs-and-autofs/04-exercise-permissions.md @@ -12,9 +12,23 @@ See user IDs cross the network: a file created on the client, a user that exists objectives=["ch19.access"] id="permissions" title="Get the permissions right" + exercise="sa-nfs-permissions" + ownExercise=true + guided=true + starter=false hosts=["workstation","servera","serverb"] outcomes=["Show that NFS carries numeric IDs.","Make a user work on both sides with a matching UID.","Build a setgid group directory on a share."] %} + {% task id="task-ee7f3e44c540" title="Start the exercise" %} + On workstation, start the exercise. It installs nfs-utils, exports `/srv/shared` read-write from serverb to the lab network, starts the NFS server and opens the `nfs` firewall service. + +```console +[student@workstation ~]$ lab start sa-nfs-permissions +``` + + Later tasks assume that you are logged in to the server they name: `ssh student@servera` from workstation, then `sudo -i` for a root shell. + {% /task %} + {% task id="task-49cd65115f6e" title="Make the share writable for student" %} The share must be exported read-write and mounted from the previous exercise (export `/srv/shared` read-write, firewall open, mounted on servera in `/mnt/shared`). On serverb, give the directory to `student`, who has UID 1000 on both machines. On servera, create a file as `student` in the share and list it by number and by name. @@ -125,15 +139,7 @@ total 8 {% /reveal %} {% /task %} - {% task id="task-718654323cf3" title="Clean up" %} - -```console -[root@servera ~]# umount /mnt/shared -[root@servera ~]# userdel -r alice; groupdel team 2> /dev/null -[root@serverb ~]# rm -rf /srv/shared/alice /srv/shared/team /srv/shared/st.txt -[root@serverb ~]# userdel -r alice; groupdel team 2> /dev/null -``` - - Keep the export and the firewall rule: the next exercises use them. + {% task id="task-d0c4c5654cc5" title="Grade and finish" %} + {% lab-finish exercise="sa-nfs-permissions" grade=true servers=true /%} {% /task %} {% /lab %} diff --git a/content/programs/rhel9-sysadmin/chapters/ch19-nfs-and-autofs/06-exercise-persistent.md b/content/programs/rhel9-sysadmin/chapters/ch19-nfs-and-autofs/06-exercise-persistent.md index 44b1cbb..d202828 100644 --- a/content/programs/rhel9-sysadmin/chapters/ch19-nfs-and-autofs/06-exercise-persistent.md +++ b/content/programs/rhel9-sysadmin/chapters/ch19-nfs-and-autofs/06-exercise-persistent.md @@ -12,21 +12,21 @@ Make servera mount the share at boot with an fstab entry and check it after a re objectives=["ch19.persistent"] id="persistent" title="Mount at boot, then on demand" + exercise="sa-nfs-persistent" + ownExercise=true + guided=true + starter=false hosts=["workstation","servera","serverb"] outcomes=["Write and test an NFS fstab entry with _netdev.","Create an autofs master entry and an indirect map.","Observe on-demand mounting and idle unmounting."] %} - {% task id="task-89a46b9e1103" title="Check the server" %} - On serverb, check that `/srv/shared` is exported read-write, the `nfs-server` service runs and the firewall allows `nfs`. - - {% reveal title="Show solution" %} + {% task id="task-342c7a774f4c" title="Start the exercise" %} + On workstation, start the exercise. It installs nfs-utils, exports `/srv/shared` from serverb to the lab network (NFS server running, firewall open) and creates the mount point `/mnt/shared` on servera. ```console -[student@workstation ~]$ ssh root@serverb 'exportfs -v | cut -c1-70; systemctl is-active nfs-server; firewall-cmd --list-services' -/srv/shared 172.25.250.0/24(sync,wdelay,hide,no_subtree_check,sec=sys,rw,secure -active -cockpit dhcpv6-client nfs ssh +[student@workstation ~]$ lab start sa-nfs-persistent ``` - {% /reveal %} + + Later tasks assume that you are logged in to the server they name: `ssh student@servera` from workstation, then `sudo -i` for a root shell. {% /task %} {% task id="task-3a1b64fd9b4a" title="An fstab entry" %} @@ -120,4 +120,8 @@ ls: cannot access '/mnt/auto/broken': No such file or directory The map entry exists, but the server has no such export, so the mount fails and the path does not appear. The error is "No such file or directory", not an NFS message, which is why `showmount`, `exportfs -v` on the server and `journalctl -u autofs` are the next stops. {% /reveal %} {% /task %} + + {% task id="task-1bbe047db674" title="Grade and finish" %} + {% lab-finish exercise="sa-nfs-persistent" grade=true servers=true /%} + {% /task %} {% /lab %} diff --git a/content/programs/rhel9-sysadmin/chapters/ch19-nfs-and-autofs/08-exercise-maps.md b/content/programs/rhel9-sysadmin/chapters/ch19-nfs-and-autofs/08-exercise-maps.md index ad105c1..8f6d4ba 100644 --- a/content/programs/rhel9-sysadmin/chapters/ch19-nfs-and-autofs/08-exercise-maps.md +++ b/content/programs/rhel9-sysadmin/chapters/ch19-nfs-and-autofs/08-exercise-maps.md @@ -12,9 +12,23 @@ Export a directory of home directories from serverb, and make servera mount each objectives=["ch19.maps"] id="maps" title="Serve home directories on demand" + exercise="sa-nfs-maps" + ownExercise=true + guided=true + starter=false hosts=["workstation","servera","serverb"] outcomes=["Create a wildcard map with * and &.","Create a direct map with the /- master entry.","Use automount -m and journalctl -u autofs to inspect maps."] %} + {% task id="task-7e46fbdf7674" title="Start the exercise" %} + On workstation, start the exercise. It installs nfs-utils and autofs, exports `/srv/shared` from serverb to the lab network (NFS server running, firewall open), and starts autofs on servera. + +```console +[student@workstation ~]$ lab start sa-nfs-maps +``` + + Later tasks assume that you are logged in to the server they name: `ssh student@servera` from workstation, then `sudo -i` for a root shell. + {% /task %} + {% task id="task-4cdf0b470d48" title="The homes on the server" %} On serverb as root, create `/srv/homes/alice` and `/srv/homes/student` with a file `readme` in each, owned by the matching user (UID 2001 for alice; create the user if needed). Export `/srv/homes` to your lab network read-write and publish. @@ -121,12 +135,7 @@ total 4 {% /reveal %} {% /task %} - {% task id="task-79dcd78cb700" title="Clean up" %} - -```console -[root@servera ~]# rm -f /etc/auto.master.d/{homes,direct,shared}.autofs /etc/auto.homes /etc/auto.direct /etc/auto.shared -[root@servera ~]# systemctl reload autofs; userdel -r alice -[root@serverb ~]# sed -i '/srv\/homes/d' /etc/exports; exportfs -ra; rm -rf /srv/homes; userdel -r alice -``` + {% task id="task-9d2d61f64c25" title="Grade and finish" %} + {% lab-finish exercise="sa-nfs-maps" grade=true servers=true /%} {% /task %} {% /lab %} diff --git a/content/programs/rhel9-sysadmin/lab/sa-lvm-build.yml b/content/programs/rhel9-sysadmin/lab/sa-lvm-build.yml new file mode 100644 index 0000000..404c25b --- /dev/null +++ b/content/programs/rhel9-sysadmin/lab/sa-lvm-build.yml @@ -0,0 +1,152 @@ +name: sa-lvm-build +title: Build your first volume group +page: ch18/exercise-build +transport: ssh +version: 1 +starter: [] +setup: +- action: wipe-disk + hosts: + - servera + device: /dev/sdb +- action: remove-lines + hosts: + - servera + path: /etc/fstab + matching: /dev/vgdata/ +- action: directory + hosts: + - servera + path: /srv/app + state: absent +- action: directory + hosts: + - servera + path: /srv/log + state: absent +- action: directory + hosts: + - servera + path: /srv/data + state: absent +- action: file + hosts: + - servera + path: /root/fstab.bak + state: absent +- action: systemd + hosts: + - servera + reload: true +- action: partition-disk + hosts: + - servera + device: /dev/sdb + partitions: + - name: pv1 + fs: lvm + startMiB: 1 + endMiB: 1537 + - name: pv2 + fs: lvm + startMiB: 1537 + endMiB: 3073 +checkpoints: + final: + files: [] + checks: + - id: vg + kind: volume-group + 'on': servera.lab.example.com + targets: + - servera.lab.example.com + message: vgdata exists on /dev/sdb1 + vg: vgdata + - id: lvapp + kind: logical-volume + 'on': servera.lab.example.com + targets: + - servera.lab.example.com + message: lvapp is at least 1 GiB + vg: vgdata + lv: lvapp + minSizeMiB: 1000 + - id: lvlog + kind: logical-volume + 'on': servera.lab.example.com + targets: + - servera.lab.example.com + message: lvlog exists (half of the free space) + vg: vgdata + lv: lvlog + minSizeMiB: 200 + - id: app + kind: mount + 'on': servera.lab.example.com + targets: + - servera.lab.example.com + message: /srv/app is XFS and mounted + path: /srv/app + fstype: xfs + persistent: false + - id: log + kind: mount + 'on': servera.lab.example.com + targets: + - servera.lab.example.com + message: /srv/log is ext4 and mounted + path: /srv/log + fstype: ext4 + persistent: false + - id: a + kind: file + 'on': servera.lab.example.com + targets: + - servera.lab.example.com + message: /srv/app/a.txt holds data + paths: + - /srv/app/a.txt + line: hello + - id: b + kind: file + 'on': servera.lab.example.com + targets: + - servera.lab.example.com + message: /srv/log/b.txt holds data + paths: + - /srv/log/b.txt + line: world +finish: +- action: wipe-disk + hosts: + - servera + device: /dev/sdb +- action: remove-lines + hosts: + - servera + path: /etc/fstab + matching: /dev/vgdata/ +- action: directory + hosts: + - servera + path: /srv/app + state: absent +- action: directory + hosts: + - servera + path: /srv/log + state: absent +- action: directory + hosts: + - servera + path: /srv/data + state: absent +- action: file + hosts: + - servera + path: /root/fstab.bak + state: absent +- action: systemd + hosts: + - servera + reload: true diff --git a/content/programs/rhel9-sysadmin/lab/sa-lvm-maintenance.yml b/content/programs/rhel9-sysadmin/lab/sa-lvm-maintenance.yml new file mode 100644 index 0000000..1478dac --- /dev/null +++ b/content/programs/rhel9-sysadmin/lab/sa-lvm-maintenance.yml @@ -0,0 +1,158 @@ +name: sa-lvm-maintenance +title: Move data to a new disk +page: ch18/exercise-maintenance +transport: ssh +version: 1 +starter: [] +setup: +- action: wipe-disk + hosts: + - servera + device: /dev/sdb +- action: remove-lines + hosts: + - servera + path: /etc/fstab + matching: /dev/vgdata/ +- action: directory + hosts: + - servera + path: /srv/app + state: absent +- action: directory + hosts: + - servera + path: /srv/log + state: absent +- action: directory + hosts: + - servera + path: /srv/data + state: absent +- action: file + hosts: + - servera + path: /root/fstab.bak + state: absent +- action: systemd + hosts: + - servera + reload: true +- action: partition-disk + hosts: + - servera + device: /dev/sdb + partitions: + - name: pv1 + fs: lvm + startMiB: 1 + endMiB: 1537 + - name: pv2 + fs: lvm + startMiB: 1537 + endMiB: 3073 +- action: lvm-build + hosts: + - servera + vg: vgdata + pvs: + - /dev/sdb1 + - /dev/sdb2 + volumes: + - name: lvapp + size: 1G + fs: xfs + mount: /srv/app + - name: lvlog + size: 256M + fs: ext4 + mount: /srv/log + label: lvlog +checkpoints: + final: + files: [] + checks: + - id: vg + kind: volume-group + 'on': servera.lab.example.com + targets: + - servera.lab.example.com + message: vgdata exists + vg: vgdata + - id: lv + kind: logical-volume + 'on': servera.lab.example.com + targets: + - servera.lab.example.com + message: the renamed volume lvstore is at least 800 MiB + vg: vgdata + lv: lvstore + minSizeMiB: 800 + - id: swaplv + kind: logical-volume + 'on': servera.lab.example.com + targets: + - servera.lab.example.com + message: lvswap is at least 256 MiB + vg: vgdata + lv: lvswap + minSizeMiB: 256 + - id: mount + kind: mount + 'on': servera.lab.example.com + targets: + - servera.lab.example.com + message: /srv/data is XFS, mounted now and at boot + path: /srv/data + fstype: xfs + persistent: true + - id: swap + kind: swap + 'on': servera.lab.example.com + targets: + - servera.lab.example.com + message: the swap volume is active and in fstab + minSizeMiB: 200 + - id: numbers + kind: file + 'on': servera.lab.example.com + targets: + - servera.lab.example.com + message: the data survived the move + paths: + - /srv/data/numbers.txt + line: '100000' +finish: +- action: wipe-disk + hosts: + - servera + device: /dev/sdb +- action: remove-lines + hosts: + - servera + path: /etc/fstab + matching: /dev/vgdata/ +- action: directory + hosts: + - servera + path: /srv/app + state: absent +- action: directory + hosts: + - servera + path: /srv/log + state: absent +- action: directory + hosts: + - servera + path: /srv/data + state: absent +- action: file + hosts: + - servera + path: /root/fstab.bak + state: absent +- action: systemd + hosts: + - servera + reload: true diff --git a/content/programs/rhel9-sysadmin/lab/sa-lvm-resize.yml b/content/programs/rhel9-sysadmin/lab/sa-lvm-resize.yml new file mode 100644 index 0000000..9ad0969 --- /dev/null +++ b/content/programs/rhel9-sysadmin/lab/sa-lvm-resize.yml @@ -0,0 +1,151 @@ +name: sa-lvm-resize +title: Grow and shrink volumes +page: ch18/exercise-resize +transport: ssh +version: 1 +starter: [] +setup: +- action: wipe-disk + hosts: + - servera + device: /dev/sdb +- action: remove-lines + hosts: + - servera + path: /etc/fstab + matching: /dev/vgdata/ +- action: directory + hosts: + - servera + path: /srv/app + state: absent +- action: directory + hosts: + - servera + path: /srv/log + state: absent +- action: directory + hosts: + - servera + path: /srv/data + state: absent +- action: file + hosts: + - servera + path: /root/fstab.bak + state: absent +- action: systemd + hosts: + - servera + reload: true +- action: partition-disk + hosts: + - servera + device: /dev/sdb + partitions: + - name: pv1 + fs: lvm + startMiB: 1 + endMiB: 1537 + - name: pv2 + fs: lvm + startMiB: 1537 + endMiB: 3073 +- action: lvm-build + hosts: + - servera + vg: vgdata + pvs: + - /dev/sdb1 + volumes: + - name: lvapp + size: 1G + fs: xfs + mount: /srv/app + - name: lvlog + size: 50%FREE + fs: ext4 + mount: /srv/log + label: lvlog +checkpoints: + final: + files: [] + checks: + - id: vg + kind: volume-group + 'on': servera.lab.example.com + targets: + - servera.lab.example.com + message: vgdata spans two physical volumes + vg: vgdata + minPhysicalVolumes: 2 + - id: lvapp + kind: logical-volume + 'on': servera.lab.example.com + targets: + - servera.lab.example.com + message: lvapp took all the free space (over 2 GiB) + vg: vgdata + lv: lvapp + minSizeMiB: 1900 + - id: lvlog + kind: logical-volume + 'on': servera.lab.example.com + targets: + - servera.lab.example.com + message: lvlog exists + vg: vgdata + lv: lvlog + minSizeMiB: 190 + - id: numbers + kind: file + 'on': servera.lab.example.com + targets: + - servera.lab.example.com + message: the data in /srv/app survived the growing + paths: + - /srv/app/numbers.txt + line: '100000' + - id: log + kind: mount + 'on': servera.lab.example.com + targets: + - servera.lab.example.com + message: /srv/log is mounted again + path: /srv/log + fstype: ext4 + persistent: false +finish: +- action: wipe-disk + hosts: + - servera + device: /dev/sdb +- action: remove-lines + hosts: + - servera + path: /etc/fstab + matching: /dev/vgdata/ +- action: directory + hosts: + - servera + path: /srv/app + state: absent +- action: directory + hosts: + - servera + path: /srv/log + state: absent +- action: directory + hosts: + - servera + path: /srv/data + state: absent +- action: file + hosts: + - servera + path: /root/fstab.bak + state: absent +- action: systemd + hosts: + - servera + reload: true diff --git a/content/programs/rhel9-sysadmin/lab/sa-lvm-troubleshooting.yml b/content/programs/rhel9-sysadmin/lab/sa-lvm-troubleshooting.yml new file mode 100644 index 0000000..92f4e40 --- /dev/null +++ b/content/programs/rhel9-sysadmin/lab/sa-lvm-troubleshooting.yml @@ -0,0 +1,144 @@ +name: sa-lvm-troubleshooting +title: Fix three LVM problems +page: ch18/exercise-troubleshooting +transport: ssh +version: 1 +starter: [] +setup: +- action: wipe-disk + hosts: + - servera + device: /dev/sdb +- action: remove-lines + hosts: + - servera + path: /etc/fstab + matching: /dev/vgdata/ +- action: directory + hosts: + - servera + path: /srv/app + state: absent +- action: directory + hosts: + - servera + path: /srv/log + state: absent +- action: directory + hosts: + - servera + path: /srv/data + state: absent +- action: file + hosts: + - servera + path: /root/fstab.bak + state: absent +- action: systemd + hosts: + - servera + reload: true +- action: partition-disk + hosts: + - servera + device: /dev/sdb + partitions: + - name: pv1 + fs: lvm + startMiB: 1 + endMiB: 1537 + - name: pv2 + fs: lvm + startMiB: 1537 + endMiB: 3073 +- action: lvm-build + hosts: + - servera + vg: vgdata + pvs: + - /dev/sdb2 + volumes: + - name: lvstore + size: 800M + fs: xfs + mount: /srv/data + - name: lvswap + size: 256M + fs: swap +- action: append-line + hosts: + - servera + path: /etc/fstab + line: /dev/vgdata/lvstore /srv/data xfs defaults 0 0 +- action: append-line + hosts: + - servera + path: /etc/fstab + line: /dev/vgdata/lvswap none swap defaults 0 0 +- action: systemd + hosts: + - servera + reload: true +checkpoints: + final: + files: [] + checks: + - id: lv + kind: logical-volume + 'on': servera.lab.example.com + targets: + - servera.lab.example.com + message: lvstore grew into the free space (over 1100 MiB) + vg: vgdata + lv: lvstore + minSizeMiB: 1100 + - id: mount + kind: mount + 'on': servera.lab.example.com + targets: + - servera.lab.example.com + message: /srv/data is XFS and mounted at boot + path: /srv/data + fstype: xfs + persistent: true + - id: swap + kind: swap + 'on': servera.lab.example.com + targets: + - servera.lab.example.com + message: lvswap is active again and in fstab + minSizeMiB: 200 +finish: +- action: wipe-disk + hosts: + - servera + device: /dev/sdb +- action: remove-lines + hosts: + - servera + path: /etc/fstab + matching: /dev/vgdata/ +- action: directory + hosts: + - servera + path: /srv/app + state: absent +- action: directory + hosts: + - servera + path: /srv/log + state: absent +- action: directory + hosts: + - servera + path: /srv/data + state: absent +- action: file + hosts: + - servera + path: /root/fstab.bak + state: absent +- action: systemd + hosts: + - servera + reload: true diff --git a/content/programs/rhel9-sysadmin/lab/sa-nfs-maps.yml b/content/programs/rhel9-sysadmin/lab/sa-nfs-maps.yml new file mode 100644 index 0000000..3064e55 --- /dev/null +++ b/content/programs/rhel9-sysadmin/lab/sa-nfs-maps.yml @@ -0,0 +1,384 @@ +name: sa-nfs-maps +title: Serve home directories on demand +page: ch19/exercise-maps +transport: ssh +version: 1 +starter: [] +setup: +- action: unmount + hosts: + - servera + path: /mnt/shared +- action: remove-lines + hosts: + - servera + path: /etc/fstab + matching: serverb:/srv/shared +- action: directory + hosts: + - servera + path: /mnt/shared + state: absent +- action: directory + hosts: + - servera + path: /mnt/auto + state: absent +- action: directory + hosts: + - servera + path: /remote + state: absent +- action: directory + hosts: + - servera + path: /mnt/direct + state: absent +- action: file + hosts: + - servera + path: /etc/auto.master.d/shared.autofs + state: absent +- action: file + hosts: + - servera + path: /etc/auto.master.d/homes.autofs + state: absent +- action: file + hosts: + - servera + path: /etc/auto.master.d/direct.autofs + state: absent +- action: file + hosts: + - servera + path: /etc/auto.shared + state: absent +- action: file + hosts: + - servera + path: /etc/auto.homes + state: absent +- action: file + hosts: + - servera + path: /etc/auto.direct + state: absent +- action: file + hosts: + - servera + path: /root/fstab.bak + state: absent +- action: service + hosts: + - servera + names: + - autofs + state: stopped + enabled: false +- action: package + hosts: + - servera + names: + - autofs + state: absent +- action: user + hosts: + - servera + name: alice + state: absent +- action: group + hosts: + - servera + name: team + state: absent +- action: systemd + hosts: + - servera + reload: true +- action: remove-lines + hosts: + - serverb + path: /etc/exports + matching: /srv/ +- action: service + hosts: + - serverb + names: + - nfs-server + state: stopped + enabled: false +- action: firewall + hosts: + - serverb + state: absent + service: nfs +- action: directory + hosts: + - serverb + path: /srv/shared + state: absent +- action: directory + hosts: + - serverb + path: /srv/homes + state: absent +- action: user + hosts: + - serverb + name: alice + state: absent +- action: group + hosts: + - serverb + name: team + state: absent +- action: package + hosts: + - serverb + names: + - nfs-utils + state: present +- action: directory + hosts: + - serverb + path: /srv/shared + state: present +- action: file + hosts: + - serverb + path: /srv/shared/hello.txt + state: present + content: 'shared file + + ' +- action: append-line + hosts: + - serverb + path: /etc/exports + line: /srv/shared 172.25.0.0/16(rw,sync) +- action: service + hosts: + - serverb + names: + - nfs-server + state: restarted + enabled: true +- action: firewall + hosts: + - serverb + state: present + service: nfs +- action: package + hosts: + - servera + names: + - nfs-utils + state: present +- action: directory + hosts: + - servera + path: /mnt/shared + state: present +- action: package + hosts: + - servera + names: + - autofs + state: present +- action: service + hosts: + - servera + names: + - autofs + state: started + enabled: true +checkpoints: + final: + files: [] + checks: + - id: homes-master + kind: file + 'on': servera.lab.example.com + targets: + - servera.lab.example.com + message: a master map serves /remote/homes + paths: + - /etc/auto.master.d/homes.autofs + matches: ^/remote/homes[[:space:]]+/etc/auto\.homes + - id: wildcard + kind: file + 'on': servera.lab.example.com + targets: + - servera.lab.example.com + message: the map is a wildcard map + paths: + - /etc/auto.homes + matches: ^\*[[:space:]]+-rw,sync[[:space:]]+serverb:/srv/homes/& + - id: direct + kind: file + 'on': servera.lab.example.com + targets: + - servera.lab.example.com + message: a direct map mounts /mnt/direct/data + paths: + - /etc/auto.direct + matches: ^/mnt/direct/data[[:space:]]+-rw[[:space:]]+serverb:/srv/shared + - id: exports + kind: file + 'on': serverb.lab.example.com + targets: + - serverb.lab.example.com + message: serverb also exports /srv/homes + paths: + - /etc/exports + contains: + - /srv/homes + - id: edited + kind: file + 'on': serverb.lab.example.com + targets: + - serverb.lab.example.com + message: alice edited her home from servera + paths: + - /srv/homes/alice/readme + owner: alice + line: edited from servera + - id: autofs + kind: service + 'on': servera.lab.example.com + targets: + - servera.lab.example.com + message: autofs is running + names: + - autofs.service + active: true +finish: +- action: unmount + hosts: + - servera + path: /mnt/shared +- action: remove-lines + hosts: + - servera + path: /etc/fstab + matching: serverb:/srv/shared +- action: directory + hosts: + - servera + path: /mnt/shared + state: absent +- action: directory + hosts: + - servera + path: /mnt/auto + state: absent +- action: directory + hosts: + - servera + path: /remote + state: absent +- action: directory + hosts: + - servera + path: /mnt/direct + state: absent +- action: file + hosts: + - servera + path: /etc/auto.master.d/shared.autofs + state: absent +- action: file + hosts: + - servera + path: /etc/auto.master.d/homes.autofs + state: absent +- action: file + hosts: + - servera + path: /etc/auto.master.d/direct.autofs + state: absent +- action: file + hosts: + - servera + path: /etc/auto.shared + state: absent +- action: file + hosts: + - servera + path: /etc/auto.homes + state: absent +- action: file + hosts: + - servera + path: /etc/auto.direct + state: absent +- action: file + hosts: + - servera + path: /root/fstab.bak + state: absent +- action: service + hosts: + - servera + names: + - autofs + state: stopped + enabled: false +- action: package + hosts: + - servera + names: + - autofs + state: absent +- action: user + hosts: + - servera + name: alice + state: absent +- action: group + hosts: + - servera + name: team + state: absent +- action: systemd + hosts: + - servera + reload: true +- action: remove-lines + hosts: + - serverb + path: /etc/exports + matching: /srv/ +- action: service + hosts: + - serverb + names: + - nfs-server + state: stopped + enabled: false +- action: firewall + hosts: + - serverb + state: absent + service: nfs +- action: directory + hosts: + - serverb + path: /srv/shared + state: absent +- action: directory + hosts: + - serverb + path: /srv/homes + state: absent +- action: user + hosts: + - serverb + name: alice + state: absent +- action: group + hosts: + - serverb + name: team + state: absent diff --git a/content/programs/rhel9-sysadmin/lab/sa-nfs-permissions.yml b/content/programs/rhel9-sysadmin/lab/sa-nfs-permissions.yml new file mode 100644 index 0000000..865856e --- /dev/null +++ b/content/programs/rhel9-sysadmin/lab/sa-nfs-permissions.yml @@ -0,0 +1,384 @@ +name: sa-nfs-permissions +title: Get the permissions right +page: ch19/exercise-permissions +transport: ssh +version: 1 +starter: [] +setup: +- action: unmount + hosts: + - servera + path: /mnt/shared +- action: remove-lines + hosts: + - servera + path: /etc/fstab + matching: serverb:/srv/shared +- action: directory + hosts: + - servera + path: /mnt/shared + state: absent +- action: directory + hosts: + - servera + path: /mnt/auto + state: absent +- action: directory + hosts: + - servera + path: /remote + state: absent +- action: directory + hosts: + - servera + path: /mnt/direct + state: absent +- action: file + hosts: + - servera + path: /etc/auto.master.d/shared.autofs + state: absent +- action: file + hosts: + - servera + path: /etc/auto.master.d/homes.autofs + state: absent +- action: file + hosts: + - servera + path: /etc/auto.master.d/direct.autofs + state: absent +- action: file + hosts: + - servera + path: /etc/auto.shared + state: absent +- action: file + hosts: + - servera + path: /etc/auto.homes + state: absent +- action: file + hosts: + - servera + path: /etc/auto.direct + state: absent +- action: file + hosts: + - servera + path: /root/fstab.bak + state: absent +- action: service + hosts: + - servera + names: + - autofs + state: stopped + enabled: false +- action: package + hosts: + - servera + names: + - autofs + state: absent +- action: user + hosts: + - servera + name: alice + state: absent +- action: group + hosts: + - servera + name: team + state: absent +- action: systemd + hosts: + - servera + reload: true +- action: remove-lines + hosts: + - serverb + path: /etc/exports + matching: /srv/ +- action: service + hosts: + - serverb + names: + - nfs-server + state: stopped + enabled: false +- action: firewall + hosts: + - serverb + state: absent + service: nfs +- action: directory + hosts: + - serverb + path: /srv/shared + state: absent +- action: directory + hosts: + - serverb + path: /srv/homes + state: absent +- action: user + hosts: + - serverb + name: alice + state: absent +- action: group + hosts: + - serverb + name: team + state: absent +- action: package + hosts: + - serverb + names: + - nfs-utils + state: present +- action: directory + hosts: + - serverb + path: /srv/shared + state: present +- action: file + hosts: + - serverb + path: /srv/shared/hello.txt + state: present + content: 'shared file + + ' +- action: append-line + hosts: + - serverb + path: /etc/exports + line: /srv/shared 172.25.0.0/16(rw,sync) +- action: service + hosts: + - serverb + names: + - nfs-server + state: restarted + enabled: true +- action: firewall + hosts: + - serverb + state: present + service: nfs +- action: package + hosts: + - servera + names: + - nfs-utils + state: present +- action: directory + hosts: + - servera + path: /mnt/shared + state: present +checkpoints: + final: + files: [] + checks: + - id: student + kind: file + 'on': serverb.lab.example.com + targets: + - serverb.lab.example.com + message: student wrote st.txt over NFS (UID 1000 on both sides) + paths: + - /srv/shared/st.txt + owner: student + line: from servera as student + - id: alice + kind: file + 'on': serverb.lab.example.com + targets: + - serverb.lab.example.com + message: alice wrote from servera, stored with her UID + paths: + - /srv/shared/alice/from-servera.txt + owner: alice + line: written from servera + - id: alice-a + kind: user + 'on': servera.lab.example.com + targets: + - servera.lab.example.com + message: alice has UID 2001 on the client + names: + - alice + uid: 2001 + - id: alice-b + kind: user + 'on': serverb.lab.example.com + targets: + - serverb.lab.example.com + message: alice has UID 2001 on the server + names: + - alice + uid: 2001 + - id: team + kind: file + 'on': serverb.lab.example.com + targets: + - serverb.lab.example.com + message: /srv/shared/team is group team with setgid + paths: + - /srv/shared/team + fileType: directory + group: team + mode: '2775' + - id: by-alice + kind: file + 'on': serverb.lab.example.com + targets: + - serverb.lab.example.com + message: alice created a file in the team directory + paths: + - /srv/shared/team/by-alice.txt + owner: alice + group: team + - id: by-student + kind: file + 'on': serverb.lab.example.com + targets: + - serverb.lab.example.com + message: student created a file in the team directory + paths: + - /srv/shared/team/by-student.txt + owner: student + group: team +finish: +- action: unmount + hosts: + - servera + path: /mnt/shared +- action: remove-lines + hosts: + - servera + path: /etc/fstab + matching: serverb:/srv/shared +- action: directory + hosts: + - servera + path: /mnt/shared + state: absent +- action: directory + hosts: + - servera + path: /mnt/auto + state: absent +- action: directory + hosts: + - servera + path: /remote + state: absent +- action: directory + hosts: + - servera + path: /mnt/direct + state: absent +- action: file + hosts: + - servera + path: /etc/auto.master.d/shared.autofs + state: absent +- action: file + hosts: + - servera + path: /etc/auto.master.d/homes.autofs + state: absent +- action: file + hosts: + - servera + path: /etc/auto.master.d/direct.autofs + state: absent +- action: file + hosts: + - servera + path: /etc/auto.shared + state: absent +- action: file + hosts: + - servera + path: /etc/auto.homes + state: absent +- action: file + hosts: + - servera + path: /etc/auto.direct + state: absent +- action: file + hosts: + - servera + path: /root/fstab.bak + state: absent +- action: service + hosts: + - servera + names: + - autofs + state: stopped + enabled: false +- action: package + hosts: + - servera + names: + - autofs + state: absent +- action: user + hosts: + - servera + name: alice + state: absent +- action: group + hosts: + - servera + name: team + state: absent +- action: systemd + hosts: + - servera + reload: true +- action: remove-lines + hosts: + - serverb + path: /etc/exports + matching: /srv/ +- action: service + hosts: + - serverb + names: + - nfs-server + state: stopped + enabled: false +- action: firewall + hosts: + - serverb + state: absent + service: nfs +- action: directory + hosts: + - serverb + path: /srv/shared + state: absent +- action: directory + hosts: + - serverb + path: /srv/homes + state: absent +- action: user + hosts: + - serverb + name: alice + state: absent +- action: group + hosts: + - serverb + name: team + state: absent diff --git a/content/programs/rhel9-sysadmin/lab/sa-nfs-persistent.yml b/content/programs/rhel9-sysadmin/lab/sa-nfs-persistent.yml new file mode 100644 index 0000000..eeaf6f6 --- /dev/null +++ b/content/programs/rhel9-sysadmin/lab/sa-nfs-persistent.yml @@ -0,0 +1,355 @@ +name: sa-nfs-persistent +title: Mount at boot, then on demand +page: ch19/exercise-persistent +transport: ssh +version: 1 +starter: [] +setup: +- action: unmount + hosts: + - servera + path: /mnt/shared +- action: remove-lines + hosts: + - servera + path: /etc/fstab + matching: serverb:/srv/shared +- action: directory + hosts: + - servera + path: /mnt/shared + state: absent +- action: directory + hosts: + - servera + path: /mnt/auto + state: absent +- action: directory + hosts: + - servera + path: /remote + state: absent +- action: directory + hosts: + - servera + path: /mnt/direct + state: absent +- action: file + hosts: + - servera + path: /etc/auto.master.d/shared.autofs + state: absent +- action: file + hosts: + - servera + path: /etc/auto.master.d/homes.autofs + state: absent +- action: file + hosts: + - servera + path: /etc/auto.master.d/direct.autofs + state: absent +- action: file + hosts: + - servera + path: /etc/auto.shared + state: absent +- action: file + hosts: + - servera + path: /etc/auto.homes + state: absent +- action: file + hosts: + - servera + path: /etc/auto.direct + state: absent +- action: file + hosts: + - servera + path: /root/fstab.bak + state: absent +- action: service + hosts: + - servera + names: + - autofs + state: stopped + enabled: false +- action: package + hosts: + - servera + names: + - autofs + state: absent +- action: user + hosts: + - servera + name: alice + state: absent +- action: group + hosts: + - servera + name: team + state: absent +- action: systemd + hosts: + - servera + reload: true +- action: remove-lines + hosts: + - serverb + path: /etc/exports + matching: /srv/ +- action: service + hosts: + - serverb + names: + - nfs-server + state: stopped + enabled: false +- action: firewall + hosts: + - serverb + state: absent + service: nfs +- action: directory + hosts: + - serverb + path: /srv/shared + state: absent +- action: directory + hosts: + - serverb + path: /srv/homes + state: absent +- action: user + hosts: + - serverb + name: alice + state: absent +- action: group + hosts: + - serverb + name: team + state: absent +- action: package + hosts: + - serverb + names: + - nfs-utils + state: present +- action: directory + hosts: + - serverb + path: /srv/shared + state: present +- action: file + hosts: + - serverb + path: /srv/shared/hello.txt + state: present + content: 'shared file + + ' +- action: append-line + hosts: + - serverb + path: /etc/exports + line: /srv/shared 172.25.0.0/16(rw,sync) +- action: service + hosts: + - serverb + names: + - nfs-server + state: restarted + enabled: true +- action: firewall + hosts: + - serverb + state: present + service: nfs +- action: package + hosts: + - servera + names: + - nfs-utils + state: present +- action: directory + hosts: + - servera + path: /mnt/shared + state: present +checkpoints: + final: + files: [] + checks: + - id: master + kind: file + 'on': servera.lab.example.com + targets: + - servera.lab.example.com + message: autofs has a master map for /mnt/auto + paths: + - /etc/auto.master.d/shared.autofs + matches: ^/mnt/auto[[:space:]]+/etc/auto\.shared + - id: map + kind: file + 'on': servera.lab.example.com + targets: + - servera.lab.example.com + message: the map holds the data key and no broken line + paths: + - /etc/auto.shared + matches: ^data[[:space:]]+-rw,sync[[:space:]]+serverb:/srv/shared + lacks: + - broken + - id: autofs + kind: service + 'on': servera.lab.example.com + targets: + - servera.lab.example.com + message: autofs is running and enabled + names: + - autofs.service + active: true + enabled: true + - id: fstab + kind: file + 'on': servera.lab.example.com + targets: + - servera.lab.example.com + message: the permanent NFS line was removed from fstab again + paths: + - /etc/fstab + lacks: + - serverb:/srv/shared +finish: +- action: unmount + hosts: + - servera + path: /mnt/shared +- action: remove-lines + hosts: + - servera + path: /etc/fstab + matching: serverb:/srv/shared +- action: directory + hosts: + - servera + path: /mnt/shared + state: absent +- action: directory + hosts: + - servera + path: /mnt/auto + state: absent +- action: directory + hosts: + - servera + path: /remote + state: absent +- action: directory + hosts: + - servera + path: /mnt/direct + state: absent +- action: file + hosts: + - servera + path: /etc/auto.master.d/shared.autofs + state: absent +- action: file + hosts: + - servera + path: /etc/auto.master.d/homes.autofs + state: absent +- action: file + hosts: + - servera + path: /etc/auto.master.d/direct.autofs + state: absent +- action: file + hosts: + - servera + path: /etc/auto.shared + state: absent +- action: file + hosts: + - servera + path: /etc/auto.homes + state: absent +- action: file + hosts: + - servera + path: /etc/auto.direct + state: absent +- action: file + hosts: + - servera + path: /root/fstab.bak + state: absent +- action: service + hosts: + - servera + names: + - autofs + state: stopped + enabled: false +- action: package + hosts: + - servera + names: + - autofs + state: absent +- action: user + hosts: + - servera + name: alice + state: absent +- action: group + hosts: + - servera + name: team + state: absent +- action: systemd + hosts: + - servera + reload: true +- action: remove-lines + hosts: + - serverb + path: /etc/exports + matching: /srv/ +- action: service + hosts: + - serverb + names: + - nfs-server + state: stopped + enabled: false +- action: firewall + hosts: + - serverb + state: absent + service: nfs +- action: directory + hosts: + - serverb + path: /srv/shared + state: absent +- action: directory + hosts: + - serverb + path: /srv/homes + state: absent +- action: user + hosts: + - serverb + name: alice + state: absent +- action: group + hosts: + - serverb + name: team + state: absent diff --git a/content/programs/rhel9-sysadmin/lab/sa-nfs-share.yml b/content/programs/rhel9-sysadmin/lab/sa-nfs-share.yml new file mode 100644 index 0000000..57d538c --- /dev/null +++ b/content/programs/rhel9-sysadmin/lab/sa-nfs-share.yml @@ -0,0 +1,337 @@ +name: sa-nfs-share +title: Export and mount a directory +page: ch19/exercise-share +transport: ssh +version: 1 +starter: [] +setup: +- action: unmount + hosts: + - servera + path: /mnt/shared +- action: remove-lines + hosts: + - servera + path: /etc/fstab + matching: serverb:/srv/shared +- action: directory + hosts: + - servera + path: /mnt/shared + state: absent +- action: directory + hosts: + - servera + path: /mnt/auto + state: absent +- action: directory + hosts: + - servera + path: /remote + state: absent +- action: directory + hosts: + - servera + path: /mnt/direct + state: absent +- action: file + hosts: + - servera + path: /etc/auto.master.d/shared.autofs + state: absent +- action: file + hosts: + - servera + path: /etc/auto.master.d/homes.autofs + state: absent +- action: file + hosts: + - servera + path: /etc/auto.master.d/direct.autofs + state: absent +- action: file + hosts: + - servera + path: /etc/auto.shared + state: absent +- action: file + hosts: + - servera + path: /etc/auto.homes + state: absent +- action: file + hosts: + - servera + path: /etc/auto.direct + state: absent +- action: file + hosts: + - servera + path: /root/fstab.bak + state: absent +- action: service + hosts: + - servera + names: + - autofs + state: stopped + enabled: false +- action: package + hosts: + - servera + names: + - autofs + state: absent +- action: user + hosts: + - servera + name: alice + state: absent +- action: group + hosts: + - servera + name: team + state: absent +- action: systemd + hosts: + - servera + reload: true +- action: remove-lines + hosts: + - serverb + path: /etc/exports + matching: /srv/ +- action: service + hosts: + - serverb + names: + - nfs-server + state: stopped + enabled: false +- action: firewall + hosts: + - serverb + state: absent + service: nfs +- action: directory + hosts: + - serverb + path: /srv/shared + state: absent +- action: directory + hosts: + - serverb + path: /srv/homes + state: absent +- action: user + hosts: + - serverb + name: alice + state: absent +- action: group + hosts: + - serverb + name: team + state: absent +- action: package + hosts: + - serverb + names: + - nfs-utils + state: present +- action: directory + hosts: + - serverb + path: /srv/shared + state: present +- action: file + hosts: + - serverb + path: /srv/shared/hello.txt + state: present + content: 'shared file + + ' +- action: package + hosts: + - servera + names: + - nfs-utils + state: present +checkpoints: + final: + files: [] + checks: + - id: exports + kind: file + 'on': serverb.lab.example.com + targets: + - serverb.lab.example.com + message: /etc/exports shares /srv/shared read-write with the lab network + paths: + - /etc/exports + matches: ^/srv/shared[[:space:]]+[^[:space:]]+\(rw,sync\)[[:space:]]*$ + - id: server + kind: service + 'on': serverb.lab.example.com + targets: + - serverb.lab.example.com + message: nfs-server is running and enabled + names: + - nfs-server.service + active: true + enabled: true + - id: firewall + kind: firewall + 'on': serverb.lab.example.com + targets: + - serverb.lab.example.com + message: the nfs firewall service is open permanently + service: nfs + - id: mountpoint + kind: file + 'on': servera.lab.example.com + targets: + - servera.lab.example.com + message: /mnt/shared exists on the client + paths: + - /mnt/shared + fileType: directory + - id: no-rootfile + kind: file + 'on': serverb.lab.example.com + targets: + - serverb.lab.example.com + message: root could not create a file (root squash) + paths: + - /srv/shared/rootfile + exists: false +finish: +- action: unmount + hosts: + - servera + path: /mnt/shared +- action: remove-lines + hosts: + - servera + path: /etc/fstab + matching: serverb:/srv/shared +- action: directory + hosts: + - servera + path: /mnt/shared + state: absent +- action: directory + hosts: + - servera + path: /mnt/auto + state: absent +- action: directory + hosts: + - servera + path: /remote + state: absent +- action: directory + hosts: + - servera + path: /mnt/direct + state: absent +- action: file + hosts: + - servera + path: /etc/auto.master.d/shared.autofs + state: absent +- action: file + hosts: + - servera + path: /etc/auto.master.d/homes.autofs + state: absent +- action: file + hosts: + - servera + path: /etc/auto.master.d/direct.autofs + state: absent +- action: file + hosts: + - servera + path: /etc/auto.shared + state: absent +- action: file + hosts: + - servera + path: /etc/auto.homes + state: absent +- action: file + hosts: + - servera + path: /etc/auto.direct + state: absent +- action: file + hosts: + - servera + path: /root/fstab.bak + state: absent +- action: service + hosts: + - servera + names: + - autofs + state: stopped + enabled: false +- action: package + hosts: + - servera + names: + - autofs + state: absent +- action: user + hosts: + - servera + name: alice + state: absent +- action: group + hosts: + - servera + name: team + state: absent +- action: systemd + hosts: + - servera + reload: true +- action: remove-lines + hosts: + - serverb + path: /etc/exports + matching: /srv/ +- action: service + hosts: + - serverb + names: + - nfs-server + state: stopped + enabled: false +- action: firewall + hosts: + - serverb + state: absent + service: nfs +- action: directory + hosts: + - serverb + path: /srv/shared + state: absent +- action: directory + hosts: + - serverb + path: /srv/homes + state: absent +- action: user + hosts: + - serverb + name: alice + state: absent +- action: group + hosts: + - serverb + name: team + state: absent diff --git a/packages/compiler/src/lab.js b/packages/compiler/src/lab.js index 6cd9569..add6911 100644 --- a/packages/compiler/src/lab.js +++ b/packages/compiler/src/lab.js @@ -32,7 +32,7 @@ const CONTROL_ONLY = new Set(['git', 'lint']); // Setup and cleanup actions that run as root on the lab servers; the rest work on the workstation. const HOST_ACTIONS = new Set([ 'package', 'service', 'group', 'user', 'directory', 'file', 'remove-lines', 'firewall', 'selinux', 'wipe-disk', 'systemd', 'linger', - 'container-reset', 'run-as', 'restore-skel', 'boot', 'timezone', 'nm-connection', 'hostname', 'http-server', 'dnf-module', 'crontab', 'partition-disk', 'format', 'append-line', 'mount-all', + 'container-reset', 'run-as', 'restore-skel', 'boot', 'timezone', 'nm-connection', 'hostname', 'http-server', 'dnf-module', 'crontab', 'partition-disk', 'format', 'append-line', 'mount-all', 'lvm-build', 'unmount', ]); /** Every file under `dir` as paths relative to it, sorted. */ diff --git a/packages/lab-tools/prepare.py b/packages/lab-tools/prepare.py index e3ca505..bca3547 100644 --- a/packages/lab-tools/prepare.py +++ b/packages/lab-tools/prepare.py @@ -200,7 +200,7 @@ def ssh_keypairs(a, ctx): # --------------------------------------------------------------------------------------- # Host actions: fixed scripts, built from validated values, run as root on lab servers. -SAFE_PATH = re.compile(r'^/(?:srv|opt|mnt|data|logs|backup|tmp|root|home|var/www|var/tmp|var/log|usr/local|etc/(?:billing\.conf|httpd/conf\.d|systemd|auto\.master\.d|auto\.[a-z0-9_-]+|cron\.d|exports\.d|sudoers\.d|profile\.d|ssh/sshd_config\.d|chrony\.d|yum\.repos\.d|logrotate\.d|rsyslog\.d|security/limits\.d|sysctl\.d|NetworkManager/system-connections|containers|firewalld/(?:services|zones)))(?:/[A-Za-z0-9_.@%+=:, -]+)*$') +SAFE_PATH = re.compile(r'^/(?:srv|opt|mnt|data|logs|remote|backup|tmp|root|home|var/www|var/tmp|var/log|usr/local|etc/(?:billing\.conf|httpd/conf\.d|systemd|auto\.master\.d|auto\.[a-z0-9_-]+|cron\.d|exports\.d|sudoers\.d|profile\.d|ssh/sshd_config\.d|chrony\.d|yum\.repos\.d|logrotate\.d|rsyslog\.d|security/limits\.d|sysctl\.d|NetworkManager/system-connections|containers|firewalld/(?:services|zones)))(?:/[A-Za-z0-9_.@%+=:, -]+)*$') PROTECTED = {'/srv', '/opt', '/mnt', '/tmp', '/root', '/home', '/var/www', '/var/tmp', '/var/log', '/usr/local', '/etc/systemd', '/etc/containers'} NAME = re.compile(r'^[a-z_][a-z0-9_-]{0,31}$') SSH = ['ssh', '-o', 'BatchMode=yes', '-o', 'ConnectTimeout=10', '-o', 'StrictHostKeyChecking=accept-new', '-o', 'LogLevel=ERROR'] @@ -481,6 +481,37 @@ def script_mount_all(a): return 'systemctl daemon-reload\nmount -a\nswapon -a' +def script_lvm_build(a): + vg = a['vg'] + if not re.match(r'^[a-z][a-z0-9_]{0,15}$', vg): + raise SetupError('unsafe volume group name') + pvs = ' '.join(q(x) for x in a['pvs'] if re.match(r'^/dev/(?:sd|vd)[b-z][0-9]{1,2}$', x)) + out = ['pvcreate -ff -y %s >/dev/null' % pvs, 'vgcreate %s %s >/dev/null' % (q(vg), pvs)] + for v in a['volumes']: + if not re.match(r'^[a-z][a-z0-9_]{0,15}$', v['name']) or not re.match(r'^(?:[0-9]{1,5}[MG]|[0-9]{1,3}%FREE)$', v['size']): + raise SetupError('unsafe volume') + size = ('-l ' if '%' in v['size'] else '-L ') + q(v['size']) + dev = '/dev/%s/%s' % (vg, v['name']) + out.append('lvcreate -y -n %s %s %s >/dev/null' % (q(v['name']), size, q(vg))) + label = ' -L %s' % q(v['label']) if v.get('label') else '' + if v['fs'] == 'xfs': + out.append('mkfs.xfs -f%s %s >/dev/null' % (label, q(dev))) + elif v['fs'] == 'ext4': + out.append('mkfs.ext4 -q -F%s %s' % (label, q(dev))) + else: + out.append('mkswap %s >/dev/null' % q(dev)) + if v.get('mount'): + out.append('mkdir -p %s && mount %s %s' % (q(safe_path(v['mount'])), q(dev), q(v['mount']))) + if v['fs'] == 'swap': + out.append('swapon %s' % q(dev)) + out.append('udevadm settle') + return '\n'.join(out) + + +def script_unmount(a): + return 'umount -l %s 2>/dev/null || true' % q(safe_path(a['path'])) + + def script_run_as(a): user = q(name(a['user'])) image = q(a['image']) @@ -495,6 +526,7 @@ def script_run_as(a): 'run-as': script_run_as, 'restore-skel': script_restore_skel, 'boot': script_boot, 'timezone': script_timezone, 'nm-connection': script_nm_connection, 'hostname': script_hostname, 'http-server': script_http_server, 'dnf-module': script_dnf_module, 'crontab': script_crontab, 'partition-disk': script_partition_disk, 'format': script_format, 'append-line': script_append_line, 'mount-all': script_mount_all, + 'lvm-build': script_lvm_build, 'unmount': script_unmount, } diff --git a/packages/schema/schemas/source/lab.schema.json b/packages/schema/schemas/source/lab.schema.json index 3147809..48e793a 100644 --- a/packages/schema/schemas/source/lab.schema.json +++ b/packages/schema/schemas/source/lab.schema.json @@ -1551,6 +1551,96 @@ "$ref": "#/$defs/serverList" } } + }, + { + "type": "object", + "additionalProperties": false, + "required": [ + "action", + "hosts", + "vg", + "pvs", + "volumes" + ], + "properties": { + "action": { + "const": "lvm-build" + }, + "hosts": { + "$ref": "#/$defs/serverList" + }, + "vg": { + "type": "string", + "pattern": "^[a-z][a-z0-9_]{0,15}$" + }, + "pvs": { + "type": "array", + "minItems": 1, + "maxItems": 4, + "items": { + "type": "string", + "pattern": "^/dev/(?:sd|vd)[b-z][0-9]{1,2}$" + } + }, + "volumes": { + "type": "array", + "minItems": 1, + "maxItems": 6, + "items": { + "type": "object", + "additionalProperties": false, + "required": [ + "name", + "size", + "fs" + ], + "properties": { + "name": { + "type": "string", + "pattern": "^[a-z][a-z0-9_]{0,15}$" + }, + "size": { + "type": "string", + "pattern": "^(?:[0-9]{1,5}[MG]|[0-9]{1,3}%FREE)$" + }, + "fs": { + "enum": [ + "xfs", + "ext4", + "swap" + ] + }, + "label": { + "type": "string", + "pattern": "^[A-Za-z0-9_-]{1,12}$" + }, + "mount": { + "$ref": "#/$defs/hostPath" + } + } + } + } + } + }, + { + "type": "object", + "additionalProperties": false, + "required": [ + "action", + "hosts", + "path" + ], + "properties": { + "action": { + "const": "unmount" + }, + "hosts": { + "$ref": "#/$defs/serverList" + }, + "path": { + "$ref": "#/$defs/hostPath" + } + } } ] }, @@ -2694,7 +2784,7 @@ "description": "A path a setup or cleanup action may create or remove on a managed host: only under directories meant for local data and drop-in configuration.", "type": "string", "maxLength": 200, - "pattern": "^/(?:srv|opt|mnt|data|logs|backup|tmp|root|home|var/www|var/tmp|var/log|usr/local|etc/(?:billing\\.conf|httpd/conf\\.d|systemd|auto\\.master\\.d|auto\\.[a-z0-9_-]+|cron\\.d|exports\\.d|sudoers\\.d|profile\\.d|ssh/sshd_config\\.d|chrony\\.d|yum\\.repos\\.d|logrotate\\.d|rsyslog\\.d|security/limits\\.d|sysctl\\.d|NetworkManager/system-connections|containers|httpd/conf\\.d|firewalld/(?:services|zones)))(?:/[A-Za-z0-9_.@%+=:, -]+)*$", + "pattern": "^/(?:srv|opt|mnt|data|logs|remote|backup|tmp|root|home|var/www|var/tmp|var/log|usr/local|etc/(?:billing\\.conf|httpd/conf\\.d|systemd|auto\\.master\\.d|auto\\.[a-z0-9_-]+|cron\\.d|exports\\.d|sudoers\\.d|profile\\.d|ssh/sshd_config\\.d|chrony\\.d|yum\\.repos\\.d|logrotate\\.d|rsyslog\\.d|security/limits\\.d|sysctl\\.d|NetworkManager/system-connections|containers|httpd/conf\\.d|firewalld/(?:services|zones)))(?:/[A-Za-z0-9_.@%+=:, -]+)*$", "not": { "pattern": "(?:^|/)\\.\\.(?:/|$)" } From 20138c0b192a2787063290678ade8e9568af2b05 Mon Sep 17 00:00:00 2001 From: Ayesh Rodrigo Date: Sun, 4 Oct 2026 12:35:48 +1300 Subject: [PATCH 8/9] Guided exercise labs for chapters 21 and 22 --- .../ch21-containers/02-exercise-run.md | 18 + .../ch21-containers/04-exercise-storage.md | 26 +- .../ch21-containers/06-exercise-build.md | 29 +- .../ch21-containers/08-exercise-service.md | 32 +- .../ch22-review/02-exercise-portal.md | 18 + .../ch22-review/04-exercise-faults.md | 18 + .../ch22-review/06-exercise-recovery.md | 25 +- .../lab/sa-containers-build.yml | 116 +++++ .../rhel9-sysadmin/lab/sa-containers-run.yml | 36 ++ .../lab/sa-containers-service.yml | 135 ++++++ .../lab/sa-containers-storage.yml | 130 ++++++ .../rhel9-sysadmin/lab/sa-portal-faults.yml | 384 +++++++++++++++ .../rhel9-sysadmin/lab/sa-portal-recovery.yml | 439 ++++++++++++++++++ .../programs/rhel9-sysadmin/lab/sa-portal.yml | 354 ++++++++++++++ packages/compiler/src/lab.js | 2 +- packages/lab-tools/prepare.py | 41 +- .../schema/schemas/source/lab.schema.json | 37 +- 17 files changed, 1767 insertions(+), 73 deletions(-) create mode 100644 content/programs/rhel9-sysadmin/lab/sa-containers-build.yml create mode 100644 content/programs/rhel9-sysadmin/lab/sa-containers-run.yml create mode 100644 content/programs/rhel9-sysadmin/lab/sa-containers-service.yml create mode 100644 content/programs/rhel9-sysadmin/lab/sa-containers-storage.yml create mode 100644 content/programs/rhel9-sysadmin/lab/sa-portal-faults.yml create mode 100644 content/programs/rhel9-sysadmin/lab/sa-portal-recovery.yml create mode 100644 content/programs/rhel9-sysadmin/lab/sa-portal.yml diff --git a/content/programs/rhel9-sysadmin/chapters/ch21-containers/02-exercise-run.md b/content/programs/rhel9-sysadmin/chapters/ch21-containers/02-exercise-run.md index 682ef01..ac19eb3 100644 --- a/content/programs/rhel9-sysadmin/chapters/ch21-containers/02-exercise-run.md +++ b/content/programs/rhel9-sysadmin/chapters/ch21-containers/02-exercise-run.md @@ -12,9 +12,23 @@ Pull an image, run a one-off container and a background one, look inside, read i objectives=["ch21.containers"] id="run" title="Run and manage containers" + exercise="sa-containers-run" + ownExercise=true + guided=true + starter=false hosts=["workstation","servera"] outcomes=["Pull an image by its full name.","Run, inspect, stop and remove containers.","Use logs and ps -a to understand a container that exited."] %} + {% task id="task-366d25c96e0e" title="Start the exercise" %} + On workstation, start the exercise. It installs podman on servera and removes containers of an earlier run. + +```console +[student@workstation ~]$ lab start sa-containers-run +``` + + Later tasks assume that you are logged in to the server they name: `ssh student@servera` from workstation, then `sudo -i` for a root shell. + {% /task %} + {% task id="task-35a22b8c74d5" title="Install Podman and pull an image" %} On servera, install `podman` with sudo if it is not there. As student, pull `registry.access.redhat.com/ubi9/ubi-minimal:latest` and list your images. Then try to pull the short name `httpd`: what does Podman say? @@ -113,4 +127,8 @@ REPOSITORY TAG IMAGE ID CREATED SIZE root has its own, empty storage. Images and containers belong to the user who ran Podman. {% /reveal %} {% /task %} + + {% task id="task-c08c7862d8b4" title="Grade and finish" %} + {% lab-finish exercise="sa-containers-run" grade=true servers=true /%} + {% /task %} {% /lab %} diff --git a/content/programs/rhel9-sysadmin/chapters/ch21-containers/04-exercise-storage.md b/content/programs/rhel9-sysadmin/chapters/ch21-containers/04-exercise-storage.md index 17cae32..4ae9f74 100644 --- a/content/programs/rhel9-sysadmin/chapters/ch21-containers/04-exercise-storage.md +++ b/content/programs/rhel9-sysadmin/chapters/ch21-containers/04-exercise-storage.md @@ -12,20 +12,21 @@ Publish a web container, meet the SELinux 403 and fix it with the right mount op objectives=["ch21.storage"] id="storage" title="Ports, mounts and volumes" + exercise="sa-containers-storage" + ownExercise=true + guided=true + starter=false hosts=["workstation","servera"] outcomes=["Publish ports and recognise the two port errors.","Fix an SELinux-blocked bind mount with :Z.","Keep data in a volume and understand the user mapping."] %} - {% task id="task-3f8b9cd220d7" title="Prepare a page" %} - As student on servera pull `registry.access.redhat.com/ubi9/httpd-24:latest` (it is a 300 MB download), and create `~/web/index.html` with the line `hello from a container`. - - {% reveal title="Show solution" %} + {% task id="task-6b26c2956c7c" title="Start the exercise" %} + On workstation, start the exercise. It installs podman, pulls the web and minimal images for student on servera and creates `~/web/index.html` with the line `hello from a container`. ```console -[student@servera ~]$ podman pull registry.access.redhat.com/ubi9/httpd-24:latest | tail -1 -4e74cc90c2a4ab4fcc97976b9e382cd2b8e0711222a30206ae8089fc39b01bb9 -[student@servera ~]$ mkdir -p ~/web; echo "hello from a container" > ~/web/index.html +[student@workstation ~]$ lab start sa-containers-storage ``` - {% /reveal %} + + Later tasks assume that you are logged in to the server they name: `ssh student@servera` from workstation, then `sudo -i` for a root shell. {% /task %} {% task id="task-72cbd79c173b" title="A bind mount that fails" %} @@ -127,12 +128,7 @@ total 4 {% /reveal %} {% /task %} - {% task id="task-226a288c35e2" title="Clean up" %} - -```console -[student@servera ~]$ podman rm -f -t 1 web -web -[student@servera ~]$ rm -rf ~/data -``` + {% task id="task-b8085e8906ea" title="Grade and finish" %} + {% lab-finish exercise="sa-containers-storage" grade=true servers=true /%} {% /task %} {% /lab %} diff --git a/content/programs/rhel9-sysadmin/chapters/ch21-containers/06-exercise-build.md b/content/programs/rhel9-sysadmin/chapters/ch21-containers/06-exercise-build.md index 41f56aa..2fdf2c2 100644 --- a/content/programs/rhel9-sysadmin/chapters/ch21-containers/06-exercise-build.md +++ b/content/programs/rhel9-sysadmin/chapters/ch21-containers/06-exercise-build.md @@ -12,9 +12,23 @@ Build a small web image from a Containerfile, read a build error, run the image, objectives=["ch21.build"] id="build" title="Build and version an image" + exercise="sa-containers-build" + ownExercise=true + guided=true + starter=false hosts=["workstation","servera"] outcomes=["Write a Containerfile and build a tagged image.","Read a failed COPY step.","Run and compare two versions of an image."] %} + {% task id="task-ac932c86ca1b" title="Start the exercise" %} + On workstation, start the exercise. It installs podman on servera and pulls the base image `httpd-24` for student. + +```console +[student@workstation ~]$ lab start sa-containers-build +``` + + Later tasks assume that you are logged in to the server they name: `ssh student@servera` from workstation, then `sudo -i` for a root shell. + {% /task %} + {% task id="task-94520ca5e1dd" title="The project directory" %} As student on servera, create `~/portal` with `index.html` containing `Portal is up`. Make sure the base image `registry.access.redhat.com/ubi9/httpd-24:latest` is pulled. @@ -119,18 +133,7 @@ sha256:ea005876daf0c5ee8024c9334503c742a37ed5391ee0e32a9051540425298d76 {% /reveal %} {% /task %} - {% task id="task-dbe804b78a04" title="Clean up" %} - -```console -[student@servera portal]$ podman rm -f -t 1 portal -portal -[student@servera portal]$ podman rmi localhost/portal:latest localhost/portal:2 localhost/portal:1 -Untagged: localhost/portal:latest -Untagged: localhost/portal:2 -Untagged: localhost/portal:1 -Deleted: 65755b3c59904919657cad23be6f3e8e87d42d648de4c04cb3cc43b15efc1f88 -Deleted: 8783f7652eb1f58df91e9c935a51986cbb9e3de30809eb517a08e5e4f883b136 -[student@servera portal]$ cd; rm -rf ~/portal -``` + {% task id="task-44921bebd9b6" title="Grade and finish" %} + {% lab-finish exercise="sa-containers-build" grade=true servers=true /%} {% /task %} {% /lab %} diff --git a/content/programs/rhel9-sysadmin/chapters/ch21-containers/08-exercise-service.md b/content/programs/rhel9-sysadmin/chapters/ch21-containers/08-exercise-service.md index fa22121..5ba215d 100644 --- a/content/programs/rhel9-sysadmin/chapters/ch21-containers/08-exercise-service.md +++ b/content/programs/rhel9-sysadmin/chapters/ch21-containers/08-exercise-service.md @@ -12,22 +12,21 @@ Turn the web container into a Quadlet service, watch systemd restart it, break a objectives=["ch21.services"] id="service" title="Run a container as a service" + exercise="sa-containers-service" + ownExercise=true + guided=true + starter=false hosts=["workstation","servera","serverb"] outcomes=["Write a Quadlet file and start the generated service.","Diagnose a wrong image name and a missing SELinux label.","Enable lingering and open the firewall so the service works after a reboot."] %} - {% task id="task-579aa32f3398" title="Prepare" %} - Log in to servera as student. Pull `registry.access.redhat.com/ubi9/httpd-24:latest`, create `~/web/index.html` with `hello from a container`, and make sure no container of the earlier exercises is running. - - {% reveal title="Show solution" %} + {% task id="task-c2bda658caa9" title="Start the exercise" %} + On workstation, start the exercise. It installs podman, pulls the web image for student on servera and creates `~/web/index.html` with the line `hello from a container`. ```console -[student@workstation ~]$ ssh student@servera -[student@servera ~]$ podman pull registry.access.redhat.com/ubi9/httpd-24:latest | tail -1 -4e74cc90c2a4ab4fcc97976b9e382cd2b8e0711222a30206ae8089fc39b01bb9 -[student@servera ~]$ mkdir -p ~/web; echo "hello from a container" > ~/web/index.html -[student@servera ~]$ podman ps -a --format "{{.Names}}" +[student@workstation ~]$ lab start sa-containers-service ``` - {% /reveal %} + + Later tasks assume that you are logged in to the server they name: `ssh student@servera` from workstation, then `sudo -i` for a root shell. {% /task %} {% task id="task-04344c24e45c" title="The Quadlet file" %} @@ -103,7 +102,6 @@ NRestarts=1 {% reveal title="Show solution" %} ```console -[student@servera ~]$ systemctl --user stop web.service [student@servera ~]$ sed -i 's#httpd-24:latest#httpd-99:latest#' ~/.config/containers/systemd/web.container [student@servera ~]$ systemctl --user daemon-reload; systemctl --user start web.service Job for web.service failed because the control process exited with error code. @@ -165,15 +163,7 @@ hello from a container {% /reveal %} {% /task %} - {% task id="task-f73b4f5b322d" title="Clean up" %} - -```console -[student@servera ~]$ systemctl --user stop web.service -[student@servera ~]$ rm ~/.config/containers/systemd/web.container; systemctl --user daemon-reload -[student@servera ~]$ sudo firewall-cmd --permanent --remove-port=8080/tcp; sudo firewall-cmd --reload -success -success -[student@servera ~]$ sudo loginctl disable-linger student -``` + {% task id="task-64109f68f491" title="Grade and finish" %} + {% lab-finish exercise="sa-containers-service" grade=true servers=true /%} {% /task %} {% /lab %} diff --git a/content/programs/rhel9-sysadmin/chapters/ch22-review/02-exercise-portal.md b/content/programs/rhel9-sysadmin/chapters/ch22-review/02-exercise-portal.md index 31f0cb0..74cebaa 100644 --- a/content/programs/rhel9-sysadmin/chapters/ch22-review/02-exercise-portal.md +++ b/content/programs/rhel9-sysadmin/chapters/ch22-review/02-exercise-portal.md @@ -12,9 +12,23 @@ Deliver the status portal on servera, one layer at a time, and prove each layer objectives=["ch22.evidence"] id="portal" title="Deliver the portal" + exercise="sa-portal" + ownExercise=true + guided=true + starter=false hosts=["workstation","servera","serverb"] outcomes=["Create the account, content, ACL, labels and port label.","Limit access to one caller with a firewall zone.","Add a persistent journal and a scheduled backup, then prove it after a reboot."] %} + {% task id="task-adcd1b489fa1" title="Start the exercise" %} + On workstation, start the exercise. It removes a portal of an earlier run from servera, so you start from a plain system. + +```console +[student@workstation ~]$ lab start sa-portal +``` + + Later tasks assume that you are logged in to the server they name: `ssh student@servera` from workstation, then `sudo -i` for a root shell. + {% /task %} + {% task id="task-9e657e0b66c2" title="Identity and software" %} On servera as root create the group `portal` (GID 4800) and the account `portaladm` (UID 3001, primary group `portal`, shell `/sbin/nologin`). Install `httpd` and check both. @@ -184,4 +198,8 @@ Enforcing ``` {% /reveal %} {% /task %} + + {% task id="task-90e051ce5e15" title="Grade and finish" %} + {% lab-finish exercise="sa-portal" grade=true servers=true /%} + {% /task %} {% /lab %} diff --git a/content/programs/rhel9-sysadmin/chapters/ch22-review/04-exercise-faults.md b/content/programs/rhel9-sysadmin/chapters/ch22-review/04-exercise-faults.md index bb9213c..bd73dfe 100644 --- a/content/programs/rhel9-sysadmin/chapters/ch22-review/04-exercise-faults.md +++ b/content/programs/rhel9-sysadmin/chapters/ch22-review/04-exercise-faults.md @@ -12,9 +12,23 @@ The portal from the previous exercise works. You will break it six ways, one at objectives=["ch22.faults"] id="faults" title="Find six faults" + exercise="sa-portal-faults" + ownExercise=true + guided=true + starter=false hosts=["workstation","servera","serverb"] outcomes=["Map each symptom to one layer.","Repair with one change and repeat the failing request.","Keep the other requirements intact."] %} + {% task id="task-956de9dc6ae5" title="Start the exercise" %} + On workstation, start the exercise. It builds the finished portal of the previous exercise on servera: account, content with ACL, labels, httpd on port 8090 and the firewall zone for serverb. + +```console +[student@workstation ~]$ lab start sa-portal-faults +``` + + Later tasks assume that you are logged in to the server they name: `ssh student@servera` from workstation, then `sudo -i` for a root shell. + {% /task %} + {% task id="task-671ba214e616" title="The test request" %} Make the portal from the previous exercise work again if needed. Then define your test: on serverb, `curl -sS -m 3 -o /dev/null -w "HTTP %{http_code}\n" http://servera:8090/` must print `HTTP 200`. @@ -151,4 +165,8 @@ HTTP 200 Every layer works and the status code is 200. Only a check of the content (or a comparison with the backup) notices. The next exercise restores it. {% /reveal %} {% /task %} + + {% task id="task-4ee76f1fbd21" title="Grade and finish" %} + {% lab-finish exercise="sa-portal-faults" grade=true servers=true /%} + {% /task %} {% /lab %} diff --git a/content/programs/rhel9-sysadmin/chapters/ch22-review/06-exercise-recovery.md b/content/programs/rhel9-sysadmin/chapters/ch22-review/06-exercise-recovery.md index 41453b9..41f5409 100644 --- a/content/programs/rhel9-sysadmin/chapters/ch22-review/06-exercise-recovery.md +++ b/content/programs/rhel9-sysadmin/chapters/ch22-review/06-exercise-recovery.md @@ -12,24 +12,21 @@ Use the backup from the build to repair the wrong-content fault, see what a care objectives=["ch22.handover"] id="recovery" title="Restore and hand over" + exercise="sa-portal-recovery" + ownExercise=true + guided=true + starter=false hosts=["workstation","servera","serverb"] outcomes=["Restore into a staging directory and replace only the bad file.","Show what a restore without ACLs and labels loses.","Write the handover note."] %} - {% task id="task-616e2ce1f293" title="A fresh backup" %} - The portal must be healthy again (repair the last fault of the previous exercise if you stopped there). Run the backup service and note the archive checksum. - - {% reveal title="Show solution" %} + {% task id="task-1c1640dedc53" title="Start the exercise" %} + On workstation, start the exercise. It builds the finished portal on servera, with the daily backup service and timer from the build exercise, and runs the backup once. ```console -[root@servera ~]# echo "Portal status: OK" > /srv/portal/index.html -[root@servera ~]# systemctl start portal-backup.service -[root@servera ~]# sha256sum /srv/backups/portal.tar.gz | cut -c1-20 -2e46d2621750dc98c8fd -[root@servera ~]# tar -tzf /srv/backups/portal.tar.gz -portal/ -portal/index.html +[student@workstation ~]$ lab start sa-portal-recovery ``` - {% /reveal %} + + Later tasks assume that you are logged in to the server they name: `ssh student@servera` from workstation, then `sudo -i` for a root shell. {% /task %} {% task id="task-ed6fdaccaf4f" title="Damage the content" %} @@ -105,4 +102,8 @@ EOT ``` {% /reveal %} {% /task %} + + {% task id="task-74facf7bb787" title="Grade and finish" %} + {% lab-finish exercise="sa-portal-recovery" grade=true servers=true /%} + {% /task %} {% /lab %} diff --git a/content/programs/rhel9-sysadmin/lab/sa-containers-build.yml b/content/programs/rhel9-sysadmin/lab/sa-containers-build.yml new file mode 100644 index 0000000..0106a0b --- /dev/null +++ b/content/programs/rhel9-sysadmin/lab/sa-containers-build.yml @@ -0,0 +1,116 @@ +name: sa-containers-build +title: Build and version an image +page: ch21/exercise-build +transport: ssh +version: 1 +starter: [] +setup: +- action: package + hosts: + - servera + names: + - podman + state: present +- action: container-reset + hosts: + - servera + user: student + keepImages: true +- action: directory + hosts: + - servera + path: /home/student/portal + state: absent +- action: run-as + hosts: + - servera + user: student + tool: podman-pull + image: registry.access.redhat.com/ubi9/httpd-24:latest +checkpoints: + final: + files: [] + checks: + - id: v1 + kind: container + 'on': servera.lab.example.com + targets: + - servera.lab.example.com + message: the image localhost/portal:1 exists + user: student + image: localhost/portal:1 + - id: v2 + kind: container + 'on': servera.lab.example.com + targets: + - servera.lab.example.com + message: the image localhost/portal:2 exists + user: student + image: localhost/portal:2 + - id: latest + kind: container + 'on': servera.lab.example.com + targets: + - servera.lab.example.com + message: the image localhost/portal:latest exists + user: student + image: localhost/portal:latest + - id: running + kind: container + 'on': servera.lab.example.com + targets: + - servera.lab.example.com + message: the portal container runs from the latest image + user: student + running: portal + - id: containerfile + kind: file + 'on': servera.lab.example.com + targets: + - servera.lab.example.com + message: the Containerfile copies the page + paths: + - /home/student/portal/Containerfile + contains: + - FROM registry.access.redhat.com/ubi9/httpd-24 + - COPY index.html + - id: page + kind: http + 'on': servera.lab.example.com + targets: + - servera.lab.example.com + message: version 2 is served on port 8081 + url: http://localhost:8081/ + containsAny: + - Portal v2 +finish: +- action: container-reset + hosts: + - servera + user: student + keepImages: true +- action: linger + hosts: + - servera + user: student + state: absent +- action: firewall + hosts: + - servera + state: absent + port: 8080/tcp +- action: directory + hosts: + - servera + path: /home/student/web + state: absent +- action: directory + hosts: + - servera + path: /home/student/data + state: absent +- action: directory + hosts: + - servera + path: /home/student/portal + state: absent diff --git a/content/programs/rhel9-sysadmin/lab/sa-containers-run.yml b/content/programs/rhel9-sysadmin/lab/sa-containers-run.yml new file mode 100644 index 0000000..4d07ede --- /dev/null +++ b/content/programs/rhel9-sysadmin/lab/sa-containers-run.yml @@ -0,0 +1,36 @@ +name: sa-containers-run +title: Run and manage containers +page: ch21/exercise-run +transport: ssh +version: 1 +starter: [] +setup: +- action: package + hosts: + - servera + names: + - podman + state: present +- action: container-reset + hosts: + - servera + user: student + keepImages: true +checkpoints: + final: + files: [] + checks: + - id: image + kind: container + 'on': servera.lab.example.com + targets: + - servera.lab.example.com + message: student has the ubi-minimal image + user: student + image: registry.access.redhat.com/ubi9/ubi-minimal:latest +finish: +- action: container-reset + hosts: + - servera + user: student + keepImages: true diff --git a/content/programs/rhel9-sysadmin/lab/sa-containers-service.yml b/content/programs/rhel9-sysadmin/lab/sa-containers-service.yml new file mode 100644 index 0000000..3eb9b6e --- /dev/null +++ b/content/programs/rhel9-sysadmin/lab/sa-containers-service.yml @@ -0,0 +1,135 @@ +name: sa-containers-service +title: Run a container as a service +page: ch21/exercise-service +transport: ssh +version: 1 +starter: [] +setup: +- action: package + hosts: + - servera + names: + - podman + state: present +- action: container-reset + hosts: + - servera + user: student + keepImages: true +- action: linger + hosts: + - servera + user: student + state: absent +- action: directory + hosts: + - servera + path: /home/student/web + state: absent +- action: run-as + hosts: + - servera + user: student + tool: podman-pull + image: registry.access.redhat.com/ubi9/httpd-24:latest +- action: directory + hosts: + - servera + path: /home/student/web + state: present + owner: student + group: student + mode: '755' +- action: file + hosts: + - servera + path: /home/student/web/index.html + state: present + content: 'hello from a container + + ' + owner: student + group: student + mode: '644' +checkpoints: + final: + files: [] + checks: + - id: quadlet + kind: file + 'on': servera.lab.example.com + targets: + - servera.lab.example.com + message: the Quadlet file defines the container and its restart policy + paths: + - /home/student/.config/containers/systemd/web.container + contains: + - Image=registry.access.redhat.com/ubi9/httpd-24 + - PublishPort=8080:8080 + - Restart=always + - WantedBy=default.target + - ',Z' + - id: running + kind: container + 'on': servera.lab.example.com + targets: + - servera.lab.example.com + message: web.service is active and systemd-web runs + user: student + unit: web.service + running: systemd-web + - id: linger + kind: container + 'on': servera.lab.example.com + targets: + - servera.lab.example.com + message: lingering is enabled for student + user: student + linger: true + - id: firewall + kind: firewall + 'on': servera.lab.example.com + targets: + - servera.lab.example.com + message: port 8080/tcp is open permanently + port: 8080/tcp + - id: remote + kind: http + 'on': serverb.lab.example.com + targets: + - serverb.lab.example.com + message: serverb reaches the container on servera + url: http://servera.lab.example.com:8080/ + containsAny: + - hello from a container +finish: +- action: container-reset + hosts: + - servera + user: student + keepImages: true +- action: linger + hosts: + - servera + user: student + state: absent +- action: firewall + hosts: + - servera + state: absent + port: 8080/tcp +- action: directory + hosts: + - servera + path: /home/student/web + state: absent +- action: directory + hosts: + - servera + path: /home/student/data + state: absent +- action: directory + hosts: + - servera + path: /home/student/portal + state: absent diff --git a/content/programs/rhel9-sysadmin/lab/sa-containers-storage.yml b/content/programs/rhel9-sysadmin/lab/sa-containers-storage.yml new file mode 100644 index 0000000..336ab9f --- /dev/null +++ b/content/programs/rhel9-sysadmin/lab/sa-containers-storage.yml @@ -0,0 +1,130 @@ +name: sa-containers-storage +title: Ports, mounts and volumes +page: ch21/exercise-storage +transport: ssh +version: 1 +starter: [] +setup: +- action: package + hosts: + - servera + names: + - podman + state: present +- action: container-reset + hosts: + - servera + user: student + keepImages: true +- action: directory + hosts: + - servera + path: /home/student/web + state: absent +- action: directory + hosts: + - servera + path: /home/student/data + state: absent +- action: run-as + hosts: + - servera + user: student + tool: podman-pull + image: registry.access.redhat.com/ubi9/httpd-24:latest +- action: run-as + hosts: + - servera + user: student + tool: podman-pull + image: registry.access.redhat.com/ubi9/ubi-minimal:latest +- action: directory + hosts: + - servera + path: /home/student/web + state: present + owner: student + group: student + mode: '755' +- action: file + hosts: + - servera + path: /home/student/web/index.html + state: present + content: 'hello from a container + + ' + owner: student + group: student + mode: '644' +checkpoints: + final: + files: [] + checks: + - id: web + kind: container + 'on': servera.lab.example.com + targets: + - servera.lab.example.com + message: the web container is running + user: student + running: web + - id: label + kind: file + 'on': servera.lab.example.com + targets: + - servera.lab.example.com + message: ~/web carries the private container label + paths: + - /home/student/web + selinuxType: container_file_t + - id: page + kind: http + 'on': servera.lab.example.com + targets: + - servera.lab.example.com + message: the page is served on port 8080 + url: http://localhost:8080/ + containsAny: + - hello from a container + - id: data + kind: file + 'on': servera.lab.example.com + targets: + - servera.lab.example.com + message: the container wrote ~/data/a.txt + paths: + - /home/student/data/a.txt + contains: + - made +finish: +- action: container-reset + hosts: + - servera + user: student + keepImages: true +- action: linger + hosts: + - servera + user: student + state: absent +- action: firewall + hosts: + - servera + state: absent + port: 8080/tcp +- action: directory + hosts: + - servera + path: /home/student/web + state: absent +- action: directory + hosts: + - servera + path: /home/student/data + state: absent +- action: directory + hosts: + - servera + path: /home/student/portal + state: absent diff --git a/content/programs/rhel9-sysadmin/lab/sa-portal-faults.yml b/content/programs/rhel9-sysadmin/lab/sa-portal-faults.yml new file mode 100644 index 0000000..df61cd7 --- /dev/null +++ b/content/programs/rhel9-sysadmin/lab/sa-portal-faults.yml @@ -0,0 +1,384 @@ +name: sa-portal-faults +title: Find six faults +page: ch22/exercise-faults +transport: ssh +version: 1 +starter: [] +setup: +- action: service + hosts: + - servera + names: + - portal-backup.timer + state: stopped + enabled: false +- action: service + hosts: + - servera + names: + - httpd + state: stopped + enabled: false +- action: firewall + hosts: + - servera + state: absent + zone: portal + deleteZone: true +- action: selinux + hosts: + - servera + kind: port + state: absent + type: http_port_t + proto: tcp + port: 8090 +- action: selinux + hosts: + - servera + kind: fcontext + state: absent + path: /srv/portal(/.*)? +- action: directory + hosts: + - servera + path: /srv/portal + state: absent +- action: directory + hosts: + - servera + path: /srv/backups + state: absent +- action: directory + hosts: + - servera + path: /root/stage + state: absent +- action: directory + hosts: + - servera + path: /root/bad + state: absent +- action: file + hosts: + - servera + path: /etc/httpd/conf.d/portal.conf + state: absent +- action: file + hosts: + - servera + path: /etc/systemd/system/portal-backup.service + state: absent +- action: file + hosts: + - servera + path: /etc/systemd/system/portal-backup.timer + state: absent +- action: file + hosts: + - servera + path: /etc/systemd/journald.conf.d/10-persistent.conf + state: absent +- action: file + hosts: + - servera + path: /root/portal-handover.txt + state: absent +- action: systemd + hosts: + - servera + reload: true +- action: user + hosts: + - servera + name: portaladm + state: absent +- action: group + hosts: + - servera + name: portal + state: absent +- action: package + hosts: + - servera + names: + - httpd + state: absent +- action: directory + hosts: + - servera + path: /var/log/journal + state: absent +- action: service + hosts: + - servera + names: + - systemd-journald + state: restarted +- action: group + hosts: + - servera + name: portal + state: present + gid: 4800 +- action: user + hosts: + - servera + name: portaladm + state: present + uid: 3001 + group: portal + shell: /sbin/nologin +- action: package + hosts: + - servera + names: + - httpd + state: present +- action: selinux + hosts: + - servera + kind: port + state: present + type: http_port_t + proto: tcp + port: 8090 +- action: selinux + hosts: + - servera + kind: fcontext + state: present + type: httpd_sys_content_t + path: /srv/portal(/.*)? +- action: directory + hosts: + - servera + path: /srv/portal + state: present + owner: portaladm + group: portal + mode: '2750' +- action: file + hosts: + - servera + path: /srv/portal/index.html + state: present + content: 'Portal status: OK + + ' + owner: portaladm + group: portal + mode: '640' +- action: acl + hosts: + - servera + path: /srv/portal + entries: + - u:apache:rX + - d:u:apache:rX +- action: acl + hosts: + - servera + path: /srv/portal/index.html + entries: + - u:apache:r +- action: file + hosts: + - servera + path: /etc/httpd/conf.d/portal.conf + state: present + content: "Listen 8090\n\n DocumentRoot /srv/portal\n \n Require all granted\n \n\n" +- action: service + hosts: + - servera + names: + - httpd + state: started + enabled: true +- action: firewall + hosts: + - servera + state: present + zone: portal + deleteZone: true +- action: firewall + hosts: + - servera + state: present + zone: portal + source: '@serverb' +- action: firewall + hosts: + - servera + state: present + zone: portal + port: 8090/tcp +checkpoints: + final: + files: [] + checks: + - id: svc + kind: service + 'on': servera.lab.example.com + targets: + - servera.lab.example.com + message: httpd is running + names: + - httpd.service + active: true + - id: label + kind: file + 'on': servera.lab.example.com + targets: + - servera.lab.example.com + message: the page carries the web content type again + paths: + - /srv/portal/index.html + selinuxType: httpd_sys_content_t + - id: acl + kind: file + 'on': servera.lab.example.com + targets: + - servera.lab.example.com + message: the ACL for apache is back + paths: + - /srv/portal/index.html + acl: + - user:apache:r-- + - id: port + kind: selinux-port + 'on': servera.lab.example.com + targets: + - servera.lab.example.com + message: TCP port 8090 is labelled http_port_t again + type: http_port_t + proto: tcp + port: 8090 + - id: zone + kind: firewall + 'on': servera.lab.example.com + targets: + - servera.lab.example.com + message: the zone portal allows 8090/tcp again + zone: portal + port: 8090/tcp + - id: content + kind: file + 'on': servera.lab.example.com + targets: + - servera.lab.example.com + message: the page says DOWN (fault 6, repaired in the next exercise) + paths: + - /srv/portal/index.html + line: 'Portal status: DOWN' +finish: +- action: service + hosts: + - servera + names: + - portal-backup.timer + state: stopped + enabled: false +- action: service + hosts: + - servera + names: + - httpd + state: stopped + enabled: false +- action: firewall + hosts: + - servera + state: absent + zone: portal + deleteZone: true +- action: selinux + hosts: + - servera + kind: port + state: absent + type: http_port_t + proto: tcp + port: 8090 +- action: selinux + hosts: + - servera + kind: fcontext + state: absent + path: /srv/portal(/.*)? +- action: directory + hosts: + - servera + path: /srv/portal + state: absent +- action: directory + hosts: + - servera + path: /srv/backups + state: absent +- action: directory + hosts: + - servera + path: /root/stage + state: absent +- action: directory + hosts: + - servera + path: /root/bad + state: absent +- action: file + hosts: + - servera + path: /etc/httpd/conf.d/portal.conf + state: absent +- action: file + hosts: + - servera + path: /etc/systemd/system/portal-backup.service + state: absent +- action: file + hosts: + - servera + path: /etc/systemd/system/portal-backup.timer + state: absent +- action: file + hosts: + - servera + path: /etc/systemd/journald.conf.d/10-persistent.conf + state: absent +- action: file + hosts: + - servera + path: /root/portal-handover.txt + state: absent +- action: systemd + hosts: + - servera + reload: true +- action: user + hosts: + - servera + name: portaladm + state: absent +- action: group + hosts: + - servera + name: portal + state: absent +- action: package + hosts: + - servera + names: + - httpd + state: absent +- action: directory + hosts: + - servera + path: /var/log/journal + state: absent +- action: service + hosts: + - servera + names: + - systemd-journald + state: restarted diff --git a/content/programs/rhel9-sysadmin/lab/sa-portal-recovery.yml b/content/programs/rhel9-sysadmin/lab/sa-portal-recovery.yml new file mode 100644 index 0000000..1ea5c1e --- /dev/null +++ b/content/programs/rhel9-sysadmin/lab/sa-portal-recovery.yml @@ -0,0 +1,439 @@ +name: sa-portal-recovery +title: Restore and hand over +page: ch22/exercise-recovery +transport: ssh +version: 1 +starter: [] +setup: +- action: service + hosts: + - servera + names: + - portal-backup.timer + state: stopped + enabled: false +- action: service + hosts: + - servera + names: + - httpd + state: stopped + enabled: false +- action: firewall + hosts: + - servera + state: absent + zone: portal + deleteZone: true +- action: selinux + hosts: + - servera + kind: port + state: absent + type: http_port_t + proto: tcp + port: 8090 +- action: selinux + hosts: + - servera + kind: fcontext + state: absent + path: /srv/portal(/.*)? +- action: directory + hosts: + - servera + path: /srv/portal + state: absent +- action: directory + hosts: + - servera + path: /srv/backups + state: absent +- action: directory + hosts: + - servera + path: /root/stage + state: absent +- action: directory + hosts: + - servera + path: /root/bad + state: absent +- action: file + hosts: + - servera + path: /etc/httpd/conf.d/portal.conf + state: absent +- action: file + hosts: + - servera + path: /etc/systemd/system/portal-backup.service + state: absent +- action: file + hosts: + - servera + path: /etc/systemd/system/portal-backup.timer + state: absent +- action: file + hosts: + - servera + path: /etc/systemd/journald.conf.d/10-persistent.conf + state: absent +- action: file + hosts: + - servera + path: /root/portal-handover.txt + state: absent +- action: systemd + hosts: + - servera + reload: true +- action: user + hosts: + - servera + name: portaladm + state: absent +- action: group + hosts: + - servera + name: portal + state: absent +- action: package + hosts: + - servera + names: + - httpd + state: absent +- action: directory + hosts: + - servera + path: /var/log/journal + state: absent +- action: service + hosts: + - servera + names: + - systemd-journald + state: restarted +- action: group + hosts: + - servera + name: portal + state: present + gid: 4800 +- action: user + hosts: + - servera + name: portaladm + state: present + uid: 3001 + group: portal + shell: /sbin/nologin +- action: package + hosts: + - servera + names: + - httpd + state: present +- action: selinux + hosts: + - servera + kind: port + state: present + type: http_port_t + proto: tcp + port: 8090 +- action: selinux + hosts: + - servera + kind: fcontext + state: present + type: httpd_sys_content_t + path: /srv/portal(/.*)? +- action: directory + hosts: + - servera + path: /srv/portal + state: present + owner: portaladm + group: portal + mode: '2750' +- action: file + hosts: + - servera + path: /srv/portal/index.html + state: present + content: 'Portal status: OK + + ' + owner: portaladm + group: portal + mode: '640' +- action: acl + hosts: + - servera + path: /srv/portal + entries: + - u:apache:rX + - d:u:apache:rX +- action: acl + hosts: + - servera + path: /srv/portal/index.html + entries: + - u:apache:r +- action: file + hosts: + - servera + path: /etc/httpd/conf.d/portal.conf + state: present + content: "Listen 8090\n\n DocumentRoot /srv/portal\n \n Require all granted\n \n\n" +- action: service + hosts: + - servera + names: + - httpd + state: started + enabled: true +- action: firewall + hosts: + - servera + state: present + zone: portal + deleteZone: true +- action: firewall + hosts: + - servera + state: present + zone: portal + source: '@serverb' +- action: firewall + hosts: + - servera + state: present + zone: portal + port: 8090/tcp +- action: directory + hosts: + - servera + path: /srv/backups + state: present + mode: '750' +- action: file + hosts: + - servera + path: /etc/systemd/system/portal-backup.service + state: present + content: '[Unit] + + Description=Back up the portal content + + + [Service] + + Type=oneshot + + ExecStart=/usr/bin/tar --acls --selinux -czf /srv/backups/portal.tar.gz -C /srv portal + + ' +- action: file + hosts: + - servera + path: /etc/systemd/system/portal-backup.timer + state: present + content: '[Unit] + + Description=Daily portal backup + + + [Timer] + + OnCalendar=daily + + Persistent=true + + + [Install] + + WantedBy=timers.target + + ' +- action: systemd + hosts: + - servera + reload: true +- action: service + hosts: + - servera + names: + - portal-backup.timer + state: started + enabled: true +- action: service + hosts: + - servera + names: + - portal-backup.service + state: started +checkpoints: + final: + files: [] + checks: + - id: page + kind: file + 'on': servera.lab.example.com + targets: + - servera.lab.example.com + message: the live page holds the restored content + paths: + - /srv/portal/index.html + line: 'Portal status: OK' + owner: portaladm + acl: + - user:apache:r-- + selinuxType: httpd_sys_content_t + - id: stage + kind: file + 'on': servera.lab.example.com + targets: + - servera.lab.example.com + message: the staging copy keeps its ACL + paths: + - /root/stage/portal/index.html + acl: + - user:apache:r-- + - id: handover + kind: file + 'on': servera.lab.example.com + targets: + - servera.lab.example.com + message: the handover note has its sections + paths: + - /root/portal-handover.txt + contains: + - Purpose + - Changes + - Operate + - Recover + - Known failures + - Limits + - id: remote + kind: http + 'on': serverb.lab.example.com + targets: + - serverb.lab.example.com + message: serverb gets the restored page + url: http://servera.lab.example.com:8090/ + containsAny: + - 'Portal status: OK' +finish: +- action: service + hosts: + - servera + names: + - portal-backup.timer + state: stopped + enabled: false +- action: service + hosts: + - servera + names: + - httpd + state: stopped + enabled: false +- action: firewall + hosts: + - servera + state: absent + zone: portal + deleteZone: true +- action: selinux + hosts: + - servera + kind: port + state: absent + type: http_port_t + proto: tcp + port: 8090 +- action: selinux + hosts: + - servera + kind: fcontext + state: absent + path: /srv/portal(/.*)? +- action: directory + hosts: + - servera + path: /srv/portal + state: absent +- action: directory + hosts: + - servera + path: /srv/backups + state: absent +- action: directory + hosts: + - servera + path: /root/stage + state: absent +- action: directory + hosts: + - servera + path: /root/bad + state: absent +- action: file + hosts: + - servera + path: /etc/httpd/conf.d/portal.conf + state: absent +- action: file + hosts: + - servera + path: /etc/systemd/system/portal-backup.service + state: absent +- action: file + hosts: + - servera + path: /etc/systemd/system/portal-backup.timer + state: absent +- action: file + hosts: + - servera + path: /etc/systemd/journald.conf.d/10-persistent.conf + state: absent +- action: file + hosts: + - servera + path: /root/portal-handover.txt + state: absent +- action: systemd + hosts: + - servera + reload: true +- action: user + hosts: + - servera + name: portaladm + state: absent +- action: group + hosts: + - servera + name: portal + state: absent +- action: package + hosts: + - servera + names: + - httpd + state: absent +- action: directory + hosts: + - servera + path: /var/log/journal + state: absent +- action: service + hosts: + - servera + names: + - systemd-journald + state: restarted diff --git a/content/programs/rhel9-sysadmin/lab/sa-portal.yml b/content/programs/rhel9-sysadmin/lab/sa-portal.yml new file mode 100644 index 0000000..de8db84 --- /dev/null +++ b/content/programs/rhel9-sysadmin/lab/sa-portal.yml @@ -0,0 +1,354 @@ +name: sa-portal +title: Deliver the portal +page: ch22/exercise-portal +transport: ssh +version: 1 +starter: [] +setup: +- action: service + hosts: + - servera + names: + - portal-backup.timer + state: stopped + enabled: false +- action: service + hosts: + - servera + names: + - httpd + state: stopped + enabled: false +- action: firewall + hosts: + - servera + state: absent + zone: portal + deleteZone: true +- action: selinux + hosts: + - servera + kind: port + state: absent + type: http_port_t + proto: tcp + port: 8090 +- action: selinux + hosts: + - servera + kind: fcontext + state: absent + path: /srv/portal(/.*)? +- action: directory + hosts: + - servera + path: /srv/portal + state: absent +- action: directory + hosts: + - servera + path: /srv/backups + state: absent +- action: directory + hosts: + - servera + path: /root/stage + state: absent +- action: directory + hosts: + - servera + path: /root/bad + state: absent +- action: file + hosts: + - servera + path: /etc/httpd/conf.d/portal.conf + state: absent +- action: file + hosts: + - servera + path: /etc/systemd/system/portal-backup.service + state: absent +- action: file + hosts: + - servera + path: /etc/systemd/system/portal-backup.timer + state: absent +- action: file + hosts: + - servera + path: /etc/systemd/journald.conf.d/10-persistent.conf + state: absent +- action: file + hosts: + - servera + path: /root/portal-handover.txt + state: absent +- action: systemd + hosts: + - servera + reload: true +- action: user + hosts: + - servera + name: portaladm + state: absent +- action: group + hosts: + - servera + name: portal + state: absent +- action: package + hosts: + - servera + names: + - httpd + state: absent +- action: directory + hosts: + - servera + path: /var/log/journal + state: absent +- action: service + hosts: + - servera + names: + - systemd-journald + state: restarted +checkpoints: + final: + files: [] + checks: + - id: user + kind: user + 'on': servera.lab.example.com + targets: + - servera.lab.example.com + message: portaladm (UID 3001) is a nologin account in the group portal + names: + - portaladm + uid: 3001 + primaryGroup: portal + shell: /sbin/nologin + - id: dir + kind: file + 'on': servera.lab.example.com + targets: + - servera.lab.example.com + message: /srv/portal belongs to portaladm:portal with mode 2750 + paths: + - /srv/portal + fileType: directory + owner: portaladm + group: portal + mode: '2750' + - id: acl + kind: file + 'on': servera.lab.example.com + targets: + - servera.lab.example.com + message: apache may read the page through an ACL + paths: + - /srv/portal/index.html + acl: + - user:apache:r-- + selinuxType: httpd_sys_content_t + - id: fcontext + kind: selinux-fcontext + 'on': servera.lab.example.com + targets: + - servera.lab.example.com + message: a stored mapping labels /srv/portal for the web server + path: /srv/portal(/.*)? + type: httpd_sys_content_t + - id: port + kind: selinux-port + 'on': servera.lab.example.com + targets: + - servera.lab.example.com + message: TCP port 8090 is labelled http_port_t + type: http_port_t + proto: tcp + port: 8090 + - id: enforcing + kind: selinux + 'on': servera.lab.example.com + targets: + - servera.lab.example.com + message: SELinux is enforcing + mode: enforcing + - id: zone + kind: firewall + 'on': servera.lab.example.com + targets: + - servera.lab.example.com + message: the zone portal allows 8090/tcp + zone: portal + port: 8090/tcp + - id: zone-file + kind: file + 'on': servera.lab.example.com + targets: + - servera.lab.example.com + message: the zone portal has a source address + paths: + - /etc/firewalld/zones/portal.xml + matches: + - id: journal + kind: file + 'on': servera.lab.example.com + targets: + - servera.lab.example.com + message: the journal is persistent + paths: + - /var/log/journal + fileType: directory + - id: timer + kind: service + 'on': servera.lab.example.com + targets: + - servera.lab.example.com + message: portal-backup.timer is enabled and running + names: + - portal-backup.timer + active: true + enabled: true + - id: archive + kind: archive + 'on': servera.lab.example.com + targets: + - servera.lab.example.com + message: the backup holds the portal page + path: /srv/backups/portal.tar.gz + format: tar.gz + members: + - portal/index.html + - id: local + kind: http + 'on': servera.lab.example.com + targets: + - servera.lab.example.com + message: the portal answers on servera + url: http://localhost:8090/ + containsAny: + - 'Portal status: OK' + - id: remote + kind: http + 'on': serverb.lab.example.com + targets: + - serverb.lab.example.com + message: serverb reaches the portal + url: http://servera.lab.example.com:8090/ + containsAny: + - 'Portal status: OK' +finish: +- action: service + hosts: + - servera + names: + - portal-backup.timer + state: stopped + enabled: false +- action: service + hosts: + - servera + names: + - httpd + state: stopped + enabled: false +- action: firewall + hosts: + - servera + state: absent + zone: portal + deleteZone: true +- action: selinux + hosts: + - servera + kind: port + state: absent + type: http_port_t + proto: tcp + port: 8090 +- action: selinux + hosts: + - servera + kind: fcontext + state: absent + path: /srv/portal(/.*)? +- action: directory + hosts: + - servera + path: /srv/portal + state: absent +- action: directory + hosts: + - servera + path: /srv/backups + state: absent +- action: directory + hosts: + - servera + path: /root/stage + state: absent +- action: directory + hosts: + - servera + path: /root/bad + state: absent +- action: file + hosts: + - servera + path: /etc/httpd/conf.d/portal.conf + state: absent +- action: file + hosts: + - servera + path: /etc/systemd/system/portal-backup.service + state: absent +- action: file + hosts: + - servera + path: /etc/systemd/system/portal-backup.timer + state: absent +- action: file + hosts: + - servera + path: /etc/systemd/journald.conf.d/10-persistent.conf + state: absent +- action: file + hosts: + - servera + path: /root/portal-handover.txt + state: absent +- action: systemd + hosts: + - servera + reload: true +- action: user + hosts: + - servera + name: portaladm + state: absent +- action: group + hosts: + - servera + name: portal + state: absent +- action: package + hosts: + - servera + names: + - httpd + state: absent +- action: directory + hosts: + - servera + path: /var/log/journal + state: absent +- action: service + hosts: + - servera + names: + - systemd-journald + state: restarted diff --git a/packages/compiler/src/lab.js b/packages/compiler/src/lab.js index add6911..e3029f9 100644 --- a/packages/compiler/src/lab.js +++ b/packages/compiler/src/lab.js @@ -32,7 +32,7 @@ const CONTROL_ONLY = new Set(['git', 'lint']); // Setup and cleanup actions that run as root on the lab servers; the rest work on the workstation. const HOST_ACTIONS = new Set([ 'package', 'service', 'group', 'user', 'directory', 'file', 'remove-lines', 'firewall', 'selinux', 'wipe-disk', 'systemd', 'linger', - 'container-reset', 'run-as', 'restore-skel', 'boot', 'timezone', 'nm-connection', 'hostname', 'http-server', 'dnf-module', 'crontab', 'partition-disk', 'format', 'append-line', 'mount-all', 'lvm-build', 'unmount', + 'container-reset', 'run-as', 'restore-skel', 'boot', 'timezone', 'nm-connection', 'hostname', 'http-server', 'dnf-module', 'crontab', 'partition-disk', 'format', 'append-line', 'mount-all', 'lvm-build', 'unmount', 'acl', ]); /** Every file under `dir` as paths relative to it, sorted. */ diff --git a/packages/lab-tools/prepare.py b/packages/lab-tools/prepare.py index bca3547..0090521 100644 --- a/packages/lab-tools/prepare.py +++ b/packages/lab-tools/prepare.py @@ -300,7 +300,7 @@ def owner_args(a, path): def label_args(a, path): if 'selinuxType' not in a: - return [] + return ['restorecon -R %s 2>/dev/null || true' % path] if not re.match(r'^[a-z_]+_t$', a['selinuxType']): raise SetupError('unsafe SELinux type') return ['chcon -t %s %s' % (q(a['selinuxType']), path)] @@ -339,25 +339,29 @@ def script_firewall(a): out = [] if a.get('deleteZone'): z = q(name(a['zone'])) - out.append(('%s --new-zone=%s' % (fc, z)) if present else ('%s --delete-zone=%s' % (fc, z))) + out.append(('%s --new-zone=%s 2>/dev/null || true' % (fc, z)) if present else ('%s --delete-zone=%s 2>/dev/null || true' % (fc, z))) elif a.get('deleteService'): s = q(a['service']) - out.append(('%s --new-service=%s' % (fc, s)) if present else ('%s --delete-service=%s' % (fc, s))) + out.append(('%s --new-service=%s 2>/dev/null || true' % (fc, s)) if present else ('%s --delete-service=%s 2>/dev/null || true' % (fc, s))) else: verb = 'add' if present else 'remove' for key, option in (('service', 'service'), ('port', 'port'), ('source', 'source'), ('forwardPort', 'forward-port')): if key in a: - out.append('%s%s --%s-%s=%s' % (fc, zone, verb, option, q(a[key]))) + value = q(a[key]) + if key == 'source' and re.match(r'^@server[a-d]$', a[key]): + value = '"$(getent ahostsv4 %s.lab.example.com | awk \'NR==1{print $1}\')"' % a[key][1:] + out.append('%s%s --%s-%s=%s%s' % (fc, zone, verb, option, value, '' if present else ' >/dev/null 2>&1 || true')) out.append('firewall-cmd --reload >/dev/null') return '\n'.join(out) def script_selinux(a): present = a.get('state', 'present') == 'present' + tail = '' if present else ' >/dev/null 2>&1 || true' if a['kind'] == 'port': - return 'semanage port %s -t %s -p %s %d' % ('-a' if present else '-d', q(a['type']), a['proto'], a['port']) + return 'semanage port %s -t %s -p %s %d%s' % ('-a' if present else '-d', q(a['type']), a['proto'], a['port'], tail) if a['kind'] == 'fcontext': - return 'semanage fcontext %s -t %s %s' % ('-a' if present else '-d', q(a['type']), q(a['path'])) if present else 'semanage fcontext -d %s' % q(a['path']) + return ('semanage fcontext -a -t %s %s' % (q(a['type']), q(a['path']))) if present else ('semanage fcontext -d %s%s' % (q(a['path']), tail)) return 'setsebool -P %s %s' % (q(a['name']), 'on' if a.get('value', True) else 'off') @@ -386,8 +390,14 @@ def script_linger(a): def script_container_reset(a): user = q(name(a['user'])) - return ('id %(u)s >/dev/null 2>&1 && { runuser -l %(u)s -c "systemctl --user stop \'*.service\' 2>/dev/null; podman system reset -f" >/dev/null 2>&1; ' - 'rm -rf /home/%(n)s/.config/containers/systemd; }; true') % {'u': user, 'n': name(a['user'])} + n = name(a['user']) + if a.get('keepImages'): + inner = ("systemctl --user stop '*.service' 2>/dev/null; podman rm -af -t 1; podman volume rm -af; " + "podman images --format '{{.Repository}}:{{.Tag}}' | grep '^localhost/' | xargs -r podman rmi -f") + else: + inner = "systemctl --user stop '*.service' 2>/dev/null; podman system reset -f" + return ('id %(u)s >/dev/null 2>&1 && { runuser -l %(u)s -c %(c)s >/dev/null 2>&1 || true; rm -rf /home/%(n)s/.config/containers/systemd; ' + 'runuser -l %(u)s -c "systemctl --user daemon-reload" >/dev/null 2>&1 || true; }; true') % {'u': user, 'n': n, 'c': q(inner)} def script_restore_skel(a): @@ -512,11 +522,22 @@ def script_unmount(a): return 'umount -l %s 2>/dev/null || true' % q(safe_path(a['path'])) +def script_acl(a): + path = q(safe_path(a['path'])) + out = [] + for entry in a['entries']: + if not re.match(r'^(?:d:)?[ug]:[a-z_][a-z0-9_-]*:[rwxX-]{1,3}$', entry): + raise SetupError('unsafe ACL entry') + out.append('setfacl %s-m %s %s' % ('-d ' if entry.startswith('d:') else '', q(entry[2:] if entry.startswith('d:') else entry), path)) + return '\n'.join(out) + + def script_run_as(a): user = q(name(a['user'])) image = q(a['image']) verb = {'podman-pull': 'podman pull', 'podman-rmi': 'podman rmi -f'}[a['tool']] - return 'runuser -l %s -c %s >/dev/null 2>&1' % (user, q('%s %s' % (verb, image))) + run = 'runuser -l %s -c %s >/dev/null 2>&1' % (user, q('%s %s' % (verb, image))) + return run if a['tool'] != 'podman-pull' else '%s || { sleep 5; %s; }' % (run, run) HOST_SCRIPTS = { @@ -526,7 +547,7 @@ def script_run_as(a): 'run-as': script_run_as, 'restore-skel': script_restore_skel, 'boot': script_boot, 'timezone': script_timezone, 'nm-connection': script_nm_connection, 'hostname': script_hostname, 'http-server': script_http_server, 'dnf-module': script_dnf_module, 'crontab': script_crontab, 'partition-disk': script_partition_disk, 'format': script_format, 'append-line': script_append_line, 'mount-all': script_mount_all, - 'lvm-build': script_lvm_build, 'unmount': script_unmount, + 'lvm-build': script_lvm_build, 'unmount': script_unmount, 'acl': script_acl, } diff --git a/packages/schema/schemas/source/lab.schema.json b/packages/schema/schemas/source/lab.schema.json index 48e793a..009eebc 100644 --- a/packages/schema/schemas/source/lab.schema.json +++ b/packages/schema/schemas/source/lab.schema.json @@ -921,7 +921,8 @@ }, "source": { "type": "string", - "pattern": "^[0-9a-fA-F:.]{2,45}(?:/[0-9]{1,3})?$" + "pattern": "^(?:[0-9a-fA-F:.]{2,45}(?:/[0-9]{1,3})?|@server[a-d])$", + "description": "An address or network, or @serverb for the address of that lab server." }, "forwardPort": { "type": "string", @@ -1084,6 +1085,10 @@ }, "user": { "$ref": "#/$defs/unixName" + }, + "keepImages": { + "type": "boolean", + "description": "Keep the images pulled from registries (only remove containers, volumes, local images and Quadlet files)." } } }, @@ -1641,6 +1646,36 @@ "$ref": "#/$defs/hostPath" } } + }, + { + "type": "object", + "additionalProperties": false, + "required": [ + "action", + "hosts", + "path", + "entries" + ], + "properties": { + "action": { + "const": "acl" + }, + "hosts": { + "$ref": "#/$defs/serverList" + }, + "path": { + "$ref": "#/$defs/hostPath" + }, + "entries": { + "type": "array", + "minItems": 1, + "maxItems": 6, + "items": { + "type": "string", + "pattern": "^(?:d:)?[ug]:[a-z_][a-z0-9_-]*:[rwxX-]{1,3}$" + } + } + } } ] }, From d7ae95aa7fba78c9b88125370624f5f6ee7b29b8 Mon Sep 17 00:00:00 2001 From: Ayesh Rodrigo Date: Sun, 4 Oct 2026 12:43:18 +1300 Subject: [PATCH 9/9] Lab tool: start, grade and finish for guided exercises, with server-side setup and cleanup Guided exercises of every chapter now have an exercise definition. lab start prepares the servers (packages, users, files, disks, firewall, containers), lab grade checks the state the tasks leave behind, and lab finish undoes it. The graded chapter labs also get cleanup actions. Co-Authored-By: Claude Sonnet 5.5 --- .../ch03-files-and-directories/09-lab.md | 2 +- .../chapters/ch05-text-and-editors/09-lab.md | 2 +- .../chapters/ch06-users-and-groups/08-lab.md | 2 +- .../ch07-permissions-and-acls/09-lab.md | 2 +- .../chapters/ch08-processes/09-lab.md | 2 +- .../chapters/ch09-systemd-and-boot/09-lab.md | 2 +- .../ch10-ssh-and-remote-access/09-lab.md | 2 +- .../chapters/ch11-logs-and-time/09-lab.md | 2 +- .../chapters/ch12-networking/09-lab.md | 2 +- .../chapters/ch13-software/09-lab.md | 2 +- .../ch14-archives-and-transfer/09-lab.md | 2 +- .../ch15-scripts-and-scheduling/09-lab.md | 2 +- .../chapters/ch16-selinux/09-lab.md | 2 +- .../ch17-storage-and-file-systems/09-lab.md | 2 +- .../chapters/ch18-lvm/09-lab.md | 2 +- .../chapters/ch19-nfs-and-autofs/09-lab.md | 2 +- .../chapters/ch20-firewalld/09-lab.md | 2 +- .../chapters/ch21-containers/09-lab.md | 2 +- .../chapters/ch22-review/07-lab.md | 2 +- .../rhel9-sysadmin/lab/sa-archives-review.yml | 16 +++ .../lab/sa-containers-review.yml | 21 ++++ .../rhel9-sysadmin/lab/sa-files-review.yml | 26 +++++ .../lab/sa-firewalld-review.yml | 36 +++++++ .../rhel9-sysadmin/lab/sa-logs-review.yml | 53 +++++++++ .../rhel9-sysadmin/lab/sa-lvm-review.yml | 29 +++++ .../rhel9-sysadmin/lab/sa-network-review.yml | 11 ++ .../rhel9-sysadmin/lab/sa-nfs-review.yml | 74 +++++++++++++ .../rhel9-sysadmin/lab/sa-perms-review.yml | 31 ++++++ .../rhel9-sysadmin/lab/sa-proc-review.yml | 19 ++++ .../rhel9-sysadmin/lab/sa-review-final.yml | 101 ++++++++++++++++++ .../rhel9-sysadmin/lab/sa-scripts-review.yml | 37 +++++++ .../rhel9-sysadmin/lab/sa-selinux-review.yml | 52 +++++++++ .../rhel9-sysadmin/lab/sa-software-review.yml | 30 ++++++ .../rhel9-sysadmin/lab/sa-ssh-review.yml | 24 +++++ .../rhel9-sysadmin/lab/sa-storage-review.yml | 29 +++++ .../rhel9-sysadmin/lab/sa-systemd-review.yml | 42 ++++++++ .../rhel9-sysadmin/lab/sa-text-review.yml | 15 +++ .../rhel9-sysadmin/lab/sa-users-review.yml | 26 +++++ docs/AUTHORING.md | 19 +++- packages/compiler/src/lab.js | 31 +++++- packages/lab-tools/README.md | 6 +- packages/lab-tools/lab | 2 +- packages/lab-tools/test/prepare_test.py | 61 +++++++++++ 43 files changed, 801 insertions(+), 28 deletions(-) diff --git a/content/programs/rhel9-sysadmin/chapters/ch03-files-and-directories/09-lab.md b/content/programs/rhel9-sysadmin/chapters/ch03-files-and-directories/09-lab.md index f167355..c0abbe0 100644 --- a/content/programs/rhel9-sysadmin/chapters/ch03-files-and-directories/09-lab.md +++ b/content/programs/rhel9-sysadmin/chapters/ch03-files-and-directories/09-lab.md @@ -162,6 +162,6 @@ podcast_s2_e2.mp3 podcast_s2_e4.mp3 podcast_s2_e6.mp3 {% task id="task-c7f85590af3a" title="Grade and finish" %} Log out of serverb, then on workstation: - {% lab-finish exercise="sa-files-review" grade=true /%} + {% lab-finish exercise="sa-files-review" grade=true servers=true /%} {% /task %} {% /lab %} diff --git a/content/programs/rhel9-sysadmin/chapters/ch05-text-and-editors/09-lab.md b/content/programs/rhel9-sysadmin/chapters/ch05-text-and-editors/09-lab.md index d57a508..df5ab60 100644 --- a/content/programs/rhel9-sysadmin/chapters/ch05-text-and-editors/09-lab.md +++ b/content/programs/rhel9-sysadmin/chapters/ch05-text-and-editors/09-lab.md @@ -140,6 +140,6 @@ vim {% /task %} {% task id="task-28043cd64cf1" title="Grade and finish" %} - {% lab-finish exercise="sa-text-review" grade=true /%} + {% lab-finish exercise="sa-text-review" grade=true servers=true /%} {% /task %} {% /lab %} diff --git a/content/programs/rhel9-sysadmin/chapters/ch06-users-and-groups/08-lab.md b/content/programs/rhel9-sysadmin/chapters/ch06-users-and-groups/08-lab.md index 9706f07..cef3b8a 100644 --- a/content/programs/rhel9-sysadmin/chapters/ch06-users-and-groups/08-lab.md +++ b/content/programs/rhel9-sysadmin/chapters/ch06-users-and-groups/08-lab.md @@ -109,6 +109,6 @@ backup:x:3200:35000::/home/backup:/sbin/nologin {% task id="task-08e46a024567" title="Grade and finish" %} Leave serverb, then on workstation: - {% lab-finish exercise="sa-users-review" grade=true /%} + {% lab-finish exercise="sa-users-review" grade=true servers=true /%} {% /task %} {% /lab %} diff --git a/content/programs/rhel9-sysadmin/chapters/ch07-permissions-and-acls/09-lab.md b/content/programs/rhel9-sysadmin/chapters/ch07-permissions-and-acls/09-lab.md index 8a89ddb..2189af7 100644 --- a/content/programs/rhel9-sysadmin/chapters/ch07-permissions-and-acls/09-lab.md +++ b/content/programs/rhel9-sysadmin/chapters/ch07-permissions-and-acls/09-lab.md @@ -101,6 +101,6 @@ ls: cannot open directory '/srv/analysis': Permission denied {% task id="task-17e922d81973" title="Grade" %} Leave servera, then on workstation: - {% lab-finish exercise="sa-perms-review" grade=true /%} + {% lab-finish exercise="sa-perms-review" grade=true servers=true /%} {% /task %} {% /lab %} diff --git a/content/programs/rhel9-sysadmin/chapters/ch08-processes/09-lab.md b/content/programs/rhel9-sysadmin/chapters/ch08-processes/09-lab.md index 5a11554..d136bcd 100644 --- a/content/programs/rhel9-sysadmin/chapters/ch08-processes/09-lab.md +++ b/content/programs/rhel9-sysadmin/chapters/ch08-processes/09-lab.md @@ -99,6 +99,6 @@ Current active profile: throughput-performance {% task id="task-63bbe29381d5" title="Record the signal numbers and grade" %} Leave servera, then on workstation edit `answers.txt` (use `kill -l` to look the numbers up) and grade: - {% lab-finish exercise="sa-proc-review" grade=true /%} + {% lab-finish exercise="sa-proc-review" grade=true servers=true /%} {% /task %} {% /lab %} diff --git a/content/programs/rhel9-sysadmin/chapters/ch09-systemd-and-boot/09-lab.md b/content/programs/rhel9-sysadmin/chapters/ch09-systemd-and-boot/09-lab.md index 29ec6ab..a1bab56 100644 --- a/content/programs/rhel9-sysadmin/chapters/ch09-systemd-and-boot/09-lab.md +++ b/content/programs/rhel9-sysadmin/chapters/ch09-systemd-and-boot/09-lab.md @@ -109,6 +109,6 @@ multi-user.target {% task id="task-c35fd4a57c68" title="Grade" %} Leave servera, then on workstation: - {% lab-finish exercise="sa-systemd-review" grade=true /%} + {% lab-finish exercise="sa-systemd-review" grade=true servers=true /%} {% /task %} {% /lab %} diff --git a/content/programs/rhel9-sysadmin/chapters/ch10-ssh-and-remote-access/09-lab.md b/content/programs/rhel9-sysadmin/chapters/ch10-ssh-and-remote-access/09-lab.md index d826a94..bc854e4 100644 --- a/content/programs/rhel9-sysadmin/chapters/ch10-ssh-and-remote-access/09-lab.md +++ b/content/programs/rhel9-sysadmin/chapters/ch10-ssh-and-remote-access/09-lab.md @@ -102,6 +102,6 @@ c.txt {% task id="task-ddaf4ab39986" title="Record the fingerprint and grade" %} Fill in `answers.txt` (use `ssh-keygen -lF servera`; the type is the word ED25519 in the listing), then: - {% lab-finish exercise="sa-ssh-review" grade=true /%} + {% lab-finish exercise="sa-ssh-review" grade=true servers=true /%} {% /task %} {% /lab %} diff --git a/content/programs/rhel9-sysadmin/chapters/ch11-logs-and-time/09-lab.md b/content/programs/rhel9-sysadmin/chapters/ch11-logs-and-time/09-lab.md index 13e9d3c..ff681ee 100644 --- a/content/programs/rhel9-sysadmin/chapters/ch11-logs-and-time/09-lab.md +++ b/content/programs/rhel9-sysadmin/chapters/ch11-logs-and-time/09-lab.md @@ -103,6 +103,6 @@ EOT {% task id="task-5d38583a50de" title="Answers and grade" %} On workstation, fill in `answers.txt` and grade: - {% lab-finish exercise="sa-logs-review" grade=true /%} + {% lab-finish exercise="sa-logs-review" grade=true servers=true /%} {% /task %} {% /lab %} diff --git a/content/programs/rhel9-sysadmin/chapters/ch12-networking/09-lab.md b/content/programs/rhel9-sysadmin/chapters/ch12-networking/09-lab.md index e9fb649..814372e 100644 --- a/content/programs/rhel9-sysadmin/chapters/ch12-networking/09-lab.md +++ b/content/programs/rhel9-sysadmin/chapters/ch12-networking/09-lab.md @@ -93,6 +93,6 @@ hosts: files dns myhostname {% task id="task-cc0c66b2e1ec" title="Grade" %} On workstation, with `answers.txt` filled in: - {% lab-finish exercise="sa-network-review" grade=true /%} + {% lab-finish exercise="sa-network-review" grade=true servers=true /%} {% /task %} {% /lab %} diff --git a/content/programs/rhel9-sysadmin/chapters/ch13-software/09-lab.md b/content/programs/rhel9-sysadmin/chapters/ch13-software/09-lab.md index 565bb93..d50dadd 100644 --- a/content/programs/rhel9-sysadmin/chapters/ch13-software/09-lab.md +++ b/content/programs/rhel9-sysadmin/chapters/ch13-software/09-lab.md @@ -110,6 +110,6 @@ passwd-0.80-12.el9.x86_64 {% task id="task-49c5be688098" title="Grade" %} On workstation, with `answers.txt` filled in: - {% lab-finish exercise="sa-software-review" grade=true /%} + {% lab-finish exercise="sa-software-review" grade=true servers=true /%} {% /task %} {% /lab %} diff --git a/content/programs/rhel9-sysadmin/chapters/ch14-archives-and-transfer/09-lab.md b/content/programs/rhel9-sysadmin/chapters/ch14-archives-and-transfer/09-lab.md index 5f44690..d0c8612 100644 --- a/content/programs/rhel9-sysadmin/chapters/ch14-archives-and-transfer/09-lab.md +++ b/content/programs/rhel9-sysadmin/chapters/ch14-archives-and-transfer/09-lab.md @@ -107,6 +107,6 @@ other::--- {% task id="task-c73a6f33acc0" title="Grade" %} - {% lab-finish exercise="sa-archives-review" grade=true /%} + {% lab-finish exercise="sa-archives-review" grade=true servers=true /%} {% /task %} {% /lab %} diff --git a/content/programs/rhel9-sysadmin/chapters/ch15-scripts-and-scheduling/09-lab.md b/content/programs/rhel9-sysadmin/chapters/ch15-scripts-and-scheduling/09-lab.md index 7ac4a2d..f526983 100644 --- a/content/programs/rhel9-sysadmin/chapters/ch15-scripts-and-scheduling/09-lab.md +++ b/content/programs/rhel9-sysadmin/chapters/ch15-scripts-and-scheduling/09-lab.md @@ -141,6 +141,6 @@ EOT {% task id="task-ba78cc1ba75b" title="Grade" %} Leave servera, then on workstation: - {% lab-finish exercise="sa-scripts-review" grade=true /%} + {% lab-finish exercise="sa-scripts-review" grade=true servers=true /%} {% /task %} {% /lab %} diff --git a/content/programs/rhel9-sysadmin/chapters/ch16-selinux/09-lab.md b/content/programs/rhel9-sysadmin/chapters/ch16-selinux/09-lab.md index 29ab37d..42428e4 100644 --- a/content/programs/rhel9-sysadmin/chapters/ch16-selinux/09-lab.md +++ b/content/programs/rhel9-sysadmin/chapters/ch16-selinux/09-lab.md @@ -95,6 +95,6 @@ Enforcing {% task id="task-b6866acf4bc6" title="Answers and grade" %} On workstation, fill in `answers.txt` (`ps -eZ | grep httpd` shows the process type), and grade: - {% lab-finish exercise="sa-selinux-review" grade=true /%} + {% lab-finish exercise="sa-selinux-review" grade=true servers=true /%} {% /task %} {% /lab %} diff --git a/content/programs/rhel9-sysadmin/chapters/ch17-storage-and-file-systems/09-lab.md b/content/programs/rhel9-sysadmin/chapters/ch17-storage-and-file-systems/09-lab.md index 7a47bf5..c7df267 100644 --- a/content/programs/rhel9-sysadmin/chapters/ch17-storage-and-file-systems/09-lab.md +++ b/content/programs/rhel9-sysadmin/chapters/ch17-storage-and-file-systems/09-lab.md @@ -88,6 +88,6 @@ EOT {% task id="task-028f1b0ef4f9" title="Grade" %} On workstation, fill in `answers.txt` and grade: - {% lab-finish exercise="sa-storage-review" grade=true /%} + {% lab-finish exercise="sa-storage-review" grade=true servers=true /%} {% /task %} {% /lab %} diff --git a/content/programs/rhel9-sysadmin/chapters/ch18-lvm/09-lab.md b/content/programs/rhel9-sysadmin/chapters/ch18-lvm/09-lab.md index a7dbd30..950e521 100644 --- a/content/programs/rhel9-sysadmin/chapters/ch18-lvm/09-lab.md +++ b/content/programs/rhel9-sysadmin/chapters/ch18-lvm/09-lab.md @@ -114,6 +114,6 @@ EOT {% task id="task-543d852bb172" title="Grade" %} On workstation, fill in `answers.txt` and grade: - {% lab-finish exercise="sa-lvm-review" grade=true /%} + {% lab-finish exercise="sa-lvm-review" grade=true servers=true /%} {% /task %} {% /lab %} diff --git a/content/programs/rhel9-sysadmin/chapters/ch19-nfs-and-autofs/09-lab.md b/content/programs/rhel9-sysadmin/chapters/ch19-nfs-and-autofs/09-lab.md index 20fdb26..7e16a9a 100644 --- a/content/programs/rhel9-sysadmin/chapters/ch19-nfs-and-autofs/09-lab.md +++ b/content/programs/rhel9-sysadmin/chapters/ch19-nfs-and-autofs/09-lab.md @@ -110,6 +110,6 @@ touch: cannot touch '/remote/archive/2025/x': Read-only file system {% task id="task-df9cf7463e73" title="Grade" %} On workstation, fill in `answers.txt` and grade: - {% lab-finish exercise="sa-nfs-review" grade=true /%} + {% lab-finish exercise="sa-nfs-review" grade=true servers=true /%} {% /task %} {% /lab %} diff --git a/content/programs/rhel9-sysadmin/chapters/ch20-firewalld/09-lab.md b/content/programs/rhel9-sysadmin/chapters/ch20-firewalld/09-lab.md index 2142918..e6c3e43 100644 --- a/content/programs/rhel9-sysadmin/chapters/ch20-firewalld/09-lab.md +++ b/content/programs/rhel9-sysadmin/chapters/ch20-firewalld/09-lab.md @@ -118,6 +118,6 @@ curl: (7) Failed to connect to servera port 80: No route to host {% task id="task-686e1f06ad46" title="Grade" %} On workstation, fill in `answers.txt` and grade: - {% lab-finish exercise="sa-firewalld-review" grade=true /%} + {% lab-finish exercise="sa-firewalld-review" grade=true servers=true /%} {% /task %} {% /lab %} diff --git a/content/programs/rhel9-sysadmin/chapters/ch21-containers/09-lab.md b/content/programs/rhel9-sysadmin/chapters/ch21-containers/09-lab.md index d9661f0..82e7716 100644 --- a/content/programs/rhel9-sysadmin/chapters/ch21-containers/09-lab.md +++ b/content/programs/rhel9-sysadmin/chapters/ch21-containers/09-lab.md @@ -112,6 +112,6 @@ Portal is up {% task id="task-b9e0a99c36c8" title="Grade" %} On workstation, fill in `answers.txt` and grade: - {% lab-finish exercise="sa-containers-review" grade=true /%} + {% lab-finish exercise="sa-containers-review" grade=true servers=true /%} {% /task %} {% /lab %} diff --git a/content/programs/rhel9-sysadmin/chapters/ch22-review/07-lab.md b/content/programs/rhel9-sysadmin/chapters/ch22-review/07-lab.md index 4370210..d232e9c 100644 --- a/content/programs/rhel9-sysadmin/chapters/ch22-review/07-lab.md +++ b/content/programs/rhel9-sysadmin/chapters/ch22-review/07-lab.md @@ -155,6 +155,6 @@ Status page: ready {% task id="task-324ae14d4e05" title="Grade" %} On workstation, fill in `answers.txt` and grade: - {% lab-finish exercise="sa-review-final" grade=true /%} + {% lab-finish exercise="sa-review-final" grade=true servers=true /%} {% /task %} {% /lab %} diff --git a/content/programs/rhel9-sysadmin/lab/sa-archives-review.yml b/content/programs/rhel9-sysadmin/lab/sa-archives-review.yml index e1f1201..cfaec6f 100644 --- a/content/programs/rhel9-sysadmin/lab/sa-archives-review.yml +++ b/content/programs/rhel9-sysadmin/lab/sa-archives-review.yml @@ -80,3 +80,19 @@ checkpoints: paths: - /srv/restore/project/cache exists: false +finish: +- action: directory + hosts: + - servera + path: /srv/backup + state: absent +- action: directory + hosts: + - serverb + path: /srv/restore + state: absent +- action: directory + hosts: + - serverb + path: /srv/backup + state: absent diff --git a/content/programs/rhel9-sysadmin/lab/sa-containers-review.yml b/content/programs/rhel9-sysadmin/lab/sa-containers-review.yml index 504032b..b0b13b9 100644 --- a/content/programs/rhel9-sysadmin/lab/sa-containers-review.yml +++ b/content/programs/rhel9-sysadmin/lab/sa-containers-review.yml @@ -123,3 +123,24 @@ checkpoints: paths: - answers.txt matches: ^LABEL_OPTION=Z[ \t]*$ +finish: +- action: container-reset + hosts: + - servera + user: student + keepImages: true +- action: linger + hosts: + - servera + user: student + state: absent +- action: firewall + hosts: + - servera + state: absent + port: 8080/tcp +- action: directory + hosts: + - servera + path: /home/student/portal + state: absent diff --git a/content/programs/rhel9-sysadmin/lab/sa-files-review.yml b/content/programs/rhel9-sysadmin/lab/sa-files-review.yml index 0e050f5..8d769bc 100644 --- a/content/programs/rhel9-sysadmin/lab/sa-files-review.yml +++ b/content/programs/rhel9-sysadmin/lab/sa-files-review.yml @@ -131,3 +131,29 @@ checkpoints: - /home/student/latest fileType: symlink resolvesTo: /home/student/Podcasts/season2 +finish: +- action: directory + hosts: + - serverb + path: /home/student/Podcasts + state: absent +- action: directory + hosts: + - serverb + path: /home/student/Documents + state: absent +- action: file + hosts: + - serverb + path: /home/student/podcast_s1_e1.mp3 + state: absent +- action: file + hosts: + - serverb + path: /home/student/podcast_s2_e6.mp3 + state: absent +- action: file + hosts: + - serverb + path: /home/student/latest + state: absent diff --git a/content/programs/rhel9-sysadmin/lab/sa-firewalld-review.yml b/content/programs/rhel9-sysadmin/lab/sa-firewalld-review.yml index 8316769..aad28f5 100644 --- a/content/programs/rhel9-sysadmin/lab/sa-firewalld-review.yml +++ b/content/programs/rhel9-sysadmin/lab/sa-firewalld-review.yml @@ -163,3 +163,39 @@ checkpoints: paths: - answers.txt matches: ^DROP_SYMPTOM=timeout[ \t]*$ +finish: +- action: firewall + hosts: + - servera + state: absent + zone: partners + deleteZone: true +- action: firewall + hosts: + - servera + state: absent + service: portal + deleteService: true +- action: service + hosts: + - servera + names: + - httpd + state: stopped + enabled: false +- action: file + hosts: + - servera + path: /etc/httpd/conf.d/extra.conf + state: absent +- action: file + hosts: + - servera + path: /var/www/html/index.html + state: absent +- action: package + hosts: + - servera + names: + - httpd + state: absent diff --git a/content/programs/rhel9-sysadmin/lab/sa-logs-review.yml b/content/programs/rhel9-sysadmin/lab/sa-logs-review.yml index 5dbb6fb..28a2678 100644 --- a/content/programs/rhel9-sysadmin/lab/sa-logs-review.yml +++ b/content/programs/rhel9-sysadmin/lab/sa-logs-review.yml @@ -113,3 +113,56 @@ checkpoints: paths: - answers.txt matches: ^SELECTED_MARK=\*[ \t]*$ +finish: +- action: file + hosts: + - servera + path: /etc/systemd/journald.conf.d/10-retention.conf + state: absent +- action: file + hosts: + - servera + path: /etc/rsyslog.d/10-appserver.conf + state: absent +- action: file + hosts: + - servera + path: /var/log/appserver.log + state: absent +- action: file + hosts: + - servera + path: /etc/logrotate.d/appserver + state: absent +- action: service + hosts: + - servera + names: + - rsyslog + state: restarted +- action: timezone + hosts: + - servera + zone: UTC +- action: remove-lines + hosts: + - servera + path: /etc/chrony.conf + matching: ^server 0[.]pool[.]ntp[.]org +- action: service + hosts: + - servera + names: + - chronyd + state: restarted +- action: directory + hosts: + - servera + path: /var/log/journal + state: absent +- action: service + hosts: + - servera + names: + - systemd-journald + state: restarted diff --git a/content/programs/rhel9-sysadmin/lab/sa-lvm-review.yml b/content/programs/rhel9-sysadmin/lab/sa-lvm-review.yml index cd407e2..abcae91 100644 --- a/content/programs/rhel9-sysadmin/lab/sa-lvm-review.yml +++ b/content/programs/rhel9-sysadmin/lab/sa-lvm-review.yml @@ -103,3 +103,32 @@ checkpoints: paths: - answers.txt matches: ^SHRINKABLE_FS=ext4[ \t]*$ +finish: +- action: wipe-disk + hosts: + - serverb + device: /dev/sdb +- action: remove-lines + hosts: + - serverb + path: /etc/fstab + matching: /dev/vgapp/ +- action: directory + hosts: + - serverb + path: /srv/web + state: absent +- action: directory + hosts: + - serverb + path: /srv/db + state: absent +- action: file + hosts: + - serverb + path: /root/fstab.bak + state: absent +- action: systemd + hosts: + - serverb + reload: true diff --git a/content/programs/rhel9-sysadmin/lab/sa-network-review.yml b/content/programs/rhel9-sysadmin/lab/sa-network-review.yml index 93cd5a7..fbfe484 100644 --- a/content/programs/rhel9-sysadmin/lab/sa-network-review.yml +++ b/content/programs/rhel9-sysadmin/lab/sa-network-review.yml @@ -76,3 +76,14 @@ checkpoints: paths: - answers.txt matches: ^NAME_ORDER=files dns[ \t]*$ +finish: +- action: nm-connection + hosts: + - servera + name: dummy1 + state: absent +- action: remove-lines + hosts: + - servera + path: /etc/hosts + matching: appnet diff --git a/content/programs/rhel9-sysadmin/lab/sa-nfs-review.yml b/content/programs/rhel9-sysadmin/lab/sa-nfs-review.yml index 3c671c5..3385c61 100644 --- a/content/programs/rhel9-sysadmin/lab/sa-nfs-review.yml +++ b/content/programs/rhel9-sysadmin/lab/sa-nfs-review.yml @@ -135,3 +135,77 @@ checkpoints: paths: - answers.txt matches: ^FIREWALL_SERVICE=nfs[[:space:]]*$ +finish: +- action: unmount + hosts: + - servera + path: /mnt/projects +- action: remove-lines + hosts: + - servera + path: /etc/fstab + matching: serverb:/srv/projects +- action: directory + hosts: + - servera + path: /mnt/projects + state: absent +- action: directory + hosts: + - servera + path: /remote + state: absent +- action: file + hosts: + - servera + path: /etc/auto.master.d/archive.autofs + state: absent +- action: file + hosts: + - servera + path: /etc/auto.archive + state: absent +- action: service + hosts: + - servera + names: + - autofs + state: stopped + enabled: false +- action: package + hosts: + - servera + names: + - autofs + state: absent +- action: systemd + hosts: + - servera + reload: true +- action: remove-lines + hosts: + - serverb + path: /etc/exports + matching: /srv/ +- action: service + hosts: + - serverb + names: + - nfs-server + state: stopped + enabled: false +- action: firewall + hosts: + - serverb + state: absent + service: nfs +- action: directory + hosts: + - serverb + path: /srv/projects + state: absent +- action: directory + hosts: + - serverb + path: /srv/archive + state: absent diff --git a/content/programs/rhel9-sysadmin/lab/sa-perms-review.yml b/content/programs/rhel9-sysadmin/lab/sa-perms-review.yml index d8cb5dc..bd24ff1 100644 --- a/content/programs/rhel9-sysadmin/lab/sa-perms-review.yml +++ b/content/programs/rhel9-sysadmin/lab/sa-perms-review.yml @@ -108,3 +108,34 @@ checkpoints: acl: - user:visitor:--x - default:user:visitor:r-- +finish: +- action: user + hosts: + - servera + name: dana + state: absent +- action: user + hosts: + - servera + name: evan + state: absent +- action: user + hosts: + - servera + name: visitor + state: absent +- action: group + hosts: + - servera + name: analysts + state: absent +- action: directory + hosts: + - servera + path: /srv/analysis + state: absent +- action: directory + hosts: + - servera + path: /srv/dropbox + state: absent diff --git a/content/programs/rhel9-sysadmin/lab/sa-proc-review.yml b/content/programs/rhel9-sysadmin/lab/sa-proc-review.yml index 1b78304..69ddd60 100644 --- a/content/programs/rhel9-sysadmin/lab/sa-proc-review.yml +++ b/content/programs/rhel9-sysadmin/lab/sa-proc-review.yml @@ -102,3 +102,22 @@ checkpoints: paths: - answers.txt matches: ^cont=18[ \t]*$ +finish: +- action: user + hosts: + - servera + name: worker + state: absent +- action: service + hosts: + - servera + names: + - tuned + state: stopped + enabled: false +- action: package + hosts: + - servera + names: + - tuned + state: absent diff --git a/content/programs/rhel9-sysadmin/lab/sa-review-final.yml b/content/programs/rhel9-sysadmin/lab/sa-review-final.yml index a06172b..fd113de 100644 --- a/content/programs/rhel9-sysadmin/lab/sa-review-final.yml +++ b/content/programs/rhel9-sysadmin/lab/sa-review-final.yml @@ -234,3 +234,104 @@ checkpoints: paths: - answers.txt matches: ^TAR_LABEL_OPTION=--selinux[ \t]*$ +finish: +- action: service + hosts: + - servera + names: + - status-backup.timer + state: stopped + enabled: false +- action: service + hosts: + - servera + names: + - httpd + state: stopped + enabled: false +- action: firewall + hosts: + - servera + state: absent + zone: statuspartners + deleteZone: true +- action: selinux + hosts: + - servera + kind: port + state: absent + type: http_port_t + proto: tcp + port: 8095 +- action: selinux + hosts: + - servera + kind: fcontext + state: absent + path: /srv/status(/.*)? +- action: directory + hosts: + - servera + path: /srv/status + state: absent +- action: directory + hosts: + - servera + path: /srv/backups + state: absent +- action: file + hosts: + - servera + path: /etc/httpd/conf.d/status.conf + state: absent +- action: file + hosts: + - servera + path: /etc/systemd/system/status-backup.service + state: absent +- action: file + hosts: + - servera + path: /etc/systemd/system/status-backup.timer + state: absent +- action: file + hosts: + - servera + path: /etc/systemd/journald.conf.d/10-persistent.conf + state: absent +- action: systemd + hosts: + - servera + reload: true +- action: user + hosts: + - servera + name: statusadm + state: absent +- action: user + hosts: + - servera + name: auditor + state: absent +- action: group + hosts: + - servera + name: ops + state: absent +- action: package + hosts: + - servera + names: + - httpd + state: absent +- action: directory + hosts: + - servera + path: /var/log/journal + state: absent +- action: service + hosts: + - servera + names: + - systemd-journald + state: restarted diff --git a/content/programs/rhel9-sysadmin/lab/sa-scripts-review.yml b/content/programs/rhel9-sysadmin/lab/sa-scripts-review.yml index ca4f2c6..da66b1a 100644 --- a/content/programs/rhel9-sysadmin/lab/sa-scripts-review.yml +++ b/content/programs/rhel9-sysadmin/lab/sa-scripts-review.yml @@ -120,3 +120,40 @@ checkpoints: - sa-report.timer active: true enabled: true +finish: +- action: file + hosts: + - servera + path: /usr/local/bin/sa-report.sh + state: absent +- action: file + hosts: + - servera + path: /etc/cron.d/sa-report + state: absent +- action: file + hosts: + - servera + path: /etc/systemd/system/sa-report.service + state: absent +- action: file + hosts: + - servera + path: /etc/systemd/system/sa-report.timer + state: absent +- action: service + hosts: + - servera + names: + - sa-report.timer + state: stopped + enabled: false +- action: crontab + hosts: + - servera + user: student + state: absent +- action: systemd + hosts: + - servera + reload: true diff --git a/content/programs/rhel9-sysadmin/lab/sa-selinux-review.yml b/content/programs/rhel9-sysadmin/lab/sa-selinux-review.yml index 3a1a8a0..37ed8e2 100644 --- a/content/programs/rhel9-sysadmin/lab/sa-selinux-review.yml +++ b/content/programs/rhel9-sysadmin/lab/sa-selinux-review.yml @@ -101,3 +101,55 @@ checkpoints: paths: - answers.txt matches: ^AUDIT_LOG=/var/log/audit/audit\.log[ \t]*$ +finish: +- action: selinux + hosts: + - servera + kind: fcontext + state: absent + path: /srv/portal(/.*)? +- action: selinux + hosts: + - servera + kind: port + state: absent + type: http_port_t + proto: tcp + port: 8090 +- action: selinux + hosts: + - servera + kind: boolean + state: present + name: httpd_can_network_connect + value: false +- action: selinux + hosts: + - servera + kind: boolean + state: present + name: httpd_enable_homedirs + value: false +- action: service + hosts: + - servera + names: + - httpd + state: stopped + enabled: false +- action: directory + hosts: + - servera + path: /srv/portal + state: absent +- action: file + hosts: + - servera + path: /etc/httpd/conf.d/portal.conf + state: absent +- action: package + hosts: + - servera + names: + - httpd + state: absent diff --git a/content/programs/rhel9-sysadmin/lab/sa-software-review.yml b/content/programs/rhel9-sysadmin/lab/sa-software-review.yml index 2b86499..0af1a22 100644 --- a/content/programs/rhel9-sysadmin/lab/sa-software-review.yml +++ b/content/programs/rhel9-sysadmin/lab/sa-software-review.yml @@ -91,3 +91,33 @@ checkpoints: paths: - answers.txt matches: ^REPO_DIR=/etc/yum\.repos\.d/?[ \t]*$ +finish: +- action: file + hosts: + - servera + path: /etc/yum.repos.d/local.repo + state: absent +- action: directory + hosts: + - servera + path: /srv/localrepo + state: absent +- action: package + hosts: + - servera + names: + - zip + - unzip + - wget + state: absent +- action: dnf-module + hosts: + - servera + name: nginx + state: reset +- action: package + hosts: + - servera + names: + - tree + state: reinstalled diff --git a/content/programs/rhel9-sysadmin/lab/sa-ssh-review.yml b/content/programs/rhel9-sysadmin/lab/sa-ssh-review.yml index 287d3d6..e947847 100644 --- a/content/programs/rhel9-sysadmin/lab/sa-ssh-review.yml +++ b/content/programs/rhel9-sysadmin/lab/sa-ssh-review.yml @@ -119,3 +119,27 @@ checkpoints: paths: - answers.txt matches: ^HOSTKEY_TYPE=ED25519[ \t]*$ +finish: +- action: user + hosts: + - servera + name: ops + state: absent +- action: file + hosts: + - servera + path: /etc/ssh/sshd_config.d/10-hardening.conf + state: absent +- action: service + hosts: + - servera + names: + - sshd + state: restarted +- action: home-remove + paths: + - .ssh/id_ops + - .ssh/id_ops.pub +- action: ssh-config-block + host: opsa + state: absent diff --git a/content/programs/rhel9-sysadmin/lab/sa-storage-review.yml b/content/programs/rhel9-sysadmin/lab/sa-storage-review.yml index 5a88ae9..0353e9d 100644 --- a/content/programs/rhel9-sysadmin/lab/sa-storage-review.yml +++ b/content/programs/rhel9-sysadmin/lab/sa-storage-review.yml @@ -92,3 +92,32 @@ checkpoints: paths: - answers.txt matches: ^INODE_COMMAND=df -i[ \t]*$ +finish: +- action: wipe-disk + hosts: + - serverb + device: /dev/sdb +- action: remove-lines + hosts: + - serverb + path: /etc/fstab + matching: /srv/projects|/srv/backups|none[[:space:]]+swap +- action: directory + hosts: + - serverb + path: /srv/projects + state: absent +- action: directory + hosts: + - serverb + path: /srv/backups + state: absent +- action: file + hosts: + - serverb + path: /root/fstab.bak + state: absent +- action: systemd + hosts: + - serverb + reload: true diff --git a/content/programs/rhel9-sysadmin/lab/sa-systemd-review.yml b/content/programs/rhel9-sysadmin/lab/sa-systemd-review.yml index f7708b8..d8e081b 100644 --- a/content/programs/rhel9-sysadmin/lab/sa-systemd-review.yml +++ b/content/programs/rhel9-sysadmin/lab/sa-systemd-review.yml @@ -87,3 +87,45 @@ checkpoints: - servera.lab.example.com message: The default target is multi-user.target target: multi-user.target +finish: +- action: service + hosts: + - servera + names: + - reporter + state: stopped + enabled: false +- action: file + hosts: + - servera + path: /etc/systemd/system/reporter.service + state: absent +- action: file + hosts: + - servera + path: /usr/local/bin/reporter.sh + state: absent +- action: file + hosts: + - servera + path: /var/log/reporter.log + state: absent +- action: directory + hosts: + - servera + path: /etc/systemd/system/reporter.service.d + state: absent +- action: service + hosts: + - servera + names: + - bluetooth + masked: false +- action: systemd + hosts: + - servera + reload: true +- action: boot + hosts: + - servera + target: graphical.target diff --git a/content/programs/rhel9-sysadmin/lab/sa-text-review.yml b/content/programs/rhel9-sysadmin/lab/sa-text-review.yml index 1fa3248..8936693 100644 --- a/content/programs/rhel9-sysadmin/lab/sa-text-review.yml +++ b/content/programs/rhel9-sysadmin/lab/sa-text-review.yml @@ -98,3 +98,18 @@ checkpoints: paths: - /home/student/.bash_profile matches: ^export EDITOR=vim +finish: +- action: directory + hosts: + - servera + path: /home/student/reports + state: absent +- action: file + hosts: + - servera + path: /home/student/notice.txt + state: absent +- action: restore-skel + hosts: + - servera + user: student diff --git a/content/programs/rhel9-sysadmin/lab/sa-users-review.yml b/content/programs/rhel9-sysadmin/lab/sa-users-review.yml index 3f28b7e..6698092 100644 --- a/content/programs/rhel9-sysadmin/lab/sa-users-review.yml +++ b/content/programs/rhel9-sysadmin/lab/sa-users-review.yml @@ -119,3 +119,29 @@ checkpoints: matches: ^%operators[[:space:]]+ALL[[:space:]]*=[[:space:]]*\(ALL(:ALL)?\)[[:space:]]+/usr/bin/systemctl([[:space:]]+\*)?[[:space:]]*$ lacks: - NOPASSWD +finish: +- action: user + hosts: + - serverb + name: opsadmin + state: absent +- action: user + hosts: + - serverb + name: auditor + state: absent +- action: user + hosts: + - serverb + name: backup + state: absent +- action: group + hosts: + - serverb + name: operators + state: absent +- action: file + hosts: + - serverb + path: /etc/sudoers.d/operators + state: absent diff --git a/docs/AUTHORING.md b/docs/AUTHORING.md index 6d62725..47de7bd 100644 --- a/docs/AUTHORING.md +++ b/docs/AUTHORING.md @@ -271,14 +271,15 @@ checkpoints: | Check `kind` | Values it takes | | --- | --- | | `service` | `names`, `active`, `enabled` | -| `firewall` | `service` or `port`, `allowed`, `runtime`, `permanent` | -| `package` | `names`, `installed` | -| `file` | `paths` (any one may satisfy), `exists`, `nonEmpty`, `contains`, `lacks`, `line`, `lines`, `matches`, `contentEquals`, `mode`, `owner`, `group`, `selinuxType`, `symlinkTo` | +| `firewall` | `service`, `port`, `source` or `forwardPort` (optionally in a `zone`), `allowed`, `runtime`, `permanent` | +| `package` | `names`, `installed`, `verify` | +| `file` | `paths` (any one may satisfy), `exists`, `nonEmpty`, `contains`, `lacks`, `line`, `lines`, `matches`, `contentEquals`, `mode`, `owner`, `group`, `selinuxType`, `symlinkTo`, `executable`, `acl` | | `file-compare` | `a`, `b` | | `archive` | `path`, `format` | | `user` | `names`, `exists`, `groups`, `passwordSet`, `authorizedKeys`, `homeFile` | | `mount`, `logical-volume` | `path`, `fstype`, `persistent`; `vg`, `lv`, `minSizeMiB` | | `http` | `url` (to the managed host itself), `insecure`, `resolveToLocalhost`, `containsAny` | +| `partition`, `container` | `device`, `minSizeMiB`, `partLabel`, `tableType`, `fsType`, `fsLabel`; `user`, `image`, `running`, `unit`, `linger` | | `selinux`, `sudoers`, `sshd`, `cron`, `boot-target`, `address`, `hostname`, `commands` | see the lab schema | | `git`, `lint` | on `control` only: the project's Git state, and `ansible-lint` in a container | @@ -287,6 +288,18 @@ checkpoints: - Setup actions: `self-signed-cert`, `htpasswd`, `password-hash-var`, `vault-encrypt`, `ssh-keypairs`, `pack-installed-collection`, `build-collection`, `collection-requirements` and `git-seed-remote`. - Starter and tree files are published with a `.lab` suffix so a browser never renders them, and a name part that starts with a dot gets a `_` in front (`files/.htaccess` is published as `files/_.htaccess.lab`), because static hosts leave dotfiles out. The lab command saves them under their real names. Do not name a file starting with `_.`; that form is reserved. +#### Guided exercises: start, grade and finish on the servers + +A guided exercise (a step-by-step page, `{% lab exercise="NAME" guided=true ownExercise=true %}`) has no challenge brief, but it is still an exercise definition. The lab tool does three jobs for it: + +- **`lab start NAME`** copies the starter files and runs the `setup` actions, which prepare what the exercise needs but does not teach: install a package, create the users and files an earlier exercise left behind, start a broken service. Tasks that only did this preparation are removed from the page and replaced by a "Start the exercise" task. +- **`lab grade NAME`** runs the typed `checks` against the state the tasks leave behind. +- **`lab finish NAME`** runs the `finish` actions on the servers, then archives the project folder. The last task of the page is `{% lab-finish exercise="NAME" grade=true servers=true /%}`, which tells the reader that the servers are cleaned. Each finish action is best effort: one that fails is reported and the others still run. `rht-vmctl reset servers` remains the way to the exact clean baseline. + +`setup` and `finish` use the same typed actions. **Host actions** run as root on the named lab servers (`hosts: [servera]`) and are built by `prepare.py` from validated values: `package`, `service`, `group`, `user`, `directory`, `file`, `remove-lines`, `append-line`, `acl`, `firewall`, `selinux`, `systemd`, `linger`, `boot`, `timezone`, `hostname`, `nm-connection`, `crontab`, `dnf-module`, `http-server`, `partition-disk`, `format`, `lvm-build`, `mount-all`, `unmount`, `wipe-disk`, `restore-skel`, `container-reset` and `run-as`. Path values are limited to places meant for local data and drop-in configuration (`/srv`, `/opt`, `/mnt`, `/data`, `/home`, `/tmp`, `/var/log`, `/etc/*.d` and a few more), the top directories themselves and whole home directories can never be removed, and `remove-lines` and `append-line` only touch `/etc/fstab`, `/etc/exports`, `/etc/hosts` and `/etc/chrony.conf`. **Workstation actions** work in the learner's home and refuse shell start-up files and the lab's own keys: `home-remove`, `ssh-config-block` and `ssh-identity`. + +Write the checks for the state at the moment the page asks to grade, and make the setup produce the state the first remaining task assumes, and say in the Start task where the reader works (for example "change to `/srv/lab7`"). Run the whole page against the lab (`lab start`, the tasks, `lab grade`, `lab finish`, `lab grade` again) before you publish it. + The compiler publishes the lab tree (the `lab` command, the grader, starter files, `INDEX`, and a `MANIFEST` per exercise) with the rest of the site, and the build also places it at `/lab/`. Try an exercise end to end with `npm run build`, then `LAB_URL=file://$PWD/dist/lab bash dist/lab/lab start NAME`, and run its solution against the lab before you publish it. ## 6. Code font diff --git a/packages/compiler/src/lab.js b/packages/compiler/src/lab.js index e3029f9..9ecd250 100644 --- a/packages/compiler/src/lab.js +++ b/packages/compiler/src/lab.js @@ -31,8 +31,35 @@ exit 1 const CONTROL_ONLY = new Set(['git', 'lint']); // Setup and cleanup actions that run as root on the lab servers; the rest work on the workstation. const HOST_ACTIONS = new Set([ - 'package', 'service', 'group', 'user', 'directory', 'file', 'remove-lines', 'firewall', 'selinux', 'wipe-disk', 'systemd', 'linger', - 'container-reset', 'run-as', 'restore-skel', 'boot', 'timezone', 'nm-connection', 'hostname', 'http-server', 'dnf-module', 'crontab', 'partition-disk', 'format', 'append-line', 'mount-all', 'lvm-build', 'unmount', 'acl', + 'package', + 'service', + 'group', + 'user', + 'directory', + 'file', + 'remove-lines', + 'firewall', + 'selinux', + 'wipe-disk', + 'systemd', + 'linger', + 'container-reset', + 'run-as', + 'restore-skel', + 'boot', + 'timezone', + 'nm-connection', + 'hostname', + 'http-server', + 'dnf-module', + 'crontab', + 'partition-disk', + 'format', + 'append-line', + 'mount-all', + 'lvm-build', + 'unmount', + 'acl', ]); /** Every file under `dir` as paths relative to it, sorted. */ diff --git a/packages/lab-tools/README.md b/packages/lab-tools/README.md index 4868311..6f1a6e3 100644 --- a/packages/lab-tools/README.md +++ b/packages/lab-tools/README.md @@ -4,8 +4,8 @@ The tools learners run on their own machine, never on a server: | File | What it does | | --- | --- | -| `lab` | The `lab` command: `start`, `finish`, `grade`, `list`, `check`, `update`. Downloads are all-or-nothing, and nothing an exercise ships is executed. | -| `prepare.py` | Runs an exercise's typed setup actions after `lab start` has copied its starter files: certificates, Vault files, SSH keys, collection archives, Git remotes. | +| `lab` | The `lab` command: `start`, `finish`, `grade`, `list`, `check`, `update`. Downloads are all-or-nothing, and nothing an exercise ships is executed. `finish` also undoes an exercise's changes on the servers. | +| `prepare.py` | Runs an exercise's typed setup actions after `lab start` has copied its starter files (certificates, Vault files, SSH keys, collection archives, Git remotes, and on the lab servers packages, users, files, firewall rules and disks), and its cleanup actions at `lab finish`. | | `grade.py` | Read-only grading. Builds a fixed command from each typed check, sends it to the managed hosts through Ansible's `raw` module, and reports PASS, FAIL or SKIP. | | `setup/` | The home-lab build script and `rht-vmctl`, published beside the exercises. | @@ -15,7 +15,7 @@ Exercises are data (see [the authoring guide](../../docs/AUTHORING.md)). The com - The catalog supplies values. Every command is built here from validated, quoted values; no value is ever run as a command. - Checks only read. Grading never runs a learner's playbook or changes a managed host. -- Setup actions write inside the project folder and `~/git-repos` only. +- Setup actions on workstation write inside the project folder, `~/git-repos` and the learner's home (never start-up files or the lab keys). Host actions run fixed scripts as root over SSH, only under allowed paths and with validated, quoted values. - Python 3.9 or newer, with no packages beyond the standard library. Tests: `npm run test:labs`. diff --git a/packages/lab-tools/lab b/packages/lab-tools/lab index 0380aae..3832a88 100755 --- a/packages/lab-tools/lab +++ b/packages/lab-tools/lab @@ -78,7 +78,7 @@ cmd_start() { || die "setup failed; exercise is not ready. Fix the reported error and restart with --force" rm -f "$dir/.lab-prepare.py" "$dir/.lab-catalog.json" "$dir/.lab-not-ready" say "~/$name is ready: cd ~/$name" - echo " Start from clean servers (on the Ubuntu host: rht-vmctl reset servers)." + echo " If an earlier exercise left changes on the servers, finish it first (lab finish NAME) or reset them on the Ubuntu host: rht-vmctl reset servers" } cmd_finish() { diff --git a/packages/lab-tools/test/prepare_test.py b/packages/lab-tools/test/prepare_test.py index 4c79b79..949b92c 100644 --- a/packages/lab-tools/test/prepare_test.py +++ b/packages/lab-tools/test/prepare_test.py @@ -124,3 +124,64 @@ def test_every_action_in_the_catalog_is_implemented(self): if __name__ == '__main__': unittest.main() + + +class HostActionTests(unittest.TestCase): + """Server actions build fixed scripts from validated values; nothing from the catalog runs as a command.""" + + def script(self, **action): + return prepare.HOST_SCRIPTS[action['action']](action) + + def test_values_are_quoted(self): + text = self.script(action='package', hosts=['servera'], names=['httpd', 'tree']) + self.assertIn("dnf install -y -q httpd tree", text) + text = self.script(action='file', hosts=['servera'], path='/srv/web/a b.txt', content='x; rm -rf /\n') + self.assertIn("'/srv/web/a b.txt'", text) + self.assertNotIn('x; rm -rf /', text) # the content travels base64 encoded + + def test_paths_outside_the_allowed_places_are_refused(self): + for path in ('/etc/passwd', '/etc/shadow', '/usr/bin/ls', '/boot/vmlinuz', '/srv/../etc/passwd', '/var/lib/rpm'): + with self.assertRaises(prepare.SetupError, msg=path): + self.script(action='directory', hosts=['servera'], path=path, state='absent') + + def test_whole_top_directories_and_home_directories_are_not_removed(self): + for path in ('/srv', '/home', '/home/student', '/opt', '/mnt', '/var/log'): + with self.assertRaises(prepare.SetupError, msg=path): + self.script(action='directory', hosts=['servera'], path=path, state='absent') + self.assertIn('rm -rf', self.script(action='directory', hosts=['servera'], path='/srv/web', state='absent')) + + def test_line_removal_is_limited_to_a_few_files(self): + with self.assertRaises(prepare.SetupError): + self.script(action='remove-lines', hosts=['servera'], path='/etc/passwd', matching='x') + self.assertIn('/etc/fstab', self.script(action='remove-lines', hosts=['servera'], path='/etc/fstab', matching='^/dev/sdb1')) + + def test_account_names_must_be_plain(self): + with self.assertRaises(prepare.SetupError): + self.script(action='user', hosts=['servera'], name='bob; reboot', state='absent') + + def test_firewall_resolves_a_server_name_for_sources(self): + text = self.script(action='firewall', hosts=['servera'], zone='portal', source='@serverb', state='present') + self.assertIn('getent ahostsv4 serverb.lab.example.com', text) + + def test_wipe_disk_only_touches_spare_disks(self): + self.assertIn('/dev/sdb', self.script(action='wipe-disk', hosts=['servera'], device='/dev/sdb')) + + def test_every_published_exercise_builds_its_setup_and_cleanup(self): + for name, exercise in CATALOG['exercises'].items(): + for key in ('setup', 'finish'): + for action in exercise.get(key, []): + if action['action'] in prepare.HOST_SCRIPTS: + self.assertTrue(self.script(**action), '%s %s %s' % (name, key, action['action'])) + else: + self.assertIn(action['action'], prepare.ACTIONS, name) + + def test_finish_runs_only_cleanup_actions(self): + calls = [] + original = prepare.run_on_host + prepare.run_on_host = lambda host, script, tolerant: calls.append((host, tolerant)) or True + try: + catalog = {'exercises': {'x': {'finish': [{'action': 'package', 'hosts': ['servera', 'serverb'], 'names': ['tree'], 'state': 'absent'}]}}} + self.assertEqual(prepare.finish('x', Path('.'), 'file:///nowhere', catalog), 1) + finally: + prepare.run_on_host = original + self.assertEqual(calls, [('servera', True), ('serverb', True)])