diff --git a/.github/workflows/autograde.yml b/.github/workflows/autograde.yml new file mode 100644 index 0000000..6be37db --- /dev/null +++ b/.github/workflows/autograde.yml @@ -0,0 +1,60 @@ +name: Autograde + +on: + push: + branches: ["**"] + pull_request: + workflow_dispatch: + +permissions: + contents: read + pull-requests: write + +jobs: + grade: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-node@v4 + with: + node-version: "20" + cache: "npm" + + - name: Install dependencies (bats) + run: npm ci + + - name: Run autograder + id: grade + continue-on-error: true + run: npm run grade + + - name: Publish score to job summary + if: always() + run: cat grade-report.md >> "$GITHUB_STEP_SUMMARY" || true + + - name: Comment score on pull request + if: always() && github.event_name == 'pull_request' + uses: actions/github-script@v7 + with: + script: | + const fs = require('fs'); + const marker = ''; + let body = marker + '\n'; + try { body += fs.readFileSync('grade-report.md', 'utf8'); } + catch { body += 'Autograder did not produce a report.'; } + const { owner, repo } = context.repo; + const issue_number = context.issue.number; + const { data: comments } = await github.rest.issues.listComments({ owner, repo, issue_number }); + const existing = comments.find(c => c.body && c.body.includes(marker)); + if (existing) { + await github.rest.issues.updateComment({ owner, repo, comment_id: existing.id, body }); + } else { + await github.rest.issues.createComment({ owner, repo, issue_number, body }); + } + + - name: Fail the check if incomplete + if: steps.grade.outcome != 'success' + run: | + echo "Exercises are not yet complete — see the autograde summary above." + exit 1 diff --git a/.gitignore b/.gitignore index 646ac51..bd41f50 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,5 @@ .DS_Store node_modules/ +.grade/ +grade-report.md +errors.log diff --git a/LEARNER_GUIDE.md b/LEARNER_GUIDE.md deleted file mode 100644 index fbbc2ef..0000000 --- a/LEARNER_GUIDE.md +++ /dev/null @@ -1,69 +0,0 @@ -# Learner Guide - -## You Are Learning to Think Like an Engineer - -This module is designed for a motivated beginner, including a senior high school student who has not used Linux before. You are not expected to already know server vocabulary. You are expected to slow down, make predictions, test them, and explain what happened in clear language. - -## You Are an Engineer, Not a Student - -The single most important mindset shift in this module: **you are a Junior Software Engineer on a team, completing real work.** Lessons are tickets. Labs are your sandbox. The notebook is your professional record. When you finish the module, you should be able to point to artifacts — scripts, configs, documentation, a working server — and say "I built that, and I can explain every line." - -This changes how you should approach problems: - -- **Investigate before you ask.** When something breaks, your first move is to look — read the error, check the logs, inspect the state. Senior engineers are not people who memorized answers; they are people who know how to find them. -- **Understand before you run.** Never execute a command you cannot explain, especially one suggested by an AI. If you do not understand it, look it up first. This is the golden rule of the module. -- **Document as you go.** If it is not written down, it did not happen. Your engineering notebook is graded as seriously as your commands. - -## How Each Lesson Works - -Work through a lesson in order: - -1. **Read the ticket.** Understand what "done" means before touching the keyboard. -2. **Study the Beginner Map and deep dive.** Learn the big idea, vocabulary, and mental model before trying to memorize commands. -3. **Do the labs using predict → run → explain.** Each lab gives you safe practice. Before each command, predict what will happen; after it runs, explain the output in your own words. -4. **Complete the assignment.** This is what actually closes the ticket. Produce the deliverables. -5. **Do the AI exercise.** Practice drafting with AI, then verifying and logging — the workflow you will use for the rest of your career. -6. **Write your reflection.** A few honest sentences about what clicked and what was hard. -7. **Check the Definition of Done.** Hold yourself to the bar before submitting. - -## What Every Assignment Must Include - -Treat these as non-negotiable parts of "done," regardless of the specific ticket: - -- **Engineering notebook updates** — what you did, what you observed, what you decided, and why -- **Documentation** — clear enough that a teammate could reproduce your work without you -- **Git commits** — small, meaningful commits with messages that explain the *why* -- **Reflection** — honest self-assessment -- **AI verification** — evidence that you checked, not just trusted, any AI output -- **Definition of Done** — the checklist at the end of each lesson, satisfied - -Use `assignments/submission-template.md` as your per-lesson deliverable format. - -## Using AI Responsibly - -AI tools are part of modern engineering, and this module teaches you to use them well rather than pretending they do not exist. The workflow is always the same: - -1. **Draft** — ask the AI for a first pass at a command, script, or explanation. -2. **Verify** — check it against documentation, `--help`, a man page, or a safe test. Confirm you understand *why* it works. -3. **Log** — record in your notebook what you asked, what you got, how you verified it, and what you changed. - -The full method, including a verification-tool table, is in `resources/ai-workflow-guide.md`. The one rule that overrides everything: **never paste a command you don't understand.** - -## When You Get Stuck - -Work the problem in this order before reaching for help: - -1. **Read the error message.** It is usually telling you exactly what is wrong. -2. **Check the relevant log** (`journalctl`, files under `/var/log`, etc.). -3. **Consult** `resources/troubleshooting-playbook.md` for a layered diagnosis recipe. -4. **Look up the command** in `resources/command-cheatsheet.md` or with `man`. -5. **Reproduce it small.** Strip the problem down to the smallest case that still fails. -6. **Then ask** — but ask with what you have already tried and observed. - -## The Server You Build Is Forever - -The development server you build, secure, and document in this module is **reused throughout the rest of the SWEXP program.** Every shortcut you take now is debt you carry into later modules; every habit you build well now pays off repeatedly. Build it like you will have to live with it — because you will. - -## How You Are Graded - -Your work is assessed against `ASSESSMENT_RUBRIC.md`. Note that technical correctness is necessary but not sufficient: documentation, engineering practice, automation quality, and responsible AI use all carry real weight. A correct command with no explanation and no notebook entry is not a finished ticket. diff --git a/Lesson_00.md b/Lesson_00.md deleted file mode 100644 index 6a669c6..0000000 --- a/Lesson_00.md +++ /dev/null @@ -1,195 +0,0 @@ -# Lesson 0 — Welcome to the Team - -> **SWEXP Role:** Junior Software Engineer, Platform & Infrastructure -> **Competency:** Engineering Onboarding & Environment Setup -> **Estimated time:** 2–3 hours - ---- - -## 🎫 Engineering Ticket - -``` -TICKET: INFRA-1001 -TITLE: New engineer onboarding — establish your working environment -PRIORITY: P2 (blocking your first sprint) -ASSIGNEE: You -REPORTER: Platform Team Lead - -DESCRIPTION: -Welcome to the Platform team. Before you can be assigned production work, -you must stand up a working Linux environment, prove you can reach it, -and open your engineering notebook + Git repository. We don't hand out -production access until a new engineer can demonstrate a reproducible, -documented setup. - -ACCEPTANCE CRITERIA: -- A Linux environment you control (VM, WSL2, cloud instance, or container). -- You can show `uname -a`, `whoami`, and `pwd` output. -- A Git repository named `swexp-engineering-notebook` exists with at - least one commit. -- Your notebook contains an "Environment" entry describing how the box - was created so a teammate could reproduce it. -``` - ---- - -## 🎯 Learning Objectives - -By the end of this lesson you will be able to: - -1. Explain what a shell, a terminal, and a kernel are, and how they relate. -2. Choose and provision a Linux environment appropriate to your hardware. -3. Navigate a filesystem and identify your current context (`whoami`, `pwd`, `id`). -4. Initialize a Git repository and make a first commit. -5. Keep an **engineering notebook** the way a professional engineer does. - ---- - - -## 🧭 Beginner Map - -### Big idea -A Linux computer is a set of layers. You type into a terminal window, the shell reads your command, and Linux does the work. Before you can solve bigger problems, you need to know where you are, who you are, and how to record what you did. - -### Key vocabulary -- **Terminal:** The window or app where you type commands. -- **Shell:** The program, usually Bash, that reads your command and starts programs. -- **Kernel:** The core of Linux that manages hardware, memory, files, and running programs. -- **Command:** A small instruction you ask the shell to run. -- **Engineering notebook:** A dated record of what you tried, what happened, and what you learned. - -### Mental model -Think of Linux like a school building. The terminal is the front desk window, the shell is the person who takes your request, and the kernel is the building operations team that can actually unlock rooms, turn on power, and move equipment. You do not talk to the operations team directly; your commands go through the shell. - -### How to study this lesson -1. Read the ticket as a story first: what problem needs solving? -2. Learn the vocabulary before memorizing commands. -3. Predict what each command will show before running it. -4. Run the command, compare the output to your prediction, and write one sentence about what changed in your understanding. - -## 📚 Technical Deep Dive - -### The mental model: kernel → shell → terminal - -Linux is a **kernel** — the program that talks to hardware, schedules -processes, and owns memory. You never talk to the kernel directly. Instead -you type into a **shell** (commonly `bash` or `zsh`), a program that reads -your commands, asks the kernel to do the work, and prints results. The -**terminal** (or terminal *emulator*) is just the window that displays the -shell. People say "the command line," "the terminal," and "the shell" -interchangeably, but they are three distinct layers. - -``` -You ⟶ Terminal (the window) ⟶ Shell (bash) ⟶ Kernel ⟶ Hardware -``` - -### Where am I, and who am I? - -Three commands answer the questions every session begins with: - -| Command | Question it answers | Example output | -|-----------|----------------------------------|---------------------------| -| `whoami` | Which user am I acting as? | `engineer` | -| `pwd` | Which directory am I in? | `/home/engineer` | -| `id` | What groups/privileges do I have?| `uid=1000(engineer) ...` | -| `uname -a`| What kernel/OS am I on? | `Linux box 6.x ... x86_64`| - -A surprising amount of production confusion comes from engineers not -knowing *which user on which host* they are. Build the habit now. - -### The engineering notebook - -Professional engineers keep a running log of what they did, why, and what -happened. It is not a diary — it is a reproducibility tool. When an incident -postmortem asks "what changed?", your notebook is the answer. We keep ours -in Markdown, in Git, so every change is timestamped and reviewable. - ---- - - -## 🔎 Guided Command Reading - -Run `whoami`, `pwd`, and `id` one at a time before running them together. After each command, say out loud: `who am I?`, `where am I?`, and `what groups am I part of?` If the answer surprises you, stop and write it down. - -When you see a command in this lesson, break it into three parts: - -1. **Program:** the command being run, such as `ls`, `grep`, or `systemctl`. -2. **Options:** flags that change behavior, such as `-l`, `-h`, or `--since`. -3. **Target:** the file, directory, service, host, or text the command acts on. - -Write the command in your notebook, then add: **What I expected**, **What happened**, and **What I think it means**. - -## ⚠️ Common Beginner Mistakes - -- Copying setup commands without knowing which machine they affect. -- Forgetting to configure Git name/email before the first commit. -- Writing notebook notes like `set up Linux` instead of exact reproducible steps. - -## 🧠 Check Your Understanding - -Before starting the assignment, answer these in your own words: - -- Explain the difference between terminal and shell without using the word `thing`. -- Point to the command output that proves which OS you are using. -- List the exact steps a friend would need to rebuild your environment. - -## 🧪 Hands-on Labs - -> Full step-by-step versions live in [`labs/lab-00-environment.md`](labs/lab-00-environment.md). - -**Lab 0.1 — Provision your box.** Pick one path: -- **WSL2** (Windows): `wsl --install -d Ubuntu` -- **VM**: Install Ubuntu Server 24.04 in VirtualBox/UTM. -- **Cloud**: Smallest available instance on any provider. -- **Container** (fastest): `docker run -it --name swexp ubuntu:24.04 bash` - -**Lab 0.2 — Identify yourself.** Run and capture the output of: -```bash -whoami; pwd; id; uname -a; cat /etc/os-release -``` - -**Lab 0.3 — Open your notebook.** -```bash -mkdir -p ~/swexp-engineering-notebook && cd ~/swexp-engineering-notebook -git init -printf '# Engineering Notebook\n\n## %s — Environment\n' "$(date +%F)" > NOTEBOOK.md -git add NOTEBOOK.md && git commit -m "INFRA-1001: open engineering notebook" -``` - ---- - -## 📝 Assignment - -Write your first **Environment** notebook entry. It must let a teammate -reproduce your setup *without asking you a single question*. Include: the -provisioning path you chose, the exact commands or steps, the OS version, -and one thing that surprised you. Commit it referencing `INFRA-1001`. - ---- - -## 🤖 AI Engineering Exercise - -Ask an AI assistant: *"Explain the difference between a shell, a terminal, -and a kernel, then quiz me with three questions."* Then **verify** its -answer against `man bash` and `uname --help`. In your notebook, record one -thing the AI got right and one thing you had to correct or confirm yourself. -**Never paste a command you don't understand into a real server** — this is -the rule that survives every later lesson. - ---- - -## 🪞 Reflection - -- What was confusing about the kernel/shell/terminal distinction? -- If your laptop died right now, how long would it take to rebuild this box? -- What will you put in your notebook that "future you" will thank you for? - ---- - -## ✅ Definition of Done - -- [ ] Linux environment provisioned and reachable. -- [ ] `whoami`, `pwd`, `id`, `uname -a` output captured in the notebook. -- [ ] Git repo `swexp-engineering-notebook` created with ≥1 commit. -- [ ] Environment entry is reproducible by a teammate. -- [ ] AI exercise completed with a verification note. diff --git a/Lesson_01.md b/Lesson_01.md deleted file mode 100644 index a345867..0000000 --- a/Lesson_01.md +++ /dev/null @@ -1,191 +0,0 @@ -# Lesson 1 — Explore an Unknown Linux System - -> **Competency:** Filesystem & Orientation -> **Estimated time:** 2–3 hours - ---- - -## 🎫 Engineering Ticket - -``` -TICKET: INFRA-1042 -TITLE: Document an undocumented legacy server before migration -PRIORITY: P2 -DESCRIPTION: -We inherited a server from a team that no longer exists. There is no -documentation. Before we migrate or decommission it, we need a map of the -system: what kind of host it is, where things live, how big the disk is, -and what looks unusual. Treat this like an archaeology dig — observe, don't -change anything yet. - -ACCEPTANCE CRITERIA: -- A written "system survey" describing OS, kernel, disk usage, and the - top-level filesystem layout. -- At least three findings worth flagging (large dirs, odd files, etc.). -- Zero modifications to the system (read-only investigation). -``` - ---- - -## 🎯 Learning Objectives - -1. Navigate the Linux filesystem hierarchy with confidence. -2. Use `ls`, `cd`, `pwd`, `tree`, `file`, and `stat` to inspect files. -3. Distinguish absolute vs. relative paths and special paths (`.`, `..`, `~`, `/`). -4. Measure disk and directory usage with `df` and `du`. -5. Read a system's identity from `/etc/os-release`, `/proc`, and `uname`. - ---- - - -## 🧭 Beginner Map - -### Big idea -A filesystem is a map. Linux starts that map at `/`, then organizes programs, settings, logs, and user files into predictable places. Your job is to observe the map without changing it. - -### Key vocabulary -- **Root `/`:** The top of the Linux filesystem tree. -- **Directory:** A folder that can contain files or other directories. -- **Absolute path:** A path that starts at `/` and works from anywhere. -- **Relative path:** A path understood from your current directory. -- **Mount:** A filesystem attached somewhere in the tree. -- **Virtual filesystem:** A folder-like view generated by Linux, such as `/proc`, not normal files on disk. - -### Mental model -Picture a family tree turned upside down. `/` is the trunk. `/home/student` and `/var/log` are branches. An absolute path names the route from the trunk; a relative path starts wherever you are standing. - -### How to study this lesson -1. Read the ticket as a story first: what problem needs solving? -2. Learn the vocabulary before memorizing commands. -3. Predict what each command will show before running it. -4. Run the command, compare the output to your prediction, and write one sentence about what changed in your understanding. - -## 📚 Technical Deep Dive - -### The Filesystem Hierarchy Standard (FHS) - -Linux organizes everything under a single root, `/`. There are no drive -letters. The most important top-level directories: - -| Path | Purpose | -|----------|-----------------------------------------------------| -| `/bin`, `/usr/bin` | Executable programs (commands) | -| `/etc` | System-wide **configuration** (text files) | -| `/home` | Per-user home directories | -| `/var` | Variable data — **logs** (`/var/log`), spools, caches | -| `/tmp` | Temporary files, wiped on reboot | -| `/proc` | Virtual files exposing **live kernel/process state**| -| `/opt` | Optional / third-party software | -| `/root` | The root user's home (not the same as `/`) | - -`/proc` is special: it isn't on disk. `cat /proc/cpuinfo` reads the CPU -straight from the kernel. This is how monitoring tools work. - -### Paths: absolute vs. relative - -- **Absolute** paths start at root: `/var/log/syslog`. They always mean the - same thing regardless of where you are. -- **Relative** paths are interpreted from your current directory: `log/syslog`. -- Special tokens: `.` = here, `..` = parent, `~` = your home, `-` = previous dir. - -### Listing and inspecting - -```bash -ls -lah # long, all (incl. hidden), human-readable sizes -file mystery.bin # what *kind* of file is this, really? -stat report.txt # size, permissions, and three timestamps -du -sh * # size of each item in this directory -df -h # disk usage per mounted filesystem -``` - -A common trap: a file named `script.txt` may actually be an ELF binary. -`file` reads the *contents*, not the name — trust `file`, not the extension. - ---- - - -## 🔎 Guided Command Reading - -When you run `ls -lah /`, do not just look at the names. Read one line: the first character tells you file type, the permission letters tell you access, the owner/group tell you who controls it, and the size/time tell you how recently it changed. - -When you see a command in this lesson, break it into three parts: - -1. **Program:** the command being run, such as `ls`, `grep`, or `systemctl`. -2. **Options:** flags that change behavior, such as `-l`, `-h`, or `--since`. -3. **Target:** the file, directory, service, host, or text the command acts on. - -Write the command in your notebook, then add: **What I expected**, **What happened**, and **What I think it means**. - -## ⚠️ Common Beginner Mistakes - -- Confusing `/root` with `/`. `/root` is the root user's home; `/` is the top of everything. -- Using `du` and `df` as if they answer the same question. `df` asks filesystems; `du` walks directories. -- Treating `/proc` sizes as normal disk usage. - -## 🧠 Check Your Understanding - -Before starting the assignment, answer these in your own words: - -- From `/home/student/projects`, where does `../notes` point? -- Why is `/var/log` usually interesting during troubleshooting? -- What command tells you whether a file's name matches its contents? - -## 🧪 Hands-on Labs - -> See [`labs/lab-01-filesystem-survey.md`](labs/lab-01-filesystem-survey.md). - -**Lab 1.1 — Identify the host.** -```bash -cat /etc/os-release; uname -r; nproc; free -h -``` - -**Lab 1.2 — Map the filesystem.** -```bash -ls -lah /; ls -lah /etc | head; ls -lah /var/log -``` - -**Lab 1.3 — Find the heavy hitters.** -```bash -df -h -du -sh /var/* 2>/dev/null | sort -rh | head -``` - -**Lab 1.4 — Classify mystery files.** Create a few files, then identify them: -```bash -echo "hello" > a.txt; cp /bin/ls ./b; file a.txt b -``` - ---- - -## 📝 Assignment - -Produce a **System Survey** document (`survey.md`) for your own box as if it -were the legacy server. Include OS/kernel, CPU/RAM, `df -h` output, -the three largest directories under `/var`, and three "findings" you would -flag to a teammate. Commit referencing `INFRA-1042`. - ---- - -## 🤖 AI Engineering Exercise - -Give an AI the raw output of `df -h` and `du -sh /var/*` and ask it to -summarize where disk is being consumed and what to investigate first. -Then **verify**: re-run the commands yourself and confirm the AI's largest -directory is actually the largest. Note any hallucinated paths. - ---- - -## 🪞 Reflection - -- Which directory surprised you in size? -- How is `/proc` different from every other directory? -- When would a relative path bite you in a script? - ---- - -## ✅ Definition of Done - -- [ ] `survey.md` committed with OS, kernel, CPU/RAM, and disk data. -- [ ] Three flagged findings documented. -- [ ] No files on the system were modified during investigation. -- [ ] AI summary verified against real command output. diff --git a/Lesson_02.md b/Lesson_02.md deleted file mode 100644 index 6ece28f..0000000 --- a/Lesson_02.md +++ /dev/null @@ -1,192 +0,0 @@ -# Lesson 2 — Recover the Lost Project - -> **Competency:** File Management -> **Estimated time:** 2–3 hours - ---- - -## 🎫 Engineering Ticket - -``` -TICKET: INFRA-1077 -TITLE: Reconstruct a developer's project from scattered backup fragments -PRIORITY: P1 -DESCRIPTION: -A departing developer left a tarball of their home directory and a mess of -loose files in /tmp. Product needs the "checkout-service" project rebuilt -into a clean, conventional layout so a new owner can take it over. Files are -duplicated, misnamed, and scattered. Rebuild it carefully — losing the only -copy of a file is unacceptable. - -ACCEPTANCE CRITERIA: -- A clean project tree under ~/projects/checkout-service. -- Source, config, and docs separated into conventional subdirectories. -- An archive (.tar.gz) of the rebuilt project as a restore point. -- A manifest listing every file and where it came from. -``` - ---- - -## 🎯 Learning Objectives - -1. Create, copy, move, rename, and delete files and directories safely. -2. Use wildcards/globbing (`*`, `?`, `[...]`, `{...}`) to act on many files. -3. Create and extract archives with `tar` and compress with `gzip`. -4. Use `find` to locate files by name, type, size, and time. -5. Apply *defensive* file operations (`-i`, `cp -a`, dry runs) to avoid data loss. - ---- - - -## 🧭 Beginner Map - -### Big idea -File management is careful movement. Engineers often need to reorganize messy files, but every copy, move, or delete should be deliberate and reversible. - -### Key vocabulary -- **Globbing:** The shell expanding patterns like `*.py` into matching filenames. -- **Archive:** One file that bundles many files together. -- **Checksum:** A fingerprint used to compare file contents. -- **Dry run:** A safe preview before doing the real action. -- **Metadata:** Information about a file, such as permissions and timestamps. - -### Mental model -Imagine cleaning a messy backpack before turning in a project. You first spread everything out, group related items, remove duplicates only when you are sure, then zip the final folder as a backup. - -### How to study this lesson -1. Read the ticket as a story first: what problem needs solving? -2. Learn the vocabulary before memorizing commands. -3. Predict what each command will show before running it. -4. Run the command, compare the output to your prediction, and write one sentence about what changed in your understanding. - -## 📚 Technical Deep Dive - -### The core verbs - -```bash -mkdir -p projects/checkout-service/{src,config,docs} # -p makes parents -cp -a source/ dest/ # archive mode: preserves perms, times, links -mv old.txt new.txt # rename (move within same dir) -rm -i file # interactive: ask before each delete -rmdir emptydir # only removes EMPTY dirs (safe) -``` - -`mv` is both "move" and "rename" — there is no separate rename command. -`cp -a` (or `-rp`) is the safe default for copying trees because it -preserves ownership, permissions, and timestamps. - -### Globbing: the shell expands it, not the command - -When you type `rm *.log`, the **shell** expands `*.log` into a list of -matching files *before* `rm` ever runs. This is why a stray space in -`rm -rf / tmp/cache` is catastrophic — it becomes `rm -rf /` plus -`tmp/cache`. Globs: - -| Pattern | Matches | -|-----------|--------------------------------------------| -| `*` | any number of characters | -| `?` | exactly one character | -| `[abc]` | one of a, b, or c | -| `{a,b}` | brace expansion → `a` and `b` | - -### Archives with tar - -`tar` (tape archive) bundles many files into one; `gzip` compresses. The -mnemonic for **c**reate: `tar -czf` = **c**reate **z**ip **f**ile. For -e**x**tract: `tar -xzf`. - -```bash -tar -czf checkout-service.tar.gz checkout-service/ # create -tar -tzf checkout-service.tar.gz # list (verify!) -tar -xzf checkout-service.tar.gz # extract -``` - -Always **list** (`-t`) before you extract into a real directory. - -### find: the search workhorse - -```bash -find . -name '*.py' # by name -find . -type f -size +1M # files over 1 MB -find . -type d -empty # empty directories -find . -newermt '2025-01-01' # modified after a date -find . -name '*.tmp' -delete # act on matches (careful!) -``` - ---- - - -## 🔎 Guided Command Reading - -Before any command with `rm`, `mv`, `cp`, or `find ... -delete`, run a preview command first. For example, run `printf '%s -' *.py` to see what the shell will expand before using those matches. - -When you see a command in this lesson, break it into three parts: - -1. **Program:** the command being run, such as `ls`, `grep`, or `systemctl`. -2. **Options:** flags that change behavior, such as `-l`, `-h`, or `--since`. -3. **Target:** the file, directory, service, host, or text the command acts on. - -Write the command in your notebook, then add: **What I expected**, **What happened**, and **What I think it means**. - -## ⚠️ Common Beginner Mistakes - -- Assuming `*.py` is passed to `cp`; the shell expands it first. -- Deleting duplicates by filename instead of comparing contents. -- Creating an archive but never listing it with `tar -tzf` to verify it. - -## 🧠 Check Your Understanding - -Before starting the assignment, answer these in your own words: - -- What does `main?.py` match that `main*.py` might not? -- Why is `cp -a` safer than plain `cp` for a project tree? -- What should you do before extracting an unfamiliar tarball? - -## 🧪 Hands-on Labs - -> See [`labs/lab-02-file-recovery.md`](labs/lab-02-file-recovery.md), which -> ships a script to generate the "scattered" project mess. - -**Lab 2.1 — Build the target tree** with `mkdir -p` and brace expansion. - -**Lab 2.2 — Locate fragments** with `find`, then `cp -a` them into place. - -**Lab 2.3 — De-duplicate** using `find ... -exec` and checksums (`md5sum`). - -**Lab 2.4 — Archive** the result and verify with `tar -tzf`. - ---- - -## 📝 Assignment - -Rebuild the provided scattered project into `~/projects/checkout-service` -with `src/`, `config/`, and `docs/`. Produce `MANIFEST.md` mapping each -final file to its origin, and a verified `.tar.gz` restore point. Commit -referencing `INFRA-1077`. - ---- - -## 🤖 AI Engineering Exercise - -Describe your scattered files to an AI and ask it to propose a `find` + -`cp` strategy. **Verify before running**: run each `find` with no action -first to see what it *would* match. Record in your notebook any case where -the AI's command would have copied or deleted the wrong files. - ---- - -## 🪞 Reflection - -- When did defensive flags (`-i`, dry-run `find`) save you? -- Why does the shell — not `rm` — expand the `*`? -- What would you automate if you had to do this 100 times? - ---- - -## ✅ Definition of Done - -- [ ] Clean `checkout-service` tree with conventional subdirectories. -- [ ] `MANIFEST.md` maps every file to its source. -- [ ] Verified `.tar.gz` restore point exists. -- [ ] No file was lost; duplicates resolved deliberately. diff --git a/Lesson_03.md b/Lesson_03.md deleted file mode 100644 index 077b916..0000000 --- a/Lesson_03.md +++ /dev/null @@ -1,196 +0,0 @@ -# Lesson 3 — Permission Denied - -> **Competency:** Permissions -> **Estimated time:** 2–3 hours - ---- - -## 🎫 Engineering Ticket - -``` -TICKET: SEC-2031 -TITLE: Deploy script fails with "Permission denied" in production -PRIORITY: P1 (deploys blocked) -DESCRIPTION: -The deploy pipeline cannot write to /opt/app/releases and the runtime user -cannot read /etc/app/secrets.env. A previous engineer "fixed" a similar -issue with chmod 777, which security has flagged. Diagnose the real -permission model, fix it correctly, and explain why 777 is dangerous. - -ACCEPTANCE CRITERIA: -- Deploy user can write to the releases directory. -- Runtime user can read (but not write) the secrets file. -- No world-writable files remain. -- A short writeup of the permission model you applied. -``` - ---- - -## 🎯 Learning Objectives - -1. Read and interpret the 10-character permission string from `ls -l`. -2. Set permissions with both symbolic (`u+x`) and octal (`750`) notation. -3. Manage ownership with `chown` and `chgrp`. -4. Explain why `777` is almost always wrong, and what to use instead. -5. Understand the role of `sudo` and the principle of least privilege. - ---- - - -## 🧭 Beginner Map - -### Big idea -Permissions are Linux's safety rules. They answer: who owns this, who can read it, who can change it, and who can run or enter it? - -### Key vocabulary -- **Owner:** The user account that controls a file. -- **Group:** A named set of users who can share access. -- **Other:** Everyone who is not the owner and not in the group. -- **Read/write/execute:** The three basic permission actions. -- **Least privilege:** Giving only the access required, not extra access. - -### Mental model -Think of a school lab cabinet. The teacher may open and restock it, lab partners may open it during class, and everyone else should not touch it. `777` is like leaving the cabinet unlocked for the entire school. - -### How to study this lesson -1. Read the ticket as a story first: what problem needs solving? -2. Learn the vocabulary before memorizing commands. -3. Predict what each command will show before running it. -4. Run the command, compare the output to your prediction, and write one sentence about what changed in your understanding. - -## 📚 Technical Deep Dive - -### Reading `ls -l` - -``` --rwxr-x--- 1 deploy engineers 4096 Jun 1 12:00 deploy.sh -│└┬┘└┬┘└┬┘ └──┬─┘ └───┬───┘ -│ │ │ │ owner group -│ │ │ └── other: --- (no access) -│ │ └───── group: r-x (read + execute) -│ └──────── owner: rwx (read + write + execute) -└────────── type: - file, d dir, l symlink -``` - -Each of the three triads is **read (4) / write (2) / execute (1)**. -Permissions apply differently to files vs. directories: - -| Bit | On a file | On a directory | -|-----|------------------|-----------------------------------------| -| r | read contents | list entries (`ls`) | -| w | modify contents | create/delete entries inside | -| x | execute as program | enter/traverse (`cd`) into it | - -This is the classic gotcha: you can have `r` on a directory but be unable to -`cd` into it without `x`. - -### Octal notation - -Add the bits per triad. `rwx` = 7, `r-x` = 5, `r--` = 4, `---` = 0. - -```bash -chmod 750 deploy.sh # owner rwx, group r-x, other none -chmod u+x,g-w file # symbolic: add owner-execute, remove group-write -chmod -R 640 config/ # recursive (watch out: this strips dir +x!) -``` - -### Ownership - -```bash -chown deploy:engineers deploy.sh # set owner AND group -chown -R appuser /opt/app/releases # whole tree -chgrp engineers report.txt # group only -``` - -### Why `777` is dangerous - -`777` means **anyone on the system can read, modify, and execute** the file -— including other services, other users, and an attacker who lands a -low-privilege shell. The correct fix is almost always to set the right -**owner/group** and grant the *minimum* bits needed. Secrets like -`secrets.env` should be `640` owned by `root:appgroup` — readable by the -service, writable by no one but root, invisible to everyone else. - -### sudo and least privilege - -`sudo` runs a single command as another user (usually root). Prefer it over -logging in as root. Grant engineers only the specific `sudo` rights they -need, not blanket root. Every `sudo` use is logged in `/var/log/auth.log`. - ---- - - -## 🔎 Guided Command Reading - -Read permissions in three chunks: owner, group, other. In `-rw-r-----`, owner is `rw-`, group is `r--`, and other is `---`. Translate each chunk into a sentence before converting to numbers. - -When you see a command in this lesson, break it into three parts: - -1. **Program:** the command being run, such as `ls`, `grep`, or `systemctl`. -2. **Options:** flags that change behavior, such as `-l`, `-h`, or `--since`. -3. **Target:** the file, directory, service, host, or text the command acts on. - -Write the command in your notebook, then add: **What I expected**, **What happened**, and **What I think it means**. - -## ⚠️ Common Beginner Mistakes - -- Using `chmod 777` because it is quick. It hides the real access problem and creates a security problem. -- Forgetting that directory `x` means the ability to enter/traverse the directory. -- Using recursive permissions on directories and files without noticing they need different execute bits. - -## 🧠 Check Your Understanding - -Before starting the assignment, answer these in your own words: - -- Why can `chmod -R 644 some_directory` break access? -- Who can read a file with mode `640` owned by `root:appgroup`? -- What is the smallest permission set that meets the ticket requirement? - -## 🧪 Hands-on Labs - -> See [`labs/lab-03-permissions.md`](labs/lab-03-permissions.md). - -**Lab 3.1 — Decode permission strings** for ten sample `ls -l` lines. - -**Lab 3.2 — Reproduce the bug**: create a dir owned by root, try to write -to it as your user, observe the denial. - -**Lab 3.3 — Fix it correctly** with `chown` + `chmod 750` instead of `777`. - -**Lab 3.4 — Lock down a secret** to `640 root:appgroup` and verify the -runtime user can read but not write. - ---- - -## 📝 Assignment - -Diagnose and fix the SEC-2031 scenario (lab provides the broken setup). -Submit `permissions-writeup.md` showing the *before* (`ls -l`) state, the -exact `chown`/`chmod` commands you ran, the *after* state, and a paragraph -explaining why `777` was wrong. Commit referencing `SEC-2031`. - ---- - -## 🤖 AI Engineering Exercise - -Paste an `ls -l` listing to an AI and ask it to translate each permission -string into plain English and flag anything insecure. **Verify** by -computing the octal yourself for at least three lines. Note any line the AI -mislabeled (world-writable files are the ones that matter). - ---- - -## 🪞 Reflection - -- Why can `chmod -R 644` *break* a directory tree? -- What's the smallest permission that still satisfies the requirement? -- Where does least privilege apply outside of file permissions? - ---- - -## ✅ Definition of Done - -- [ ] Deploy user can write to releases; runtime user can read secrets. -- [ ] No world-writable (`o+w`) files remain (`find / -perm -002` is clean for app dirs). -- [ ] Writeup explains the model and why 777 was rejected. -- [ ] All changes made via `chown`/`chmod`, not `777`. diff --git a/Lesson_04.md b/Lesson_04.md deleted file mode 100644 index b01a895..0000000 --- a/Lesson_04.md +++ /dev/null @@ -1,191 +0,0 @@ -# Lesson 4 — Onboard a New Engineering Team - -> **Competency:** Users & Groups -> **Estimated time:** 2–3 hours - ---- - -## 🎫 Engineering Ticket - -``` -TICKET: INFRA-1120 -TITLE: Provision accounts and access for the new Payments squad -PRIORITY: P2 -DESCRIPTION: -A four-person Payments squad starts Monday. They need user accounts, a -shared `payments` group, a shared working directory only their group can -touch, and sane defaults so new files they create stay group-accessible. -Two of them need limited sudo (service restarts only). Do this -reproducibly — we onboard teams constantly. - -ACCEPTANCE CRITERIA: -- Four user accounts with home directories and a default shell. -- A `payments` group containing all four. -- A shared dir /srv/payments that only the group can read/write. -- New files in the shared dir inherit the group (setgid). -- Two users granted sudo for systemctl restart only. -``` - ---- - -## 🎯 Learning Objectives - -1. Create and manage users (`useradd`, `passwd`, `usermod`, `userdel`). -2. Create and manage groups and supplementary group membership. -3. Read `/etc/passwd`, `/etc/group`, and `/etc/shadow`. -4. Use the **setgid** bit and shared directories for team collaboration. -5. Grant scoped `sudo` rights via `/etc/sudoers.d/`. - ---- - - -## 🧭 Beginner Map - -### Big idea -Users and groups let many people safely share one Linux system. Instead of giving everyone full control, you create accounts, put the right people in the right groups, and share directories through group rules. - -### Key vocabulary -- **User account:** An identity Linux can assign files and processes to. -- **Primary group:** The default group for a user's new files. -- **Supplementary group:** An extra group a user belongs to. -- **setgid directory:** A directory where new files inherit the directory's group. -- **sudoers:** Configuration controlling who may run commands as another user. - -### Mental model -A group is like a club roster. If a folder belongs to the `payments` club, then club members can collaborate there while non-members stay out. - -### How to study this lesson -1. Read the ticket as a story first: what problem needs solving? -2. Learn the vocabulary before memorizing commands. -3. Predict what each command will show before running it. -4. Run the command, compare the output to your prediction, and write one sentence about what changed in your understanding. - -## 📚 Technical Deep Dive - -### Where users live - -`/etc/passwd` has one line per account: -``` -alice:x:1001:1001:Alice Ng:/home/alice:/bin/bash -└─┬─┘ │ └─┬┘ └─┬┘ └──┬──┘ └───┬────┘ └───┬───┘ - name pwd uid gid GECOS home shell -``` -The `x` means the password hash lives in `/etc/shadow` (readable only by -root). `/etc/group` maps group names to GIDs and lists members. - -### Creating users and groups - -```bash -groupadd payments -useradd -m -s /bin/bash -G payments alice # -m home, -s shell, -G group -passwd alice # set initial password -usermod -aG payments bob # ADD to group (-a is critical!) -``` - -> **Trap:** `usermod -G payments bob` *replaces* all of bob's supplementary -> groups. Always use `-aG` to *append*. - -### Shared directories and setgid - -A normal new file gets the creator's primary group. For a shared team dir -you want every new file to belong to `payments` automatically. The -**setgid** bit on a directory does exactly that: - -```bash -mkdir -p /srv/payments -chgrp payments /srv/payments -chmod 2770 /srv/payments # leading 2 = setgid; 770 = group rwx, other none -``` - -Now any file created inside inherits the `payments` group. The `2` is the -setgid bit; `1` would be the sticky bit (used on `/tmp`). - -### Scoped sudo - -Never hand out full root. Drop a file in `/etc/sudoers.d/`: -``` -# /etc/sudoers.d/payments-restart -alice ALL=(root) NOPASSWD: /usr/bin/systemctl restart payments.service -``` -Validate it with `visudo -c` — a syntax error in sudoers can lock everyone out. - ---- - - -## 🔎 Guided Command Reading - -When reading `/etc/passwd`, split the line on colons. Each field has a job: username, password marker, UID, GID, description, home directory, and shell. Do not memorize first; learn to decode. - -When you see a command in this lesson, break it into three parts: - -1. **Program:** the command being run, such as `ls`, `grep`, or `systemctl`. -2. **Options:** flags that change behavior, such as `-l`, `-h`, or `--since`. -3. **Target:** the file, directory, service, host, or text the command acts on. - -Write the command in your notebook, then add: **What I expected**, **What happened**, and **What I think it means**. - -## ⚠️ Common Beginner Mistakes - -- Running `usermod -G group user` on an existing user and accidentally replacing their other groups. -- Making shared directories world-writable instead of using group ownership and setgid. -- Editing sudoers without validation. - -## 🧠 Check Your Understanding - -Before starting the assignment, answer these in your own words: - -- What does `-a` add to `usermod -aG`? -- How can you prove a new file inherited the `payments` group? -- Why should sudo access be limited to specific commands? - -## 🧪 Hands-on Labs - -> See [`labs/lab-04-users-groups.md`](labs/lab-04-users-groups.md). - -**Lab 4.1 — Create the squad**: four users + `payments` group. - -**Lab 4.2 — Build the shared dir** with setgid; prove inheritance by -creating a file as one user and checking its group. - -**Lab 4.3 — Cross-user test**: as user A, create a file; as user B (same -group), edit it. Confirm a non-member is denied. - -**Lab 4.4 — Scoped sudo**: grant restart rights and verify other commands -are still refused. - ---- - -## 📝 Assignment - -Onboard the Payments squad on your box. Deliver an **idempotent** -`onboard-payments.sh` script (it should be safe to run twice), plus -`onboarding-report.md` showing `/etc/group`, the shared dir permissions, -and a successful cross-user write test. Commit referencing `INFRA-1120`. - ---- - -## 🤖 AI Engineering Exercise - -Ask an AI to write the onboarding script for you. Then **audit it line by -line**: does it use `-aG` or the destructive `-G`? Does it set passwords -insecurely? Does it make the script idempotent? Record at least one issue -you found and fixed. (Most AI-generated user scripts get group append or -idempotency wrong.) - ---- - -## 🪞 Reflection - -- Why is `usermod -aG` vs `-G` such a common production incident? -- What does setgid buy you that plain group ownership doesn't? -- How would you offboard the squad just as cleanly? - ---- - -## ✅ Definition of Done - -- [ ] Four accounts + `payments` group exist. -- [ ] `/srv/payments` is setgid and group-restricted. -- [ ] New files inherit the group (demonstrated). -- [ ] Two users have scoped sudo; `visudo -c` passes. -- [ ] Onboarding script is idempotent. diff --git a/Lesson_05.md b/Lesson_05.md deleted file mode 100644 index 7f61c02..0000000 --- a/Lesson_05.md +++ /dev/null @@ -1,187 +0,0 @@ -# Lesson 5 — Investigate a Production Outage Using Log Files - -> **Competency:** Text Processing -> **Estimated time:** 3 hours - ---- - -## 🎫 Engineering Ticket - -``` -TICKET: OPS-3301 -TITLE: Checkout API returned 500s for ~20 minutes — find the cause -PRIORITY: P1 (revenue impact) -DESCRIPTION: -Between 14:00 and 14:25 UTC customers saw checkout failures. We have the -access log and the application log. Find when it started, how many requests -were affected, which endpoint, and the most likely root cause — using only -the command line. No log-aggregation UI is available on this box. - -ACCEPTANCE CRITERIA: -- The exact first and last timestamp of the 500 spike. -- Count of affected requests and the top offending endpoint. -- The application error message correlated with the spike. -- A timeline reconstructed purely from log text. -``` - ---- - -## 🎯 Learning Objectives - -1. View and follow logs with `cat`, `less`, `head`, `tail -f`. -2. Search text with `grep` (and `-i`, `-v`, `-c`, `-n`, `-E`, `-r`). -3. Extract and reshape fields with `cut`, `awk`, and `sort | uniq -c`. -4. Edit streams with `sed`. -5. Reconstruct an incident timeline from raw logs. - ---- - - -## 🧭 Beginner Map - -### Big idea -Logs are the computer's diary. Text tools help you find patterns in that diary: when a problem started, how many users were affected, and what probably caused it. - -### Key vocabulary -- **Log:** A text record of events from a system or application. -- **Filter:** Keep only lines that match a condition. -- **Field:** One piece of a structured line, such as status code or URL. -- **Regex:** A pattern language for matching text. -- **Timeline:** Events ordered by time so cause and effect are easier to see. - -### Mental model -Investigating logs is like reviewing security camera timestamps. First find the suspicious window, then count what happened, then match it with another camera angle. - -### How to study this lesson -1. Read the ticket as a story first: what problem needs solving? -2. Learn the vocabulary before memorizing commands. -3. Predict what each command will show before running it. -4. Run the command, compare the output to your prediction, and write one sentence about what changed in your understanding. - -## 📚 Technical Deep Dive - -### The viewers - -`less` is the pager you live in: `/pattern` searches, `n`/`N` jumps between -hits, `G` goes to the end, `q` quits. For live logs, `tail -f file.log` -streams new lines as they're written — the single most-used incident command. - -### grep: find the needle - -```bash -grep "500" access.log # lines containing 500 -grep -c "500" access.log # COUNT matching lines -grep -i "error" app.log # case-insensitive -grep -v "healthcheck" access.log # INVERT: lines NOT matching -grep -n "Exception" app.log # show line numbers -grep -E "50[0-9]" access.log # extended regex: 500–509 -grep -r "panic" /var/log/ # recursive across files -``` - -### awk: columns and counting - -A typical access-log line is space-separated. `awk` treats each field as -`$1, $2, ...`: -```bash -# Count requests per HTTP status (field 9 in common log format) -awk '{print $9}' access.log | sort | uniq -c | sort -rn - -# Show only requests with status 500 -awk '$9 == 500 {print $7}' access.log # $7 = the URL path -``` - -`sort | uniq -c | sort -rn` is the **"top N" idiom** — memorize it. `uniq` -only collapses *adjacent* duplicates, so you must `sort` first. - -### cut and sed - -```bash -cut -d' ' -f1 access.log # first space-delimited field (IP) -cut -d: -f1 /etc/passwd # usernames from passwd -sed -n '100,120p' app.log # print only lines 100–120 -sed 's/secret=[^ ]*/secret=REDACTED/' app.log # redact in stream -``` - -### Putting it together — a timeline - -```bash -grep " 500 " access.log | awk '{print $4, $7}' | head # when + what -grep -i exception app.log | head -5 # the why -``` - ---- - - -## 🔎 Guided Command Reading - -For an access log line, number the fields with your finger before using `awk`. In common log format, `$1` is often IP, `$7` is path, and `$9` is status. Verify this with `head -1 access.log` before trusting a command. - -When you see a command in this lesson, break it into three parts: - -1. **Program:** the command being run, such as `ls`, `grep`, or `systemctl`. -2. **Options:** flags that change behavior, such as `-l`, `-h`, or `--since`. -3. **Target:** the file, directory, service, host, or text the command acts on. - -Write the command in your notebook, then add: **What I expected**, **What happened**, and **What I think it means**. - -## ⚠️ Common Beginner Mistakes - -- Counting a sample and treating it like the whole file. -- Using `uniq -c` before sorting, which splits identical items into separate groups. -- Reporting 500 errors as the cause instead of the symptom. - -## 🧠 Check Your Understanding - -Before starting the assignment, answer these in your own words: - -- What does each stage in `awk '{print $9}' access.log | sort | uniq -c | sort -rn` do? -- Which command gives you the first matching error? -- What evidence links the app log to the access log? - -## 🧪 Hands-on Labs - -> See [`labs/lab-05-log-investigation.md`](labs/lab-05-log-investigation.md); -> it generates realistic `access.log` and `app.log` files with a planted incident. - -**Lab 5.1 — Status breakdown** with the top-N idiom. - -**Lab 5.2 — Bound the incident**: first and last 500 timestamps. - -**Lab 5.3 — Find the endpoint** driving the errors. - -**Lab 5.4 — Correlate** the app-log exception to the spike window. - ---- - -## 📝 Assignment - -Investigate the planted outage and write `incident-timeline.md`: a minute-by- -minute reconstruction with the commands you used, the affected count, the -offending endpoint, and your root-cause hypothesis. Commit referencing -`OPS-3301`. - ---- - -## 🤖 AI Engineering Exercise - -Paste 50 log lines into an AI and ask it to summarize the incident. Then -run the equivalent `grep`/`awk` yourself on the *full* file. AIs are great -at narrating a sample but will miss volume — note where your full-file count -differed from the AI's eyeballed estimate. - ---- - -## 🪞 Reflection - -- Why must you `sort` before `uniq -c`? -- Which single command would you reach for first in a live outage? -- What did the app log tell you that the access log couldn't? - ---- - -## ✅ Definition of Done - -- [ ] First/last 500 timestamps identified. -- [ ] Affected request count and top endpoint reported. -- [ ] App-log error correlated to the window. -- [ ] Timeline reproducible from the documented commands. diff --git a/Lesson_06.md b/Lesson_06.md deleted file mode 100644 index 4927eaf..0000000 --- a/Lesson_06.md +++ /dev/null @@ -1,189 +0,0 @@ -# Lesson 6 — Build a Production Log Processing Pipeline - -> **Competency:** Pipelines, Redirection & Regex -> **Estimated time:** 3 hours - ---- - -## 🎫 Engineering Ticket - -``` -TICKET: OPS-3344 -TITLE: Nightly log report — turn raw access logs into a metrics summary -PRIORITY: P2 -DESCRIPTION: -After OPS-3301 we want a repeatable report we can run on demand: total -requests, status-code breakdown, top 10 endpoints, top 10 client IPs, and a -list of any 5xx lines saved to a separate file for review. It must read -from a file OR from stdin (so it can sit in a pipeline), and write clean -output we could email. - -ACCEPTANCE CRITERIA: -- A single pipeline/script producing the report. -- Reads from a file argument or stdin. -- 5xx lines saved to errors.log via redirection. -- Output is deterministic and human-readable. -``` - ---- - -## 🎯 Learning Objectives - -1. Compose commands with pipes (`|`) into data pipelines. -2. Redirect stdin/stdout/stderr (`<`, `>`, `>>`, `2>`, `2>&1`, `tee`). -3. Understand the three standard streams and exit codes. -4. Write extended regular expressions for matching and extraction. -5. Combine `grep`/`awk`/`sort`/`uniq`/`tee` into a reusable report. - ---- - - -## 🧭 Beginner Map - -### Big idea -A pipeline is an assembly line for text. Each command does one small job, then passes its output to the next command. - -### Key vocabulary -- **stdin:** Input stream number 0. -- **stdout:** Normal output stream number 1. -- **stderr:** Error output stream number 2. -- **Pipe:** Connects one command's stdout to the next command's stdin. -- **Redirection:** Sends input/output to or from files. -- **Deterministic:** Produces the same output every time for the same input. - -### Mental model -Imagine a school lunch line: tray → food → drink → checkout. A pipeline is similar: raw log → filter → extract → count → report. - -### How to study this lesson -1. Read the ticket as a story first: what problem needs solving? -2. Learn the vocabulary before memorizing commands. -3. Predict what each command will show before running it. -4. Run the command, compare the output to your prediction, and write one sentence about what changed in your understanding. - -## 📚 Technical Deep Dive - -### Three streams, three numbers - -Every process has **stdin (0)**, **stdout (1)**, and **stderr (2)**. By -default stdout and stderr both go to your terminal, but they are separate -channels so you can route them independently. - -```bash -cmd > out.txt # stdout to file (truncate) -cmd >> out.txt # stdout appended -cmd 2> err.txt # stderr to file -cmd > out.txt 2>&1 # both to same file (order matters!) -cmd < input.txt # feed file as stdin -cmd 2>/dev/null # discard stderr -``` - -`2>&1` means "send stream 2 to wherever stream 1 currently points." Put it -*after* the redirect of stdout, or it won't follow. - -### Pipes: stdout of one → stdin of next - -```bash -cat access.log | grep " 500 " | awk '{print $7}' | sort | uniq -c | sort -rn -``` - -Each `|` connects programs into a small assembly line. Prefer -`grep ... file` over `cat file | grep ...` when there's a single input — -but in teaching pipelines, `cat` makes the data flow obvious. - -### tee: split the stream - -`tee` writes to a file **and** passes data onward, so you can save and keep -processing: -```bash -grep -E " 5[0-9][0-9] " access.log | tee errors.log | wc -l -``` - -### Reading from file OR stdin - -A robust report reads `"$1"` if given, else stdin. The idiom: -```bash -cat "${1:-/dev/stdin}" | ... # use arg 1, or stdin if absent -``` - -### Regex levels - -- **BRE** (basic, default grep): `\+`, `\?` are literal-ish; escape groups. -- **ERE** (`grep -E`, `awk`): `+ ? | ( )` work without backslashes. -Use `grep -E` for anything non-trivial. Common atoms: `^` start, `$` end, -`[0-9]` digit class, `.` any char, `\.` literal dot. - ---- - - -## 🔎 Guided Command Reading - -Build pipelines one pipe at a time. Run the first command and inspect the output. Then add the next `| command`. If the output becomes wrong, the bug is probably in the stage you just added. - -When you see a command in this lesson, break it into three parts: - -1. **Program:** the command being run, such as `ls`, `grep`, or `systemctl`. -2. **Options:** flags that change behavior, such as `-l`, `-h`, or `--since`. -3. **Target:** the file, directory, service, host, or text the command acts on. - -Write the command in your notebook, then add: **What I expected**, **What happened**, and **What I think it means**. - -## ⚠️ Common Beginner Mistakes - -- Forgetting that stderr is separate from stdout. -- Putting `2>&1` before redirecting stdout and expecting both streams to go to the file. -- Optimizing a pipeline before proving the output is correct. - -## 🧠 Check Your Understanding - -Before starting the assignment, answer these in your own words: - -- What flows through a pipe? -- Why does `tee` help when you need to save and continue processing? -- How can you prove two versions of a report are identical? - -## 🧪 Hands-on Labs - -> See [`labs/lab-06-pipeline.md`](labs/lab-06-pipeline.md). - -**Lab 6.1 — Stream routing**: redirect stdout and stderr to different files. - -**Lab 6.2 — Build the report pipeline** incrementally, adding one stage at a time. - -**Lab 6.3 — `tee` the 5xx lines** to `errors.log` while counting them. - -**Lab 6.4 — Make it stdin-capable** so `cat access.log | report.sh` works. - ---- - -## 📝 Assignment - -Write `logreport.sh`: reads a file arg or stdin, prints total requests, -status breakdown, top 10 endpoints, top 10 IPs, and tees 5xx lines to -`errors.log`. Include sample output in `report-sample.md`. Commit -referencing `OPS-3344`. - ---- - -## 🤖 AI Engineering Exercise - -Ask an AI to "explain this pipeline" — paste your longest pipe chain. Verify -each stage by running the pipeline truncated at each `|`. Then ask the AI to -optimize it; test whether its "optimization" produces *identical* output -(it often changes ordering). Record the diff. - ---- - -## 🪞 Reflection - -- Why does the position of `2>&1` matter? -- When is `cat file |` an anti-pattern, and when is it fine? -- How would you turn this report into a nightly cron job? (Foreshadows L13.) - ---- - -## ✅ Definition of Done - -- [ ] `logreport.sh` reads file arg AND stdin. -- [ ] Report includes counts, status breakdown, top endpoints/IPs. -- [ ] 5xx lines tee'd to `errors.log`. -- [ ] Output is deterministic across runs. diff --git a/Lesson_07.md b/Lesson_07.md deleted file mode 100644 index 5009a34..0000000 --- a/Lesson_07.md +++ /dev/null @@ -1,187 +0,0 @@ -# Lesson 7 — Build Your Engineering Workspace - -> **Competency:** Shell Environment & Configuration -> **Estimated time:** 2 hours - ---- - -## 🎫 Engineering Ticket - -``` -TICKET: INFRA-1180 -TITLE: Standardize the team's shell environment -PRIORITY: P3 -DESCRIPTION: -Every engineer's shell is configured differently, which makes pairing and -debugging painful. Define a sane, documented default environment: a useful -prompt, common aliases, sensible PATH handling, and an editor default. It -must be version-controlled (dotfiles) so a fresh box can be configured in -one command. - -ACCEPTANCE CRITERIA: -- A versioned dotfiles repo with .bashrc / .bash_aliases. -- A clear, informative prompt (PS1) showing user, host, and cwd. -- At least 8 useful, safe aliases and 2 functions. -- A one-command bootstrap that symlinks dotfiles into place. -``` - ---- - -## 🎯 Learning Objectives - -1. Distinguish environment variables from shell variables; use `export`. -2. Explain `PATH` and how the shell resolves commands. -3. Customize the prompt (`PS1`) and create aliases and functions. -4. Understand startup files: `.bashrc` vs `.bash_profile` vs `.profile`. -5. Manage dotfiles in Git for reproducible environments. - ---- - - -## 🧭 Beginner Map - -### Big idea -Your shell environment is your workbench. Variables, PATH, aliases, functions, and dotfiles decide what tools are easy to reach and how repeatable your setup is. - -### Key vocabulary -- **Shell variable:** A value known only to the current shell. -- **Environment variable:** A value exported to child programs. -- **PATH:** Directories searched when you type a command name. -- **Alias:** A shortcut for a command. -- **Function:** A reusable shell command that can accept arguments. -- **Dotfile:** A hidden configuration file, usually starting with `.`. - -### Mental model -A clean workbench has labeled drawers and tools in predictable places. Dotfiles are the instructions for rebuilding that workbench on a new computer. - -### How to study this lesson -1. Read the ticket as a story first: what problem needs solving? -2. Learn the vocabulary before memorizing commands. -3. Predict what each command will show before running it. -4. Run the command, compare the output to your prediction, and write one sentence about what changed in your understanding. - -## 📚 Technical Deep Dive - -### Variables and the environment - -```bash -NAME="checkout" # shell variable (this shell only) -export NAME # now exported to child processes (environment) -echo "$NAME"; printenv NAME -``` -Child processes inherit the *environment*, not plain shell variables. This -is why `export` matters for things like `PATH` and `EDITOR`. - -### PATH: how a command is found - -`PATH` is a colon-separated list of directories. When you type `ls`, the -shell searches each directory in `PATH` in order and runs the first match. -`which ls` / `type ls` shows what would run. -```bash -echo "$PATH" -export PATH="$HOME/bin:$PATH" # prepend your own bin (takes priority) -``` -Appending vs prepending changes precedence — prepend to override, append to -keep system commands first. - -### Startup files (the confusing part) - -- **Login shell** (SSH in, console): reads `.bash_profile` → which usually - sources `.bashrc`. -- **Interactive non-login shell** (new terminal tab): reads `.bashrc`. -- Best practice: put everything in `.bashrc`, and have `.bash_profile` - source it, so behavior is consistent. - -### A useful prompt - -```bash -# user@host:cwd$ with color -PS1='\[\e[32m\]\u@\h\[\e[0m\]:\[\e[34m\]\w\[\e[0m\]\$ ' -``` -`\u` user, `\h` host, `\w` working dir, `\$` becomes `#` for root. - -### Aliases and functions - -```bash -alias ll='ls -lah' -alias gs='git status' -alias ..='cd ..' -mkcd() { mkdir -p "$1" && cd "$1"; } # a function takes arguments -``` -Aliases can't take positional args the way functions can — reach for a -function when you need `$1`. - ---- - - -## 🔎 Guided Command Reading - -Use `type command_name` before assuming what will run. It tells you whether a word is an alias, function, shell builtin, or executable file found through PATH. - -When you see a command in this lesson, break it into three parts: - -1. **Program:** the command being run, such as `ls`, `grep`, or `systemctl`. -2. **Options:** flags that change behavior, such as `-l`, `-h`, or `--since`. -3. **Target:** the file, directory, service, host, or text the command acts on. - -Write the command in your notebook, then add: **What I expected**, **What happened**, and **What I think it means**. - -## ⚠️ Common Beginner Mistakes - -- Changing PATH in a way that hides system commands. -- Putting interactive-only commands in startup files used by non-interactive scripts. -- Creating clever aliases that are unsafe or confusing to teammates. - -## 🧠 Check Your Understanding - -Before starting the assignment, answer these in your own words: - -- Why does `export EDITOR=vim` affect child programs but `EDITOR=vim` alone may not? -- When should you use a function instead of an alias? -- How does a bootstrap script lower the cost of a new laptop? - -## 🧪 Hands-on Labs - -> See [`labs/lab-07-dotfiles.md`](labs/lab-07-dotfiles.md). - -**Lab 7.1 — Inspect** your current `PATH`, `HOME`, and `SHELL`. - -**Lab 7.2 — Build a prompt** and reload with `source ~/.bashrc`. - -**Lab 7.3 — Add aliases + an `mkcd` function**; prove they survive a new shell. - -**Lab 7.4 — Bootstrap script** that symlinks dotfiles from the repo. - ---- - -## 📝 Assignment - -Create a `dotfiles` Git repo with `.bashrc`, `.bash_aliases`, and an -`install.sh` that symlinks them into `$HOME` (backing up any existing -files). Document each alias/function in the repo README. Commit referencing -`INFRA-1180`. - ---- - -## 🤖 AI Engineering Exercise - -Ask an AI to design a `PS1` prompt that shows the current git branch. Verify -it doesn't slow your shell to a crawl (the naive version runs `git` on every -prompt). Measure with `time` and note the tradeoff in your notebook. - ---- - -## 🪞 Reflection - -- Why does `export` matter, and what breaks without it? -- What's the difference between an alias and a function, practically? -- How does versioning dotfiles change the cost of a new laptop? - ---- - -## ✅ Definition of Done - -- [ ] Dotfiles repo with `.bashrc`/`.bash_aliases` committed. -- [ ] Informative colored prompt working. -- [ ] ≥8 aliases + 2 functions, all documented. -- [ ] `install.sh` reproducibly configures a fresh shell. diff --git a/Lesson_08.md b/Lesson_08.md deleted file mode 100644 index 47e9855..0000000 --- a/Lesson_08.md +++ /dev/null @@ -1,192 +0,0 @@ -# Lesson 8 — Automate Developer Onboarding - -> **Competency:** Bash Scripting & Automation -> **Estimated time:** 3–4 hours - ---- - -## 🎫 Engineering Ticket - -``` -TICKET: INFRA-1205 -TITLE: One-command onboarding script for new developer machines -PRIORITY: P2 -DESCRIPTION: -New hires spend a day manually configuring their dev box. Automate it: the -script should create the project directory layout, install required tools -(idempotently), clone repos from a list, validate that prerequisites exist, -and print a clear success/failure summary with exit codes a CI system can -trust. Re-running it must be safe. - -ACCEPTANCE CRITERIA: -- A robust Bash script with strict mode and error handling. -- Idempotent: safe to run repeatedly. -- Validates prerequisites and fails loudly with a non-zero exit code. -- Logs actions and prints a final summary. -``` - ---- - -## 🎯 Learning Objectives - -1. Write Bash scripts with shebangs, arguments, and `set -euo pipefail`. -2. Use variables, conditionals, loops, and functions. -3. Test conditions with `[[ ... ]]`, file tests, and exit codes (`$?`). -4. Make scripts idempotent and defensive. -5. Handle errors with `trap` and meaningful exit codes. - ---- - - -## 🧭 Beginner Map - -### Big idea -Automation turns a checklist into a repeatable program. A good script is safe to run twice, explains failures clearly, and exits with a code that other tools can trust. - -### Key vocabulary -- **Script:** A file of commands run by an interpreter. -- **Shebang:** The first line that chooses the interpreter. -- **Strict mode:** Bash options that make hidden bugs fail loudly. -- **Idempotent:** Safe to run repeatedly with the same final result. -- **Exit code:** A number reporting success or failure to other programs. -- **Trap:** A handler that runs when an error or signal happens. - -### Mental model -A script is like a recipe for a robot. If a step fails, the robot should stop and say exactly which ingredient or tool is missing. - -### How to study this lesson -1. Read the ticket as a story first: what problem needs solving? -2. Learn the vocabulary before memorizing commands. -3. Predict what each command will show before running it. -4. Run the command, compare the output to your prediction, and write one sentence about what changed in your understanding. - -## 📚 Technical Deep Dive - -### Anatomy of a robust script - -```bash -#!/usr/bin/env bash -set -euo pipefail # exit on error, unset var, and pipe failures -IFS=$'\n\t' # safer word-splitting - -log() { printf '[%s] %s\n' "$(date +%T)" "$*"; } -fail() { printf 'ERROR: %s\n' "$*" >&2; exit 1; } -``` - -`set -e` exits on any command failure; `set -u` errors on undefined -variables (catches typos); `set -o pipefail` makes a pipeline fail if *any* -stage fails, not just the last. This trio prevents the most common silent -script bugs. - -### Conditionals and tests - -```bash -if [[ -d "$dir" ]]; then log "exists"; else mkdir -p "$dir"; fi -[[ -f "$file" ]] # file exists -[[ -x "$bin" ]] # executable exists -[[ -z "$var" ]] # string empty -[[ "$a" == "$b" ]] # string equality -command -v git >/dev/null || fail "git is required" -``` -Use `[[ ]]` (Bash) over `[ ]` (POSIX) — it's safer with spaces and supports -`&&`, `||`, and pattern matching. - -### Loops - -```bash -for repo in "${REPOS[@]}"; do - [[ -d "$repo" ]] && { log "skip $repo (exists)"; continue; } - git clone "https://example.com/$repo.git" || fail "clone $repo failed" -done -``` - -### Idempotency - -An idempotent script produces the same end state whether run once or ten -times. Patterns: `mkdir -p` (no error if exists), check-before-create, -`install` only if `command -v` is missing. This is what separates a toy -script from a production one. - -### Traps and exit codes - -```bash -trap 'fail "interrupted at line $LINENO"' ERR -``` -Return `0` on success, non-zero on failure — CI and other scripts rely on -exit codes to know what happened. - ---- - - -## 🔎 Guided Command Reading - -Read `set -euo pipefail` as three safety switches: stop on failed commands, stop on missing variables, and stop if any part of a pipeline fails. - -When you see a command in this lesson, break it into three parts: - -1. **Program:** the command being run, such as `ls`, `grep`, or `systemctl`. -2. **Options:** flags that change behavior, such as `-l`, `-h`, or `--since`. -3. **Target:** the file, directory, service, host, or text the command acts on. - -Write the command in your notebook, then add: **What I expected**, **What happened**, and **What I think it means**. - -## ⚠️ Common Beginner Mistakes - -- Leaving variables unquoted, which breaks paths with spaces. -- Printing an error but still exiting with `0`. -- Writing a script that works once but fails or duplicates work on the second run. - -## 🧠 Check Your Understanding - -Before starting the assignment, answer these in your own words: - -- What exact checks make your script idempotent? -- How would CI know your script failed? -- What did ShellCheck catch that your eyes missed? - -## 🧪 Hands-on Labs - -> See [`labs/lab-08-onboarding-script.md`](labs/lab-08-onboarding-script.md). - -**Lab 8.1 — Hello, strict mode**: a script that fails loudly on a bad command. - -**Lab 8.2 — Prereq checker** using `command -v` in a loop. - -**Lab 8.3 — Idempotent directory + clone** logic. - -**Lab 8.4 — Summary + exit codes**: count successes/failures, exit non-zero on any failure. - ---- - -## 📝 Assignment - -Write `onboard-dev.sh`: strict mode, prerequisite validation, idempotent -directory/repo setup, logging, and a final summary with correct exit codes. -Run it twice in your notebook to prove idempotency. Commit referencing -`INFRA-1205`. - ---- - -## 🤖 AI Engineering Exercise - -Have an AI generate the onboarding script, then run it through `shellcheck` -(`shellcheck onboard-dev.sh`). Record every warning ShellCheck raised that -the AI missed — typically unquoted variables and missing `set -u`. Fix them. -This is the core lesson: AI drafts, tools verify. - ---- - -## 🪞 Reflection - -- Which `set` option caught a bug you didn't expect? -- What made your script idempotent — and how did you test it? -- Why do exit codes matter more than printed messages to a machine? - ---- - -## ✅ Definition of Done - -- [ ] Script uses `set -euo pipefail` and a `trap`. -- [ ] Prerequisites validated; missing ones fail with non-zero exit. -- [ ] Idempotent (proven by running twice). -- [ ] Passes `shellcheck` with no errors. diff --git a/Lesson_09.md b/Lesson_09.md deleted file mode 100644 index 935bb15..0000000 --- a/Lesson_09.md +++ /dev/null @@ -1,194 +0,0 @@ -# Lesson 9 — Production Service Failure - -> **Competency:** Process & Service Management -> **Estimated time:** 3 hours - ---- - -## 🎫 Engineering Ticket - -``` -TICKET: OPS-3401 -TITLE: payments.service keeps dying under load — stabilize it -PRIORITY: P1 -DESCRIPTION: -The payments service is flapping: it starts, runs for a minute, then exits. -A runaway process is also pinning a CPU core. Identify the offending -processes, understand why the service won't stay up, and bring it back to a -healthy, auto-restarting state under systemd. Capture evidence so we can -write the postmortem. - -ACCEPTANCE CRITERIA: -- Identify the runaway process and its resource usage. -- Determine why the service exits (logs + exit status). -- Restore the service to running + enabled (survives reboot). -- Evidence captured for the postmortem. -``` - ---- - -## 🎯 Learning Objectives - -1. Inspect processes with `ps`, `top`/`htop`, and `/proc`. -2. Understand PIDs, parent/child, foreground/background, and signals. -3. Send signals with `kill`, `kill -9`, and `pkill`. -4. Manage services with `systemctl` and read logs with `journalctl`. -5. Diagnose why a service fails to stay running. - ---- - - -## 🧭 Beginner Map - -### Big idea -Processes are programs while they are running. Service management is how Linux starts, stops, restarts, and records long-running programs. - -### Key vocabulary -- **Process:** A running program. -- **PID:** The unique number for a process. -- **Signal:** A message sent to a process. -- **Service:** A managed background program. -- **systemd:** The service manager on many Linux systems. -- **Journal:** systemd's log store. -- **Zombie:** A finished process whose parent has not collected its status. - -### Mental model -A process is like a student currently doing a task. A PID is their seat number. A signal is a note passed to them: please stop, reload, or stop immediately. - -### How to study this lesson -1. Read the ticket as a story first: what problem needs solving? -2. Learn the vocabulary before memorizing commands. -3. Predict what each command will show before running it. -4. Run the command, compare the output to your prediction, and write one sentence about what changed in your understanding. - -## 📚 Technical Deep Dive - -### Seeing processes - -```bash -ps aux # every process, with CPU/MEM -ps aux --sort=-%cpu | head # top CPU consumers -top # live, interactive (press 'q' to quit, 'M' sort by memory) -pgrep -fl payments # find PIDs by name -``` -Each process has a **PID** and a **PPID** (parent). When a parent dies, -children may be re-parented to PID 1. A **zombie** (`Z` state) is a finished -process whose parent hasn't collected its exit status — harmless unless they pile up. - -### Signals - -A signal is a message to a process. The important ones: - -| Signal | Number | Meaning | -|-------------|-------:|------------------------------------------| -| `SIGTERM` | 15 | Polite "please shut down" (default `kill`)| -| `SIGKILL` | 9 | Forceful, uncatchable — last resort | -| `SIGHUP` | 1 | "reload config" for many daemons | -| `SIGINT` | 2 | Ctrl-C | - -```bash -kill 4242 # SIGTERM (let it clean up) -kill -9 4242 # SIGKILL (only if TERM fails) -pkill -f runaway.py # by command pattern -``` -Always try `SIGTERM` first. `kill -9` skips cleanup and can corrupt state. - -### systemd: the service manager - -```bash -systemctl status payments.service # is it running? recent logs -systemctl start|stop|restart payments.service -systemctl enable payments.service # start on boot -systemctl enable --now payments.service # enable AND start -journalctl -u payments.service -e # this service's logs, jump to end -journalctl -u payments.service --since "10 min ago" -``` -`status` shows the last exit code and a few log lines — your first stop when -a service won't stay up. A non-zero `Result:` plus `Restart=` thrashing -usually means the process is crashing on startup; `journalctl` tells you why. - -### Foreground, background, jobs - -```bash -long_task & # run in background -jobs # list background jobs -fg %1 # bring job 1 to foreground -nohup cmd & # survive terminal logout -``` - ---- - - -## 🔎 Guided Command Reading - -Start with polite shutdown. `kill PID` sends SIGTERM. Wait and check. Only use `kill -9` if the process ignores the polite request and you understand the cleanup risk. - -When you see a command in this lesson, break it into three parts: - -1. **Program:** the command being run, such as `ls`, `grep`, or `systemctl`. -2. **Options:** flags that change behavior, such as `-l`, `-h`, or `--since`. -3. **Target:** the file, directory, service, host, or text the command acts on. - -Write the command in your notebook, then add: **What I expected**, **What happened**, and **What I think it means**. - -## ⚠️ Common Beginner Mistakes - -- Using `kill -9` first. -- Confusing a service that is not enabled with a service that is not currently running. -- Reading only `ps` and missing the reason for failure in `journalctl`. - -## 🧠 Check Your Understanding - -Before starting the assignment, answer these in your own words: - -- What does PID identify? -- What can `systemctl status` show that `ps` cannot? -- Why should evidence for a postmortem include logs and not just commands? - -## 🧪 Hands-on Labs - -> See [`labs/lab-09-process-management.md`](labs/lab-09-process-management.md); -> it ships a fake crashing service and a CPU-burner you can safely kill. - -**Lab 9.1 — Spot the runaway** with `ps --sort=-%cpu` and stop it gracefully. - -**Lab 9.2 — Signals**: start a backgrounded sleep, kill with TERM, then KILL. - -**Lab 9.3 — Read why a service died** via `systemctl status` + `journalctl`. - -**Lab 9.4 — Restore** the service to running + enabled. - ---- - -## 📝 Assignment - -Stabilize the failing service in the lab environment. Submit -`postmortem.md` with: the runaway PID and its usage, the service's exit -status and the log line explaining the crash, the commands used to restore -it, and a prevention idea. Commit referencing `OPS-3401`. - ---- - -## 🤖 AI Engineering Exercise - -Paste a `systemctl status` block and the tail of `journalctl` into an AI and -ask for the likely root cause. Verify by reproducing the failure and -confirming the fix actually keeps the service up across a restart. Note -whether the AI distinguished "crashing" from "misconfigured to not restart." - ---- - -## 🪞 Reflection - -- When is `kill -9` justified, and what does it risk? -- What does `systemctl status` tell you that `ps` cannot? -- Why "enable" *and* "start" — what's the difference? - ---- - -## ✅ Definition of Done - -- [ ] Runaway process identified and stopped gracefully. -- [ ] Root cause of the service exit documented from logs. -- [ ] Service is `active (running)` and `enabled`. -- [ ] Postmortem captures evidence and a prevention idea. diff --git a/Lesson_10.md b/Lesson_10.md deleted file mode 100644 index 8132662..0000000 --- a/Lesson_10.md +++ /dev/null @@ -1,189 +0,0 @@ -# Lesson 10 — Secure Remote Infrastructure - -> **Competency:** Networking -> **Estimated time:** 3 hours - ---- - -## 🎫 Engineering Ticket - -``` -TICKET: NET-4001 -TITLE: New server can't reach the database; audit its network exposure -PRIORITY: P1 -DESCRIPTION: -A freshly provisioned app server can't connect to the database, but can -reach the internet. Separately, security wants to know exactly which ports -this box is listening on and to whom. Diagnose the connectivity problem and -produce a network exposure report — what's open, what's connecting out, and -what should be closed. - -ACCEPTANCE CRITERIA: -- Root cause of the DB connectivity failure (DNS? port? firewall? service down?). -- A report of all listening ports and their processes. -- Verification of which outbound connections succeed/fail. -- Recommendation for closing unnecessary exposure. -``` - ---- - -## 🎯 Learning Objectives - -1. Inspect interfaces and addresses with `ip addr` / `ip route`. -2. Test connectivity with `ping`, `curl`, and `nc`. -3. Resolve names with `dig`/`nslookup` and understand `/etc/hosts`. -4. List listening sockets and owning processes with `ss`. -5. Reason about TCP ports, the loopback interface, and firewalls. - ---- - - -## 🧭 Beginner Map - -### Big idea -Networking problems are easier when you test one layer at a time: address, route, name, reachability, port, service, firewall. - -### Key vocabulary -- **Interface:** A network connection on the machine. -- **IP address:** A numeric address for a host or interface. -- **Route:** A rule for where traffic should go. -- **DNS:** The system that turns names into IP addresses. -- **Port:** A numbered door for a network service. -- **Loopback:** The host talking to itself, usually `127.0.0.1`. -- **Firewall:** Rules that allow or block traffic. - -### Mental model -Think of sending a package. You need your own address, a road out, the recipient's address, an open door, and permission from security. Networking diagnosis checks each part in order. - -### How to study this lesson -1. Read the ticket as a story first: what problem needs solving? -2. Learn the vocabulary before memorizing commands. -3. Predict what each command will show before running it. -4. Run the command, compare the output to your prediction, and write one sentence about what changed in your understanding. - -## 📚 Technical Deep Dive - -### The layers of "it can't connect" - -When connectivity fails, isolate the layer methodically: - -1. **Interface up?** `ip addr` — does the NIC have an IP? -2. **Route exists?** `ip route` — is there a path to the target? -3. **Name resolves?** `dig db.internal` — does DNS return an address? -4. **Host reachable?** `ping ` — does the host answer (if ICMP allowed)? -5. **Port open?** `nc -zv db.internal 5432` — is the *service* listening/reachable? -6. **Service up on the other end?** `ss -tlnp` on the DB host. - -Most "DB unreachable" tickets are DNS (wrong/missing record), a firewall -blocking the port, or the database simply not running. Walk the layers; don't guess. - -### Addresses and routes - -```bash -ip addr # interfaces and their IPs (look for inet ...) -ip route # routing table; the 'default via' is your gateway -``` -`127.0.0.1` (loopback) is the host talking to itself — a service bound only -to loopback is unreachable from other machines, a frequent cause of -"works locally, fails remotely." - -### Names - -```bash -dig +short db.internal # what IP does this name resolve to? -cat /etc/hosts # static overrides checked before DNS -``` - -### Ports and connections - -```bash -nc -zv host 5432 # is the port open? (-z scan, -v verbose) -curl -v https://api.internal/health # full HTTP handshake detail -ss -tlnp # TCP, listening, numeric, with process -ss -tnp # established connections -``` -A **port** identifies a service on a host (5432 = PostgreSQL, 22 = SSH, -80/443 = HTTP/S). `ss -tlnp` is the modern replacement for `netstat` and is -your "what is this box exposing?" command. - -### Firewalls - -`ufw status` or `iptables -L -n` shows allow/deny rules. A port can be -listening yet blocked by a firewall — both must permit the traffic. - ---- - - -## 🔎 Guided Command Reading - -Do not jump from `can't connect` to `the network is down`. Ask one small question at a time: do I have an IP, do I have a route, does the name resolve, is the port reachable? - -When you see a command in this lesson, break it into three parts: - -1. **Program:** the command being run, such as `ls`, `grep`, or `systemctl`. -2. **Options:** flags that change behavior, such as `-l`, `-h`, or `--since`. -3. **Target:** the file, directory, service, host, or text the command acts on. - -Write the command in your notebook, then add: **What I expected**, **What happened**, and **What I think it means**. - -## ⚠️ Common Beginner Mistakes - -- Assuming ping failure means a host is down; ICMP may be blocked. -- Exposing a service on `0.0.0.0` when it only needed local access. -- Reading listening ports without checking which process owns them. - -## 🧠 Check Your Understanding - -Before starting the assignment, answer these in your own words: - -- What is the difference between `127.0.0.1:8080` and `0.0.0.0:8080`? -- Which command tests DNS? -- Which command shows listening TCP ports and process names? - -## 🧪 Hands-on Labs - -> See [`labs/lab-10-networking.md`](labs/lab-10-networking.md). - -**Lab 10.1 — Map your interfaces and routes**; identify your gateway. - -**Lab 10.2 — Walk the layers** against a known host (e.g. resolve + `nc` to 443). - -**Lab 10.3 — Listening-port audit** with `ss -tlnp`; identify each process. - -**Lab 10.4 — Loopback trap**: start a service bound to `127.0.0.1`, prove it's -unreachable externally, then rebind to `0.0.0.0`. - ---- - -## 📝 Assignment - -Produce `network-report.md`: your interfaces/routes, a layer-by-layer -connectivity diagnosis to one external host, a table of every listening port -with its owning process, and recommendations for what to close. Commit -referencing `NET-4001`. - ---- - -## 🤖 AI Engineering Exercise - -Give an AI your `ss -tlnp` output and ask which ports are risky to expose. -Verify each claim against what the process actually is (`ss` shows the -program). Note any port the AI flagged incorrectly — context (internal vs -internet-facing) changes the answer, and the AI can't see your topology. - ---- - -## 🪞 Reflection - -- Which layer is the most common culprit in your experience? -- Why is a service bound to `127.0.0.1` invisible to other hosts? -- "Listening" vs "reachable through the firewall" — why both matter? - ---- - -## ✅ Definition of Done - -- [ ] DB connectivity root cause identified by walking the layers. -- [ ] Listening-port table with owning processes. -- [ ] Loopback-vs-external behavior demonstrated. -- [ ] Concrete exposure-reduction recommendations made. diff --git a/Lesson_11.md b/Lesson_11.md deleted file mode 100644 index bb4fbcc..0000000 --- a/Lesson_11.md +++ /dev/null @@ -1,192 +0,0 @@ -# Lesson 11 — Secure SSH Like a Professional - -> **Competency:** SSH & Remote Access -> **Estimated time:** 3 hours - ---- - -## 🎫 Engineering Ticket - -``` -TICKET: SEC-2099 -TITLE: Harden SSH on the bastion host -PRIORITY: P1 -DESCRIPTION: -Our bastion accepts password logins and allows direct root login — auth.log -shows thousands of brute-force attempts daily. Move the team to key-based -authentication, disable passwords and root login, and document a safe -rollout so nobody locks themselves out. Keep a tested fallback session open -during changes. - -ACCEPTANCE CRITERIA: -- Key-based auth working for all team members. -- Password auth and direct root login disabled. -- A safe change procedure that prevents lockout. -- Reduced brute-force noise demonstrated in auth.log. -``` - ---- - -## 🎯 Learning Objectives - -1. Generate and manage SSH key pairs (`ssh-keygen`, `ssh-copy-id`). -2. Understand public/private key authentication vs passwords. -3. Configure the client with `~/.ssh/config`. -4. Harden `sshd_config` (disable passwords/root, change defaults). -5. Use `scp`/`rsync` and SSH tunneling for secure transfer and access. - ---- - - -## 🧭 Beginner Map - -### Big idea -SSH is a secure way to control another computer. Hardening SSH means proving trusted users can still get in while making password guessing and root login much harder. - -### Key vocabulary -- **SSH:** Secure Shell, a protocol for remote login. -- **Key pair:** A private key kept secret and a public key shared with the server. -- **authorized_keys:** The server file listing public keys allowed to log in. -- **sshd:** The SSH server daemon. -- **Fallback session:** An already-open connection kept in case the new configuration fails. -- **Tunnel:** A secure path through SSH to reach another service. - -### Mental model -A public key is like a lock you give the server. Your private key is the only key that can open it. The server can test you without ever seeing your private key. - -### How to study this lesson -1. Read the ticket as a story first: what problem needs solving? -2. Learn the vocabulary before memorizing commands. -3. Predict what each command will show before running it. -4. Run the command, compare the output to your prediction, and write one sentence about what changed in your understanding. - -## 📚 Technical Deep Dive - -### How key auth works - -You generate a **key pair**: a private key (stays on your laptop, never -shared) and a public key (copied to the server). The server challenges you -to prove you hold the private key without ever sending it. This defeats -password brute-forcing entirely. - -```bash -ssh-keygen -t ed25519 -C "you@team" # modern, strong, short keys -ssh-copy-id user@server # installs your pubkey on the server -ssh user@server # now logs in with the key -``` -The public key lands in the server's `~/.ssh/authorized_keys`. Protect the -private key: `chmod 600 ~/.ssh/id_ed25519`. Use a passphrase + `ssh-agent`. - -### Client config - -`~/.ssh/config` turns long commands into short aliases: -``` -Host bastion - HostName 203.0.113.10 - User engineer - IdentityFile ~/.ssh/id_ed25519 - Port 22 -``` -Now `ssh bastion` just works. - -### Hardening sshd (the careful part) - -Edit `/etc/ssh/sshd_config`: -``` -PermitRootLogin no -PasswordAuthentication no -PubkeyAuthentication yes -``` -**Golden rule:** before applying, confirm key login works in a *separate* -session, and keep your current session open. Validate config with -`sshd -t`, then `systemctl reload ssh`. If you disable password auth without -a working key, you lock yourself out — which is why we keep a fallback session. - -### Moving data - -```bash -scp file.txt bastion:/tmp/ # copy a file over SSH -rsync -avz --progress dir/ bastion:/srv/dir/ # efficient, resumable sync -ssh -L 5432:db.internal:5432 bastion # tunnel: local 5432 → db via bastion -``` -`rsync` only transfers differences — far better than `scp` for large or -repeated transfers. The tunnel lets you reach an internal DB through the -bastion without exposing it publicly. - ---- - - -## 🔎 Guided Command Reading - -Safe rollout order matters: create key, prove key login works, keep current session open, validate config with `sshd -t`, reload, test a new session, then close the old one. - -When you see a command in this lesson, break it into three parts: - -1. **Program:** the command being run, such as `ls`, `grep`, or `systemctl`. -2. **Options:** flags that change behavior, such as `-l`, `-h`, or `--since`. -3. **Target:** the file, directory, service, host, or text the command acts on. - -Write the command in your notebook, then add: **What I expected**, **What happened**, and **What I think it means**. - -## ⚠️ Common Beginner Mistakes - -- Disabling passwords before proving key login works. -- Sharing or emailing a private key. -- Stopping SSH instead of reloading it during a remote change. - -## 🧠 Check Your Understanding - -Before starting the assignment, answer these in your own words: - -- Which half of the key pair may be copied to a server? -- What command validates sshd config before reload? -- How does a fallback session prevent lockout? - -## 🧪 Hands-on Labs - -> See [`labs/lab-11-ssh.md`](labs/lab-11-ssh.md). If you have one box, you can -> practice loopback SSH (`ssh localhost`) safely. - -**Lab 11.1 — Generate an ed25519 key** and inspect both halves. - -**Lab 11.2 — Authorize the key** and log in without a password. - -**Lab 11.3 — Write a `~/.ssh/config`** alias and connect with it. - -**Lab 11.4 — Harden safely**: validate with `sshd -t`, reload, confirm in a -second session *before* closing the first. - ---- - -## 📝 Assignment - -Harden SSH on your box (or a loopback target). Submit `ssh-hardening.md` -documenting your safe rollout procedure, the `sshd_config` diffs, proof that -key auth works and password/root login are refused, and the fallback plan -that prevented lockout. Commit referencing `SEC-2099`. - ---- - -## 🤖 AI Engineering Exercise - -Ask an AI for a "secure sshd_config." Critically review it: does it preserve -a way back in? Does it disable something you actually rely on? Cross-check -each directive against `man sshd_config`. Document one suggestion you -*rejected* because it would have caused lockout in your setup. - ---- - -## 🪞 Reflection - -- Why is key auth fundamentally safer than even a strong password? -- What is the single most likely way to lock yourself out, and how did you avoid it? -- When would you reach for `rsync` over `scp`? - ---- - -## ✅ Definition of Done - -- [ ] Key-based login works; private key permissions are `600`. -- [ ] Password auth and root login disabled and verified. -- [ ] `sshd -t` passes; change made without losing access. -- [ ] Safe rollout procedure documented. diff --git a/Lesson_12.md b/Lesson_12.md deleted file mode 100644 index 8f23ce1..0000000 --- a/Lesson_12.md +++ /dev/null @@ -1,192 +0,0 @@ -# Lesson 12 — Secure the Software Supply Chain - -> **Competency:** Package & Software Management -> **Estimated time:** 2–3 hours - ---- - -## 🎫 Engineering Ticket - -``` -TICKET: SEC-2150 -TITLE: Audit and patch packages; stop installing from random scripts -PRIORITY: P2 -DESCRIPTION: -A scan flagged outdated packages with known CVEs, and we discovered someone -installed a tool via `curl | sudo bash`. Bring the system up to date safely, -inventory what's installed, remove an unnecessary package, and establish a -policy for trusted installation. Document what changed in case we need to -roll back. - -ACCEPTANCE CRITERIA: -- System packages updated; security updates applied. -- An inventory of installed packages captured. -- One unneeded package removed cleanly. -- A documented "trusted install" policy replacing curl|bash. -``` - ---- - -## 🎯 Learning Objectives - -1. Use `apt` to update, upgrade, search, install, and remove packages. -2. Understand repositories, package signing, and why they provide trust. -3. Inventory installed packages and inspect package contents/ownership. -4. Explain the risk of `curl | bash` and safer alternatives. -5. Apply security updates and capture a change record for rollback. - ---- - - -## 🧭 Beginner Map - -### Big idea -Package managers help you install software with a chain of trust. They track versions, files, dependencies, and signatures so you are not running random code blindly. - -### Key vocabulary -- **Package:** A bundled piece of software and metadata. -- **Repository:** A trusted source of packages. -- **Signature:** Cryptographic proof that a package came from a trusted source. -- **Dependency:** Software another package needs. -- **Inventory:** A list of what is installed. -- **Purge:** Remove a package and its configuration files. - -### Mental model -A signed package repository is like buying lab equipment from an approved school supplier instead of accepting an unlabeled box from a stranger. - -### How to study this lesson -1. Read the ticket as a story first: what problem needs solving? -2. Learn the vocabulary before memorizing commands. -3. Predict what each command will show before running it. -4. Run the command, compare the output to your prediction, and write one sentence about what changed in your understanding. - -## 📚 Technical Deep Dive - -### Package managers exist to provide *trust* - -A package manager like `apt` (Debian/Ubuntu) downloads software from -**signed repositories**. The signatures prove the package came from a -trusted source and wasn't tampered with in transit. That is the entire point -— it's a supply-chain security mechanism, not just a convenience. - -```bash -sudo apt update # refresh the package index (does NOT upgrade) -sudo apt upgrade # install available upgrades -sudo apt full-upgrade # upgrades that may add/remove packages -apt search nginx # find packages -sudo apt install --no-install-recommends htop -sudo apt remove --purge oldpkg # remove + delete its config -sudo apt autoremove # clean up orphaned dependencies -``` -`apt update` refreshes *metadata*; `apt upgrade` actually installs. Running -`update` alone never changes installed software — a common point of confusion. - -### Inventory and inspection - -```bash -apt list --installed | wc -l # how many packages? -dpkg -l | grep nginx # is it installed, what version? -dpkg -L nginx # what files did this package install? -dpkg -S /usr/sbin/nginx # which package owns this file? -apt-cache policy nginx # candidate vs installed version -``` - -### Security updates specifically - -```bash -sudo apt update -apt list --upgradable # what's pending -# unattended-upgrades can auto-apply security patches -``` - -### Why `curl | sudo bash` is dangerous - -`curl https://site/install.sh | sudo bash` runs **arbitrary unreviewed code -as root**, fetched over the network, with no signature, no version pin, and -no record of what it did. If the site is compromised (or MITM'd), you've -handed root to an attacker. Safer pattern: download, **read it**, pin a -version/checksum, then run: -```bash -curl -fsSLO https://site/install.sh -less install.sh # actually read it -sha256sum install.sh # compare to a published checksum -bash install.sh # only after review -``` -Better still: install from the distro repo or a vendor's signed repository. - ---- - - -## 🔎 Guided Command Reading - -Separate `apt update` from `apt upgrade`: update refreshes the catalog; upgrade installs newer packages from that catalog. - -When you see a command in this lesson, break it into three parts: - -1. **Program:** the command being run, such as `ls`, `grep`, or `systemctl`. -2. **Options:** flags that change behavior, such as `-l`, `-h`, or `--since`. -3. **Target:** the file, directory, service, host, or text the command acts on. - -Write the command in your notebook, then add: **What I expected**, **What happened**, and **What I think it means**. - -## ⚠️ Common Beginner Mistakes - -- Thinking `apt update` patched the system by itself. -- Running `curl | sudo bash` without reading or verifying the script. -- Removing a package without checking what depends on it or what files it owns. - -## 🧠 Check Your Understanding - -Before starting the assignment, answer these in your own words: - -- Which command tells you what package owns `/bin/ls`? -- Why does signing matter? -- What safer steps replace `curl | bash`? - -## 🧪 Hands-on Labs - -> See [`labs/lab-12-packages.md`](labs/lab-12-packages.md). - -**Lab 12.1 — Update + list upgradable**; note any security updates. - -**Lab 12.2 — Inventory**: count installed packages; find which package owns -`/bin/ls` with `dpkg -S`. - -**Lab 12.3 — Install and inspect** a package; list its files with `dpkg -L`. - -**Lab 12.4 — Remove + purge** an unneeded package and `autoremove` orphans. - ---- - -## 📝 Assignment - -Patch and audit your box. Submit `package-audit.md` with the before/after -package counts, the list of security updates applied, the package you -removed and why, and a one-paragraph "trusted install policy" that replaces -`curl | bash`. Commit referencing `SEC-2150`. - ---- - -## 🤖 AI Engineering Exercise - -Ask an AI how to install a specific tool. If it suggests `curl | bash`, -challenge it to provide a verifiable alternative (signed repo, checksum, -read-before-run). Document the safer procedure you settled on and why the -original suggestion was risky. - ---- - -## 🪞 Reflection - -- What's the real difference between `apt update` and `apt upgrade`? -- How does package signing protect you? -- When, if ever, is running a remote install script acceptable? - ---- - -## ✅ Definition of Done - -- [ ] Security updates applied; before/after recorded. -- [ ] Package inventory captured. -- [ ] One package removed and purged cleanly. -- [ ] Trusted-install policy documented. diff --git a/Lesson_13.md b/Lesson_13.md deleted file mode 100644 index c5a0f68..0000000 --- a/Lesson_13.md +++ /dev/null @@ -1,209 +0,0 @@ -# Lesson 13 — Build an Automated Operations Platform - -> **Competency:** Scheduling & Operations Automation -> **Estimated time:** 3–4 hours - ---- - -## 🎫 Engineering Ticket - -``` -TICKET: OPS-3500 -TITLE: Automate routine ops: backups, log rotation, and health checks -PRIORITY: P2 -DESCRIPTION: -Routine operations are done manually and inconsistently. Build a small -operations platform: a scheduled nightly backup, automatic log rotation, and -a periodic health check that alerts (writes to a status file / sends a -notification) on failure. Everything must be scheduled, logged, and -observable — no more "did someone run the backup?" - -ACCEPTANCE CRITERIA: -- A scheduled, logged nightly backup with retention. -- Log rotation configured so disks don't fill. -- A periodic health check that records pass/fail. -- All jobs observable: you can prove they ran. -``` - ---- - -## 🎯 Learning Objectives - -1. Schedule jobs with `cron` (and understand the crontab format). -2. Use `systemd` timers as a modern alternative to cron. -3. Configure `logrotate` to manage log growth. -4. Build backup scripts with retention policies. -5. Make scheduled jobs observable (logging, exit codes, status files). - ---- - - -## 🧭 Beginner Map - -### Big idea -Scheduled operations let the computer do routine work without a person remembering. The job is not complete unless you can prove it ran and see whether it succeeded. - -### Key vocabulary -- **cron:** A classic scheduler that runs commands at times you define. -- **systemd timer:** A modern scheduler tied to systemd services. -- **logrotate:** A tool that keeps log files from growing forever. -- **Retention:** A rule for how long backups are kept. -- **Observable:** Easy to prove what happened through logs or status files. - -### Mental model -A scheduled job is like an automatic school bell. It must ring at the right time, leave a record that it rang, and have a plan if the bell fails. - -### How to study this lesson -1. Read the ticket as a story first: what problem needs solving? -2. Learn the vocabulary before memorizing commands. -3. Predict what each command will show before running it. -4. Run the command, compare the output to your prediction, and write one sentence about what changed in your understanding. - -## 📚 Technical Deep Dive - -### cron: the five-field schedule - -``` -┌─ minute (0–59) -│ ┌─ hour (0–23) -│ │ ┌─ day of month (1–31) -│ │ │ ┌─ month (1–12) -│ │ │ │ ┌─ day of week (0–7, 0/7 = Sunday) -│ │ │ │ │ -* * * * * command_to_run -``` -```bash -crontab -e # edit your user's jobs -crontab -l # list them -# 02:30 every night: -30 2 * * * /usr/local/bin/backup.sh >> /var/log/backup.log 2>&1 -``` -Always redirect output to a log (`>> ... 2>&1`) — otherwise cron emails it -into the void and you have no record. Cron runs with a **minimal -environment** (tiny PATH, no aliases), so use absolute paths in cron scripts. - -### systemd timers (the modern way) - -A timer + service pair, more powerful than cron: it logs to the journal, -supports `OnCalendar=`, handles missed runs (`Persistent=true`), and is -observable with `systemctl list-timers`. -```ini -# /etc/systemd/system/backup.timer -[Timer] -OnCalendar=*-*-* 02:30:00 -Persistent=true -[Install] -WantedBy=timers.target -``` -```bash -systemctl enable --now backup.timer -systemctl list-timers # when did/will jobs run? -journalctl -u backup.service # the job's output -``` - -### logrotate: keep disks from filling - -``` -# /etc/logrotate.d/myapp -/var/log/myapp/*.log { - daily - rotate 14 # keep 14 days - compress - missingok - notifempty - copytruncate -} -``` -Unrotated logs are a classic way to fill `/var` and crash a host (you'll -relive this in Lesson 14). - -### Backups with retention - -A real backup script: timestamp the archive, write to a backup dir, then -**prune** old backups so they don't accumulate forever. -```bash -tar -czf "/backups/app-$(date +%F).tar.gz" /srv/app -find /backups -name 'app-*.tar.gz' -mtime +14 -delete # keep 14 days -``` - -### Observability - -A scheduled job nobody can verify is worthless. Make every job: (1) log -start/end with timestamps, (2) exit non-zero on failure, (3) write a status -file (`/var/run/backup.status`) you or a check can read. - ---- - - -## 🔎 Guided Command Reading - -Read cron left to right: minute, hour, day-of-month, month, day-of-week. Write the English sentence first, then the cron expression. - -When you see a command in this lesson, break it into three parts: - -1. **Program:** the command being run, such as `ls`, `grep`, or `systemctl`. -2. **Options:** flags that change behavior, such as `-l`, `-h`, or `--since`. -3. **Target:** the file, directory, service, host, or text the command acts on. - -Write the command in your notebook, then add: **What I expected**, **What happened**, and **What I think it means**. - -## ⚠️ Common Beginner Mistakes - -- Forgetting cron has a minimal environment and may not know your PATH. -- Creating backups without deleting old ones. -- Scheduling a job but leaving no logs, so nobody can prove it ran. - -## 🧠 Check Your Understanding - -Before starting the assignment, answer these in your own words: - -- What does `30 2 * * *` mean in English? -- Why should cron output be redirected? -- What evidence proves a systemd timer ran? - -## 🧪 Hands-on Labs - -> See [`labs/lab-13-scheduling.md`](labs/lab-13-scheduling.md). - -**Lab 13.1 — A logging cron job** that runs every minute and appends a timestamp. - -**Lab 13.2 — Backup script** with timestamped archive + retention prune. - -**Lab 13.3 — Convert it to a systemd timer**; confirm via `list-timers`. - -**Lab 13.4 — logrotate** a sample log dir and force a rotation with `-f`. - ---- - -## 📝 Assignment - -Build the ops platform: a scheduled backup (cron or timer) with retention, -logrotate for an app log dir, and a health check that records pass/fail. -Submit `ops-platform.md` proving each job ran (log excerpts, `list-timers`). -Commit referencing `OPS-3500`. - ---- - -## 🤖 AI Engineering Exercise - -Ask an AI for a cron entry to "back up nightly." Verify the five fields -mean what you think (AIs routinely swap day-of-month and day-of-week, or -forget output redirection). Test your final entry's schedule against a cron -expression checker and document the correction. - ---- - -## 🪞 Reflection - -- cron vs systemd timers — when would you pick each? -- Why is output redirection essential for a cron job? -- How do you *prove* an unattended job actually ran? - ---- - -## ✅ Definition of Done - -- [ ] Scheduled backup with retention, logged. -- [ ] logrotate configured and a rotation demonstrated. -- [ ] Health check records pass/fail. -- [ ] Every job's execution is verifiable. diff --git a/Lesson_14.md b/Lesson_14.md deleted file mode 100644 index a729b27..0000000 --- a/Lesson_14.md +++ /dev/null @@ -1,187 +0,0 @@ -# Lesson 14 — Diagnose a Production Server Running Out of Resources - -> **Competency:** Storage, Memory & Observability -> **Estimated time:** 3 hours - ---- - -## 🎫 Engineering Ticket - -``` -TICKET: OPS-3600 -TITLE: Server alerting "disk full" and "out of memory" — recover it -PRIORITY: P0 (host degraded) -DESCRIPTION: -Monitoring is screaming: the root filesystem is at 98% and the OOM killer -has started terminating processes. Writes are failing. Find what's eating -disk and memory, recover safely without destroying data we need, and put a -guardrail in place so this doesn't recur. This is a live-fire diagnosis. - -ACCEPTANCE CRITERIA: -- Identify what filled the disk and what consumed memory. -- Recover free space and stabilize memory safely. -- Confirm the host is healthy (df, free, no OOM events). -- A guardrail (alert/rotation/quota) to prevent recurrence. -``` - ---- - -## 🎯 Learning Objectives - -1. Diagnose disk usage with `df`, `du`, and find large/old files. -2. Distinguish "disk full" from "inodes full." -3. Diagnose memory with `free`, `top`, and understand swap and the OOM killer. -4. Read the journal for OOM and disk-full events. -5. Recover safely and add a preventive guardrail. - ---- - - -## 🧭 Beginner Map - -### Big idea -Resource troubleshooting asks what the machine is running out of: disk blocks, inodes, memory, or swap. Recovery must protect data, not just delete things quickly. - -### Key vocabulary -- **Block usage:** How much disk space is used. -- **Inode:** A filesystem record for a file; too many tiny files can exhaust them. -- **Deleted-but-open file:** A removed file still held by a running process. -- **Available memory:** Memory Linux can give to programs if needed. -- **OOM killer:** Kernel feature that kills a process when memory is exhausted. -- **Guardrail:** A prevention mechanism such as rotation, alerts, quotas, or limits. - -### Mental model -A server is like a classroom with desks and storage bins. You can run out of floor space, run out of labels for bins, or have one project using all the desks. Each shortage needs a different fix. - -### How to study this lesson -1. Read the ticket as a story first: what problem needs solving? -2. Learn the vocabulary before memorizing commands. -3. Predict what each command will show before running it. -4. Run the command, compare the output to your prediction, and write one sentence about what changed in your understanding. - -## 📚 Technical Deep Dive - -### Disk: where did it all go? - -```bash -df -h # per-filesystem usage — find the full one -df -i # INODES — a dir of millions of tiny files - # can exhaust inodes with df -h at 40% -du -xh / 2>/dev/null | sort -rh | head -20 # biggest dirs on root fs (-x stays on one fs) -find /var/log -type f -size +100M # giant log files (the usual culprit) -``` -Two distinct "full" conditions: **blocks** (real space, `df -h`) and -**inodes** (file *count*, `df -i`). A surprise inode exhaustion looks like -"disk full" while `df -h` shows space free. - -**Deleted-but-open trap:** if you `rm` a huge log a process still has open, -the space is *not* freed until the process closes it. Find it with -`lsof +L1` (files with link count 0 still held open), then restart/signal -the holder. - -### Memory and the OOM killer - -```bash -free -h # used/free/available/swap -top # press M to sort by memory -ps aux --sort=-%mem | head -``` -Linux uses free RAM for cache (that's healthy — look at **available**, not -"free"). When memory truly runs out and there's no swap headroom, the -kernel's **OOM killer** terminates the process with the worst "badness" -score. You'll see it in the journal: -```bash -journalctl -k | grep -i "out of memory\|oom" -dmesg | grep -i oom -``` - -### Recover safely - -- Free disk: rotate/compress/truncate logs (`truncate -s 0 huge.log` if a - process holds it), clear `/tmp`, `apt clean`, prune old backups — never - blindly delete files you can't identify. -- Memory: stop the offending process, add/enable swap as a stopgap, fix the - leak. Adding swap buys time but doesn't fix a leak. - -### Guardrails - -logrotate (Lesson 13), disk-usage alerts, `MemoryMax=` in a systemd unit to -cap a service, and filesystem quotas all prevent recurrence. The postmortem -must end with a guardrail, not just a cleanup. - ---- - - -## 🔎 Guided Command Reading - -Use evidence before cleanup: `df -h` for space, `df -i` for inodes, `du` for directory sizes, `lsof +L1` for deleted-open files, and `free -h` for memory. - -When you see a command in this lesson, break it into three parts: - -1. **Program:** the command being run, such as `ls`, `grep`, or `systemctl`. -2. **Options:** flags that change behavior, such as `-l`, `-h`, or `--since`. -3. **Target:** the file, directory, service, host, or text the command acts on. - -Write the command in your notebook, then add: **What I expected**, **What happened**, and **What I think it means**. - -## ⚠️ Common Beginner Mistakes - -- Deleting files you cannot identify. -- Missing deleted-but-open files and wondering why space did not return. -- Panicking at low `free` memory instead of reading `available`. - -## 🧠 Check Your Understanding - -Before starting the assignment, answer these in your own words: - -- How can a disk be full when `df -h` shows space free? -- Why might `rm huge.log` not immediately free space? -- What guardrail would prevent the same incident next time? - -## 🧪 Hands-on Labs - -> See [`labs/lab-14-resources.md`](labs/lab-14-resources.md); it safely fills a -> scratch directory and spawns a memory hog you can kill. - -**Lab 14.1 — Disk hunt**: fill a scratch dir, find it with `du | sort -rh`, reclaim it. - -**Lab 14.2 — Inode demo**: create thousands of empty files, watch `df -i` climb. - -**Lab 14.3 — Deleted-but-open**: hold a file open, `rm` it, see space not freed, fix with the holder. - -**Lab 14.4 — Memory + OOM**: run a bounded memory hog, observe `free`, find the OOM line in the journal. - ---- - -## 📝 Assignment - -Diagnose and recover the simulated incident. Submit `resource-postmortem.md`: -what filled disk and memory (with the exact diagnostic commands), how you -recovered safely, before/after `df -h` and `free -h`, and the guardrail you -added. Commit referencing `OPS-3600`. - ---- - -## 🤖 AI Engineering Exercise - -Give an AI your `df -h` + `du` + `free -h` output and ask for a recovery -plan. Verify it does NOT recommend deleting something unsafe (active logs a -process holds open, package caches you need, /var/lib data). Document one -dangerous suggestion you caught — this is exactly where blind trust causes outages. - ---- - -## 🪞 Reflection - -- How can a disk be "full" with `df -h` showing free space? (inodes / open files) -- Why look at "available" memory rather than "free"? -- What guardrail would have prevented this entirely? - ---- - -## ✅ Definition of Done - -- [ ] Disk and memory consumers identified with evidence. -- [ ] Space and memory recovered without data loss. -- [ ] `df -h` and `free -h` show a healthy host post-recovery. -- [ ] A preventive guardrail is in place. diff --git a/Lesson_15.md b/Lesson_15.md deleted file mode 100644 index 0003f5b..0000000 --- a/Lesson_15.md +++ /dev/null @@ -1,174 +0,0 @@ -# Lesson 15 — Linux Infrastructure Capstone - -> **Competency:** Infrastructure Delivery (Integrative) -> **Estimated time:** 8–12 hours (multi-session) - ---- - -## 🎫 Engineering Ticket (Capstone Engagement) - -``` -EPIC: CAP-9000 -TITLE: Deliver a production-ready Linux development server -PRIORITY: P1 (graduation gate) -DESCRIPTION: -The Platform team is commissioning a hardened, automated, observable -development server that the rest of the SWEXP program will build on. You own -it end to end. It must be secure, documented, automated, and reproducible — -if your laptop died, a teammate should be able to rebuild it from your repo. -This integrates every competency from Lessons 0–14. - -ACCEPTANCE CRITERIA: see the Definition of Done below. -``` - -> The full brief, rubric mapping, and submission checklist live in -> [`assignments/capstone-brief.md`](assignments/capstone-brief.md). - ---- - -## 🎯 Learning Objectives - -This capstone is **integrative** — it assesses your ability to combine, -under realistic constraints, everything you've practiced: - -1. Provision and document a server (L0–L1). -2. Organize a clean filesystem and project layout (L2). -3. Apply correct permissions and least privilege (L3). -4. Manage users, groups, and shared access (L4). -5. Process logs and build a reporting pipeline (L5–L6). -6. Standardize the shell environment via versioned dotfiles (L7). -7. Automate setup with robust, idempotent scripts (L8). -8. Run services reliably under systemd (L9). -9. Audit and constrain network exposure (L10). -10. Harden SSH for key-only access (L11). -11. Patch and secure the software supply chain (L12). -12. Schedule backups, rotation, and health checks (L13). -13. Add resource guardrails and observability (L14). - ---- - - -## 🧭 Beginner Map - -### Big idea -The capstone is where separate skills become a complete system. You are proving that you can build, secure, automate, observe, and explain a Linux server from scratch. - -### Key vocabulary -- **Provision:** Create and configure a system so it is ready to use. -- **Runbook:** Instructions for operating and fixing a system. -- **Hardening:** Reducing unnecessary risk and exposure. -- **Evidence map:** A table connecting each requirement to proof. -- **Release tag:** A Git marker for a finished version. -- **Integration:** Making parts work together as one system. - -### Mental model -This is like building and presenting a science fair project: the device must work, but your notebook, diagram, safety choices, and repeatable procedure are also part of the grade. - -### How to study this lesson -1. Read the ticket as a story first: what problem needs solving? -2. Learn the vocabulary before memorizing commands. -3. Predict what each command will show before running it. -4. Run the command, compare the output to your prediction, and write one sentence about what changed in your understanding. - -## 📐 What You Will Build - -A single server (your reused SWEXP box) configured as a **production-ready -development server**, delivered as a Git repository containing: - -1. **`provision.sh`** — an idempotent bootstrap that, run on a fresh box, - reproduces your entire configuration (users, dirs, packages, dotfiles, - services, schedules). Strict mode, logging, `shellcheck`-clean. -2. **A demo service** managed by systemd (`active` + `enabled`), bound - appropriately (not accidentally on `0.0.0.0` if it shouldn't be). -3. **Hardened SSH** (key-only, no root, no passwords) with a documented safe - rollout. -4. **An ops layer**: scheduled backup with retention, logrotate, and a - health check, all observable. -5. **A log-reporting pipeline** (`logreport.sh` from L6, generalized). -6. **Versioned dotfiles** with a one-command install. -7. **Documentation**: `README.md` (architecture + how to rebuild), - `RUNBOOK.md` (operate it: backup, restore, restart, where logs are), - `SECURITY.md` (the hardening you applied and why), and your ongoing - engineering notebook. - ---- - - -## 🔎 Guided Command Reading - -Do not build the capstone as one giant task. Finish one phase, commit it, test it, and write down proof before moving to the next phase. - -When you see a command in this lesson, break it into three parts: - -1. **Program:** the command being run, such as `ls`, `grep`, or `systemctl`. -2. **Options:** flags that change behavior, such as `-l`, `-h`, or `--since`. -3. **Target:** the file, directory, service, host, or text the command acts on. - -Write the command in your notebook, then add: **What I expected**, **What happened**, and **What I think it means**. - -## ⚠️ Common Beginner Mistakes - -- Treating the capstone as a checklist instead of a system someone else must operate. -- Leaving old shortcuts in place, especially broad permissions or unverified scripts. -- Writing documentation after the fact and forgetting the decisions that matter. - -## 🧠 Check Your Understanding - -Before starting the assignment, answer these in your own words: - -- Could another learner rebuild your server without asking you questions? -- Where is every acceptance criterion proven? -- What would fail safely if one part of your system broke? - -## 🧪 Build Phases - -Work in phases, committing at each one (your Git history is graded): - -- **Phase 1 — Foundation:** provision, filesystem, users/groups, permissions. -- **Phase 2 — Automation:** the idempotent `provision.sh`, dotfiles install. -- **Phase 3 — Services & Network:** demo service under systemd, network audit. -- **Phase 4 — Security:** SSH hardening, package patching, least privilege. -- **Phase 5 — Operations:** backups, rotation, health checks, guardrails. -- **Phase 6 — Documentation & Demo:** README, RUNBOOK, SECURITY, and a - recorded or written walkthrough showing a from-scratch rebuild. - ---- - -## 📝 Assignment / Deliverable - -Submit the Git repository URL plus a `CAPSTONE-SUBMISSION.md` that maps each -acceptance criterion to the file/command that satisfies it, and includes the -output of a clean `provision.sh` run on a fresh box. Tag the release -`v1.0-capstone`. This server is **reused for the rest of SWEXP**, so build it -to live with. - ---- - -## 🤖 AI Engineering Exercise - -Throughout the capstone, use AI as a pair — to draft scripts, review configs, -and explain errors — but **every AI-suggested change must be verified** with -the appropriate tool (`shellcheck`, `sshd -t`, `visudo -c`, a test run) and -logged in your notebook. Your submission must include an "AI workflow" section -describing where AI helped, what it got wrong, and how you caught it. - ---- - -## 🪞 Reflection - -- If your laptop died tonight, how long to rebuild this server from your repo? -- Which lesson's competency was hardest to integrate, and why? -- What would you do differently knowing the whole program builds on this box? - ---- - -## ✅ Definition of Done - -- [ ] `provision.sh` rebuilds the server idempotently and is `shellcheck`-clean. -- [ ] Demo service is `active (running)` and `enabled`, correctly bound. -- [ ] SSH is key-only; root and password auth disabled; `sshd -t` passes. -- [ ] Packages patched; least-privilege sudo; no world-writable app files. -- [ ] Scheduled backup (with retention), logrotate, and health check all observable. -- [ ] Dotfiles versioned with one-command install. -- [ ] README, RUNBOOK, SECURITY, and notebook complete. -- [ ] `CAPSTONE-SUBMISSION.md` maps every criterion to evidence; release tagged. diff --git a/MODULE_SYLLABUS.md b/MODULE_SYLLABUS.md deleted file mode 100644 index 2bbeb96..0000000 --- a/MODULE_SYLLABUS.md +++ /dev/null @@ -1,74 +0,0 @@ -# Module Syllabus — Linux & Command Line - -## Module Description - -A 16-lesson engineering rotation in which learners operate as Junior Software Engineers responsible for the Linux systems their team depends on. Learners close realistic tickets, investigate incidents, automate operations, and deliver a production-ready development server as the capstone. - -## Prerequisites - -- A computer capable of running Linux (native, WSL2, a VM, a container, or a cloud instance — see `resources/environment-setup.md`) -- Basic comfort with typing commands and reading on-screen output -- High-school-level reading and problem-solving persistence -- A willingness to predict, test, observe, and explain in your own words -- No prior Linux, programming, networking, or server administration experience is assumed; Lesson 0 establishes the baseline - -## Duration & Pacing - -The module is 16 lessons. It is designed to flex to several delivery cadences: - -| Cadence | Pace | Total | -|---------|------|-------| -| Intensive bootcamp | 1 lesson/day | ~3–4 weeks | -| Part-time cohort | 2 lessons/week | ~8 weeks | -| Self-paced | 1 lesson/week | ~16 weeks | - -Budget roughly **3–5 hours per lesson** (deep dive + labs + assignment), and **10–15 hours** for the Lesson 15 capstone. - -## Module Arc - -The lessons are sequenced deliberately. Each phase builds the foundation for the next. - -| Phase | Lessons | Theme | -|-------|---------|-------| -| **Foundations** | 0–4 | Orient on the system: filesystem, files, permissions, users | -| **Investigation** | 5–6 | Read the system: text processing and pipelines for log analysis | -| **Engineering** | 7–9 | Shape the system: shell environment, automation, process control | -| **Infrastructure** | 10–14 | Run the system: networking, SSH, packages, scheduling, observability | -| **Delivery** | 15 | Integrate everything into a production-ready server (capstone) | - -## Lesson Structure - -Every lesson is built from the same repeatable components so you always know where to look: - -- **🎫 Engineering Ticket** — the problem you have been assigned, with acceptance criteria -- **🎯 Learning Objectives** — the competency the ticket develops -- **🧭 Beginner Map** — the big idea, vocabulary, analogy, and study strategy -- **📚 Technical Deep Dive** — the mental model and the commands that serve it -- **🔎 Guided Command Reading** — command anatomy, annotated interpretation habits, beginner mistakes, and understanding checks -- **🧪 Hands-on Labs** — reproducible practice in a safe sandbox (`labs/`) -- **📝 Assignment** — the deliverables that close the ticket -- **🤖 AI Engineering Exercise** — practicing the draft → verify → log workflow -- **🪞 Reflection** — consolidating what you learned -- **✅ Definition of Done** — the bar your work must clear - -## Deliverables - -Across the module you will produce: - -- An **engineering notebook** documenting every investigation and decision -- A **Git repository** of your scripts, dotfiles, and configuration -- Closed tickets with reproducible evidence for each lesson -- A **capstone**: a documented, secured, automated development server - -## Final Assessment - -The **Production Linux Infrastructure Capstone** (Lesson 15, ticket `CAP-9000`). See `assignments/capstone-brief.md` for the full specification. The capstone integrates every competency from the module and is graded against `ASSESSMENT_RUBRIC.md`. - -## Support Materials - -- `resources/beginner-learning-pattern.md` — predict → run → explain workflow for new learners -- `resources/command-cheatsheet.md` — every command in the module, by lesson -- `resources/glossary.md` — key terms defined -- `resources/troubleshooting-playbook.md` — layered diagnosis recipes -- `resources/ai-workflow-guide.md` — how to use AI responsibly -- `resources/git-workflow.md` — committing your work professionally diff --git a/README.md b/README.md index 9cf23d1..e7002bf 100644 --- a/README.md +++ b/README.md @@ -1,92 +1,119 @@ -# SWEXP Module 1 — Linux & Command Line +# SWEXP Module 01 — Linux & Command Line · Interactive Workspace -> **Software Engineering Experience Program** -> You are not taking a course. You have joined an engineering team as a Junior Software Engineer, and this module is your first rotation: owning the Linux systems your team runs on. +> **Software Engineering Experience Program** — work-along starter workspace. +> This is the **hands-on companion** to Module 01. The lessons, deep dives, and the +> full capstone live in the Forge LMS; this repository is where you *do the work*: +> open an exercise, complete a `solution.sh`, run the tests, and push to be graded. + +It is designed to run inside the LMS **code-server** (or any Linux box / Codespace). +Every exercise is a small, sandbox-safe Bash task with a `bats` test spec — no `sudo`, +no mutating your real system. You practise the gradable *core* of each lesson here; +the system-state work (real `useradd`, hardening `sshd`, systemd timers) is done in +your LMS box and written up in your engineering notebook. --- -## What This Module Is - -Most Linux courses teach commands in isolation: here is `ls`, here is `chmod`, memorize the flags. This module does the opposite. It is written for learners who may be encountering Linux for the first time — roughly the level of a motivated senior high school student — so every professional scenario is paired with plain-language explanations, vocabulary, examples, and prediction-based practice. Every lesson opens with a **realistic engineering ticket** — a production incident, an onboarding request, a security hardening task — and you learn the commands because you need them to close the ticket. - -By the end you will have built, secured, automated, and documented a **production-ready development server** that you carry forward into every later SWEXP module. The work is cumulative. The server is real. The habits are the ones professional engineers actually use. - -## How You Will Work - -Every lesson follows the same engineering loop. The ticket gives the professional story, but the teaching path starts from first principles so new learners can understand the ideas before they use the commands: - -| Stage | What you do | -|-------|-------------| -| 🎫 **Engineering Ticket** | Read the assigned ticket and its acceptance criteria | -| 🎯 **Learning Objectives** | Understand what competency the ticket builds | -| 🧭 **Beginner Map** | Learn the big idea, vocabulary, and analogy in plain language | -| 📚 **Technical Deep Dive** | Learn the underlying mental model, not just syntax | -| 🔎 **Guided Command Reading** | Break commands into program, options, target, risk, and proof | -| 🧪 **Hands-on Labs** | Practice in a safe sandbox using predict → run → explain | -| 📝 **Assignment** | Close the ticket with real deliverables | -| 🤖 **AI Engineering Exercise** | Use AI the way professionals do: draft → verify → log | -| 🪞 **Reflection** | Write down what you learned and what surprised you | -| ✅ **Definition of Done** | Self-check against the same bar a senior engineer would | - -## Learning Outcomes - -By completing this module you will be able to: - -- Navigate and reason about any Linux filesystem from first principles -- Manage files, users, groups, and permissions safely on multi-user systems -- Process text and build pipelines to investigate logs and data at scale -- Diagnose production incidents: CPU, memory, disk, networking, and failed services -- Secure remote infrastructure with hardened SSH and a trustworthy software supply chain -- Automate repetitive operations with robust, idempotent Bash scripts and scheduled jobs -- Document your work to a professional standard so others can reproduce it -- Use AI tools responsibly, verifying every suggestion before trusting it - -## Lesson Index - -| # | Lesson | Primary Competency | Ticket | -|---|--------|--------------------|--------| -| 0 | Welcome to the Team | Environment & Workflow | INFRA-1001 | -| 1 | Explore an Unknown Linux System | Filesystem | INFRA-1042 | -| 2 | Recover the Lost Project | File Management | INFRA-1077 | -| 3 | Permission Denied | Permissions | SEC-2031 | -| 4 | Onboard a New Engineering Team | Users & Groups | INFRA-1120 | -| 5 | Investigate a Production Outage Using Log Files | Text Processing | OPS-3301 | -| 6 | Build a Production Log Processing Pipeline | Pipelines | OPS-3344 | -| 7 | Build Your Engineering Workspace | Shell Environment | INFRA-1180 | -| 8 | Automate Developer Onboarding | Bash Automation | INFRA-1205 | -| 9 | Production Service Failure | Process Management | OPS-3401 | -| 10 | Secure Remote Infrastructure | Networking | NET-4001 | -| 11 | Secure SSH Like a Professional | SSH | SEC-2099 | -| 12 | Secure the Software Supply Chain | Package Management | SEC-2150 | -| 13 | Build an Automated Operations Platform | Scheduling | OPS-3500 | -| 14 | Diagnose a Production Server Running Out of Resources | Storage & Observability | OPS-3600 | -| 15 | Linux Infrastructure Capstone | Infrastructure Delivery | CAP-9000 | - -## Repository Layout +## Quick start +```bash +npm install # installs bats (the only dependency) +npm test # run every exercise's tests (the spec) +npm run grade # score yourself exactly like the autograder does +npm run check # shell-syntax gate (bash -n) over every solution.sh ``` -SWEXP_Module_01_Linux_Command_Line/ -├── README.md ← you are here -├── MODULE_SYLLABUS.md ← schedule, pacing, prerequisites -├── LEARNER_GUIDE.md ← how to succeed as a learner -├── INSTRUCTOR_GUIDE.md ← how to facilitate the module -├── COMPETENCY_MATRIX.md ← lesson → competency mapping -├── ASSESSMENT_RUBRIC.md ← how work is graded -├── Lesson_00.md … Lesson_15.md ← the 16 lessons -├── labs/ ← hands-on lab guides + sandbox generators -├── solutions/ ← worked solutions and answer keys -├── assignments/ ← submission templates + capstone brief -├── resources/ ← beginner pattern, cheatsheets, glossary, AI workflow guide -└── instructor-notes/ ← per-lesson facilitation notes + +Work one exercise at a time: + +```bash +npx bats labs/lab-00-environment/tests # run just this lab's tests ``` -## Getting Started +Open the folder, read its `README.md`, complete the `# TODO`s in `solution.sh`, +and re-run the tests until they are green. **The `tests/*.bats` file is the spec** — +it defines exactly what "done" means. + +--- + +## How it works + +Each exercise folder contains: + +| File | Purpose | +|------|---------| +| `README.md` | what the task is and how it maps to the lesson | +| `solution.sh` | **you edit this** — a starter with `# TODO` guidance (it fails until you complete it) | +| `fixtures/` | sample data (an `os-release`, an `access.log`, a `df` dump, …) where needed | +| `tests/*.bats` | the grading spec — runs your `solution.sh` and asserts its effects/output | + +The autograder (`scripts/grade.mjs`) auto-discovers every folder under `labs/` and +`assignments/` that has a `tests/` directory, runs its `bats` tests, applies a +`bash -n` syntax gate, and prints a per-exercise score. You get the **same** report +locally (`npm run grade`) that CI posts on your pull request. + +--- -> **Tip:** open `dashboard.html` in a browser for an interactive, filterable view of the whole module — every ticket, its competency and phase, and direct links to the lesson, lab, solution, and notes. +## Exercises + +| Folder | Topic | What your `solution.sh` does | +|--------|-------|------------------------------| +| `labs/lab-00-environment` | Environment | Parse `/etc/os-release` and print `PRETTY_NAME` | +| `labs/lab-01-filesystem-survey` | Filesystem | Count files/dirs and name the largest file in a tree | +| `labs/lab-02-file-recovery` | File management | `find` `*.log` files and copy them into `recovered/` | +| `labs/lab-03-permissions` | Permissions | Tighten a secret to `640`, list world-writable files | +| `labs/lab-04-users-groups` | Users & groups | Print a group's members from an `/etc/group` file | +| `labs/lab-05-log-investigation` | Text processing | Busiest client IP + total 4xx/5xx count from an access log | +| `labs/lab-06-pipeline` | Pipelines | Status-code tally + tee 5xx lines to `errors.log` | +| `labs/lab-07-dotfiles` | Shell environment | Idempotently symlink dotfiles into a home dir | +| `labs/lab-08-onboarding-script` | Bash automation | Prereq check + idempotent onboarding | +| `labs/lab-09-process-management` | Processes | Find the highest-`%CPU` PID from `ps aux` output | +| `labs/lab-10-networking` | Networking | List unique listening TCP ports from `ss -ltn` output | +| `labs/lab-11-ssh` | SSH | Emit a valid `~/.ssh/config` `Host` block | +| `labs/lab-12-packages` | Packages | Report which wanted packages are missing | +| `labs/lab-13-scheduling` | Scheduling | Build the weekday-09:00 crontab line | +| `labs/lab-14-resources` | Storage | Flag filesystems over a use% threshold from `df -P` | +| `assignments/capstone` | Capstone core | Health check: world-writable + disk + missing packages | + +> The numbering matches the lessons. Labs 00–14 are the per-lesson cores; the capstone +> integrates several of them into one report. The **full** CAP-9000 capstone (idempotent +> `provision.sh`, systemd service, hardened SSH, ops layer, docs, `v1.0-capstone` tag) is +> completed in your LMS box — see `assignments/capstone-brief.md` and the submission +> template. -1. Read `MODULE_SYLLABUS.md` for pacing and prerequisites. -2. Read `LEARNER_GUIDE.md` to understand how you are expected to work. -3. Set up your environment with `resources/environment-setup.md`. -4. Open `Lesson_00.md` and claim your first ticket. +--- + +## Grading & submission + +1. Complete an exercise's `solution.sh`; run `npm test` (or the per-lab `npx bats …`). +2. When `npm run grade` shows the exercise green, commit and push: + ```bash + git add -A + git commit -m "feat: complete lab-00 environment" + git push + ``` +3. The **Autograde** GitHub Action (`.github/workflows/autograde.yml`) re-runs the + grader on every push and on pull requests, and posts your score to the job summary + (and as a PR comment). The check is green only when **every** exercise passes and + every `solution.sh` parses cleanly. + +For lesson write-ups and reflections, copy `assignments/submission-template.md` +(and `assignments/capstone-submission-template.md` for the capstone) into your notebook. + +--- + +## Repository layout + +``` +. +├── README.md ← you are here +├── package.json ← npm scripts: test / grade / check +├── scripts/grade.mjs ← the autograder (don't edit) +├── .github/workflows/ ← Autograde CI +├── .devcontainer/ ← one-click Codespaces / code-server setup +├── labs/ ← lab-00 … lab-14 (README + solution.sh + tests) +├── assignments/ ← capstone core + submission templates +└── resources/ ← cheatsheet, glossary, AI-workflow guide, setup notes +``` -> **The golden rule of this module:** never run a command — or paste one from an AI — that you cannot explain. Understanding is the deliverable; the closed ticket is just the evidence. +> **The golden rule of this module:** never run a command — or paste one from an AI — +> that you cannot explain. Understanding is the deliverable; the green check is just +> the evidence. diff --git a/assignments/README.md b/assignments/README.md deleted file mode 100644 index 29d19b6..0000000 --- a/assignments/README.md +++ /dev/null @@ -1,13 +0,0 @@ -# Assignments - -Each lesson contains its own assignment (see the lesson file). This directory -holds the materials that span lessons: - -| File | Purpose | -|------|---------| -| `capstone-brief.md` | Full Lesson 15 capstone specification | -| `submission-template.md` | Per-lesson deliverable submission format | -| `capstone-submission-template.md` | The capstone submission map | - -All deliverables are committed to your Git repo and reference the ticket ID -from the lesson. diff --git a/assignments/capstone-brief.md b/assignments/capstone-brief.md deleted file mode 100644 index 0cfdda0..0000000 --- a/assignments/capstone-brief.md +++ /dev/null @@ -1,63 +0,0 @@ -# Capstone Brief — CAP-9000: Production-Ready Linux Development Server - -> The capstone for Module 1. Integrates Lessons 0–14. Budget 8–12 hours -> across multiple sessions. This server is **reused for the rest of SWEXP.** - -## The engagement - -You own the delivery of a hardened, automated, observable Linux development -server, handed off as a Git repository. The bar: **if your laptop died -tonight, a teammate could rebuild the server from your repo without asking -you anything.** - -## Required deliverables - -1. **`provision.sh`** — idempotent bootstrap (strict mode, logging, - `shellcheck`-clean) that reproduces the whole configuration on a fresh box: - users/groups, directory layout + permissions, baseline packages, dotfiles, - the demo service, and the scheduled ops jobs. Running it twice changes nothing. -2. **Demo service under systemd** — `active (running)` and `enabled`, bound to - the correct interface (don't expose on `0.0.0.0` unless intended), with a - working `Restart=` policy. -3. **Hardened SSH** — key-only auth, `PermitRootLogin no`, - `PasswordAuthentication no`, validated with `sshd -t`, rolled out without - lockout (documented procedure). -4. **Operations layer** — scheduled backup with retention, `logrotate` for app - logs, and a periodic health check; all observable (logs / `list-timers` / - status file). -5. **Log-reporting pipeline** — a generalized `logreport.sh` (from Lesson 6). -6. **Versioned dotfiles** — with a one-command `install.sh`. -7. **Documentation:** - - `README.md` — architecture + exact rebuild steps. - - `RUNBOOK.md` — operate it: how to back up, restore, restart, where logs live. - - `SECURITY.md` — what you hardened and why. - - Engineering notebook — ongoing, including an "AI workflow" section. -8. **`CAPSTONE-SUBMISSION.md`** — maps every acceptance criterion to the - file/command that satisfies it, plus the output of a clean `provision.sh` - run on a fresh box. Tag the release `v1.0-capstone`. - -## Acceptance criteria → rubric mapping - -| Criterion | Rubric category | Evidence | -|-----------|-----------------|----------| -| `provision.sh` idempotent + shellcheck-clean | Automation (20%) | two clean runs | -| Demo service running/enabled, bound correctly | Linux Admin (20%) | `systemctl status`, `ss -tlnp` | -| SSH key-only, no root/password | Security (15%) | `sshd -t`, failed password attempt | -| Packages patched, least-privilege sudo | Security (15%) | `apt` log, `sudoers.d`, `visudo -c` | -| Backup+retention, rotation, health check | Automation / Admin | `list-timers`, backup dir | -| README/RUNBOOK/SECURITY/notebook | Documentation (15%) | the files | -| Clean Git history, ticket refs, tag | Engineering Practices (15%) | `git log` | -| Recovery scenario handled | Troubleshooting (10%) | postmortem section | -| AI verification logged throughout | AI Workflow (5%) | notebook section | - -## Suggested phase plan -- **P1 Foundation:** provision, fs, users/groups, permissions. -- **P2 Automation:** `provision.sh`, dotfiles install. -- **P3 Services & Network:** demo service, network audit. -- **P4 Security:** SSH hardening, patching, least privilege. -- **P5 Operations:** backups, rotation, health checks, guardrails. -- **P6 Docs & Demo:** README/RUNBOOK/SECURITY + from-scratch rebuild proof. - -## Definition of Done -See the checklist at the bottom of [`../Lesson_15.md`](../Lesson_15.md). Every -box must be checked and evidenced in `CAPSTONE-SUBMISSION.md`. diff --git a/assignments/capstone/README.md b/assignments/capstone/README.md new file mode 100644 index 0000000..68ff388 --- /dev/null +++ b/assignments/capstone/README.md @@ -0,0 +1,44 @@ +# Capstone — CAP-9000 (gradable core) + +**Goal:** integrate three Module-01 skills into one **server health check** that a +teammate could run on a fresh box. + +The full capstone (`../capstone-brief.md`) is an 8–12 hour engagement: an idempotent +`provision.sh`, a systemd demo service, hardened SSH, an operations layer, dotfiles, +and full docs — all done in the LMS code-server and tagged `v1.0-capstone`. Here we +grade the gradable core that ties the labs together: **one script that audits a box's +state and reports problems.** + +## What you do +Complete [`solution.sh`](solution.sh). Given a directory `` containing: + +- files/subdirs to audit, +- `df.txt` — `df -P` output, +- `installed.txt` — installed packages (one per line), +- `wanted.txt` — required packages (one per line), +- `threshold` — an integer use% (e.g. `80`), + +print a report with these three sections (combining lab-03, lab-14, and lab-12): + +``` +== WORLD-WRITABLE == + > +== DISK OVER THRESHOLD == + +== MISSING PACKAGES == + +``` + +A ready-to-run sample lives in [`fixtures/`](fixtures/) (copy them into a dir and run). + +```bash +npx bats assignments/capstone/tests +``` + +## Definition of done +- `npm test` for the capstone is green; `npm run check` is clean. +- Then complete the full CAP-9000 deliverables in your LMS box and fill in + [`../capstone-submission-template.md`](../capstone-submission-template.md). + +## Submit +Commit and push. The autograder scores this core; the full capstone is reviewed in the LMS. diff --git a/assignments/capstone/fixtures/df.txt b/assignments/capstone/fixtures/df.txt new file mode 100644 index 0000000..be92c71 --- /dev/null +++ b/assignments/capstone/fixtures/df.txt @@ -0,0 +1,4 @@ +Filesystem 1024-blocks Used Available Capacity Mounted on +/dev/root 40593412 12118552 26458476 32% / +/dev/sdb1 103081248 96152040 1719224 92% /data +/dev/sdc1 20511356 19485788 0 100% /var/log diff --git a/assignments/capstone/fixtures/installed.txt b/assignments/capstone/fixtures/installed.txt new file mode 100644 index 0000000..42189db --- /dev/null +++ b/assignments/capstone/fixtures/installed.txt @@ -0,0 +1,6 @@ +bash +coreutils +curl +git +openssh-server +tar diff --git a/assignments/capstone/fixtures/threshold b/assignments/capstone/fixtures/threshold new file mode 100644 index 0000000..d15a2cc --- /dev/null +++ b/assignments/capstone/fixtures/threshold @@ -0,0 +1 @@ +80 diff --git a/assignments/capstone/fixtures/wanted.txt b/assignments/capstone/fixtures/wanted.txt new file mode 100644 index 0000000..dbf9145 --- /dev/null +++ b/assignments/capstone/fixtures/wanted.txt @@ -0,0 +1,5 @@ +git +curl +ufw +fail2ban +tar diff --git a/assignments/capstone/solution.sh b/assignments/capstone/solution.sh new file mode 100755 index 0000000..e53ba2b --- /dev/null +++ b/assignments/capstone/solution.sh @@ -0,0 +1,40 @@ +#!/usr/bin/env bash +# Capstone (CAP-9000 core) — Server health check. See README.md. +# Usage: ./solution.sh +# +# Integrates three Module-01 cores into one report. contains: +# - any files/subdirs to audit for world-writable permissions +# - df.txt : `df -P` output (threshold check) +# - installed.txt : installed packages, 1/line +# - wanted.txt : required packages, 1/line (missing check) +# - threshold : a single integer use% (e.g. 80) +# +# Print a report with these three section headers, EXACTLY, each followed by +# zero or more indented findings: +# +# == WORLD-WRITABLE == +# # every world-writable regular file under +# == DISK OVER THRESHOLD == +# # every filesystem in df.txt whose use% > threshold +# == MISSING PACKAGES == +# # every package in wanted.txt absent from installed.txt +set -euo pipefail +dir="${1:?usage: solution.sh }" + +threshold="$(cat "$dir/threshold")" + +# TODO 1: print the line: == WORLD-WRITABLE == +# then, indented by two spaces, every world-writable regular file under +# "$dir" (find "$dir" -perm -002 -type f). Skip the control files +# (df.txt, installed.txt, wanted.txt, threshold) is NOT required — the +# tests only make real data files world-writable. + +# TODO 2: print the line: == DISK OVER THRESHOLD == +# then, indented by two spaces, the mount point of each filesystem in +# "$dir/df.txt" whose use% (field 5, strip '%') is > "$threshold" +# (reuse your lab-14 logic). + +# TODO 3: print the line: == MISSING PACKAGES == +# then, indented by two spaces, each package in "$dir/wanted.txt" that +# does NOT appear as a whole line in "$dir/installed.txt" +# (reuse your lab-12 logic). diff --git a/assignments/capstone/tests/capstone.bats b/assignments/capstone/tests/capstone.bats new file mode 100644 index 0000000..7ecfc5e --- /dev/null +++ b/assignments/capstone/tests/capstone.bats @@ -0,0 +1,49 @@ +setup() { + LAB_DIR="$(cd "$(dirname "$BATS_TEST_FILENAME")/.." && pwd)" + SOL="$LAB_DIR/solution.sh" + FIX="$LAB_DIR/fixtures" + WORK="$(mktemp -d)" + # Assemble a self-contained audit dir from the fixtures. + cp "$FIX/df.txt" "$FIX/installed.txt" "$FIX/wanted.txt" "$FIX/threshold" "$WORK/" + mkdir -p "$WORK/app" + printf 'secret\n' > "$WORK/app/leak.conf"; chmod 666 "$WORK/app/leak.conf" # world-writable + printf 'ok\n' > "$WORK/app/safe.txt"; chmod 644 "$WORK/app/safe.txt" +} +teardown() { rm -rf "$WORK"; } + +@test "report contains all three section headers in order" { + run bash "$SOL" "$WORK" + [ "$status" -eq 0 ] + echo "$output" | grep -q '== WORLD-WRITABLE ==' + echo "$output" | grep -q '== DISK OVER THRESHOLD ==' + echo "$output" | grep -q '== MISSING PACKAGES ==' + ww="$(echo "$output" | grep -n '== WORLD-WRITABLE ==' | cut -d: -f1)" + dk="$(echo "$output" | grep -n '== DISK OVER THRESHOLD ==' | cut -d: -f1)" + pk="$(echo "$output" | grep -n '== MISSING PACKAGES ==' | cut -d: -f1)" + [ "$ww" -lt "$dk" ] + [ "$dk" -lt "$pk" ] +} + +@test "flags the world-writable file but not the safe one" { + run bash "$SOL" "$WORK" + [ "$status" -eq 0 ] + echo "$output" | grep -q 'leak.conf' + ! echo "$output" | grep -q 'safe.txt' +} + +@test "flags over-threshold filesystems from df.txt" { + run bash "$SOL" "$WORK" + [ "$status" -eq 0 ] + echo "$output" | grep -q '/data' + echo "$output" | grep -q '/var/log' + ! echo "$output" | grep -Eq '[[:space:]]/$' +} + +@test "lists packages that are wanted but not installed" { + run bash "$SOL" "$WORK" + [ "$status" -eq 0 ] + echo "$output" | grep -qx ' ufw' + echo "$output" | grep -qx ' fail2ban' + ! echo "$output" | grep -qx ' git' + ! echo "$output" | grep -qx ' curl' +} diff --git a/labs/README.md b/labs/README.md deleted file mode 100644 index 51d3209..0000000 --- a/labs/README.md +++ /dev/null @@ -1,38 +0,0 @@ -# Labs - -Each lab corresponds to a lesson and is meant to be done **on your own Linux -box**, not just read. Many labs ship a small `setup` snippet that generates a -realistic, safe scenario (fake logs, a crashing service, a scattered project) -so you practice on something that behaves like production without touching -production. - -## Conventions - -- Every lab starts by creating an isolated scratch directory so nothing - outside it is affected: `mkdir -p ~/swexp-lab && cd ~/swexp-lab`. -- Destructive practice (kill, rm, fill disk) is confined to scratch dirs and - bounded so it cannot harm the host. -- Record commands and output in your engineering notebook as you go. Use the **predict → run → explain** loop: predict what a command will do, run exactly one step, then explain the output in your own words before moving on. - -## Lab index - -| Lab | Lesson | Focus | -|-----|--------|-------| -| `lab-00-environment.md` | 0 | Provision + identify your box | -| `lab-01-filesystem-survey.md` | 1 | Filesystem orientation | -| `lab-02-file-recovery.md` | 2 | File management & archives | -| `lab-03-permissions.md` | 3 | Permissions & ownership | -| `lab-04-users-groups.md` | 4 | Users, groups, setgid | -| `lab-05-log-investigation.md` | 5 | grep/awk incident hunt | -| `lab-06-pipeline.md` | 6 | Pipes, redirection, report | -| `lab-07-dotfiles.md` | 7 | Shell environment & dotfiles | -| `lab-08-onboarding-script.md` | 8 | Robust Bash scripting | -| `lab-09-process-management.md` | 9 | Processes, signals, systemd | -| `lab-10-networking.md` | 10 | Connectivity & port audit | -| `lab-11-ssh.md` | 11 | SSH keys & hardening | -| `lab-12-packages.md` | 12 | Package audit & supply chain | -| `lab-13-scheduling.md` | 13 | cron, timers, logrotate | -| `lab-14-resources.md` | 14 | Disk/memory diagnosis | - -> Solution keys for every lab are in [`../solutions/`](../solutions/). -> **Try the lab fully before opening the solution.** diff --git a/labs/lab-00-environment.md b/labs/lab-00-environment.md deleted file mode 100644 index af665de..0000000 --- a/labs/lab-00-environment.md +++ /dev/null @@ -1,42 +0,0 @@ -# Lab 00 — Environment (Lesson 0) - -## Goal -Stand up a Linux box you control and open your engineering notebook. - - -## How to Learn From This Lab - -Use the **predict → run → explain** loop: - -1. **Predict:** write what you think the command will do. -2. **Run:** execute exactly one step. -3. **Explain:** write what the output means in plain language. -4. **Verify:** use a second command or the filesystem state to prove the result. - -## Beginner Checkpoints - -- Predict: before running `whoami`, guess the username Linux will report. -- Run each identity command separately, then write one sentence explaining its output. -- Challenge: ask a classmate to rebuild your environment from your notes only. - -## Tasks -1. **Provision** (pick one): - - WSL2: `wsl --install -d Ubuntu` - - Docker: `docker run -it --name swexp ubuntu:24.04 bash` - - VM/cloud: smallest Ubuntu 24.04 instance. -2. **Identify the box:** - ```bash - whoami; pwd; id; uname -a; cat /etc/os-release - ``` -3. **Open the notebook:** - ```bash - mkdir -p ~/swexp-engineering-notebook && cd ~/swexp-engineering-notebook - git init - git config user.name "Your Name"; git config user.email "you@example.com" - printf '# Engineering Notebook\n\n## %s — Environment\n\nProvisioning path:\nOS:\nNotes:\n' "$(date +%F)" > NOTEBOOK.md - git add NOTEBOOK.md && git commit -m "INFRA-1001: open engineering notebook" - ``` - -## Deliverable -A committed `NOTEBOOK.md` whose Environment entry lets a teammate reproduce -your box. Solution: [`../solutions/lab-00-solution.md`](../solutions/lab-00-solution.md). diff --git a/labs/lab-00-environment/README.md b/labs/lab-00-environment/README.md new file mode 100644 index 0000000..db07131 --- /dev/null +++ b/labs/lab-00-environment/README.md @@ -0,0 +1,31 @@ +# Lab 00 — Environment + +**Goal:** identify the box you are on by parsing `/etc/os-release` — the file every +distro ships so tools (and humans) can tell which OS they are running on. + +In the LMS box you would provision a real Ubuntu instance and run +`whoami; pwd; id; uname -a; cat /etc/os-release`. Here we grade the one +machine-checkable core: **reading a key=value file and extracting a value.** + +## What you do +Complete [`solution.sh`](solution.sh). It takes the path to an `os-release`-style +file and must: + +1. Find the `PRETTY_NAME=...` line. +2. Print **only** its value, with the surrounding double quotes stripped + (e.g. `Ubuntu 24.04.1 LTS`). + +A sample file lives in [`fixtures/os-release`](fixtures/os-release). + +```bash +npx bats labs/lab-00-environment/tests +./solution.sh fixtures/os-release +``` + +## Definition of done +- `npm test` for this lab is green; `npm run check` (shell syntax) is clean. +- In your LMS notebook, record the output of `whoami; id; uname -a` on your real box + and explain what `PRETTY_NAME` is used for. + +## Submit +Commit and push. The autograder scores it automatically. diff --git a/labs/lab-00-environment/fixtures/os-release b/labs/lab-00-environment/fixtures/os-release new file mode 100644 index 0000000..cd9db34 --- /dev/null +++ b/labs/lab-00-environment/fixtures/os-release @@ -0,0 +1,11 @@ +PRETTY_NAME="Ubuntu 24.04.1 LTS" +NAME="Ubuntu" +VERSION_ID="24.04" +VERSION="24.04.1 LTS (Noble Numbat)" +VERSION_CODENAME=noble +ID=ubuntu +ID_LIKE=debian +HOME_URL="https://www.ubuntu.com/" +SUPPORT_URL="https://help.ubuntu.com/" +BUG_REPORT_URL="https://bugs.launchpad.net/ubuntu/" +UBUNTU_CODENAME=noble diff --git a/labs/lab-00-environment/solution.sh b/labs/lab-00-environment/solution.sh new file mode 100755 index 0000000..7051d45 --- /dev/null +++ b/labs/lab-00-environment/solution.sh @@ -0,0 +1,11 @@ +#!/usr/bin/env bash +# Lab 00 — Environment. See README.md. +# Usage: ./solution.sh +# Print the value of PRETTY_NAME (without the surrounding quotes) from an +# /etc/os-release-style key=value file. +set -euo pipefail +file="${1:?usage: solution.sh }" + +# TODO: read "$file", find the PRETTY_NAME=... line, and print ONLY the value +# with the surrounding double quotes stripped. +# hint: grep '^PRETTY_NAME=' "$file" | cut -d= -f2- | tr -d '"' diff --git a/labs/lab-00-environment/tests/environment.bats b/labs/lab-00-environment/tests/environment.bats new file mode 100644 index 0000000..ac8fedd --- /dev/null +++ b/labs/lab-00-environment/tests/environment.bats @@ -0,0 +1,26 @@ +setup() { + LAB_DIR="$(cd "$(dirname "$BATS_TEST_FILENAME")/.." && pwd)" + SOL="$LAB_DIR/solution.sh" + OSREL="$LAB_DIR/fixtures/os-release" + WORK="$(mktemp -d)" +} +teardown() { rm -rf "$WORK"; } + +@test "prints the PRETTY_NAME value without quotes" { + run bash "$SOL" "$OSREL" + [ "$status" -eq 0 ] + [ "$output" = "Ubuntu 24.04.1 LTS" ] +} + +@test "works on a different os-release file" { + printf 'ID=debian\nPRETTY_NAME="Debian GNU/Linux 12 (bookworm)"\nVERSION_ID="12"\n' > "$WORK/os-release" + run bash "$SOL" "$WORK/os-release" + [ "$status" -eq 0 ] + [ "$output" = "Debian GNU/Linux 12 (bookworm)" ] +} + +@test "output contains no surrounding double quotes" { + run bash "$SOL" "$OSREL" + [ "$status" -eq 0 ] + ! echo "$output" | grep -q '"' +} diff --git a/labs/lab-01-filesystem-survey.md b/labs/lab-01-filesystem-survey.md deleted file mode 100644 index 6dcc6ab..0000000 --- a/labs/lab-01-filesystem-survey.md +++ /dev/null @@ -1,39 +0,0 @@ -# Lab 01 — Filesystem Survey (Lesson 1) - -## Goal -Map an unfamiliar system read-only and flag findings. - - -## How to Learn From This Lab - -Use the **predict → run → explain** loop: - -1. **Predict:** write what you think the command will do. -2. **Run:** execute exactly one step. -3. **Explain:** write what the output means in plain language. -4. **Verify:** use a second command or the filesystem state to prove the result. - -## Beginner Checkpoints - -- Predict which top-level directory will contain logs before listing `/`. -- Run `pwd` before every relative-path command until you can predict where it will act. -- Challenge: draw a small tree showing `/`, `/home`, `/etc`, `/var`, and your current directory. - -## Tasks -1. **Identity:** `cat /etc/os-release; uname -r; nproc; free -h` -2. **Layout:** `ls -lah /; ls -lah /etc | head; ls -lah /var/log` -3. **Heavy hitters:** - ```bash - df -h - du -sh /var/* 2>/dev/null | sort -rh | head - ``` -4. **Classify files (trust `file`, not the extension):** - ```bash - cd ~/swexp-lab && mkdir -p l01 && cd l01 - echo hello > a.txt; cp /bin/ls ./b; file a.txt b - stat a.txt - ``` - -## Deliverable -`survey.md` with OS/kernel, CPU/RAM, `df -h`, three largest `/var` dirs, and -three findings. Solution: [`../solutions/lab-01-solution.md`](../solutions/lab-01-solution.md). diff --git a/labs/lab-01-filesystem-survey/README.md b/labs/lab-01-filesystem-survey/README.md new file mode 100644 index 0000000..65f0d9c --- /dev/null +++ b/labs/lab-01-filesystem-survey/README.md @@ -0,0 +1,31 @@ +# Lab 01 — Filesystem Survey + +**Goal:** map an unfamiliar tree read-only and report findings — the first thing you +do on any unknown box. + +On the real box you would run `ls -lah /`, `df -h`, and `du -sh /var/* | sort -rh`. +Here we grade the gradable core: **counting and ranking files in a tree.** + +## What you do +Complete [`solution.sh`](solution.sh). Given a directory, it must print exactly +three lines: + +``` +files # number of regular files under +dirs # number of sub-directories under +largest # basename of the largest regular file +``` + +The test builds a known tree in a temp dir, so your counts must be exact. + +```bash +npx bats labs/lab-01-filesystem-survey/tests +``` + +## Definition of done +- `npm test` for this lab is green; `npm run check` is clean. +- In your LMS notebook, paste a real `du -sh /var/* | sort -rh | head` and explain the + three largest directories. + +## Submit +Commit and push. diff --git a/labs/lab-01-filesystem-survey/solution.sh b/labs/lab-01-filesystem-survey/solution.sh new file mode 100755 index 0000000..2559901 --- /dev/null +++ b/labs/lab-01-filesystem-survey/solution.sh @@ -0,0 +1,19 @@ +#!/usr/bin/env bash +# Lab 01 — Filesystem survey. See README.md. +# Usage: ./solution.sh +# Survey a directory tree and print three lines, exactly: +# files +# dirs +# largest +set -euo pipefail +dir="${1:?usage: solution.sh }" + +# TODO 1: count regular files under "$dir" and print: files +# hint: find "$dir" -type f | wc -l + +# TODO 2: count directories under "$dir" (excluding "$dir" itself) and print: dirs +# hint: find "$dir" -mindepth 1 -type d | wc -l + +# TODO 3: print the basename of the LARGEST regular file: largest +# hint: find "$dir" -type f -printf '%s %p\n' | sort -rn | head -1 +# then take the path (field 2..) and run basename on it. diff --git a/labs/lab-01-filesystem-survey/tests/survey.bats b/labs/lab-01-filesystem-survey/tests/survey.bats new file mode 100644 index 0000000..42ef41b --- /dev/null +++ b/labs/lab-01-filesystem-survey/tests/survey.bats @@ -0,0 +1,29 @@ +setup() { + LAB_DIR="$(cd "$(dirname "$BATS_TEST_FILENAME")/.." && pwd)" + SOL="$LAB_DIR/solution.sh" + WORK="$(mktemp -d)" + # Build a small known tree: 3 files, 2 dirs (sub, sub/deep). + mkdir -p "$WORK/tree/sub/deep" + printf 'aaa\n' > "$WORK/tree/small.txt" # 4 bytes + printf 'medium-content\n' > "$WORK/tree/sub/mid.log" # 15 bytes + head -c 200 /dev/zero > "$WORK/tree/sub/deep/biggest.bin" # 200 bytes (largest) +} +teardown() { rm -rf "$WORK"; } + +@test "reports the file count" { + run bash "$SOL" "$WORK/tree" + [ "$status" -eq 0 ] + echo "$output" | grep -Eq '^files[[:space:]]+3$' +} + +@test "reports the directory count" { + run bash "$SOL" "$WORK/tree" + [ "$status" -eq 0 ] + echo "$output" | grep -Eq '^dirs[[:space:]]+2$' +} + +@test "reports the basename of the largest file" { + run bash "$SOL" "$WORK/tree" + [ "$status" -eq 0 ] + echo "$output" | grep -Eq '^largest[[:space:]]+biggest\.bin$' +} diff --git a/labs/lab-02-file-recovery.md b/labs/lab-02-file-recovery.md deleted file mode 100644 index 954cc65..0000000 --- a/labs/lab-02-file-recovery.md +++ /dev/null @@ -1,51 +0,0 @@ -# Lab 02 — File Recovery (Lesson 2) - -## Goal -Rebuild a scattered project into a clean, conventional tree. - - -## How to Learn From This Lab - -Use the **predict → run → explain** loop: - -1. **Predict:** write what you think the command will do. -2. **Run:** execute exactly one step. -3. **Explain:** write what the output means in plain language. -4. **Verify:** use a second command or the filesystem state to prove the result. - -## Beginner Checkpoints - -- Predict what each glob will match before you run it. -- Use preview commands before every copy or delete. -- Challenge: add one extra duplicate and prove it is a duplicate using a checksum. - -## Setup — generate the mess -```bash -mkdir -p ~/swexp-lab/l02/dump && cd ~/swexp-lab/l02/dump -echo "print('app')" > main.py -echo "print('app')" > main_COPY.py # duplicate -echo "DEBUG=true" > settings.conf -echo "# Checkout Service" > readme.txt -echo "def pay(): pass" > payments.py -mkdir -p stray && mv payments.py stray/ -``` - -## Tasks -1. **Target tree:** `mkdir -p ~/projects/checkout-service/{src,config,docs}` -2. **Find + place:** - ```bash - find ~/swexp-lab/l02/dump -name '*.py' # preview matches first - cp -a ~/swexp-lab/l02/dump/main.py ~/swexp-lab/l02/dump/stray/payments.py ~/projects/checkout-service/src/ - cp -a ~/swexp-lab/l02/dump/*.conf ~/projects/checkout-service/config/ - cp -a ~/swexp-lab/l02/dump/readme.txt ~/projects/checkout-service/docs/ - ``` -3. **De-dupe by checksum:** `md5sum ~/swexp-lab/l02/dump/main*.py` -4. **Archive + verify:** - ```bash - cd ~/projects && tar -czf checkout-service.tar.gz checkout-service - tar -tzf checkout-service.tar.gz - ``` - -## Deliverable -The clean tree, `MANIFEST.md`, and a verified `.tar.gz`. Solution: -[`../solutions/lab-02-solution.md`](../solutions/lab-02-solution.md). diff --git a/labs/lab-02-file-recovery/README.md b/labs/lab-02-file-recovery/README.md new file mode 100644 index 0000000..b828be6 --- /dev/null +++ b/labs/lab-02-file-recovery/README.md @@ -0,0 +1,26 @@ +# Lab 02 — File Recovery + +**Goal:** rebuild a scattered project by finding files that match a pattern and +collecting them in one place. + +The full lab rebuilds a conventional `src/config/docs` tree and archives it. Here +we grade the gradable core: **`find` by pattern, then copy.** + +## What you do +Complete [`solution.sh`](solution.sh). Given a directory, it must: + +1. Create `/recovered/`. +2. Find every `*.log` file under `` and copy each into `/recovered/` + (do not copy non-log files; do not descend back into `recovered/`). + +```bash +npx bats labs/lab-02-file-recovery/tests +``` + +## Definition of done +- `npm test` for this lab is green; `npm run check` is clean. +- In your LMS notebook, explain why you **preview** a `find` before you `cp`/`rm`, and + prove a duplicate with `md5sum`. + +## Submit +Commit and push. diff --git a/labs/lab-02-file-recovery/solution.sh b/labs/lab-02-file-recovery/solution.sh new file mode 100755 index 0000000..7bd4a94 --- /dev/null +++ b/labs/lab-02-file-recovery/solution.sh @@ -0,0 +1,15 @@ +#!/usr/bin/env bash +# Lab 02 — File recovery. See README.md. +# Usage: ./solution.sh +# Find every *.log file under and copy it into /recovered/, +# preserving the basename. Existing recovered/ contents are fine to overwrite. +set -euo pipefail +dir="${1:?usage: solution.sh }" + +# TODO 1: create the destination directory "$dir/recovered". +# hint: mkdir -p "$dir/recovered" + +# TODO 2: find every file matching *.log under "$dir" (skip the recovered/ dir +# itself) and copy each into "$dir/recovered/". +# hint: find "$dir" -path "$dir/recovered" -prune -o -name '*.log' -type f -print +# loop over the results and cp each one into "$dir/recovered/". diff --git a/labs/lab-02-file-recovery/tests/recovery.bats b/labs/lab-02-file-recovery/tests/recovery.bats new file mode 100644 index 0000000..bb8a4f0 --- /dev/null +++ b/labs/lab-02-file-recovery/tests/recovery.bats @@ -0,0 +1,30 @@ +setup() { + LAB_DIR="$(cd "$(dirname "$BATS_TEST_FILENAME")/.." && pwd)" + SOL="$LAB_DIR/solution.sh" + WORK="$(mktemp -d)" + mkdir -p "$WORK/dump/stray" + printf 'a\n' > "$WORK/dump/app.log" + printf 'b\n' > "$WORK/dump/stray/worker.log" + printf 'c\n' > "$WORK/dump/notes.txt" # not a .log + printf 'd\n' > "$WORK/dump/config.conf" # not a .log +} +teardown() { rm -rf "$WORK"; } + +@test "recovers all .log files into recovered/" { + run bash "$SOL" "$WORK/dump" + [ "$status" -eq 0 ] + [ -f "$WORK/dump/recovered/app.log" ] + [ -f "$WORK/dump/recovered/worker.log" ] +} + +@test "does not recover non-log files" { + bash "$SOL" "$WORK/dump" >/dev/null + [ ! -f "$WORK/dump/recovered/notes.txt" ] + [ ! -f "$WORK/dump/recovered/config.conf" ] +} + +@test "recovered exactly two log files" { + bash "$SOL" "$WORK/dump" >/dev/null + run bash -c "find '$WORK/dump/recovered' -name '*.log' -type f | wc -l" + [ "$output" -eq 2 ] +} diff --git a/labs/lab-03-permissions.md b/labs/lab-03-permissions.md deleted file mode 100644 index c585c46..0000000 --- a/labs/lab-03-permissions.md +++ /dev/null @@ -1,45 +0,0 @@ -# Lab 03 — Permissions (Lesson 3) - -## Goal -Reproduce a "permission denied" bug and fix it correctly (no 777). - - -## How to Learn From This Lab - -Use the **predict → run → explain** loop: - -1. **Predict:** write what you think the command will do. -2. **Run:** execute exactly one step. -3. **Explain:** write what the output means in plain language. -4. **Verify:** use a second command or the filesystem state to prove the result. - -## Beginner Checkpoints - -- Translate every permission string into a sentence before changing it. -- After `chmod`, test what changed using `ls -l` instead of trusting memory. -- Challenge: explain why a secret file should usually not be readable by `other`. - -## Setup -```bash -mkdir -p ~/swexp-lab/l03 && cd ~/swexp-lab/l03 -echo "API_KEY=shhh" > secrets.env -chmod 666 secrets.env # too open, on purpose -mkdir releases -``` - -## Tasks -1. **Decode** ten permission strings (use real `ls -l /usr/bin | head`). -2. **Inspect:** `ls -l secrets.env` — what is wrong with `666`? -3. **Fix the secret** to least privilege: - ```bash - chmod 640 secrets.env - ls -l secrets.env # owner rw, group r, other none - ``` -4. **Find anything world-writable** in your lab dir: - ```bash - find ~/swexp-lab/l03 -perm -002 -type f - ``` - -## Deliverable -`permissions-writeup.md` with before/after `ls -l` and why 640 (not 777). -Solution: [`../solutions/lab-03-solution.md`](../solutions/lab-03-solution.md). diff --git a/labs/lab-03-permissions/README.md b/labs/lab-03-permissions/README.md new file mode 100644 index 0000000..a8c273e --- /dev/null +++ b/labs/lab-03-permissions/README.md @@ -0,0 +1,22 @@ +# Lab 03 — Permissions + +**Goal:** fix a "too open" secret to least privilege, and surface world-writable files — no `chmod 777`. + +## What you do +Complete [`solution.sh`](solution.sh). It takes a directory and must: + +1. Tighten `/secrets.env` to **640** (owner rw, group r, other none). +2. Print every **world-writable** regular file under ``, one path per line (hint: `find … -perm -002 -type f`). + +Run the tests: +```bash +npx bats labs/lab-03-permissions/tests +# or everything: npm test +``` + +## Definition of done +- `npm test` for this lab is green; `npm run check` (shell syntax) is clean. +- In your LMS notebook, explain why `640` (not `777`), and decode a few `ls -l` strings. + +## Submit +Commit and push. The autograder scores it automatically. diff --git a/labs/lab-03-permissions/solution.sh b/labs/lab-03-permissions/solution.sh new file mode 100755 index 0000000..63429bc --- /dev/null +++ b/labs/lab-03-permissions/solution.sh @@ -0,0 +1,10 @@ +#!/usr/bin/env bash +# Lab 03 — Permissions. See README.md. +# Usage: ./solution.sh +set -euo pipefail +dir="${1:?usage: solution.sh }" + +# TODO 1: tighten /secrets.env to least-privilege 640 (owner rw, group r, other none). + +# TODO 2: print every WORLD-WRITABLE regular file under , one path per line. +# hint: find "$dir" -perm -002 -type f diff --git a/labs/lab-03-permissions/tests/permissions.bats b/labs/lab-03-permissions/tests/permissions.bats new file mode 100644 index 0000000..defd92b --- /dev/null +++ b/labs/lab-03-permissions/tests/permissions.bats @@ -0,0 +1,26 @@ +setup() { + LAB_DIR="$(cd "$(dirname "$BATS_TEST_FILENAME")/.." && pwd)" + SOL="$LAB_DIR/solution.sh" + WORK="$(mktemp -d)" + printf 'API_KEY=shhh\n' > "$WORK/secrets.env"; chmod 666 "$WORK/secrets.env" + printf 'data\n' > "$WORK/open.bin"; chmod 662 "$WORK/open.bin" # world-writable + printf 'ok\n' > "$WORK/safe.txt"; chmod 644 "$WORK/safe.txt" +} +teardown() { rm -rf "$WORK"; } + +@test "secrets.env is tightened to 640" { + bash "$SOL" "$WORK" >/dev/null + run stat -c '%a' "$WORK/secrets.env" + [ "$output" = "640" ] +} + +@test "world-writable files are reported" { + run bash "$SOL" "$WORK" + [ "$status" -eq 0 ] + echo "$output" | grep -q 'open.bin' +} + +@test "safe files are not flagged as world-writable" { + run bash "$SOL" "$WORK" + ! echo "$output" | grep -q 'safe.txt' +} diff --git a/labs/lab-04-users-groups.md b/labs/lab-04-users-groups.md deleted file mode 100644 index df3c1a0..0000000 --- a/labs/lab-04-users-groups.md +++ /dev/null @@ -1,48 +0,0 @@ -# Lab 04 — Users & Groups (Lesson 4) - -> Requires `sudo`. Practice on a disposable box/VM/container. - -## Goal -Onboard a team with a shared, setgid working directory. - - -## How to Learn From This Lab - -Use the **predict → run → explain** loop: - -1. **Predict:** write what you think the command will do. -2. **Run:** execute exactly one step. -3. **Explain:** write what the output means in plain language. -4. **Verify:** use a second command or the filesystem state to prove the result. - -## Beginner Checkpoints - -- Predict which group a new file in `/srv/payments` will have. -- After creating each user/group, verify with `id` or `getent`. -- Challenge: create a non-member user and prove access is denied. - -## Tasks -1. **Group + users:** - ```bash - sudo groupadd payments - for u in alice bob carol dave; do sudo useradd -m -s /bin/bash -G payments "$u"; done - getent group payments - ``` -2. **Shared setgid dir:** - ```bash - sudo mkdir -p /srv/payments - sudo chgrp payments /srv/payments - sudo chmod 2770 /srv/payments - ls -ld /srv/payments # note the 's' in group perms - ``` -3. **Prove inheritance:** - ```bash - sudo -u alice bash -c 'touch /srv/payments/from-alice && ls -l /srv/payments' - # group should be 'payments', not 'alice' - ``` -4. **Scoped sudo (optional):** create `/etc/sudoers.d/payments-restart` and - validate with `sudo visudo -c`. - -## Deliverable -`onboarding-report.md` showing the group, dir perms, and inheritance proof. -Solution: [`../solutions/lab-04-solution.md`](../solutions/lab-04-solution.md). diff --git a/labs/lab-04-users-groups/README.md b/labs/lab-04-users-groups/README.md new file mode 100644 index 0000000..864bc6f --- /dev/null +++ b/labs/lab-04-users-groups/README.md @@ -0,0 +1,27 @@ +# Lab 04 — Users & Groups + +**Goal:** read group membership the way `getent group ` shows it. + +> Creating real users (`useradd`), a shared **setgid** directory (`chmod 2770`), and +> proving group inheritance all require `sudo` and a disposable box — you do that in +> the LMS code-server. Here we grade the gradable core: **parsing the group database.** + +## What you do +Complete [`solution.sh`](solution.sh). Given a group name and a `/etc/group`-style +file (`name:x:gid:member1,member2,...`), it must print each member of that group +on its own line. An existing-but-empty group prints nothing and still exits 0. + +A fixture lives in [`fixtures/group`](fixtures/group). + +```bash +npx bats labs/lab-04-users-groups/tests +./solution.sh payments fixtures/group +``` + +## Definition of done +- `npm test` for this lab is green; `npm run check` is clean. +- In your LMS notebook, explain why a shared dir uses `2770` (setgid) and what the `s` + in `ls -ld` group permissions means. + +## Submit +Commit and push. diff --git a/labs/lab-04-users-groups/fixtures/group b/labs/lab-04-users-groups/fixtures/group new file mode 100644 index 0000000..0c11efb --- /dev/null +++ b/labs/lab-04-users-groups/fixtures/group @@ -0,0 +1,5 @@ +root:x:0: +sudo:x:27:alice +payments:x:1001:alice,bob,carol +web:x:1002:dave +empty:x:1003: diff --git a/labs/lab-04-users-groups/solution.sh b/labs/lab-04-users-groups/solution.sh new file mode 100755 index 0000000..0007fc4 --- /dev/null +++ b/labs/lab-04-users-groups/solution.sh @@ -0,0 +1,14 @@ +#!/usr/bin/env bash +# Lab 04 — Users & groups. See README.md. +# Usage: ./solution.sh +# Print the members of , one per line, parsed from a getent/`/etc/group` +# style file: name:passwd:gid:member1,member2,member3 +# Print nothing (exit 0) if the group exists but has no members. +set -euo pipefail +group="${1:?usage: solution.sh }" +file="${2:?usage: solution.sh }" + +# TODO: find the line whose first colon-field equals "$group", take the 4th +# field (the comma-separated member list), and print each member on its +# own line. +# hint: awk -F: -v g="$group" '$1==g {print $4}' "$file" | tr ',' '\n' | sed '/^$/d' diff --git a/labs/lab-04-users-groups/tests/groups.bats b/labs/lab-04-users-groups/tests/groups.bats new file mode 100644 index 0000000..8966be0 --- /dev/null +++ b/labs/lab-04-users-groups/tests/groups.bats @@ -0,0 +1,26 @@ +setup() { + LAB_DIR="$(cd "$(dirname "$BATS_TEST_FILENAME")/.." && pwd)" + SOL="$LAB_DIR/solution.sh" + GRP="$LAB_DIR/fixtures/group" +} + +@test "lists all members of the payments group, one per line" { + run bash "$SOL" payments "$GRP" + [ "$status" -eq 0 ] + [ "${lines[0]}" = "alice" ] + [ "${lines[1]}" = "bob" ] + [ "${lines[2]}" = "carol" ] + [ "${#lines[@]}" -eq 3 ] +} + +@test "lists the single member of the web group" { + run bash "$SOL" web "$GRP" + [ "$status" -eq 0 ] + [ "$output" = "dave" ] +} + +@test "prints nothing for an empty group but still succeeds" { + run bash "$SOL" empty "$GRP" + [ "$status" -eq 0 ] + [ -z "$output" ] +} diff --git a/labs/lab-05-log-investigation.md b/labs/lab-05-log-investigation.md deleted file mode 100644 index f0711c6..0000000 --- a/labs/lab-05-log-investigation.md +++ /dev/null @@ -1,82 +0,0 @@ -# Lab 05 — Investigate a Production Outage (Lesson 5) - -## Goal -Find the start/end of a 500-error spike, the affected endpoint, the request -count, and the correlated application error — using only the command line. - - -## How to Learn From This Lab - -Use the **predict → run → explain** loop: - -1. **Predict:** write what you think the command will do. -2. **Run:** execute exactly one step. -3. **Explain:** write what the output means in plain language. -4. **Verify:** use a second command or the filesystem state to prove the result. - -## Beginner Checkpoints - -- Before using `awk`, label the fields in one sample log line. -- Build the investigation from count → window → endpoint → cause. -- Challenge: write the incident timeline without using the solution file, then compare. - -## Setup — generate a realistic incident - -Paste this to create `access.log` and `app.log` with a planted outage: - -```bash -mkdir -p ~/swexp-lab/l05 && cd ~/swexp-lab/l05 - -awk 'BEGIN{ - srand(7); - paths["/health"]=1; paths["/api/products"]=1; paths["/api/cart"]=1; paths["/api/checkout"]=1; - split("GET POST", m, " "); - for(min=0; min<60; min++){ - for(r=0; r<60; r++){ - h=14; mm=sprintf("%02d", min); ss=sprintf("%02d", r); - ip=sprintf("10.0.%d.%d", int(rand()*4), int(rand()*254)+1); - n=int(rand()*4)+1; cnt=0; path="/health"; - for(p in paths){ cnt++; if(cnt==n) path=p; } - status=200; - # plant a /api/checkout 500 spike from 14:00 to 14:25 - if(path=="/api/checkout" && min>=0 && min<=25 && rand()<0.7) status=500; - printf "%s - - [01/Jun/2025:%02d:%s:%s +0000] \"%s %s HTTP/1.1\" %d 512\n", ip, h, mm, ss, m[int(rand()*2)+1], path, status; - } - } -}' > access.log - -{ - echo "2025-06-01 13:59:10 INFO app started, pool=10" - echo "2025-06-01 14:00:05 ERROR PaymentGateway timeout: connection pool exhausted (max=10)" - echo "2025-06-01 14:12:44 ERROR PaymentGateway timeout: connection pool exhausted (max=10)" - echo "2025-06-01 14:25:31 INFO pool size increased to 50; recovering" - echo "2025-06-01 14:26:00 INFO error rate back to baseline" -} > app.log - -wc -l access.log app.log -``` - -## Tasks - -1. **Status breakdown** (the top-N idiom): - ```bash - awk '{print $9}' access.log | sort | uniq -c | sort -rn - ``` -2. **Bound the incident** — first and last 500: - ```bash - grep ' 500 ' access.log | head -1 - grep ' 500 ' access.log | tail -1 - ``` -3. **Affected count and endpoint:** - ```bash - grep ' 500 ' access.log | awk '{print $7}' | sort | uniq -c | sort -rn - ``` -4. **Correlate the cause** in the app log: - ```bash - grep -i error app.log - ``` - -## Deliverable -Write `incident-timeline.md`: the spike window, affected count, offending -endpoint, and the root-cause line from `app.log`. Compare your answer to -[`../solutions/lab-05-solution.md`](../solutions/lab-05-solution.md). diff --git a/labs/lab-05-log-investigation/README.md b/labs/lab-05-log-investigation/README.md new file mode 100644 index 0000000..6c1de10 --- /dev/null +++ b/labs/lab-05-log-investigation/README.md @@ -0,0 +1,30 @@ +# Lab 05 — Log Investigation + +**Goal:** turn a raw access log into incident facts using only the command line. + +The full lab bounds a 500-spike window and correlates it with an app log. Here we +grade two deterministic facts: **the busiest client and the error volume.** + +## What you do +Complete [`solution.sh`](solution.sh). Given an access log in combined format +(field 1 = client IP, field 9 = HTTP status), print exactly two lines: + +``` +busiest # the client IP responsible for the most requests +errors # total number of 4xx + 5xx responses +``` + +A fixture lives in [`fixtures/access.log`](fixtures/access.log). + +```bash +npx bats labs/lab-05-log-investigation/tests +./solution.sh fixtures/access.log +``` + +## Definition of done +- `npm test` for this lab is green; `npm run check` is clean. +- In your LMS notebook, label the fields of one log line and write the incident + timeline (window, endpoint, root cause). + +## Submit +Commit and push. diff --git a/labs/lab-05-log-investigation/fixtures/access.log b/labs/lab-05-log-investigation/fixtures/access.log new file mode 100644 index 0000000..2e49e1d --- /dev/null +++ b/labs/lab-05-log-investigation/fixtures/access.log @@ -0,0 +1,10 @@ +10.0.0.7 - - [01/Jun/2025:14:00:01 +0000] "GET /api/checkout HTTP/1.1" 500 512 +10.0.0.7 - - [01/Jun/2025:14:00:02 +0000] "GET /api/checkout HTTP/1.1" 500 512 +10.0.0.7 - - [01/Jun/2025:14:00:03 +0000] "POST /api/cart HTTP/1.1" 200 128 +10.0.0.7 - - [01/Jun/2025:14:00:04 +0000] "GET /api/products HTTP/1.1" 404 64 +10.0.0.7 - - [01/Jun/2025:14:00:05 +0000] "GET /health HTTP/1.1" 200 8 +10.0.0.3 - - [01/Jun/2025:14:00:06 +0000] "GET /api/products HTTP/1.1" 200 256 +10.0.0.3 - - [01/Jun/2025:14:00:07 +0000] "GET /api/checkout HTTP/1.1" 503 0 +10.0.0.3 - - [01/Jun/2025:14:00:08 +0000] "GET /health HTTP/1.1" 200 8 +10.0.0.9 - - [01/Jun/2025:14:00:09 +0000] "GET /api/cart HTTP/1.1" 401 32 +10.0.0.9 - - [01/Jun/2025:14:00:10 +0000] "GET /health HTTP/1.1" 200 8 diff --git a/labs/lab-05-log-investigation/solution.sh b/labs/lab-05-log-investigation/solution.sh new file mode 100755 index 0000000..9e9d235 --- /dev/null +++ b/labs/lab-05-log-investigation/solution.sh @@ -0,0 +1,16 @@ +#!/usr/bin/env bash +# Lab 05 — Log investigation. See README.md. +# Usage: ./solution.sh +# An access log in combined format (field 1 = client IP, field 9 = HTTP status). +# Print exactly two lines: +# busiest # the client IP with the most requests +# errors # total number of 4xx + 5xx responses +set -euo pipefail +log="${1:?usage: solution.sh }" + +# TODO 1: print the busiest client IP: busiest +# hint: awk '{print $1}' "$log" | sort | uniq -c | sort -rn | head -1 +# -> the IP is the second column of that top line. + +# TODO 2: print the count of 4xx and 5xx responses: errors +# hint: awk '$9 ~ /^[45][0-9][0-9]$/' "$log" | wc -l diff --git a/labs/lab-05-log-investigation/tests/investigate.bats b/labs/lab-05-log-investigation/tests/investigate.bats new file mode 100644 index 0000000..2055daa --- /dev/null +++ b/labs/lab-05-log-investigation/tests/investigate.bats @@ -0,0 +1,23 @@ +setup() { + LAB_DIR="$(cd "$(dirname "$BATS_TEST_FILENAME")/.." && pwd)" + SOL="$LAB_DIR/solution.sh" + LOG="$LAB_DIR/fixtures/access.log" +} + +@test "identifies the busiest client IP" { + run bash "$SOL" "$LOG" + [ "$status" -eq 0 ] + echo "$output" | grep -Eq '^busiest[[:space:]]+10\.0\.0\.7$' +} + +@test "counts all 4xx and 5xx responses" { + run bash "$SOL" "$LOG" + [ "$status" -eq 0 ] + echo "$output" | grep -Eq '^errors[[:space:]]+5$' +} + +@test "does not count 2xx responses as errors" { + run bash "$SOL" "$LOG" + [ "$status" -eq 0 ] + ! echo "$output" | grep -Eq '^errors[[:space:]]+10$' +} diff --git a/labs/lab-06-pipeline.md b/labs/lab-06-pipeline.md deleted file mode 100644 index 0eef26a..0000000 --- a/labs/lab-06-pipeline.md +++ /dev/null @@ -1,48 +0,0 @@ -# Lab 06 — Log Processing Pipeline (Lesson 6) - -## Goal -Turn raw access logs into a deterministic report; tee 5xx lines aside. - - -## How to Learn From This Lab - -Use the **predict → run → explain** loop: - -1. **Predict:** write what you think the command will do. -2. **Run:** execute exactly one step. -3. **Explain:** write what the output means in plain language. -4. **Verify:** use a second command or the filesystem state to prove the result. - -## Beginner Checkpoints - -- Predict what stream goes to which file before running the redirection command. -- Build your report one pipe at a time and save the intermediate observations. -- Challenge: compare file-input and stdin-input output with `diff`. - -## Setup -Reuse `access.log` from Lab 05, or regenerate it. - -## Tasks -1. **Stream routing:** - ```bash - ls /nope /etc > out.txt 2> err.txt; cat out.txt; echo "---"; cat err.txt - ``` -2. **Build the pipeline incrementally** (add one stage, inspect, repeat): - ```bash - awk '{print $9}' access.log | sort | uniq -c | sort -rn # status - awk '{print $7}' access.log | sort | uniq -c | sort -rn | head # endpoints - awk '{print $1}' access.log | sort | uniq -c | sort -rn | head # IPs - ``` -3. **Tee the 5xx lines while counting:** - ```bash - grep -E ' 5[0-9][0-9] ' access.log | tee errors.log | wc -l - ``` -4. **Make it stdin-capable** so both work: - ```bash - ./logreport.sh access.log - cat access.log | ./logreport.sh - ``` - -## Deliverable -`logreport.sh` + `report-sample.md`. Solution: -[`../solutions/lab-06-solution.md`](../solutions/lab-06-solution.md). diff --git a/labs/lab-06-pipeline/README.md b/labs/lab-06-pipeline/README.md new file mode 100644 index 0000000..60d2752 --- /dev/null +++ b/labs/lab-06-pipeline/README.md @@ -0,0 +1,23 @@ +# Lab 06 — Log Processing Pipeline + +**Goal:** turn a raw access log into a deterministic report and tee the 5xx lines aside. + +## What you do +Complete [`solution.sh`](solution.sh). Given an access log (a file argument **or** stdin), it must: + +1. Print a status-code tally — lines of ` ` sorted by count, descending + (hint: `awk '{print $9}' | sort | uniq -c | sort -rn`). +2. Write every **5xx** request line to `./errors.log` (hint: `grep -E ' 5[0-9][0-9] ' | tee errors.log`). + +It must work both ways: +```bash +npx bats labs/lab-06-pipeline/tests +./solution.sh fixtures/access.log +cat fixtures/access.log | ./solution.sh +``` + +## Definition of done +- Tests green; `npm run check` clean. The report is identical from file or stdin. + +## Submit +Commit and push. diff --git a/labs/lab-06-pipeline/fixtures/access.log b/labs/lab-06-pipeline/fixtures/access.log new file mode 100644 index 0000000..e14e8fe --- /dev/null +++ b/labs/lab-06-pipeline/fixtures/access.log @@ -0,0 +1,5 @@ +10.0.0.1 - - [10/Oct/2026:13:55:01 +0000] "GET /a HTTP/1.1" 200 100 +10.0.0.2 - - [10/Oct/2026:13:55:02 +0000] "GET /b HTTP/1.1" 404 50 +10.0.0.1 - - [10/Oct/2026:13:55:03 +0000] "GET /c HTTP/1.1" 500 0 +10.0.0.3 - - [10/Oct/2026:13:55:04 +0000] "GET /d HTTP/1.1" 200 100 +10.0.0.1 - - [10/Oct/2026:13:55:05 +0000] "GET /e HTTP/1.1" 503 0 diff --git a/labs/lab-06-pipeline/solution.sh b/labs/lab-06-pipeline/solution.sh new file mode 100755 index 0000000..dce45e0 --- /dev/null +++ b/labs/lab-06-pipeline/solution.sh @@ -0,0 +1,13 @@ +#!/usr/bin/env bash +# Lab 06 — Log processing pipeline. See README.md. +# Usage: ./solution.sh [access.log] (reads stdin when no file is given) +set -euo pipefail + +# Capture input from the file argument, or stdin if none was given: +src="$(cat -- "${1:-/dev/stdin}")" + +# TODO 1: print a status-code tally (" ", sorted by count desc) to stdout. +# hint: printf '%s\n' "$src" | awk '{print $9}' | sort | uniq -c | sort -rn + +# TODO 2: write every 5xx request line to ./errors.log. +# hint: printf '%s\n' "$src" | grep -E ' 5[0-9][0-9] ' | tee errors.log >/dev/null diff --git a/labs/lab-06-pipeline/tests/pipeline.bats b/labs/lab-06-pipeline/tests/pipeline.bats new file mode 100644 index 0000000..22d968c --- /dev/null +++ b/labs/lab-06-pipeline/tests/pipeline.bats @@ -0,0 +1,27 @@ +setup() { + LAB_DIR="$(cd "$(dirname "$BATS_TEST_FILENAME")/.." && pwd)" + SOL="$LAB_DIR/solution.sh" + LOG="$LAB_DIR/fixtures/access.log" + WORK="$(mktemp -d)" + cd "$WORK" +} +teardown() { rm -rf "$WORK"; } + +@test "status tally puts '2 200' on top" { + run bash "$SOL" "$LOG" + [ "$status" -eq 0 ] + echo "$output" | grep -Eq '^[[:space:]]*2[[:space:]]+200' +} + +@test "produces the same tally from stdin" { + run bash -c "cat '$LOG' | bash '$SOL'" + [ "$status" -eq 0 ] + echo "$output" | grep -Eq '2[[:space:]]+200' +} + +@test "tees exactly the two 5xx lines to errors.log" { + bash "$SOL" "$LOG" >/dev/null + [ -f errors.log ] + run wc -l < errors.log + [ "$output" -eq 2 ] +} diff --git a/labs/lab-07-dotfiles.md b/labs/lab-07-dotfiles.md deleted file mode 100644 index 6c65511..0000000 --- a/labs/lab-07-dotfiles.md +++ /dev/null @@ -1,44 +0,0 @@ -# Lab 07 — Dotfiles (Lesson 7) - -## Goal -Build a versioned, reproducible shell environment. - - -## How to Learn From This Lab - -Use the **predict → run → explain** loop: - -1. **Predict:** write what you think the command will do. -2. **Run:** execute exactly one step. -3. **Explain:** write what the output means in plain language. -4. **Verify:** use a second command or the filesystem state to prove the result. - -## Beginner Checkpoints - -- Predict whether `NAME=value` will appear in `printenv NAME` before and after `export`. -- Use `type` to check aliases/functions before relying on them. -- Challenge: open a new shell and prove your changes survived. - -## Tasks -1. **Inspect environment:** `echo "$PATH"; echo "$HOME"; echo "$SHELL"; type ll 2>/dev/null` -2. **Prompt:** add to `~/.bashrc`, then `source ~/.bashrc`: - ```bash - PS1='\[\e[32m\]\u@\h\[\e[0m\]:\[\e[34m\]\w\[\e[0m\]\$ ' - ``` -3. **Aliases + function:** - ```bash - alias ll='ls -lah'; alias ..='cd ..'; alias gs='git status' - mkcd(){ mkdir -p "$1" && cd "$1"; } - ``` -4. **Repo + bootstrap:** - ```bash - mkdir -p ~/dotfiles && cd ~/dotfiles && git init - cp ~/.bashrc ./bashrc - printf '#!/usr/bin/env bash\nset -euo pipefail\nln -sfn "$PWD/bashrc" "$HOME/.bashrc"\n' > install.sh - chmod +x install.sh - git add . && git commit -m "INFRA-1180: dotfiles" - ``` - -## Deliverable -A `dotfiles` repo with documented aliases and an `install.sh`. Solution: -[`../solutions/lab-07-solution.md`](../solutions/lab-07-solution.md). diff --git a/labs/lab-07-dotfiles/README.md b/labs/lab-07-dotfiles/README.md new file mode 100644 index 0000000..fb4a0c2 --- /dev/null +++ b/labs/lab-07-dotfiles/README.md @@ -0,0 +1,25 @@ +# Lab 07 — Dotfiles + +**Goal:** build a reproducible shell environment that you install with one command — +the install step is a set of **symlinks** from a versioned `dotfiles/` repo into `$HOME`. + +The full lab also writes a `PS1`, aliases, and a function. Here we grade the core of +`install.sh`: **idempotent symlinking.** + +## What you do +Complete [`solution.sh`](solution.sh). Given a dotfiles directory and a home +directory, for every file `x` in the dotfiles dir create a symlink at +`/.x` pointing back to the source. Running it twice must succeed and leave +identical links. + +```bash +npx bats labs/lab-07-dotfiles/tests +``` + +## Definition of done +- `npm test` for this lab is green; `npm run check` is clean. +- In your LMS notebook, open a new shell and prove your aliases/prompt survived, and + explain why symlinks (not copies) make dotfiles reproducible. + +## Submit +Commit and push. diff --git a/labs/lab-07-dotfiles/solution.sh b/labs/lab-07-dotfiles/solution.sh new file mode 100755 index 0000000..6e0f0c3 --- /dev/null +++ b/labs/lab-07-dotfiles/solution.sh @@ -0,0 +1,16 @@ +#!/usr/bin/env bash +# Lab 07 — Dotfiles install. See README.md. +# Usage: ./solution.sh +# For every regular file named "x" in , create a symlink at +# /.x that points back to the source file. Must be IDEMPOTENT: +# running it a second time succeeds and leaves the same links. +set -euo pipefail +src="${1:?usage: solution.sh }" +home="${2:?usage: solution.sh }" + +# TODO 1: ensure "$home" exists. hint: mkdir -p "$home" + +# TODO 2: for each regular file in "$src", symlink it to "$home/.". +# Use an idempotent, force flag so a second run does not error. +# hint: ln -sfn "$src/$name" "$home/.$name" +# (ln -s = symlink, -f = replace existing, -n = treat existing link as a file) diff --git a/labs/lab-07-dotfiles/tests/dotfiles.bats b/labs/lab-07-dotfiles/tests/dotfiles.bats new file mode 100644 index 0000000..d0a0920 --- /dev/null +++ b/labs/lab-07-dotfiles/tests/dotfiles.bats @@ -0,0 +1,31 @@ +setup() { + LAB_DIR="$(cd "$(dirname "$BATS_TEST_FILENAME")/.." && pwd)" + SOL="$LAB_DIR/solution.sh" + WORK="$(mktemp -d)" + mkdir -p "$WORK/dotfiles" "$WORK/home" + printf 'export EDITOR=vim\n' > "$WORK/dotfiles/bashrc" + printf '[user]\n name = dev\n' > "$WORK/dotfiles/gitconfig" +} +teardown() { rm -rf "$WORK"; } + +@test "creates symlinks named . in home pointing at the source" { + run bash "$SOL" "$WORK/dotfiles" "$WORK/home" + [ "$status" -eq 0 ] + [ -L "$WORK/home/.bashrc" ] + [ -L "$WORK/home/.gitconfig" ] + [ "$(readlink "$WORK/home/.bashrc")" = "$WORK/dotfiles/bashrc" ] +} + +@test "the symlink resolves to the source content" { + bash "$SOL" "$WORK/dotfiles" "$WORK/home" >/dev/null + run cat "$WORK/home/.gitconfig" + echo "$output" | grep -q 'name = dev' +} + +@test "is idempotent — a second run still succeeds with the same links" { + bash "$SOL" "$WORK/dotfiles" "$WORK/home" >/dev/null + run bash "$SOL" "$WORK/dotfiles" "$WORK/home" + [ "$status" -eq 0 ] + [ -L "$WORK/home/.bashrc" ] + [ "$(readlink "$WORK/home/.bashrc")" = "$WORK/dotfiles/bashrc" ] +} diff --git a/labs/lab-08-onboarding-script.md b/labs/lab-08-onboarding-script.md deleted file mode 100644 index 4953334..0000000 --- a/labs/lab-08-onboarding-script.md +++ /dev/null @@ -1,39 +0,0 @@ -# Lab 08 — Onboarding Script (Lesson 8) - -## Goal -Write a robust, idempotent Bash script and verify it with ShellCheck. - - -## How to Learn From This Lab - -Use the **predict → run → explain** loop: - -1. **Predict:** write what you think the command will do. -2. **Run:** execute exactly one step. -3. **Explain:** write what the output means in plain language. -4. **Verify:** use a second command or the filesystem state to prove the result. - -## Beginner Checkpoints - -- Predict which line will fail if a required command is missing. -- Run the script twice and compare the final state, not just the printed messages. -- Challenge: deliberately break one prerequisite and confirm the script exits non-zero. - -## Tasks -1. **Strict-mode skeleton:** - ```bash - cat > onboard-dev.sh <<'SH' - #!/usr/bin/env bash - set -euo pipefail - log(){ printf '[%s] %s\n' "$(date +%T)" "$*"; } - fail(){ printf 'ERROR: %s\n' "$*" >&2; exit 1; } - SH - ``` -2. **Prereq check:** `for c in git curl; do command -v "$c" >/dev/null || fail "$c missing"; done` -3. **Idempotent setup:** `mkdir -p "$HOME/projects"` and clone-only-if-absent logic. -4. **Summary + exit code:** track failures, `exit 1` if any. -5. **Verify:** `shellcheck onboard-dev.sh` — fix every warning. Run twice to prove idempotency. - -## Deliverable -A `shellcheck`-clean, idempotent `onboard-dev.sh`. Solution: -[`../solutions/lab-08-solution.md`](../solutions/lab-08-solution.md). diff --git a/labs/lab-08-onboarding-script/README.md b/labs/lab-08-onboarding-script/README.md new file mode 100644 index 0000000..7753a5a --- /dev/null +++ b/labs/lab-08-onboarding-script/README.md @@ -0,0 +1,22 @@ +# Lab 08 — Onboarding Script + +**Goal:** write a robust, **idempotent** Bash script with a prerequisite check. + +## What you do +Complete [`solution.sh`](solution.sh). Given a target directory, it must: + +1. **Fail** (exit non-zero, message to stderr) if any command listed in `$REQUIRE` + (default `git bash`) is missing — hint: `command -v "$c"`. +2. **Idempotently** create `/projects` and a `/.onboarded` marker. + Running it twice must succeed and leave the same state (use `mkdir -p`, guard the marker). + +```bash +npx bats labs/lab-08-onboarding-script/tests +``` + +## Definition of done +- Tests green; `npm run check` clean. In your LMS notebook, prove idempotency by running it + twice and comparing the resulting state (and run ShellCheck if you have it). + +## Submit +Commit and push. diff --git a/labs/lab-08-onboarding-script/solution.sh b/labs/lab-08-onboarding-script/solution.sh new file mode 100755 index 0000000..a62c8b2 --- /dev/null +++ b/labs/lab-08-onboarding-script/solution.sh @@ -0,0 +1,14 @@ +#!/usr/bin/env bash +# Lab 08 — Idempotent onboarding script. See README.md. +# Usage: ./solution.sh +# Env: REQUIRE="git bash" (space-separated required commands) +set -euo pipefail +target="${1:?usage: solution.sh }" +REQUIRE="${REQUIRE:-git bash}" + +log() { printf '[%s] %s\n' "$(date +%T)" "$*"; } +fail() { printf 'ERROR: %s\n' "$*" >&2; exit 1; } + +# TODO 1: for each command in $REQUIRE, fail() if it is not available. + +# TODO 2: idempotently create "$target/projects" and the marker "$target/.onboarded". diff --git a/labs/lab-08-onboarding-script/tests/onboard.bats b/labs/lab-08-onboarding-script/tests/onboard.bats new file mode 100644 index 0000000..d0c32ea --- /dev/null +++ b/labs/lab-08-onboarding-script/tests/onboard.bats @@ -0,0 +1,27 @@ +setup() { + LAB_DIR="$(cd "$(dirname "$BATS_TEST_FILENAME")/.." && pwd)" + SOL="$LAB_DIR/solution.sh" + WORK="$(mktemp -d)" +} +teardown() { rm -rf "$WORK"; } + +@test "creates the projects dir and the marker" { + run bash "$SOL" "$WORK/box" + [ "$status" -eq 0 ] + [ -d "$WORK/box/projects" ] + [ -f "$WORK/box/.onboarded" ] +} + +@test "is idempotent — a second run still succeeds" { + bash "$SOL" "$WORK/box" >/dev/null + run bash "$SOL" "$WORK/box" + [ "$status" -eq 0 ] + [ -d "$WORK/box/projects" ] +} + +@test "fails when a required command is missing" { + export REQUIRE="git definitely-not-a-real-cmd-xyz" + run bash "$SOL" "$WORK/box" + unset REQUIRE + [ "$status" -ne 0 ] +} diff --git a/labs/lab-09-process-management.md b/labs/lab-09-process-management.md deleted file mode 100644 index d1012a8..0000000 --- a/labs/lab-09-process-management.md +++ /dev/null @@ -1,67 +0,0 @@ -# Lab 09 — Production Service Failure (Lesson 9) - -## Goal -Find a runaway process, stop it gracefully, and understand why a service -won't stay up — then restore it. - - -## How to Learn From This Lab - -Use the **predict → run → explain** loop: - -1. **Predict:** write what you think the command will do. -2. **Run:** execute exactly one step. -3. **Explain:** write what the output means in plain language. -4. **Verify:** use a second command or the filesystem state to prove the result. - -## Beginner Checkpoints - -- Predict which process will appear near the top of CPU usage. -- Try SIGTERM before SIGKILL and record what happened. -- Challenge: explain the difference between `start` and `enable` in one sentence. - -## Setup — safe CPU burner + a "crashing service" simulation - -```bash -mkdir -p ~/swexp-lab/l09 && cd ~/swexp-lab/l09 - -# A bounded CPU burner you can find and kill (auto-stops after 120s) -cat > burner.sh <<'SH' -#!/usr/bin/env bash -end=$(( $(date +%s) + 120 )) -while [ "$(date +%s)" -lt "$end" ]; do :; done -SH -chmod +x burner.sh -./burner.sh & -echo "burner PID: $!" -``` - -## Tasks - -1. **Find the runaway** by CPU: - ```bash - ps aux --sort=-%cpu | head -5 - pgrep -fl burner - ``` -2. **Stop it gracefully, then forcefully if needed:** - ```bash - kill # SIGTERM - sleep 2; kill -0 2>/dev/null && kill -9 # escalate if still alive - ``` -3. **Signals practice:** - ```bash - sleep 600 & echo "sleep PID $!" - kill -SIGTERM %1 # job spec also works - jobs - ``` -4. **(If you have systemd + a sample unit)** read why a unit died: - ```bash - systemctl status --no-pager - journalctl -u -e --no-pager | tail -30 - ``` - Look for the non-zero `Result:`/exit code and the last log line before exit. - -## Deliverable -`postmortem.md`: the runaway PID + its %CPU, the exact signals you sent, and -(if applicable) the log line explaining a service exit. Compare to -[`../solutions/lab-09-solution.md`](../solutions/lab-09-solution.md). diff --git a/labs/lab-09-process-management/README.md b/labs/lab-09-process-management/README.md new file mode 100644 index 0000000..63f097b --- /dev/null +++ b/labs/lab-09-process-management/README.md @@ -0,0 +1,27 @@ +# Lab 09 — Process Management + +**Goal:** find a runaway process by CPU usage — the first step before you decide +which signal to send it. + +On a real box you would run `ps aux --sort=-%cpu | head`, then `kill`/`kill -9`. +Here we grade the gradable core: **finding the top-CPU PID in `ps aux` output.** + +## What you do +Complete [`solution.sh`](solution.sh). Given a `ps aux`-style table (a header +line, then rows of `USER PID %CPU %MEM ... COMMAND`), print **only** the PID of +the process using the most CPU. + +A fixture lives in [`fixtures/ps.txt`](fixtures/ps.txt). + +```bash +npx bats labs/lab-09-process-management/tests +./solution.sh fixtures/ps.txt +``` + +## Definition of done +- `npm test` for this lab is green; `npm run check` is clean. +- In your LMS notebook, record what happened when you sent SIGTERM before SIGKILL, + and explain the difference between `start` and `enable`. + +## Submit +Commit and push. diff --git a/labs/lab-09-process-management/fixtures/ps.txt b/labs/lab-09-process-management/fixtures/ps.txt new file mode 100644 index 0000000..929f712 --- /dev/null +++ b/labs/lab-09-process-management/fixtures/ps.txt @@ -0,0 +1,7 @@ +USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND +root 1 0.0 0.1 168924 11876 ? Ss 09:00 0:01 /sbin/init +dev 812 3.4 0.5 712044 44120 ? Sl 09:05 0:12 node server.js +dev 1337 98.7 0.2 12044 2048 pts/0 R 09:10 1:55 ./burner.sh +dev 1402 12.1 1.0 998220 80112 ? Sl 09:11 0:08 python3 worker.py +postgres 945 5.6 2.3 412880 188044 ? Ss 09:02 0:30 postgres: writer +dev 1500 0.1 0.1 8920 1024 pts/1 S+ 09:12 0:00 bash diff --git a/labs/lab-09-process-management/solution.sh b/labs/lab-09-process-management/solution.sh new file mode 100755 index 0000000..fc2435c --- /dev/null +++ b/labs/lab-09-process-management/solution.sh @@ -0,0 +1,11 @@ +#!/usr/bin/env bash +# Lab 09 — Process management. See README.md. +# Usage: ./solution.sh +# A `ps aux`-style table (header line, then USER PID %CPU %MEM ... COMMAND). +# Print ONLY the PID of the process using the most CPU. +set -euo pipefail +file="${1:?usage: solution.sh }" + +# TODO: skip the header line, sort the remaining rows by %CPU (field 3) +# descending, and print the PID (field 2) of the top row. +# hint: tail -n +2 "$file" | sort -k3 -rn | head -1 | awk '{print $2}' diff --git a/labs/lab-09-process-management/tests/process.bats b/labs/lab-09-process-management/tests/process.bats new file mode 100644 index 0000000..0df243f --- /dev/null +++ b/labs/lab-09-process-management/tests/process.bats @@ -0,0 +1,29 @@ +setup() { + LAB_DIR="$(cd "$(dirname "$BATS_TEST_FILENAME")/.." && pwd)" + SOL="$LAB_DIR/solution.sh" + PS="$LAB_DIR/fixtures/ps.txt" + WORK="$(mktemp -d)" +} +teardown() { rm -rf "$WORK"; } + +@test "prints the PID of the highest-%CPU process" { + run bash "$SOL" "$PS" + [ "$status" -eq 0 ] + [ "$output" = "1337" ] +} + +@test "ignores the header row" { + run bash "$SOL" "$PS" + [ "$status" -eq 0 ] + ! echo "$output" | grep -qi 'pid' +} + +@test "works on a different table" { + printf 'USER PID %%CPU %%MEM CMD\n' > "$WORK/ps.txt" + printf 'dev 200 4.0 1.0 a\n' >> "$WORK/ps.txt" + printf 'dev 201 50.5 1.0 b\n' >> "$WORK/ps.txt" + printf 'dev 202 9.9 1.0 c\n' >> "$WORK/ps.txt" + run bash "$SOL" "$WORK/ps.txt" + [ "$status" -eq 0 ] + [ "$output" = "201" ] +} diff --git a/labs/lab-10-networking.md b/labs/lab-10-networking.md deleted file mode 100644 index 05b4648..0000000 --- a/labs/lab-10-networking.md +++ /dev/null @@ -1,44 +0,0 @@ -# Lab 10 — Networking (Lesson 10) - -## Goal -Walk the connectivity layers and audit listening ports. - - -## How to Learn From This Lab - -Use the **predict → run → explain** loop: - -1. **Predict:** write what you think the command will do. -2. **Run:** execute exactly one step. -3. **Explain:** write what the output means in plain language. -4. **Verify:** use a second command or the filesystem state to prove the result. - -## Beginner Checkpoints - -- Predict whether `github.com` will fail at DNS, route, port, or HTTP before testing. -- Record each layer's evidence separately. -- Challenge: explain why a service bound to loopback is safer for local-only tools. - -## Tasks -1. **Interfaces + routes:** `ip addr; ip route` — identify your default gateway. -2. **Walk the layers to a known host:** - ```bash - dig +short github.com - nc -zv github.com 443 - curl -sS -o /dev/null -w '%{http_code}\n' https://github.com - ``` -3. **Listening-port audit:** - ```bash - ss -tlnp 2>/dev/null # add sudo to see process names - ``` -4. **Loopback trap:** - ```bash - python3 -m http.server 8080 --bind 127.0.0.1 & - ss -tlnp | grep 8080 # bound to 127.0.0.1 only - kill %1 - ``` - -## Deliverable -`network-report.md` with interfaces/routes, a layer-by-layer diagnosis, and a -listening-port table. Solution: -[`../solutions/lab-10-solution.md`](../solutions/lab-10-solution.md). diff --git a/labs/lab-10-networking/README.md b/labs/lab-10-networking/README.md new file mode 100644 index 0000000..7cef2ef --- /dev/null +++ b/labs/lab-10-networking/README.md @@ -0,0 +1,26 @@ +# Lab 10 — Networking + +**Goal:** audit which TCP ports a host is listening on — the core of any exposure review. + +On a real box you would walk DNS → route → port → HTTP and run `ss -tlnp`. Here we +grade the gradable core: **extracting the listening ports from `ss` output.** + +## What you do +Complete [`solution.sh`](solution.sh). Given the output of `ss -ltn` (a header +line, then `LISTEN` rows whose 4th field is `Address:Port`), print the **unique** +listening ports, one per line, **sorted numerically ascending**. + +A fixture lives in [`fixtures/ss.txt`](fixtures/ss.txt). + +```bash +npx bats labs/lab-10-networking/tests +./solution.sh fixtures/ss.txt +``` + +## Definition of done +- `npm test` for this lab is green; `npm run check` is clean. +- In your LMS notebook, explain why a service bound to `127.0.0.1` is safer than one + bound to `0.0.0.0`, using a port from the fixture as an example. + +## Submit +Commit and push. diff --git a/labs/lab-10-networking/fixtures/ss.txt b/labs/lab-10-networking/fixtures/ss.txt new file mode 100644 index 0000000..d73a6c9 --- /dev/null +++ b/labs/lab-10-networking/fixtures/ss.txt @@ -0,0 +1,7 @@ +State Recv-Q Send-Q Local Address:Port Peer Address:Port Process +LISTEN 0 4096 127.0.0.1:8080 0.0.0.0:* +LISTEN 0 511 0.0.0.0:80 0.0.0.0:* +LISTEN 0 128 0.0.0.0:22 0.0.0.0:* +LISTEN 0 4096 127.0.0.1:5432 0.0.0.0:* +LISTEN 0 511 [::]:80 [::]:* +LISTEN 0 128 [::]:22 [::]:* diff --git a/labs/lab-10-networking/solution.sh b/labs/lab-10-networking/solution.sh new file mode 100755 index 0000000..b72b516 --- /dev/null +++ b/labs/lab-10-networking/solution.sh @@ -0,0 +1,12 @@ +#!/usr/bin/env bash +# Lab 10 — Networking. See README.md. +# Usage: ./solution.sh +# Output of `ss -ltn` (header line, then LISTEN rows). Field 4 is the local +# socket "Address:Port". Print the unique listening ports, one per line, +# sorted numerically ascending. +set -euo pipefail +file="${1:?usage: solution.sh }" + +# TODO: skip the header, take field 4 (Local Address:Port), keep only the part +# after the LAST colon (the port), de-duplicate, and sort numerically. +# hint: tail -n +2 "$file" | awk '{print $4}' | sed 's/.*://' | sort -n | uniq diff --git a/labs/lab-10-networking/tests/networking.bats b/labs/lab-10-networking/tests/networking.bats new file mode 100644 index 0000000..757dcb3 --- /dev/null +++ b/labs/lab-10-networking/tests/networking.bats @@ -0,0 +1,27 @@ +setup() { + LAB_DIR="$(cd "$(dirname "$BATS_TEST_FILENAME")/.." && pwd)" + SOL="$LAB_DIR/solution.sh" + SS="$LAB_DIR/fixtures/ss.txt" +} + +@test "lists unique listening ports sorted ascending" { + run bash "$SOL" "$SS" + [ "$status" -eq 0 ] + [ "${lines[0]}" = "22" ] + [ "${lines[1]}" = "80" ] + [ "${lines[2]}" = "5432" ] + [ "${lines[3]}" = "8080" ] + [ "${#lines[@]}" -eq 4 ] +} + +@test "de-duplicates ports that appear on both IPv4 and IPv6" { + run bash "$SOL" "$SS" + [ "$status" -eq 0 ] + [ "$(echo "$output" | grep -c '^80$')" -eq 1 ] + [ "$(echo "$output" | grep -c '^22$')" -eq 1 ] +} + +@test "omits the header line from the output" { + run bash "$SOL" "$SS" + ! echo "$output" | grep -qi 'port' +} diff --git a/labs/lab-11-ssh.md b/labs/lab-11-ssh.md deleted file mode 100644 index 0b312b1..0000000 --- a/labs/lab-11-ssh.md +++ /dev/null @@ -1,45 +0,0 @@ -# Lab 11 — SSH (Lesson 11) - -> Single-box practice uses `ssh localhost`. Keep a second terminal open -> before changing `sshd_config` so you cannot lock yourself out. - -## Goal -Set up key auth and harden sshd safely. - - -## How to Learn From This Lab - -Use the **predict → run → explain** loop: - -1. **Predict:** write what you think the command will do. -2. **Run:** execute exactly one step. -3. **Explain:** write what the output means in plain language. -4. **Verify:** use a second command or the filesystem state to prove the result. - -## Beginner Checkpoints - -- Identify the private key and public key filenames before copying anything. -- Keep a fallback session open before changing server settings. -- Challenge: explain the safe rollout order from memory before editing config. - -## Tasks -1. **Generate a key:** `ssh-keygen -t ed25519 -C "swexp" -f ~/.ssh/swexp_ed25519` -2. **Authorize it:** - ```bash - mkdir -p ~/.ssh && chmod 700 ~/.ssh - cat ~/.ssh/swexp_ed25519.pub >> ~/.ssh/authorized_keys - chmod 600 ~/.ssh/authorized_keys - ssh -i ~/.ssh/swexp_ed25519 localhost 'echo key-login-ok' - ``` -3. **Client config:** add a `Host` block to `~/.ssh/config`. -4. **Harden (careful):** in `/etc/ssh/sshd_config` set `PermitRootLogin no`, - `PasswordAuthentication no`. Then: - ```bash - sudo sshd -t # validate FIRST - sudo systemctl reload ssh # reload, do not stop - # confirm key login in a NEW session before closing this one - ``` - -## Deliverable -`ssh-hardening.md` documenting the safe rollout. Solution: -[`../solutions/lab-11-solution.md`](../solutions/lab-11-solution.md). diff --git a/labs/lab-11-ssh/README.md b/labs/lab-11-ssh/README.md new file mode 100644 index 0000000..221714a --- /dev/null +++ b/labs/lab-11-ssh/README.md @@ -0,0 +1,32 @@ +# Lab 11 — SSH + +**Goal:** write a clean `~/.ssh/config` `Host` block so `ssh prod` just works — +no long `-i`/`-l` flags to remember. + +Generating keys, authorizing them, and hardening `sshd_config` happen on the LMS box +(and risk lockout, so they need a fallback session). Here we grade the gradable core: +**emitting a correct client-config block.** + +## What you do +Complete [`solution.sh`](solution.sh). Given ` `, +print a valid SSH config block to stdout containing: + +``` +Host + HostName + User + IdentityFile +``` + +```bash +npx bats labs/lab-11-ssh/tests +./solution.sh prod 203.0.113.10 deploy '~/.ssh/swexp_ed25519' +``` + +## Definition of done +- `npm test` for this lab is green; `npm run check` is clean. +- In your LMS notebook, write the **safe rollout order** for hardening sshd + (`sshd -t` → reload → confirm in a new session) from memory. + +## Submit +Commit and push. diff --git a/labs/lab-11-ssh/solution.sh b/labs/lab-11-ssh/solution.sh new file mode 100755 index 0000000..1b5b74d --- /dev/null +++ b/labs/lab-11-ssh/solution.sh @@ -0,0 +1,18 @@ +#!/usr/bin/env bash +# Lab 11 — SSH client config. See README.md. +# Usage: ./solution.sh +# Emit a valid ~/.ssh/config block to stdout, e.g.: +# Host prod +# HostName 203.0.113.10 +# User deploy +# IdentityFile ~/.ssh/swexp_ed25519 +set -euo pipefail +host="${1:?usage: solution.sh }" +hostname="${2:?usage: solution.sh }" +user="${3:?usage: solution.sh }" +identity="${4:?usage: solution.sh }" + +# TODO: print a Host block with four lines: Host, HostName, User, IdentityFile. +# The Host line starts at column 0; the other three are indented. +# hint: printf 'Host %s\n HostName %s\n User %s\n IdentityFile %s\n' \ +# "$host" "$hostname" "$user" "$identity" diff --git a/labs/lab-11-ssh/tests/ssh.bats b/labs/lab-11-ssh/tests/ssh.bats new file mode 100644 index 0000000..d024f84 --- /dev/null +++ b/labs/lab-11-ssh/tests/ssh.bats @@ -0,0 +1,28 @@ +setup() { + LAB_DIR="$(cd "$(dirname "$BATS_TEST_FILENAME")/.." && pwd)" + SOL="$LAB_DIR/solution.sh" +} + +@test "emits a Host block with all four directives" { + run bash "$SOL" prod 203.0.113.10 deploy '~/.ssh/swexp_ed25519' + [ "$status" -eq 0 ] + echo "$output" | grep -Eq '^Host[[:space:]]+prod$' + echo "$output" | grep -Eq 'HostName[[:space:]]+203\.0\.113\.10' + echo "$output" | grep -Eq 'User[[:space:]]+deploy' + echo "$output" | grep -Eq 'IdentityFile[[:space:]]+~/\.ssh/swexp_ed25519' +} + +@test "the Host line is the first line and not indented" { + run bash "$SOL" staging 198.51.100.5 ci '~/.ssh/id_ed25519' + [ "$status" -eq 0 ] + [ "${lines[0]}" = "Host staging" ] +} + +@test "reflects different arguments" { + run bash "$SOL" db 10.0.0.5 postgres '/home/dev/.ssh/db_key' + [ "$status" -eq 0 ] + echo "$output" | grep -Eq '^Host[[:space:]]+db$' + echo "$output" | grep -q '10.0.0.5' + echo "$output" | grep -q 'postgres' + echo "$output" | grep -q '/home/dev/.ssh/db_key' +} diff --git a/labs/lab-12-packages.md b/labs/lab-12-packages.md deleted file mode 100644 index 71eaf44..0000000 --- a/labs/lab-12-packages.md +++ /dev/null @@ -1,50 +0,0 @@ -# Lab 12 — Packages & Supply Chain (Lesson 12) - -> Requires `sudo`/`apt` (Debian/Ubuntu). On other distros, map to the local -> package manager. - -## Goal -Patch, inventory, and lock down package installation. - - -## How to Learn From This Lab - -Use the **predict → run → explain** loop: - -1. **Predict:** write what you think the command will do. -2. **Run:** execute exactly one step. -3. **Explain:** write what the output means in plain language. -4. **Verify:** use a second command or the filesystem state to prove the result. - -## Beginner Checkpoints - -- Predict whether `apt update` will change installed software. -- Use inventory commands before and after install/remove. -- Challenge: write a safer alternative for one install guide that suggests `curl | bash`. - -## Tasks -1. **Refresh + see what's pending:** - ```bash - sudo apt update - apt list --upgradable 2>/dev/null - ``` -2. **Inventory + ownership:** - ```bash - apt list --installed 2>/dev/null | wc -l - dpkg -S /bin/ls # which package owns this file? - ``` -3. **Install + inspect:** - ```bash - sudo apt install -y tree - dpkg -L tree | head - ``` -4. **Remove + purge + autoremove:** - ```bash - sudo apt remove --purge -y tree - sudo apt autoremove -y - ``` - -## Deliverable -`package-audit.md` with before/after counts and a "trusted install" policy -that replaces `curl | bash`. Solution: -[`../solutions/lab-12-solution.md`](../solutions/lab-12-solution.md). diff --git a/labs/lab-12-packages/README.md b/labs/lab-12-packages/README.md new file mode 100644 index 0000000..bbebba0 --- /dev/null +++ b/labs/lab-12-packages/README.md @@ -0,0 +1,27 @@ +# Lab 12 — Packages & Supply Chain + +**Goal:** inventory installed software and spot what is missing — the basis of a +reproducible, auditable box. + +On a real box you would use `apt list --installed`, `dpkg -S`, and a "trusted install" +policy. Here we grade the gradable core: **diffing a wanted set against the installed list.** + +## What you do +Complete [`solution.sh`](solution.sh). Given an installed-list file (one package +per line) and a list of wanted packages as arguments, print the wanted packages +that are **missing** from the installed list, one per line, preserving argument +order. Match whole package names only (`vi` must not match `vim`). + +A fixture lives in [`fixtures/installed.txt`](fixtures/installed.txt). + +```bash +npx bats labs/lab-12-packages/tests +./solution.sh fixtures/installed.txt git tree htop curl +``` + +## Definition of done +- `npm test` for this lab is green; `npm run check` is clean. +- In your LMS notebook, write a safer alternative to a `curl | bash` install guide. + +## Submit +Commit and push. diff --git a/labs/lab-12-packages/fixtures/installed.txt b/labs/lab-12-packages/fixtures/installed.txt new file mode 100644 index 0000000..101ca52 --- /dev/null +++ b/labs/lab-12-packages/fixtures/installed.txt @@ -0,0 +1,9 @@ +bash +coreutils +curl +git +openssh-client +openssh-server +python3 +tar +vim diff --git a/labs/lab-12-packages/solution.sh b/labs/lab-12-packages/solution.sh new file mode 100755 index 0000000..19301a0 --- /dev/null +++ b/labs/lab-12-packages/solution.sh @@ -0,0 +1,16 @@ +#!/usr/bin/env bash +# Lab 12 — Packages. See README.md. +# Usage: ./solution.sh +# lists installed package names, one per line. +# For each argument, print it (one per line, in the order given) ONLY if +# it is NOT present in the installed list. +set -euo pipefail +installed="${1:?usage: solution.sh }" +shift + +# TODO: for each remaining argument, check whether it appears as a whole line +# in "$installed"; if it does NOT, print the package name. +# hint: for pkg in "$@"; do +# grep -qxF "$pkg" "$installed" || printf '%s\n' "$pkg" +# done +# (grep -x = whole line match, -F = fixed string, -q = quiet) diff --git a/labs/lab-12-packages/tests/packages.bats b/labs/lab-12-packages/tests/packages.bats new file mode 100644 index 0000000..9c22724 --- /dev/null +++ b/labs/lab-12-packages/tests/packages.bats @@ -0,0 +1,26 @@ +setup() { + LAB_DIR="$(cd "$(dirname "$BATS_TEST_FILENAME")/.." && pwd)" + SOL="$LAB_DIR/solution.sh" + INST="$LAB_DIR/fixtures/installed.txt" +} + +@test "reports only the missing packages" { + run bash "$SOL" "$INST" git tree htop curl + [ "$status" -eq 0 ] + echo "$output" | grep -qx 'tree' + echo "$output" | grep -qx 'htop' + ! echo "$output" | grep -qx 'git' + ! echo "$output" | grep -qx 'curl' +} + +@test "prints nothing when all packages are installed" { + run bash "$SOL" "$INST" git curl vim + [ "$status" -eq 0 ] + [ -z "$output" ] +} + +@test "does not partial-match (vi must not match vim)" { + run bash "$SOL" "$INST" vi + [ "$status" -eq 0 ] + echo "$output" | grep -qx 'vi' +} diff --git a/labs/lab-13-scheduling.md b/labs/lab-13-scheduling.md deleted file mode 100644 index c6f3b26..0000000 --- a/labs/lab-13-scheduling.md +++ /dev/null @@ -1,42 +0,0 @@ -# Lab 13 — Scheduling (Lesson 13) - -## Goal -Schedule a backup with retention; rotate logs; make jobs observable. - - -## How to Learn From This Lab - -Use the **predict → run → explain** loop: - -1. **Predict:** write what you think the command will do. -2. **Run:** execute exactly one step. -3. **Explain:** write what the output means in plain language. -4. **Verify:** use a second command or the filesystem state to prove the result. - -## Beginner Checkpoints - -- Translate the cron schedule into English before installing it. -- Look for proof that a scheduled job ran, not just that it was configured. -- Challenge: intentionally dry-run logrotate and explain what it would do. - -## Tasks -1. **A logging cron job (every minute, then remove it):** - ```bash - ( crontab -l 2>/dev/null; echo '* * * * * date >> /tmp/cron-demo.log 2>&1' ) | crontab - - sleep 65; cat /tmp/cron-demo.log - crontab -l | grep -v cron-demo | crontab - # clean up - ``` -2. **Backup script with retention:** - ```bash - mkdir -p ~/swexp-lab/l13/{src,backups}; echo data > ~/swexp-lab/l13/src/file - tar -czf ~/swexp-lab/l13/backups/app-$(date +%F).tar.gz -C ~/swexp-lab/l13 src - find ~/swexp-lab/l13/backups -name 'app-*.tar.gz' -mtime +14 -delete - ``` -3. **systemd timer (if available):** create `backup.timer` + `backup.service`, - `systemctl enable --now backup.timer`, then `systemctl list-timers`. -4. **logrotate dry run:** write `/etc/logrotate.d/swexp-demo` and - `sudo logrotate -d /etc/logrotate.d/swexp-demo` (debug, no changes). - -## Deliverable -`ops-platform.md` proving each job ran. Solution: -[`../solutions/lab-13-solution.md`](../solutions/lab-13-solution.md). diff --git a/labs/lab-13-scheduling/README.md b/labs/lab-13-scheduling/README.md new file mode 100644 index 0000000..707a985 --- /dev/null +++ b/labs/lab-13-scheduling/README.md @@ -0,0 +1,30 @@ +# Lab 13 — Scheduling + +**Goal:** write a correct cron schedule — the five-field expression is where most +scheduling bugs live. + +The full lab installs cron jobs, a backup with retention, and a logrotate dry run. +Here we grade the gradable core: **producing the exact schedule expression.** + +## What you do +Complete [`solution.sh`](solution.sh). Given a command, print a single crontab +line that runs it **every weekday at 09:00**: + +``` +0 9 * * 1-5 +``` + +(minute=0, hour=9, any day-of-month, any month, day-of-week 1-5 = Mon–Fri.) + +```bash +npx bats labs/lab-13-scheduling/tests +./solution.sh '/usr/local/bin/backup.sh' +``` + +## Definition of done +- `npm test` for this lab is green; `npm run check` is clean. +- In your LMS notebook, translate the schedule into plain English and describe how + you would *prove* a scheduled job actually ran. + +## Submit +Commit and push. diff --git a/labs/lab-13-scheduling/solution.sh b/labs/lab-13-scheduling/solution.sh new file mode 100755 index 0000000..f7350d8 --- /dev/null +++ b/labs/lab-13-scheduling/solution.sh @@ -0,0 +1,14 @@ +#!/usr/bin/env bash +# Lab 13 — Scheduling. See README.md. +# Usage: ./solution.sh +# Print a single crontab line that runs every weekday at 09:00, i.e. the +# 5-field schedule "0 9 * * 1-5" followed by the command. +set -euo pipefail +cmd="${1:?usage: solution.sh }" + +# Cron fields: minute hour day-of-month month day-of-week command +# 0 9 * * 1-5 +# (day-of-week 1-5 = Monday..Friday) + +# TODO: print exactly one line: 0 9 * * 1-5 +# hint: printf '0 9 * * 1-5 %s\n' "$cmd" diff --git a/labs/lab-13-scheduling/tests/scheduling.bats b/labs/lab-13-scheduling/tests/scheduling.bats new file mode 100644 index 0000000..6de6eea --- /dev/null +++ b/labs/lab-13-scheduling/tests/scheduling.bats @@ -0,0 +1,23 @@ +setup() { + LAB_DIR="$(cd "$(dirname "$BATS_TEST_FILENAME")/.." && pwd)" + SOL="$LAB_DIR/solution.sh" +} + +@test "emits the exact weekday-09:00 schedule with the command" { + run bash "$SOL" '/usr/local/bin/backup.sh' + [ "$status" -eq 0 ] + [ "$output" = "0 9 * * 1-5 /usr/local/bin/backup.sh" ] +} + +@test "the schedule expression is exactly five fields" { + run bash "$SOL" 'true' + [ "$status" -eq 0 ] + sched="$(echo "$output" | awk '{print $1, $2, $3, $4, $5}')" + [ "$sched" = "0 9 * * 1-5" ] +} + +@test "carries a different command through unchanged" { + run bash "$SOL" 'date >> /tmp/run.log' + [ "$status" -eq 0 ] + [ "$output" = "0 9 * * 1-5 date >> /tmp/run.log" ] +} diff --git a/labs/lab-14-resources.md b/labs/lab-14-resources.md deleted file mode 100644 index b5fa679..0000000 --- a/labs/lab-14-resources.md +++ /dev/null @@ -1,66 +0,0 @@ -# Lab 14 — Out of Resources (Lesson 14) - -> All operations are confined to a scratch directory and are bounded so they -> cannot harm your host. Read each step before running it. - -## Goal -Diagnose simulated disk and memory pressure and recover safely. - - -## How to Learn From This Lab - -Use the **predict → run → explain** loop: - -1. **Predict:** write what you think the command will do. -2. **Run:** execute exactly one step. -3. **Explain:** write what the output means in plain language. -4. **Verify:** use a second command or the filesystem state to prove the result. - -## Beginner Checkpoints - -- Predict whether the disk test consumes blocks, inodes, or memory. -- Record before/after output so recovery is visible. -- Challenge: explain the deleted-but-open trap using a classroom analogy. - -## Setup - -```bash -mkdir -p ~/swexp-lab/l14 && cd ~/swexp-lab/l14 -``` - -## Tasks - -1. **Fill a scratch dir, then find it:** - ```bash - # create a 200 MB file (adjust if space is tight) - dd if=/dev/zero of=big.bin bs=1M count=200 status=none - du -sh ~/swexp-lab/l14/* - du -xh "$HOME" 2>/dev/null | sort -rh | head - rm big.bin && echo "reclaimed" - ``` -2. **Inode demonstration (scratch only):** - ```bash - mkdir tiny && (cd tiny && for i in $(seq 1 5000); do : > "f$i"; done) - df -i . # note IFree dropping - rm -rf tiny - ``` -3. **Deleted-but-open trap:** - ```bash - dd if=/dev/zero of=held.log bs=1M count=100 status=none - sleep 300 < held.log & # a process holds the file open - rm held.log # space NOT yet freed - lsof +L1 2>/dev/null | grep held # find the holder - kill %1 # release it -> space freed - ``` -4. **Memory + OOM evidence (bounded):** - ```bash - free -h - journalctl -k 2>/dev/null | grep -i 'out of memory\|oom' | tail - # Inspect top memory users WITHOUT exhausting RAM: - ps aux --sort=-%mem | head -5 - ``` - -## Deliverable -`resource-postmortem.md`: what consumed disk (and the deleted-but-open -finding), before/after `df -h`/`free -h`, and a guardrail you'd add. Compare -to [`../solutions/lab-14-solution.md`](../solutions/lab-14-solution.md). diff --git a/labs/lab-14-resources/README.md b/labs/lab-14-resources/README.md new file mode 100644 index 0000000..3f27273 --- /dev/null +++ b/labs/lab-14-resources/README.md @@ -0,0 +1,26 @@ +# Lab 14 — Out of Resources + +**Goal:** spot filesystems that are about to fill up — before they take a service down. + +The full lab simulates disk/inode/memory pressure and the deleted-but-open trap. Here +we grade the gradable core: **flagging over-threshold filesystems from `df` output.** + +## What you do +Complete [`solution.sh`](solution.sh). Given `df -P` output and an integer +threshold, print the **mount point** of every filesystem whose use% is strictly +greater than the threshold, one per line, in the order they appear. + +A fixture lives in [`fixtures/df.txt`](fixtures/df.txt). + +```bash +npx bats labs/lab-14-resources/tests +./solution.sh fixtures/df.txt 80 +``` + +## Definition of done +- `npm test` for this lab is green; `npm run check` is clean. +- In your LMS notebook, explain the deleted-but-open trap (why `rm` may not free space) + and one guardrail you would add to catch a filling disk early. + +## Submit +Commit and push. diff --git a/labs/lab-14-resources/fixtures/df.txt b/labs/lab-14-resources/fixtures/df.txt new file mode 100644 index 0000000..9882e5c --- /dev/null +++ b/labs/lab-14-resources/fixtures/df.txt @@ -0,0 +1,6 @@ +Filesystem 1024-blocks Used Available Capacity Mounted on +/dev/root 40593412 12118552 26458476 32% / +tmpfs 4030120 0 4030120 0% /dev/shm +/dev/sda15 106858 6182 100676 6% /boot/efi +/dev/sdb1 103081248 96152040 1719224 92% /data +/dev/sdc1 20511356 19485788 0 100% /var/log diff --git a/labs/lab-14-resources/solution.sh b/labs/lab-14-resources/solution.sh new file mode 100755 index 0000000..4ff321d --- /dev/null +++ b/labs/lab-14-resources/solution.sh @@ -0,0 +1,14 @@ +#!/usr/bin/env bash +# Lab 14 — Out of resources. See README.md. +# Usage: ./solution.sh +# is `df -P` output (header, then rows). Field 5 is the use% capacity +# (e.g. "92%"); field 6 is the mount point. Print the MOUNT POINT of every +# filesystem whose use% is STRICTLY GREATER than (an integer), +# one per line, in the order they appear. +set -euo pipefail +file="${1:?usage: solution.sh }" +threshold="${2:?usage: solution.sh }" + +# TODO: skip the header line; for each row strip the '%' from field 5, and if +# that number is greater than "$threshold", print field 6 (the mount point). +# hint: awk -v t="$threshold" 'NR>1 { pct=$5; sub(/%/,"",pct); if (pct+0 > t) print $6 }' "$file" diff --git a/labs/lab-14-resources/tests/resources.bats b/labs/lab-14-resources/tests/resources.bats new file mode 100644 index 0000000..8d00040 --- /dev/null +++ b/labs/lab-14-resources/tests/resources.bats @@ -0,0 +1,26 @@ +setup() { + LAB_DIR="$(cd "$(dirname "$BATS_TEST_FILENAME")/.." && pwd)" + SOL="$LAB_DIR/solution.sh" + DF="$LAB_DIR/fixtures/df.txt" +} + +@test "reports filesystems above an 80% threshold" { + run bash "$SOL" "$DF" 80 + [ "$status" -eq 0 ] + echo "$output" | grep -qx '/data' + echo "$output" | grep -qx '/var/log' + [ "${#lines[@]}" -eq 2 ] +} + +@test "does not report filesystems at or below the threshold" { + run bash "$SOL" "$DF" 80 + [ "$status" -eq 0 ] + ! echo "$output" | grep -qx '/' + ! echo "$output" | grep -qx '/boot/efi' +} + +@test "a higher threshold narrows the result to the fullest filesystem" { + run bash "$SOL" "$DF" 95 + [ "$status" -eq 0 ] + [ "$output" = "/var/log" ] +} diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..f1dc617 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,25 @@ +{ + "name": "swexp-module-01-linux", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "swexp-module-01-linux", + "version": "1.0.0", + "devDependencies": { + "bats": "^1.13.0" + } + }, + "node_modules/bats": { + "version": "1.13.0", + "resolved": "https://registry.npmjs.org/bats/-/bats-1.13.0.tgz", + "integrity": "sha512-giSYKGTOcPZyJDbfbTtzAedLcNWdjCLbXYU3/MwPnjyvDXzu6Dgw8d2M+8jHhZXSmsCMSQqCp+YBsJ603UO4vQ==", + "dev": true, + "license": "MIT", + "bin": { + "bats": "bin/bats" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..a044448 --- /dev/null +++ b/package.json @@ -0,0 +1,14 @@ +{ + "name": "swexp-module-01-linux", + "version": "1.0.0", + "private": true, + "description": "Forge SWEXP Module 01 — interactive Linux/shell exercises (edit solution.sh, run tests, submit).", + "scripts": { + "test": "bats -r labs assignments", + "check": "bash -n labs/*/solution.sh assignments/*/solution.sh", + "grade": "node scripts/grade.mjs" + }, + "devDependencies": { + "bats": "^1.13.0" + } +} diff --git a/scripts/grade.mjs b/scripts/grade.mjs new file mode 100644 index 0000000..81aed43 --- /dev/null +++ b/scripts/grade.mjs @@ -0,0 +1,88 @@ +#!/usr/bin/env node +/** + * Forge SWEXP autograder (Module 01 — Linux/shell, bats harness). + * Runs each exercise's bats tests, plus a shell-syntax gate (`bash -n`), + * then prints a per-exercise score and writes a Markdown report for CI. + * + * Each exercise is a folder under labs/ or assignments/ containing + * `solution.sh` (the student edits) and `tests/*.bats` (the spec). + * No answer keys are shipped. + */ +import { execSync } from 'node:child_process'; +import { readdirSync, existsSync, writeFileSync, appendFileSync, statSync } from 'node:fs'; +import { join } from 'node:path'; + +const BATS = join('node_modules', '.bin', 'bats'); + +function listExercises() { + const out = []; + for (const group of ['labs', 'assignments']) { + if (!existsSync(group)) continue; + for (const name of readdirSync(group).sort()) { + const dir = join(group, name); + if (statSync(dir).isDirectory() && existsSync(join(dir, 'tests'))) { + out.push({ key: `${group}/${name}`, dir }); + } + } + } + return out; +} + +function runBats(testsDir) { + let out = ''; + try { + out = execSync(`${BATS} --formatter tap "${testsDir}"`, { stdio: ['ignore', 'pipe', 'pipe'] }).toString(); + } catch (e) { + out = `${e.stdout ?? ''}${e.stderr ?? ''}`; + } + let passed = 0; + let total = 0; + for (const line of out.split('\n')) { + if (/^ok\b/.test(line)) { passed++; total++; } + else if (/^not ok\b/.test(line)) { total++; } + } + return { passed, total }; +} + +function syntaxGate() { + try { + execSync('bash -n labs/*/solution.sh assignments/*/solution.sh', { stdio: ['ignore', 'pipe', 'pipe'], shell: '/bin/bash' }); + return { ok: true }; + } catch { + return { ok: false }; + } +} + +const exercises = listExercises(); +const tally = exercises.map((e) => ({ ...e, ...runBats(join(e.dir, 'tests')) })); +const gate = syntaxGate(); + +const passed = tally.reduce((s, t) => s + t.passed, 0); +const total = tally.reduce((s, t) => s + t.total, 0); +const pct = total ? Math.round((passed / total) * 100) : 0; +const complete = passed === total && total > 0 && gate.ok; + +const rows = tally.map((t) => { + const mark = t.total > 0 && t.passed === t.total ? '✅' : '❌'; + return `| \`${t.key}\` | ${t.passed}/${t.total} | ${mark} |`; +}); + +const md = [ + `## Forge SWEXP — Module 01 autograde`, + ``, + `**Score: ${passed}/${total} tests (${pct}%)** · Shell syntax: ${gate.ok ? '✅ clean' : '❌ errors'}`, + ``, + `| Exercise | Tests | Status |`, + `| --- | --- | --- |`, + ...rows, + ``, + complete + ? `🎉 **All exercises complete and every script parses cleanly.**` + : `Keep going — open each exercise folder, complete the \`# TODO\`s in its \`solution.sh\`, and run \`npm test\`. The \`tests/*.bats\` files are the spec.`, +].join('\n'); + +writeFileSync('grade-report.md', md + '\n'); +console.log('\n' + md + '\n'); +if (process.env.GITHUB_STEP_SUMMARY) appendFileSync(process.env.GITHUB_STEP_SUMMARY, md + '\n'); + +process.exit(complete ? 0 : 1);