feat(root02): author Linux Administration & Networking
Adds all 6 ROOT 02 modules (Linux system administration, storage & filesystems, the Networking Mental Model, Linux network inspection, DNS & HTTP, Linux troubleshooting) — 37 activities, each with a real lesson, demonstration/guided exercise, knowledge check, hands-on Lab, and reflection. The Networking Mental Model module (M3) gets deliberately extra depth per its "critical module" designation, with no lab by design — conceptual mastery via knowledge check and guided exercise, matching root-05-m00's own precedent. Adds 6 new hands-on Linux/networking scenarios plus 2 MCQs: linux-service-inspection (inspects the platform's own real nginx without touching it — a genuinely safe, real service-inspection target that avoids ever risking the Lab's own connectivity), linux-service-lifecycle (start/ verify/stop a fully controllable service), linux-storage-inspection, linux-network-inspection (ip/ss/getent/curl, each framed as answering one specific diagnostic question), linux-dns-http-diagnosis (a genuine DNS-resolves-but-wrong-port failure, diagnosed and fixed), and linux-troubleshooting-framework (a real missing-config-file failure, resolved via the RootNode Observe/Evidence/Hypothesis/Test/Repair/Validate/Prevent framework). All tested end-to-end via the real scenario-validation API, including negative tests confirming they fail when unsolved. Wires rootnode-linux-networking-assessment (2 required + 4 random-draw scenarios) using the existing Phase 3B assessment engine. Co-Authored-By: Claude Sonnet 5 <[email protected]>
This commit is contained in:
@@ -0,0 +1,42 @@
|
||||
{
|
||||
"id": "linux-dns-http-diagnosis",
|
||||
"title": "DNS Resolves, But the Connection Still Fails — Why?",
|
||||
"category": "Networking",
|
||||
"isolation_type": "shared_namespace",
|
||||
"difficulty": "Medium",
|
||||
"type": "task",
|
||||
"weight": 6,
|
||||
"description": "## DNS Resolves, But the Connection Still Fails — Why?\n\nAn application is supposed to be reachable at `http://broken-app.test:9091/`. A teammate insists \"DNS must be broken.\" Prove or disprove that, then actually fix the problem.\n\n**Your task:**\n\n1. Confirm whether `broken-app.test` actually resolves (`getent hosts broken-app.test`) — record your finding\n2. Try `curl http://broken-app.test:9091/` and observe what actually happens\n3. Find out what's REALLY running and on which port (hint: something already answers on a different port)\n4. Fix it so `curl http://broken-app.test:9091/` succeeds with a 200 response\n\nDo not just change the app's advertised port in documentation — make the SERVICE actually listen on 9091.",
|
||||
"hints": [
|
||||
{
|
||||
"title": "Separate DNS from connectivity",
|
||||
"body": "`getent hosts broken-app.test` only tells you whether the NAME resolves — it says nothing about whether anything is listening on the port you're trying to reach. Test them as two separate questions.",
|
||||
"command": "getent hosts broken-app.test"
|
||||
},
|
||||
{
|
||||
"title": "Finding what's actually running",
|
||||
"body": "`ss -lnt` lists every port something is actually listening on right now — compare that against 9091.",
|
||||
"command": "ss -lnt"
|
||||
},
|
||||
{
|
||||
"title": "The fix",
|
||||
"body": "Stop whatever's running on the wrong port, then start the real service correctly on 9091.",
|
||||
"command": "kill $(cat /srv/dns-http-lab/wrong.pid)\npython3 -m http.server 9091 --directory /srv/dns-http-lab &\necho $! > /srv/dns-http-lab/fixed.pid"
|
||||
}
|
||||
],
|
||||
"setup_commands": [
|
||||
{ "command": "mkdir -p /srv/dns-http-lab && echo 'app running' > /srv/dns-http-lab/index.html" },
|
||||
{ "command": "grep -q broken-app.test /etc/hosts || echo '127.0.0.1 broken-app.test' >> /etc/hosts" },
|
||||
{ "command": "python3 -m http.server 9095 --directory /srv/dns-http-lab >/srv/dns-http-lab/wrong.log 2>&1 & echo $! > /srv/dns-http-lab/wrong.pid" }
|
||||
],
|
||||
"validation": {
|
||||
"commands": [
|
||||
{ "description": "broken-app.test genuinely resolves (DNS was never the actual problem)", "command": "getent hosts broken-app.test | awk '{print $1}'", "expected_output": "127.0.0.1", "match": "exact" },
|
||||
{ "description": "the service now actually answers on port 9091", "command": "curl -sf http://broken-app.test:9091/ -o /dev/null -w '%{http_code}'", "expected_output": "200", "match": "exact" }
|
||||
]
|
||||
},
|
||||
"default_namespace": "default",
|
||||
"teardown_commands": [
|
||||
{ "command": "[ -f /srv/dns-http-lab/wrong.pid ] && kill $(cat /srv/dns-http-lab/wrong.pid) 2>/dev/null; [ -f /srv/dns-http-lab/fixed.pid ] && kill $(cat /srv/dns-http-lab/fixed.pid) 2>/dev/null; rm -rf /srv/dns-http-lab" }
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
{
|
||||
"id": "linux-network-inspection",
|
||||
"title": "Answer Three Real Questions With Three Real Commands",
|
||||
"category": "Networking",
|
||||
"isolation_type": "shared_namespace",
|
||||
"difficulty": "Easy",
|
||||
"type": "task",
|
||||
"weight": 4,
|
||||
"description": "## Answer Three Real Questions With Three Real Commands\n\nEach networking command answers a specific question. Practice treating them that way.\n\n**Your task:**\n\n1. \"What IP address does this machine have?\" — write your `eth0` IPv4 address to `/srv/network-lab/my-ip.txt`\n2. \"What's listening on port 80?\" — confirm something is, and write `80` to `/srv/network-lab/listening-port.txt`\n3. \"What does DNS say `lab-internal.test` resolves to?\" — a hosts entry for it already exists on this machine; write the resolved IP to `/srv/network-lab/resolved-ip.txt`",
|
||||
"hints": [
|
||||
{
|
||||
"title": "Your own IP",
|
||||
"body": "`ip -4 addr show eth0` prints your interface's IPv4 address in an `inet a.b.c.d/nn` line.",
|
||||
"command": "ip -4 addr show eth0"
|
||||
},
|
||||
{
|
||||
"title": "What's listening",
|
||||
"body": "`ss -lnt` lists every TCP socket in LISTEN state and its port.",
|
||||
"command": "ss -lnt"
|
||||
},
|
||||
{
|
||||
"title": "Resolving a name",
|
||||
"body": "`getent hosts <name>` resolves a hostname using the same lookup mechanism the rest of the system uses (checks /etc/hosts, then DNS) — a reliable choice when you're not sure which one will answer.",
|
||||
"command": "getent hosts lab-internal.test"
|
||||
}
|
||||
],
|
||||
"setup_commands": [
|
||||
{ "command": "mkdir -p /srv/network-lab" },
|
||||
{ "command": "grep -q lab-internal.test /etc/hosts || echo '10.99.99.42 lab-internal.test' >> /etc/hosts" }
|
||||
],
|
||||
"validation": {
|
||||
"commands": [
|
||||
{ "description": "correctly identified a real IPv4 address", "command": "cat /srv/network-lab/my-ip.txt", "expected_output": "^([0-9]{1,3}\\.){3}[0-9]{1,3}$", "match": "regex" },
|
||||
{ "description": "correctly identified port 80 as listening", "command": "cat /srv/network-lab/listening-port.txt", "expected_output": "80", "match": "exact" },
|
||||
{ "description": "correctly resolved lab-internal.test", "command": "cat /srv/network-lab/resolved-ip.txt", "expected_output": "10.99.99.42", "match": "exact" }
|
||||
]
|
||||
},
|
||||
"default_namespace": "default",
|
||||
"teardown_commands": [
|
||||
{ "command": "rm -rf /srv/network-lab" }
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
{
|
||||
"id": "linux-service-inspection",
|
||||
"title": "Inspect a Real Running Service",
|
||||
"category": "Linux Administration",
|
||||
"isolation_type": "shared_namespace",
|
||||
"difficulty": "Easy",
|
||||
"type": "task",
|
||||
"weight": 4,
|
||||
"description": "## Inspect a Real Running Service\n\nThis Lab environment is itself served by a real running service: nginx, reverse-proxying the very terminal you're using right now. Answer the four questions every service-administration task starts with, using ONLY inspection commands (do not stop or restart it).\n\n**Your task:**\n\nCreate `/srv/service-lab/report.txt` with exactly three lines:\n\n```\nLISTENING_PORT=<the TCP port nginx is listening on>\nCONFIG_PATH=<the full path to its config file>\nLOG_DIR=<the directory its logs are written to>\n```",
|
||||
"hints": [
|
||||
{
|
||||
"title": "Finding the listening port",
|
||||
"body": "`ss -lnt` lists every TCP socket in LISTEN state and the port it's bound to.",
|
||||
"command": "ss -lnt"
|
||||
},
|
||||
{
|
||||
"title": "Finding the config and logs",
|
||||
"body": "nginx's config path and log directives are visible right in its own config file.",
|
||||
"command": "cat /etc/nginx/nginx.conf | head -10"
|
||||
}
|
||||
],
|
||||
"setup_commands": [
|
||||
{ "command": "mkdir -p /srv/service-lab" }
|
||||
],
|
||||
"validation": {
|
||||
"commands": [
|
||||
{ "description": "correctly identified the listening port (80)", "command": "cat /srv/service-lab/report.txt | grep '^LISTENING_PORT='", "expected_output": "LISTENING_PORT=80", "match": "exact" },
|
||||
{ "description": "correctly identified the config path", "command": "cat /srv/service-lab/report.txt | grep '^CONFIG_PATH='", "expected_output": "CONFIG_PATH=/etc/nginx/nginx.conf", "match": "exact" },
|
||||
{ "description": "correctly identified the log directory", "command": "cat /srv/service-lab/report.txt | grep '^LOG_DIR='", "expected_output": "LOG_DIR=/var/log/nginx", "match": "exact" },
|
||||
{ "description": "nginx itself was never stopped during this exercise", "command": "pgrep -x nginx >/dev/null && echo still-running || echo stopped", "expected_output": "still-running", "match": "exact" }
|
||||
]
|
||||
},
|
||||
"default_namespace": "default",
|
||||
"teardown_commands": [
|
||||
{ "command": "rm -rf /srv/service-lab" }
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
{
|
||||
"id": "linux-service-lifecycle",
|
||||
"title": "Start, Verify, and Stop a Service",
|
||||
"category": "Linux Administration",
|
||||
"isolation_type": "shared_namespace",
|
||||
"difficulty": "Easy",
|
||||
"type": "task",
|
||||
"weight": 4,
|
||||
"description": "## Start, Verify, and Stop a Service\n\nUnlike the previous exercise (inspecting a service you must never touch), this one is yours to fully control.\n\n**Your task:**\n\n1. Start a small HTTP service in the background, listening on port 8890: `python3 -m http.server 8890 --directory /srv/service-lab &`, and save its PID to `/srv/service-lab/service.pid`\n2. Confirm it's actually listening, and record the result to `/srv/service-lab/status-before.txt` (write exactly `listening`)\n3. Stop it using the PID you saved\n4. Confirm it's actually gone, and record the result to `/srv/service-lab/status-after.txt` (write exactly `stopped`)",
|
||||
"hints": [
|
||||
{
|
||||
"title": "Starting and recording the PID",
|
||||
"body": "`$!` after backgrounding a command holds its PID.",
|
||||
"command": "mkdir -p /srv/service-lab\npython3 -m http.server 8890 --directory /srv/service-lab >/srv/service-lab/service.log 2>&1 &\necho $! > /srv/service-lab/service.pid"
|
||||
},
|
||||
{
|
||||
"title": "Verifying and stopping",
|
||||
"body": "`ss -lnt | grep 8890` confirms it's listening. `kill $(cat .../service.pid)` stops it using the PID you recorded, not a hardcoded number.",
|
||||
"command": "ss -lnt | grep 8890 && echo listening > /srv/service-lab/status-before.txt\nkill $(cat /srv/service-lab/service.pid)\nsleep 1\nss -lnt | grep 8890 || echo stopped > /srv/service-lab/status-after.txt"
|
||||
}
|
||||
],
|
||||
"setup_commands": [
|
||||
{ "command": "mkdir -p /srv/service-lab" }
|
||||
],
|
||||
"validation": {
|
||||
"commands": [
|
||||
{ "description": "confirmed the service was actually listening before stopping it", "command": "cat /srv/service-lab/status-before.txt", "expected_output": "listening", "match": "exact" },
|
||||
{ "description": "confirmed the service is actually stopped now", "command": "cat /srv/service-lab/status-after.txt", "expected_output": "stopped", "match": "exact" },
|
||||
{ "description": "the process is genuinely no longer running", "command": "kill -0 $(cat /srv/service-lab/service.pid) 2>/dev/null && echo RUNNING || echo GONE", "expected_output": "GONE", "match": "exact" },
|
||||
{ "description": "port 8890 is genuinely no longer bound", "command": "ss -lnt | grep -q 8890 && echo BOUND || echo FREE", "expected_output": "FREE", "match": "exact" }
|
||||
]
|
||||
},
|
||||
"default_namespace": "default",
|
||||
"teardown_commands": [
|
||||
{ "command": "[ -f /srv/service-lab/service.pid ] && kill $(cat /srv/service-lab/service.pid) 2>/dev/null; rm -rf /srv/service-lab" }
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"id": "linux-service-state-mcq",
|
||||
"title": "Verifying a Service Is Actually Running",
|
||||
"category": "Linux Administration",
|
||||
"isolation_type": "shared_namespace",
|
||||
"difficulty": "Easy",
|
||||
"type": "mcq",
|
||||
"weight": 3,
|
||||
"description": "## Verifying a Service Is Actually Running\n\nA colleague says \"I restarted the service, it should be fine now.\" What's the single most reliable way to actually confirm the service is both running AND reachable on its expected port, without asking them?",
|
||||
"options": [
|
||||
{ "id": "a", "text": "Trust their statement — restarting almost always works" },
|
||||
{ "id": "b", "text": "Check that its process exists (e.g. via ps/pgrep) AND that its expected port is in LISTEN state (e.g. via ss) — a running process alone doesn't guarantee it bound its port successfully" },
|
||||
{ "id": "c", "text": "Check the service's source code for bugs" },
|
||||
{ "id": "d", "text": "Wait 24 hours and see if anyone complains" }
|
||||
],
|
||||
"correct_option": "b",
|
||||
"explanation": "A process can be running (visible in ps) while having failed to bind its expected port (e.g. it's already in use, or it crashed after startup logging but before binding) — checking both facts independently is what actually confirms \"running and reachable,\" not either one alone.",
|
||||
"hints": [],
|
||||
"setup_commands": [],
|
||||
"default_namespace": "default",
|
||||
"teardown_commands": []
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
{
|
||||
"id": "linux-storage-inspection",
|
||||
"title": "Find What's Actually Using the Disk",
|
||||
"category": "Linux Administration",
|
||||
"isolation_type": "shared_namespace",
|
||||
"difficulty": "Easy",
|
||||
"type": "task",
|
||||
"weight": 4,
|
||||
"description": "## Find What's Actually Using the Disk\n\nThree directories under `/srv/storage-lab/` (`logs`, `cache`, `data`) contain different amounts of data. \"It's probably the logs\" is a guess — find the real answer.\n\n**Your task:**\n\n1. Use `du` to measure the actual size of each of the three directories\n2. Use `df` to check overall filesystem usage on `/`\n3. Write the name of the directory (just `logs`, `cache`, or `data`) that uses the MOST space to `/srv/storage-lab/biggest.txt`",
|
||||
"hints": [
|
||||
{
|
||||
"title": "Measuring each directory",
|
||||
"body": "`du -sh <dir>` summarizes total size for one directory. Run it against all three, or use a wildcard to see them side by side.",
|
||||
"command": "du -sh /srv/storage-lab/*/"
|
||||
},
|
||||
{
|
||||
"title": "Overall filesystem usage",
|
||||
"body": "`df -h /` shows total/used/available space for the filesystem root lives on — a different, complementary question from `du` (which measures a specific directory's contents).",
|
||||
"command": "df -h /"
|
||||
}
|
||||
],
|
||||
"setup_commands": [
|
||||
{ "command": "mkdir -p /srv/storage-lab/logs /srv/storage-lab/cache /srv/storage-lab/data" },
|
||||
{ "command": "dd if=/dev/zero of=/srv/storage-lab/logs/app.log bs=1M count=1 2>/dev/null" },
|
||||
{ "command": "dd if=/dev/zero of=/srv/storage-lab/cache/blob.bin bs=1M count=10 2>/dev/null" },
|
||||
{ "command": "dd if=/dev/zero of=/srv/storage-lab/data/small.txt bs=1K count=2 2>/dev/null" }
|
||||
],
|
||||
"validation": {
|
||||
"commands": [
|
||||
{ "description": "correctly identified cache/ as the largest consumer", "command": "cat /srv/storage-lab/biggest.txt", "expected_output": "cache", "match": "exact" }
|
||||
]
|
||||
},
|
||||
"default_namespace": "default",
|
||||
"teardown_commands": [
|
||||
{ "command": "rm -rf /srv/storage-lab" }
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
{
|
||||
"id": "linux-troubleshooting-framework",
|
||||
"title": "A Service Won't Start — Apply the Framework, Don't Just Guess",
|
||||
"category": "Troubleshooting",
|
||||
"isolation_type": "shared_namespace",
|
||||
"difficulty": "Medium",
|
||||
"type": "task",
|
||||
"weight": 6,
|
||||
"description": "## A Service Won't Start — Apply the Framework, Don't Just Guess\n\nA startup script at `/srv/trouble-lab/start.sh` is failing. Don't just poke at it randomly — apply OBSERVE → GATHER EVIDENCE → FORM HYPOTHESIS → TEST → REPAIR → VALIDATE → PREVENT.\n\n**Your task:**\n\n1. **Observe**: run `/srv/trouble-lab/start.sh` and note that it fails\n2. **Gather evidence**: the script's own error output tells you exactly what's missing — read it\n3. **Form a hypothesis and repair**: create whatever the error says is missing, with the correct content\n4. **Validate**: rerun the script and confirm it now succeeds AND that `/srv/trouble-lab/service.ready` is created",
|
||||
"hints": [
|
||||
{
|
||||
"title": "Reading the real error",
|
||||
"body": "Redirect both stdout and stderr to a log file, then read it — the script's failure message names the exact missing path.",
|
||||
"command": "/srv/trouble-lab/start.sh > /srv/trouble-lab/service.log 2>&1; cat /srv/trouble-lab/service.log"
|
||||
},
|
||||
{
|
||||
"title": "The fix",
|
||||
"body": "The script expects a config file at a specific path with SOME content — any non-empty content satisfies it, since the script only checks that the file exists.",
|
||||
"command": "mkdir -p /srv/trouble-lab/config\necho 'mode=production' > /srv/trouble-lab/config/settings.conf\n/srv/trouble-lab/start.sh"
|
||||
}
|
||||
],
|
||||
"setup_commands": [
|
||||
{ "command": "mkdir -p /srv/trouble-lab" },
|
||||
{ "command": "printf '#!/bin/bash\\nCONFIG=/srv/trouble-lab/config/settings.conf\\nif [ ! -f \"$CONFIG\" ]; then\\n echo \"FATAL: config file not found: $CONFIG\" >&2\\n exit 1\\nfi\\necho \"Started with config: $(cat $CONFIG)\"\\necho ready > /srv/trouble-lab/service.ready\\n' > /srv/trouble-lab/start.sh" },
|
||||
{ "command": "chmod +x /srv/trouble-lab/start.sh" }
|
||||
],
|
||||
"validation": {
|
||||
"commands": [
|
||||
{ "description": "the service now starts successfully", "command": "/srv/trouble-lab/start.sh >/dev/null 2>&1; echo $?", "expected_output": "0", "match": "exact" },
|
||||
{ "description": "the ready marker was actually created", "command": "cat /srv/trouble-lab/service.ready", "expected_output": "ready", "match": "exact" }
|
||||
]
|
||||
},
|
||||
"default_namespace": "default",
|
||||
"teardown_commands": [
|
||||
{ "command": "rm -rf /srv/trouble-lab" }
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"id": "linux-troubleshooting-order-mcq",
|
||||
"title": "Where Does the Failure Actually Live?",
|
||||
"category": "Troubleshooting",
|
||||
"isolation_type": "shared_namespace",
|
||||
"difficulty": "Medium",
|
||||
"type": "mcq",
|
||||
"weight": 4,
|
||||
"description": "## Where Does the Failure Actually Live?\n\n`getent hosts myapp.internal` correctly returns an IP address. `curl http://myapp.internal:8080/` returns \"Connection refused.\" What does this combination actually tell you?",
|
||||
"options": [
|
||||
{ "id": "a", "text": "DNS is broken — fix the hosts file or DNS server first" },
|
||||
{ "id": "b", "text": "The name resolves correctly, so the problem is downstream of DNS — most likely nothing is listening on port 8080 at that address" },
|
||||
{ "id": "c", "text": "The application returned an error page" },
|
||||
{ "id": "d", "text": "The network cable is unplugged" }
|
||||
],
|
||||
"correct_option": "b",
|
||||
"explanation": "A successful DNS resolution proves the name-to-IP step works — it tells you nothing about whether anything is listening on the port you're trying to reach. \"Connection refused\" specifically means a TCP SYN reached the destination and was actively rejected, most commonly because no process is listening on that port. This is exactly the kind of layer-by-layer reasoning the module's framework teaches: never re-diagnose a layer that already gave you a clean answer.",
|
||||
"hints": [],
|
||||
"setup_commands": [],
|
||||
"default_namespace": "default",
|
||||
"teardown_commands": []
|
||||
}
|
||||
Reference in New Issue
Block a user