feat(root01): author Computing & Linux Foundations, and the real 01->05 prerequisite chain
Adds all 6 ROOT 01 modules (how computers run software, the shell, filesystem navigation, files & text, processes, users & permissions foundations), each with a real lesson, demonstration/guided exercise, knowledge check where warranted, a hands-on Lab, and a reflection — 35 activities, 29 new content files. Adds 6 new hands-on Linux scenarios (linux-inspect-hardware, linux-shell-basics, linux-filesystem-navigation, linux-files-and-text, linux-process-management, linux-permissions-foundations) plus 2 MCQ scenarios, using the existing generic scenario/ validation engine (docs/ROOT_FOUNDATIONS_LAB_STRATEGY.md §1) — every one tested end-to-end via the real scenario setup/validate API, including negative tests confirming they correctly FAIL when not actually solved (e.g. the process-management scenario fails while the target process is still running; the permissions scenario is tested against a real `su - labuser` denial). Wires rootnode-linux-foundations-assessment (2 required + 4 random-draw scenarios) using the existing Phase 3B assessment engine. Adds a real, server-side ROOT 01->02->03->04->05 course prerequisite chain (courses.prerequisites) — verified live on two students: one who completed ROOT 01 sees ROOT 02 unlock; a fresh student sees ROOT 02 correctly locked while 03/04/05 stay vacuously satisfied (their own prerequisite courses are still empty). ROOT 06+ and the Capstone are unaffected. Co-Authored-By: Claude Sonnet 5 <[email protected]>
This commit is contained in:
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"id": "linux-exit-codes-mcq",
|
||||
"title": "Reading Exit Code Conventions",
|
||||
"category": "Linux Fundamentals",
|
||||
"isolation_type": "shared_namespace",
|
||||
"difficulty": "Easy",
|
||||
"type": "mcq",
|
||||
"weight": 3,
|
||||
"description": "## Reading Exit Code Conventions\n\nYou run `grep pattern file.txt` and it exits with code `1`, printing no error message at all. What actually happened?",
|
||||
"options": [
|
||||
{ "id": "a", "text": "The file doesn't exist" },
|
||||
{ "id": "b", "text": "grep found no lines matching the pattern — this is grep's normal way of reporting \"no match,\" not an error" },
|
||||
{ "id": "c", "text": "You don't have permission to read the file" },
|
||||
{ "id": "d", "text": "The command is broken and needs to be reinstalled" }
|
||||
],
|
||||
"correct_option": "b",
|
||||
"explanation": "grep's exit code means \"did I find at least one match,\" not \"did something go wrong.\" Exit 1 with no error message is grep's normal, expected way of reporting zero matches — a very common early surprise, since silence plus a non-zero code doesn't always mean a real failure occurred.",
|
||||
"hints": [],
|
||||
"setup_commands": [],
|
||||
"default_namespace": "default",
|
||||
"teardown_commands": []
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
{
|
||||
"id": "linux-files-and-text",
|
||||
"title": "Extract the Signal From a Log File",
|
||||
"category": "Linux Fundamentals",
|
||||
"isolation_type": "shared_namespace",
|
||||
"difficulty": "Easy",
|
||||
"type": "task",
|
||||
"weight": 4,
|
||||
"description": "## Extract the Signal From a Log File\n\nA real application log (`/srv/text-lab/app.log`) has been generated for you, mixing `INFO` and `ERROR` lines. Reading raw logs line-by-line doesn't scale — filtering does.\n\n**Your task:**\n\n1. Use `grep` to extract only the `ERROR` lines from `/srv/text-lab/app.log`\n2. Redirect the result into a new file: `/srv/text-lab/errors.txt`\n3. Count how many ERROR lines there were and save the count to `/srv/text-lab/error-count.txt`",
|
||||
"hints": [
|
||||
{
|
||||
"title": "Filtering and redirecting",
|
||||
"body": "`grep PATTERN file > newfile` writes only matching lines to a new file. Pipe to `wc -l` to count them.",
|
||||
"command": "grep ERROR /srv/text-lab/app.log > /srv/text-lab/errors.txt\ngrep -c ERROR /srv/text-lab/app.log > /srv/text-lab/error-count.txt"
|
||||
}
|
||||
],
|
||||
"setup_commands": [
|
||||
{ "command": "mkdir -p /srv/text-lab && printf 'INFO: boot ok\\nERROR: disk full\\nINFO: retry\\nERROR: timeout\\nINFO: done\\n' > /srv/text-lab/app.log" }
|
||||
],
|
||||
"validation": {
|
||||
"commands": [
|
||||
{ "description": "errors.txt contains only ERROR lines", "command": "grep -vc ERROR /srv/text-lab/errors.txt || true", "expected_output": "0", "match": "exact" },
|
||||
{ "description": "errors.txt has exactly the 2 real ERROR lines", "command": "wc -l < /srv/text-lab/errors.txt | tr -d ' '", "expected_output": "2", "match": "exact" },
|
||||
{ "description": "error-count.txt records the count", "command": "cat /srv/text-lab/error-count.txt", "expected_output": "2", "match": "exact" }
|
||||
]
|
||||
},
|
||||
"default_namespace": "default",
|
||||
"teardown_commands": [
|
||||
{ "command": "rm -rf /srv/text-lab" }
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
{
|
||||
"id": "linux-filesystem-navigation",
|
||||
"title": "Build a Real Project Directory Structure",
|
||||
"category": "Linux Fundamentals",
|
||||
"isolation_type": "shared_namespace",
|
||||
"difficulty": "Easy",
|
||||
"type": "task",
|
||||
"weight": 3,
|
||||
"description": "## Build a Real Project Directory Structure\n\nEvery real codebase has a directory structure that means something. Practice building and navigating one.\n\n**Your task:**\n\nUnder `/srv/fs-lab/`, create:\n\n1. A directory tree: `project/src`, `project/docs`, `project/tests`\n2. A hidden file at `project/.config` containing exactly the text `ready`\n3. Navigate into `project/src` using a RELATIVE path from `/srv/fs-lab` (not an absolute path) to confirm you understand `cd project/src` vs `cd /srv/fs-lab/project/src`",
|
||||
"hints": [
|
||||
{
|
||||
"title": "Creating the tree in one command",
|
||||
"body": "`mkdir -p` creates every intermediate directory in one call, and brace expansion lets you create three siblings at once.",
|
||||
"command": "mkdir -p /srv/fs-lab/project/{src,docs,tests}\necho -n 'ready' > /srv/fs-lab/project/.config"
|
||||
}
|
||||
],
|
||||
"setup_commands": [
|
||||
{ "command": "mkdir -p /srv/fs-lab" }
|
||||
],
|
||||
"validation": {
|
||||
"commands": [
|
||||
{ "description": "project/src exists", "command": "[ -d /srv/fs-lab/project/src ] && echo yes", "expected_output": "yes", "match": "exact" },
|
||||
{ "description": "project/docs exists", "command": "[ -d /srv/fs-lab/project/docs ] && echo yes", "expected_output": "yes", "match": "exact" },
|
||||
{ "description": "project/tests exists", "command": "[ -d /srv/fs-lab/project/tests ] && echo yes", "expected_output": "yes", "match": "exact" },
|
||||
{ "description": "hidden .config file has the right content", "command": "cat /srv/fs-lab/project/.config", "expected_output": "ready", "match": "exact" }
|
||||
]
|
||||
},
|
||||
"default_namespace": "default",
|
||||
"teardown_commands": [
|
||||
{ "command": "rm -rf /srv/fs-lab" }
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
{
|
||||
"id": "linux-inspect-hardware",
|
||||
"title": "Inspect What This Machine Actually Has",
|
||||
"category": "Linux Fundamentals",
|
||||
"isolation_type": "shared_namespace",
|
||||
"difficulty": "Easy",
|
||||
"type": "task",
|
||||
"weight": 3,
|
||||
"description": "## Inspect What This Machine Actually Has\n\nThe kernel manages CPU, memory, and storage on behalf of every process running here — before you go further, look at what's actually available.\n\n**Your task:**\n\n1. Find the number of CPUs available (`nproc`)\n2. Find the total memory in MB (`free -m`)\n3. Find the size of the root filesystem in GB (`df -BG /`)\n4. Create a file at `/srv/hardware-lab/report.txt` with exactly three lines in this format (replace `<n>` with the real numbers you found):\n\n```\nCPUS=<n>\nMEM_MB=<n>\nDISK_GB=<n>\n```",
|
||||
"hints": [
|
||||
{
|
||||
"title": "Getting each number",
|
||||
"body": "`nproc` alone prints the CPU count. `free -m` prints a table — the second column of the `Mem:` row is total memory in MB. `df -BG /` prints a table — the second column of the second row is the root filesystem size in GB (the number, not including the trailing G).",
|
||||
"command": "nproc; free -m; df -BG /"
|
||||
},
|
||||
{
|
||||
"title": "Writing the report",
|
||||
"body": "Use a heredoc or several `echo` lines with `>>` to build the file.",
|
||||
"command": "mkdir -p /srv/hardware-lab && cat <<EOF > /srv/hardware-lab/report.txt\nCPUS=$(nproc)\nMEM_MB=$(free -m | awk '/Mem:/ {print $2}')\nDISK_GB=$(df -BG / | awk 'NR==2{gsub(\"G\",\"\",$2); print $2}')\nEOF"
|
||||
}
|
||||
],
|
||||
"setup_commands": [
|
||||
{ "command": "mkdir -p /srv/hardware-lab" }
|
||||
],
|
||||
"validation": {
|
||||
"commands": [
|
||||
{ "description": "report.txt exists and has a CPUS line", "command": "grep -E '^CPUS=[0-9]+$' /srv/hardware-lab/report.txt", "expected_output": "^CPUS=[0-9]+$", "match": "regex" },
|
||||
{ "description": "report.txt has a MEM_MB line", "command": "grep -E '^MEM_MB=[0-9]+$' /srv/hardware-lab/report.txt", "expected_output": "^MEM_MB=[0-9]+$", "match": "regex" },
|
||||
{ "description": "report.txt has a DISK_GB line", "command": "grep -E '^DISK_GB=[0-9]+$' /srv/hardware-lab/report.txt", "expected_output": "^DISK_GB=[0-9]+$", "match": "regex" }
|
||||
]
|
||||
},
|
||||
"default_namespace": "default",
|
||||
"teardown_commands": [
|
||||
{ "command": "rm -rf /srv/hardware-lab" }
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
{
|
||||
"id": "linux-permissions-foundations",
|
||||
"title": "Fix a Real Permission Denial",
|
||||
"category": "Linux Fundamentals",
|
||||
"isolation_type": "shared_namespace",
|
||||
"difficulty": "Easy",
|
||||
"type": "task",
|
||||
"weight": 4,
|
||||
"description": "## Fix a Real Permission Denial\n\nA second user, `labuser`, already exists on this system. A file at `/srv/perm-lab/data.txt` currently has permissions `600` (owner read/write only) — `labuser` cannot read it. Confirm the denial is real, then fix it correctly.\n\n**Your task:**\n\n1. As `labuser`, try to read `/srv/perm-lab/data.txt` and see the permission denial for yourself: `su - labuser -c \"cat /srv/perm-lab/data.txt\"`\n2. Change the file's permissions so it's readable by everyone but still only writable by its owner (this is exactly what `644` means)\n3. Confirm `labuser` can now read it",
|
||||
"hints": [
|
||||
{
|
||||
"title": "Reading permissions as a number",
|
||||
"body": "Each of the three permission groups (owner/group/other) is one octal digit: read=4, write=2, execute=1, summed. `644` = owner read+write (6), group read-only (4), other read-only (4).",
|
||||
"command": "stat -c '%a %U' /srv/perm-lab/data.txt"
|
||||
},
|
||||
{
|
||||
"title": "Fixing it",
|
||||
"body": "`chmod 644 <file>` sets exactly that permission pattern.",
|
||||
"command": "chmod 644 /srv/perm-lab/data.txt"
|
||||
}
|
||||
],
|
||||
"setup_commands": [
|
||||
{ "command": "id labuser >/dev/null 2>&1 || useradd -m labuser" },
|
||||
{ "command": "mkdir -p /srv/perm-lab && chmod 755 /srv/perm-lab" },
|
||||
{ "command": "echo sensitive > /srv/perm-lab/data.txt && chmod 600 /srv/perm-lab/data.txt && chown root:root /srv/perm-lab/data.txt" }
|
||||
],
|
||||
"validation": {
|
||||
"commands": [
|
||||
{ "description": "data.txt is now exactly 644", "command": "stat -c '%a' /srv/perm-lab/data.txt", "expected_output": "644", "match": "exact" },
|
||||
{ "description": "labuser can actually read it now", "command": "su - labuser -c 'cat /srv/perm-lab/data.txt' 2>&1", "expected_output": "sensitive", "match": "exact" }
|
||||
]
|
||||
},
|
||||
"default_namespace": "default",
|
||||
"teardown_commands": [
|
||||
{ "command": "rm -rf /srv/perm-lab" }
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"id": "linux-permissions-mcq",
|
||||
"title": "Choosing the Minimal Permission",
|
||||
"category": "Linux Fundamentals",
|
||||
"isolation_type": "shared_namespace",
|
||||
"difficulty": "Easy",
|
||||
"type": "mcq",
|
||||
"weight": 3,
|
||||
"description": "## Choosing the Minimal Permission\n\nA private SSH key file should be readable and writable by its owner only — nobody else should be able to read, write, or execute it. Which `chmod` octal value is correct?",
|
||||
"options": [
|
||||
{ "id": "a", "text": "777" },
|
||||
{ "id": "b", "text": "644" },
|
||||
{ "id": "c", "text": "600" },
|
||||
{ "id": "d", "text": "444" }
|
||||
],
|
||||
"correct_option": "c",
|
||||
"explanation": "600 grants the owner read+write (4+2=6) and nothing at all to group or other (0, 0) — the minimum needed, and the actual permission SSH itself enforces for private key files. 644 incorrectly leaves the file readable by everyone; 777 grants full access to everyone, the opposite of least privilege.",
|
||||
"hints": [],
|
||||
"setup_commands": [],
|
||||
"default_namespace": "default",
|
||||
"teardown_commands": []
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
{
|
||||
"id": "linux-process-management",
|
||||
"title": "Find and Stop a Runaway Process",
|
||||
"category": "Linux Fundamentals",
|
||||
"isolation_type": "shared_namespace",
|
||||
"difficulty": "Easy",
|
||||
"type": "task",
|
||||
"weight": 4,
|
||||
"description": "## Find and Stop a Runaway Process\n\nA background process has been started for you and its PID recorded at `/srv/proc-lab/target.pid`. Every process in Kubernetes and Docker is, underneath, exactly this: a PID the kernel is tracking.\n\n**Your task:**\n\n1. Confirm the process is currently running (`kill -0 <pid>` succeeds silently if a process exists)\n2. Send it a termination signal: `kill <pid>` (using the PID recorded in the file, not a hardcoded number — it will be different every time)\n3. Confirm it is actually gone afterward",
|
||||
"hints": [
|
||||
{
|
||||
"title": "Reading the PID and checking it's alive",
|
||||
"body": "`kill -0` sends no real signal — it only checks whether the process exists, which is why it's the standard way to test \"is this PID still running\" without side effects.",
|
||||
"command": "cat /srv/proc-lab/target.pid\nkill -0 $(cat /srv/proc-lab/target.pid) && echo RUNNING"
|
||||
},
|
||||
{
|
||||
"title": "Terminating it",
|
||||
"body": "A plain `kill <pid>` sends SIGTERM, the polite \"please shut down\" signal — the correct first choice before ever reaching for `kill -9`.",
|
||||
"command": "kill $(cat /srv/proc-lab/target.pid)"
|
||||
}
|
||||
],
|
||||
"setup_commands": [
|
||||
{ "command": "mkdir -p /srv/proc-lab && (sleep 900 & echo $! > /srv/proc-lab/target.pid)" }
|
||||
],
|
||||
"validation": {
|
||||
"commands": [
|
||||
{ "description": "the target process has actually been terminated", "command": "kill -0 $(cat /srv/proc-lab/target.pid) 2>/dev/null && echo RUNNING || echo GONE", "expected_output": "GONE", "match": "exact" }
|
||||
]
|
||||
},
|
||||
"default_namespace": "default",
|
||||
"teardown_commands": [
|
||||
{ "command": "kill -9 $(cat /srv/proc-lab/target.pid) 2>/dev/null; rm -rf /srv/proc-lab" }
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
{
|
||||
"id": "linux-shell-basics",
|
||||
"title": "Reading Exit Codes Like an Engineer",
|
||||
"category": "Linux Fundamentals",
|
||||
"isolation_type": "shared_namespace",
|
||||
"difficulty": "Easy",
|
||||
"type": "task",
|
||||
"weight": 3,
|
||||
"description": "## Reading Exit Codes Like an Engineer\n\nEvery command you run returns an exit code — `0` means success, anything else means failure. Scripts, CI pipelines, and Kubernetes probes all rely on this exact convention.\n\n**Your task:**\n\n1. Run a command that succeeds (e.g. `true`, or `ls` on a directory that exists) and save its exit code to `/srv/shell-lab/success-exit-code.txt`\n2. Run a command that fails (e.g. `ls` on a path that does NOT exist) and save its exit code to `/srv/shell-lab/fail-exit-code.txt`\n3. Look up the manual page for `ls` (`man ls`) and find one option you didn't know about",
|
||||
"hints": [
|
||||
{
|
||||
"title": "Capturing an exit code",
|
||||
"body": "The special variable `$?` holds the exit code of the LAST command that ran — capture it immediately, before running anything else.",
|
||||
"command": "mkdir -p /srv/shell-lab\ntrue\necho $? > /srv/shell-lab/success-exit-code.txt\nls /this-path-does-not-exist 2>/dev/null\necho $? > /srv/shell-lab/fail-exit-code.txt"
|
||||
}
|
||||
],
|
||||
"setup_commands": [
|
||||
{ "command": "mkdir -p /srv/shell-lab" }
|
||||
],
|
||||
"validation": {
|
||||
"commands": [
|
||||
{ "description": "success-exit-code.txt records 0", "command": "cat /srv/shell-lab/success-exit-code.txt", "expected_output": "0", "match": "exact" },
|
||||
{ "description": "fail-exit-code.txt records a non-zero code", "command": "cat /srv/shell-lab/fail-exit-code.txt", "expected_output": "^[1-9][0-9]*$", "match": "regex" }
|
||||
]
|
||||
},
|
||||
"default_namespace": "default",
|
||||
"teardown_commands": [
|
||||
{ "command": "rm -rf /srv/shell-lab" }
|
||||
]
|
||||
}
|
||||
Reference in New Issue
Block a user