gini-toolkit 6.0.1.dev0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- gini/__init__.py +12 -0
- gini/__main__.py +107 -0
- gini/_version.py +24 -0
- gini/agent/__init__.py +17 -0
- gini/agent/agent_gamemaster.py +140 -0
- gini/agent/api.py +291 -0
- gini/agent/ask.py +123 -0
- gini/agent/authoring.py +72 -0
- gini/agent/blackboard.py +114 -0
- gini/agent/contracts.py +142 -0
- gini/agent/domains.py +91 -0
- gini/agent/embed.py +123 -0
- gini/agent/gamemaster.py +256 -0
- gini/agent/kb.py +148 -0
- gini/agent/lesson_resolver.py +261 -0
- gini/agent/llm/__init__.py +5 -0
- gini/agent/llm/backend.py +43 -0
- gini/agent/llm/fake.py +25 -0
- gini/agent/llm/ollama.py +206 -0
- gini/agent/loop.py +258 -0
- gini/agent/mcp_server.py +86 -0
- gini/agent/meaning.py +225 -0
- gini/agent/mission.py +210 -0
- gini/agent/mission_controller.py +208 -0
- gini/agent/narration.py +116 -0
- gini/agent/notifier.py +86 -0
- gini/agent/personas.py +79 -0
- gini/agent/reasoning.py +172 -0
- gini/agent/recall.py +248 -0
- gini/agent/session.py +79 -0
- gini/agent/teaching_center.py +482 -0
- gini/agent/tools/__init__.py +3 -0
- gini/agent/tools/registry.py +193 -0
- gini/agent/twin/__init__.py +28 -0
- gini/agent/twin/authoring.py +71 -0
- gini/agent/twin/contracts.py +54 -0
- gini/agent/twin/dialectic.py +189 -0
- gini/agent/twin/harness.py +93 -0
- gini/agent/twin/justify.py +156 -0
- gini/agent/twin/learner.py +64 -0
- gini/agent/twin/mission.py +60 -0
- gini/agent/twin/os_coach.py +79 -0
- gini/agent/twin/salience.py +30 -0
- gini/agent/understand.py +250 -0
- gini/agent/verifiers.py +106 -0
- gini/agent/wizard.py +178 -0
- gini/agent/xv6_pack.py +74 -0
- gini/app/__init__.py +3 -0
- gini/app/context.py +368 -0
- gini/app/paths.py +121 -0
- gini/data/README.md +21 -0
- gini/domain/__init__.py +9 -0
- gini/domain/assembly.py +209 -0
- gini/domain/authoring.py +353 -0
- gini/domain/blueprints.py +5 -0
- gini/domain/capabilities.py +177 -0
- gini/domain/catalog.py +85 -0
- gini/domain/certify.py +201 -0
- gini/domain/compose.py +413 -0
- gini/domain/composition.py +88 -0
- gini/domain/concepts.py +383 -0
- gini/domain/connection_rules.py +269 -0
- gini/domain/constraints.py +153 -0
- gini/domain/content.py +59 -0
- gini/domain/cpu_journey.py +89 -0
- gini/domain/devices.py +747 -0
- gini/domain/diagnose.py +201 -0
- gini/domain/element_guide.py +327 -0
- gini/domain/explain.py +90 -0
- gini/domain/fingerprint.py +201 -0
- gini/domain/firewall.py +34 -0
- gini/domain/flowlog.py +61 -0
- gini/domain/flowtable.py +179 -0
- gini/domain/fragment_yaml.py +230 -0
- gini/domain/fragments.py +169 -0
- gini/domain/games/__init__.py +2 -0
- gini/domain/games/paging_games.py +119 -0
- gini/domain/games/policy_game.py +86 -0
- gini/domain/games/process_game.py +48 -0
- gini/domain/games/thrash_game.py +75 -0
- gini/domain/games/translate_game.py +60 -0
- gini/domain/games/trap_game.py +86 -0
- gini/domain/grader.py +155 -0
- gini/domain/grouping.py +67 -0
- gini/domain/legality.py +103 -0
- gini/domain/lesson.py +241 -0
- gini/domain/lexicon.py +150 -0
- gini/domain/machine_state.py +410 -0
- gini/domain/missions/networking/basic-lan.yaml +32 -0
- gini/domain/missions/networking/cache-in-front.yaml +23 -0
- gini/domain/missions/networking/decouple-with-queue.yaml +31 -0
- gini/domain/missions/networking/drive-load.yaml +20 -0
- gini/domain/missions/networking/fix-the-address.yaml +75 -0
- gini/domain/missions/networking/fix-the-lan.yaml +43 -0
- gini/domain/missions/networking/inspect-flows.yaml +16 -0
- gini/domain/missions/networking/k8s-autoscale.yaml +27 -0
- gini/domain/missions/networking/least-privilege.yaml +21 -0
- gini/domain/missions/networking/load-balanced-web.yaml +29 -0
- gini/domain/missions/networking/observe-it.yaml +24 -0
- gini/domain/missions/networking/put-in-vpc.yaml +30 -0
- gini/domain/missions/networking/reachability-boundary.yaml +56 -0
- gini/domain/missions/networking/sdn-reactive.yaml +35 -0
- gini/domain/missions/networking/send-request.yaml +19 -0
- gini/domain/missions/networking/serverless-api.yaml +25 -0
- gini/domain/missions/networking/service-chain.yaml +33 -0
- gini/domain/missions/os/lottery-fix.yaml +19 -0
- gini/domain/missions/os/priority-fix.yaml +24 -0
- gini/domain/missions.py +111 -0
- gini/domain/modulechain.py +36 -0
- gini/domain/objectives.py +488 -0
- gini/domain/os_zoo.py +79 -0
- gini/domain/paging_sim.py +141 -0
- gini/domain/pricing.py +199 -0
- gini/domain/probes.py +226 -0
- gini/domain/profile.py +142 -0
- gini/domain/recipes.py +738 -0
- gini/domain/riders.py +309 -0
- gini/domain/router_modules.py +224 -0
- gini/domain/routetable.py +67 -0
- gini/domain/scoring.py +76 -0
- gini/domain/staging.py +122 -0
- gini/domain/syscall_builder.py +144 -0
- gini/domain/topic_cloud.py +62 -0
- gini/domain/topology.py +213 -0
- gini/domain/vocabulary.py +51 -0
- gini/domain/xv6.py +808 -0
- gini/domain/xv6_fs.py +250 -0
- gini/domain/xv6_runner.py +113 -0
- gini/domain/xv6_vm.py +385 -0
- gini/gloader.py +17 -0
- gini/runtime/__init__.py +18 -0
- gini/runtime/cloudfabric_agent.py +370 -0
- gini/runtime/console.py +68 -0
- gini/runtime/control.py +70 -0
- gini/runtime/frame.py +138 -0
- gini/runtime/gbridge.py +638 -0
- gini/runtime/grouter.py +223 -0
- gini/runtime/hostsim.py +90 -0
- gini/runtime/shuttle.py +348 -0
- gini/runtime/switch.py +109 -0
- gini/runtime/transport.py +77 -0
- gini/runtime/xv6_bridge.py +312 -0
- gini/server/__init__.py +22 -0
- gini/server/__main__.py +74 -0
- gini/server/app.py +140 -0
- gini/server/auth.py +82 -0
- gini/server/policy.py +57 -0
- gini/server/session.py +23 -0
- gini/services/__init__.py +15 -0
- gini/services/boardflash.py +248 -0
- gini/services/boardsetup.py +374 -0
- gini/services/cloud_catalog.py +143 -0
- gini/services/compiler.py +1858 -0
- gini/services/discovery.py +324 -0
- gini/services/gloader.py +183 -0
- gini/services/orchestrator.py +1460 -0
- gini/services/persistence.py +28 -0
- gini/services/probe_runner.py +149 -0
- gini/services/project.py +217 -0
- gini/services/remote.py +93 -0
- gini/services/rider_runner.py +96 -0
- gini/services/rider_session.py +171 -0
- gini/services/shadow_store.py +52 -0
- gini/services/terminal.py +45 -0
- gini/setup/__init__.py +17 -0
- gini/setup/cli.py +109 -0
- gini/setup/images.py +33 -0
- gini/setup/marker.py +43 -0
- gini/setup/runtime.py +69 -0
- gini/ui/__init__.py +3 -0
- gini/ui/assets/app_icon.icns +0 -0
- gini/ui/assets/app_icon.ico +0 -0
- gini/ui/assets/app_icon.png +0 -0
- gini/ui/assets/app_icon_1024.png +0 -0
- gini/ui/assets/cue/_w.txt +1 -0
- gini/ui/assets/cue/ai.png +0 -0
- gini/ui/assets/cue/canvas.png +0 -0
- gini/ui/assets/cue/cloud.png +0 -0
- gini/ui/assets/cue/cost.png +0 -0
- gini/ui/assets/cue/dark/ai.png +0 -0
- gini/ui/assets/cue/dark/canvas.png +0 -0
- gini/ui/assets/cue/dark/cloud.png +0 -0
- gini/ui/assets/cue/dark/cost.png +0 -0
- gini/ui/assets/cue/dark/metrics.png +0 -0
- gini/ui/assets/cue/dark/router.png +0 -0
- gini/ui/assets/cue/dark/run.png +0 -0
- gini/ui/assets/cue/dark/serverless.png +0 -0
- gini/ui/assets/cue/dark/settings.png +0 -0
- gini/ui/assets/cue/dark/welcome.png +0 -0
- gini/ui/assets/cue/dark/wizard.png +0 -0
- gini/ui/assets/cue/ginibrand/ai.png +0 -0
- gini/ui/assets/cue/ginibrand/canvas.png +0 -0
- gini/ui/assets/cue/ginibrand/cloud.png +0 -0
- gini/ui/assets/cue/ginibrand/cost.png +0 -0
- gini/ui/assets/cue/ginibrand/metrics.png +0 -0
- gini/ui/assets/cue/ginibrand/router.png +0 -0
- gini/ui/assets/cue/ginibrand/run.png +0 -0
- gini/ui/assets/cue/ginibrand/serverless.png +0 -0
- gini/ui/assets/cue/ginibrand/settings.png +0 -0
- gini/ui/assets/cue/ginibrand/welcome.png +0 -0
- gini/ui/assets/cue/ginibrand/wizard.png +0 -0
- gini/ui/assets/cue/highcontrast/ai.png +0 -0
- gini/ui/assets/cue/highcontrast/canvas.png +0 -0
- gini/ui/assets/cue/highcontrast/cloud.png +0 -0
- gini/ui/assets/cue/highcontrast/cost.png +0 -0
- gini/ui/assets/cue/highcontrast/metrics.png +0 -0
- gini/ui/assets/cue/highcontrast/router.png +0 -0
- gini/ui/assets/cue/highcontrast/run.png +0 -0
- gini/ui/assets/cue/highcontrast/serverless.png +0 -0
- gini/ui/assets/cue/highcontrast/settings.png +0 -0
- gini/ui/assets/cue/highcontrast/welcome.png +0 -0
- gini/ui/assets/cue/highcontrast/wizard.png +0 -0
- gini/ui/assets/cue/light/ai.png +0 -0
- gini/ui/assets/cue/light/canvas.png +0 -0
- gini/ui/assets/cue/light/cloud.png +0 -0
- gini/ui/assets/cue/light/cost.png +0 -0
- gini/ui/assets/cue/light/metrics.png +0 -0
- gini/ui/assets/cue/light/router.png +0 -0
- gini/ui/assets/cue/light/run.png +0 -0
- gini/ui/assets/cue/light/serverless.png +0 -0
- gini/ui/assets/cue/light/settings.png +0 -0
- gini/ui/assets/cue/light/welcome.png +0 -0
- gini/ui/assets/cue/light/wizard.png +0 -0
- gini/ui/assets/cue/metrics.png +0 -0
- gini/ui/assets/cue/router.png +0 -0
- gini/ui/assets/cue/run.png +0 -0
- gini/ui/assets/cue/serverless.png +0 -0
- gini/ui/assets/cue/settings.png +0 -0
- gini/ui/assets/cue/welcome.png +0 -0
- gini/ui/assets/cue/wizard.png +0 -0
- gini/ui/assistant.py +2111 -0
- gini/ui/author_dialog.py +184 -0
- gini/ui/board_dialog.py +247 -0
- gini/ui/branding.py +21 -0
- gini/ui/canvas.py +2007 -0
- gini/ui/chat_panel.py +7 -0
- gini/ui/cpu_journey.py +212 -0
- gini/ui/cpu_lab.py +306 -0
- gini/ui/cue_cards.py +214 -0
- gini/ui/dashboard.py +222 -0
- gini/ui/diagnose_game.py +336 -0
- gini/ui/fingerprint_lab.py +219 -0
- gini/ui/flash_dialog.py +244 -0
- gini/ui/flow_layout.py +63 -0
- gini/ui/fragment_manager.py +1415 -0
- gini/ui/game_catalog.py +184 -0
- gini/ui/game_renderers.py +340 -0
- gini/ui/games_lab.py +90 -0
- gini/ui/inspector.py +1055 -0
- gini/ui/live_metrics.py +130 -0
- gini/ui/machine_lab.py +1412 -0
- gini/ui/main_window.py +3153 -0
- gini/ui/memory_lab.py +371 -0
- gini/ui/mission_panel.py +302 -0
- gini/ui/mode_indicator.py +227 -0
- gini/ui/palette.py +112 -0
- gini/ui/peripherals.py +218 -0
- gini/ui/process_tree.py +130 -0
- gini/ui/reset_dialog.py +179 -0
- gini/ui/router_lab.py +776 -0
- gini/ui/run_button.py +183 -0
- gini/ui/settings_dialog.py +234 -0
- gini/ui/signin_dialog.py +111 -0
- gini/ui/storage_lab.py +219 -0
- gini/ui/syscall_builder.py +235 -0
- gini/ui/syscall_lab.py +152 -0
- gini/ui/theme/__init__.py +5 -0
- gini/ui/theme/icons.py +145 -0
- gini/ui/theme/manager.py +291 -0
- gini/ui/theme/tokens.py +194 -0
- gini/ui/trap_lab.py +270 -0
- gini/ui/worker_host.py +102 -0
- gini/ui/zoo_lab.py +112 -0
- gini_toolkit-6.0.1.dev0.dist-info/METADATA +77 -0
- gini_toolkit-6.0.1.dev0.dist-info/RECORD +278 -0
- gini_toolkit-6.0.1.dev0.dist-info/WHEEL +5 -0
- gini_toolkit-6.0.1.dev0.dist-info/entry_points.txt +3 -0
- gini_toolkit-6.0.1.dev0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,410 @@
|
|
|
1
|
+
"""MachineState — the shared bridge between a running xv6 Machine and the rest of GINI.
|
|
2
|
+
|
|
3
|
+
Decision (signed off): the *state model* is the bridge, not the Machine Lab dialog. One
|
|
4
|
+
`MachineState` owns the provider, the latest `Snapshot`, the scheduling timeline, and a
|
|
5
|
+
`StateWatcher`; the Lab renders from it and the Ask GINI agent reads from it, so help works
|
|
6
|
+
whether or not the dialog is open and both sides see one atomic snapshot.
|
|
7
|
+
|
|
8
|
+
Everything here is pure (no Qt): the provider is injected (the offline `DemoScheduler` today,
|
|
9
|
+
a GDB bridge on the Mac later), so the serializer, the deltas, and the event watcher are all
|
|
10
|
+
unit-tested without QEMU.
|
|
11
|
+
|
|
12
|
+
• `state_card(...)` — a compact, progressive (L0/L1/L2) text block the agent is fed.
|
|
13
|
+
• `StateWatcher` — diffs successive snapshots into pedagogical events for Coach.
|
|
14
|
+
• `MachineState` — owns provider + timeline + watcher; refresh/step/controls + card().
|
|
15
|
+
"""
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
from dataclasses import dataclass, field
|
|
19
|
+
|
|
20
|
+
from .xv6 import DemoScheduler, SchedTimeline, Snapshot
|
|
21
|
+
from .xv6_fs import DemoDisk, fs_summary
|
|
22
|
+
from .xv6_vm import DemoVm, memory_summary
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
# --------------------------------------------------------------------------- #
|
|
26
|
+
# Pedagogical events (Coach consumes these)
|
|
27
|
+
# --------------------------------------------------------------------------- #
|
|
28
|
+
@dataclass
|
|
29
|
+
class OsEvent:
|
|
30
|
+
kind: str # starvation | cpu_monopoly | zombie_leak | idle | control
|
|
31
|
+
detail: str # human one-liner
|
|
32
|
+
pid: int | None = None
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@dataclass
|
|
36
|
+
class CoachLedger:
|
|
37
|
+
"""'Measured help' bookkeeping: a per-machine hint BUDGET plus an instructor-visible LOG.
|
|
38
|
+
The moat is that GINI's help is grounded in live kernel state a foreign agent can't see;
|
|
39
|
+
the measure is that the help is finite and recorded, so offloading shows up as a signal."""
|
|
40
|
+
budget: int = 8
|
|
41
|
+
used: int = 0
|
|
42
|
+
log: list = field(default_factory=list) # [{n, events:[(kind,detail,pid)], hint}]
|
|
43
|
+
|
|
44
|
+
def remaining(self) -> int:
|
|
45
|
+
return max(0, self.budget - self.used)
|
|
46
|
+
|
|
47
|
+
def can_help(self) -> bool:
|
|
48
|
+
return self.remaining() > 0
|
|
49
|
+
|
|
50
|
+
def record(self, events, hint: str = "") -> int:
|
|
51
|
+
self.used += 1
|
|
52
|
+
self.log.append({"n": self.used,
|
|
53
|
+
"events": [(e.kind, e.detail, e.pid) for e in events],
|
|
54
|
+
"hint": hint})
|
|
55
|
+
return self.remaining()
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
class StateWatcher:
|
|
59
|
+
"""Edge-triggered detector: turns a stream of snapshots into a few teachable events.
|
|
60
|
+
|
|
61
|
+
Fires each condition once per episode (when it becomes true), and re-arms when it clears,
|
|
62
|
+
so Coach gets a nudge on the *transition*, not every tick."""
|
|
63
|
+
|
|
64
|
+
def __init__(self, starve: int = 4, monopoly: int = 5, zombie: int = 3) -> None:
|
|
65
|
+
self.starve, self.monopoly, self.zombie = starve, monopoly, zombie
|
|
66
|
+
self._runnable_streak: dict[int, int] = {}
|
|
67
|
+
self._running_streak: dict[int, int] = {}
|
|
68
|
+
self._zombie_age: dict[int, int] = {}
|
|
69
|
+
self._fired: set = set() # (kind, pid) currently-active conditions
|
|
70
|
+
|
|
71
|
+
def observe(self, snap: Snapshot) -> list[OsEvent]:
|
|
72
|
+
events: list[OsEvent] = []
|
|
73
|
+
present = {p.pid for p in snap.procs}
|
|
74
|
+
others_runnable = sum(1 for p in snap.procs if p.state == "runnable")
|
|
75
|
+
any_active = False
|
|
76
|
+
for p in snap.procs:
|
|
77
|
+
if p.state == "running":
|
|
78
|
+
any_active = True
|
|
79
|
+
self._running_streak[p.pid] = self._running_streak.get(p.pid, 0) + 1
|
|
80
|
+
self._runnable_streak[p.pid] = 0
|
|
81
|
+
elif p.state == "runnable":
|
|
82
|
+
any_active = True
|
|
83
|
+
self._runnable_streak[p.pid] = self._runnable_streak.get(p.pid, 0) + 1
|
|
84
|
+
self._running_streak[p.pid] = 0
|
|
85
|
+
else:
|
|
86
|
+
self._running_streak[p.pid] = 0
|
|
87
|
+
self._runnable_streak[p.pid] = 0
|
|
88
|
+
if p.state == "zombie":
|
|
89
|
+
self._zombie_age[p.pid] = self._zombie_age.get(p.pid, 0) + 1
|
|
90
|
+
else:
|
|
91
|
+
self._zombie_age.pop(p.pid, None)
|
|
92
|
+
|
|
93
|
+
# starvation — runnable a long time without ever getting the CPU
|
|
94
|
+
for pid, streak in self._runnable_streak.items():
|
|
95
|
+
self._edge(events, ("starvation", pid), streak >= self.starve,
|
|
96
|
+
lambda pid=pid, s=streak: OsEvent(
|
|
97
|
+
"starvation",
|
|
98
|
+
f"pid {pid} has stayed RUNNABLE for {s} slices without running "
|
|
99
|
+
"— it may be starving under this policy.", pid))
|
|
100
|
+
# cpu monopoly — one pid holds the CPU while others are ready
|
|
101
|
+
for pid, streak in self._running_streak.items():
|
|
102
|
+
self._edge(events, ("cpu_monopoly", pid),
|
|
103
|
+
streak >= self.monopoly and others_runnable > 0,
|
|
104
|
+
lambda pid=pid, s=streak: OsEvent(
|
|
105
|
+
"cpu_monopoly",
|
|
106
|
+
f"pid {pid} has held the CPU for {s} slices while others are "
|
|
107
|
+
"runnable — is the time-slice too large, or the policy unfair?", pid))
|
|
108
|
+
# zombie not reaped
|
|
109
|
+
for pid, age in self._zombie_age.items():
|
|
110
|
+
self._edge(events, ("zombie_leak", pid), age >= self.zombie,
|
|
111
|
+
lambda pid=pid, a=age: OsEvent(
|
|
112
|
+
"zombie_leak",
|
|
113
|
+
f"pid {pid} has been a ZOMBIE for {a} snapshots — its parent "
|
|
114
|
+
"hasn't wait()ed for it.", pid))
|
|
115
|
+
# nothing runnable/running — idle or stuck
|
|
116
|
+
self._edge(events, ("idle", None), not any_active,
|
|
117
|
+
lambda: OsEvent("idle",
|
|
118
|
+
"no process is runnable or running — the CPU is idle "
|
|
119
|
+
"(all sleeping/zombie). Deadlock, or just waiting?"))
|
|
120
|
+
# drop fired flags for pids that vanished
|
|
121
|
+
self._fired = {(k, pid) for (k, pid) in self._fired
|
|
122
|
+
if pid is None or pid in present}
|
|
123
|
+
return events
|
|
124
|
+
|
|
125
|
+
def _edge(self, out, key, cond, make) -> None:
|
|
126
|
+
if cond and key not in self._fired:
|
|
127
|
+
out.append(make())
|
|
128
|
+
self._fired.add(key)
|
|
129
|
+
elif not cond:
|
|
130
|
+
self._fired.discard(key)
|
|
131
|
+
|
|
132
|
+
def active(self, kind: str | None = None) -> set:
|
|
133
|
+
"""The pids whose conditions are CURRENTLY active (not just fired once) — so the UI can
|
|
134
|
+
badge a starving/monopolising proc for as long as it's true, then clear it. Optionally
|
|
135
|
+
filter by kind ('starvation' | 'cpu_monopoly' | 'zombie_leak')."""
|
|
136
|
+
return {pid for (k, pid) in self._fired
|
|
137
|
+
if pid is not None and (kind is None or k == kind)}
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
# --------------------------------------------------------------------------- #
|
|
141
|
+
# The state card (what the agent is fed)
|
|
142
|
+
# --------------------------------------------------------------------------- #
|
|
143
|
+
_STATE_ORDER = {"running": 0, "runnable": 1, "sleeping": 2, "zombie": 3, "used": 4,
|
|
144
|
+
"unused": 5}
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def state_card(snap: Snapshot, timeline: SchedTimeline | None = None,
|
|
148
|
+
meta: dict | None = None, deltas: list[str] | None = None,
|
|
149
|
+
level: int = 0) -> str:
|
|
150
|
+
"""Render a compact, LLM-facing snapshot of the xv6 kernel. `level` is progressive:
|
|
151
|
+
0 = scheduling picture (always on), 1 = + registers & kernel stack, 2 = + memory/FS."""
|
|
152
|
+
if snap is None:
|
|
153
|
+
return ""
|
|
154
|
+
meta = meta or {}
|
|
155
|
+
lines = ["xv6 Machine — live kernel state (ground truth; describe only what appears here)"]
|
|
156
|
+
pol = meta.get("policy", "round-robin")
|
|
157
|
+
ts = meta.get("timeslice")
|
|
158
|
+
head = f"policy: {pol}"
|
|
159
|
+
if ts is not None:
|
|
160
|
+
head += f" · time-slice: {ts} tick{'s' if ts != 1 else ''}"
|
|
161
|
+
if snap.ticks is not None:
|
|
162
|
+
head += f" · ticks: {snap.ticks}"
|
|
163
|
+
lines.append(head)
|
|
164
|
+
run = snap.running_pid
|
|
165
|
+
runp = next((p for p in snap.procs if p.pid == run), None)
|
|
166
|
+
lines.append(f"running: pid {run} ({runp.name})" if runp else "running: (none — idle)")
|
|
167
|
+
if snap.procs:
|
|
168
|
+
lines.append("processes (proc[]):")
|
|
169
|
+
for p in sorted(snap.procs, key=lambda p: (_STATE_ORDER.get(p.state, 9), p.pid)):
|
|
170
|
+
mark = " *" if p.pid == run else " "
|
|
171
|
+
lines.append(f" {mark} pid {p.pid:<3} {p.state:<9} {p.name}")
|
|
172
|
+
if timeline is not None:
|
|
173
|
+
tail = ",".join(str(s.pid) for s in timeline.recent(10))
|
|
174
|
+
lines.append(f"context switches: {timeline.switches()} · recent CPU: {tail}")
|
|
175
|
+
if deltas:
|
|
176
|
+
lines.append("changes since your last question: " + "; ".join(deltas))
|
|
177
|
+
|
|
178
|
+
if level >= 1:
|
|
179
|
+
cpu = snap.cpu
|
|
180
|
+
if cpu is not None:
|
|
181
|
+
regs = " ".join(f"{k}={cpu.key(k)}" for k in
|
|
182
|
+
("pc", "sp", "ra", "satp", "a0", "a7"))
|
|
183
|
+
lines.append("CPU registers: " + regs)
|
|
184
|
+
if snap.stack:
|
|
185
|
+
lines.append("kernel stack (bt):")
|
|
186
|
+
for i, f in enumerate(snap.stack):
|
|
187
|
+
loc = f" {f.loc}" if f.loc else ""
|
|
188
|
+
lines.append(f" #{i} {f.fn}{loc}")
|
|
189
|
+
if level >= 2:
|
|
190
|
+
pt = getattr(snap, "page_table", None)
|
|
191
|
+
lines.append("memory: " + (pt if pt else
|
|
192
|
+
"page-table / FS detail not exported in this build"))
|
|
193
|
+
return "\n".join(lines)
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
# --------------------------------------------------------------------------- #
|
|
197
|
+
# MachineState — the owner
|
|
198
|
+
# --------------------------------------------------------------------------- #
|
|
199
|
+
@dataclass
|
|
200
|
+
class MachineState:
|
|
201
|
+
"""Owns the live state for one xv6 Machine. `provider` has snapshot()/step()/
|
|
202
|
+
set_timeslice() (offline DemoScheduler or the Mac GDB bridge)."""
|
|
203
|
+
provider: object # scheduler feed (snapshot()/step()/…)
|
|
204
|
+
device_id: str = ""
|
|
205
|
+
policy: str = "round-robin"
|
|
206
|
+
mode: str = "real" # "real" (live kernel) | "demo" (stand-in). A
|
|
207
|
+
# USER choice — never auto-switched (see set_mode)
|
|
208
|
+
latest: Snapshot | None = None
|
|
209
|
+
timeline: SchedTimeline = field(default_factory=SchedTimeline)
|
|
210
|
+
cpu_timelines: dict = field(default_factory=dict) # cpu_index -> SchedTimeline (SMP)
|
|
211
|
+
watcher: StateWatcher = field(default_factory=StateWatcher)
|
|
212
|
+
ledger: CoachLedger = field(default_factory=CoachLedger)
|
|
213
|
+
vm: object = None # virtual-memory reader (snapshot()->VmSnapshot)
|
|
214
|
+
fs: object = None # file-system reader (snapshot()->FsSnapshot)
|
|
215
|
+
on_event: object = None # callback(self) fired on new pedagogical events
|
|
216
|
+
_events: list = field(default_factory=list)
|
|
217
|
+
_prev_card: dict = field(default_factory=dict)
|
|
218
|
+
# The two data "planes" the mode toggles between: each is (provider, vm, fs). The injected
|
|
219
|
+
# trio becomes the plane matching the initial `mode`; the other is created lazily. `_real`
|
|
220
|
+
# may stay None when no xv6 is running — then Real mode shows an error, never demo data.
|
|
221
|
+
_real: tuple | None = None
|
|
222
|
+
_demo: tuple | None = None
|
|
223
|
+
|
|
224
|
+
def __post_init__(self) -> None:
|
|
225
|
+
if self.vm is None:
|
|
226
|
+
self.vm = DemoVm() # offline stand-ins; Mac bridge overrides
|
|
227
|
+
if self.fs is None:
|
|
228
|
+
self.fs = DemoDisk()
|
|
229
|
+
trio = (self.provider, self.vm, self.fs)
|
|
230
|
+
if self.mode == "demo":
|
|
231
|
+
self._demo = self._demo or trio
|
|
232
|
+
else:
|
|
233
|
+
self._real = self._real or trio
|
|
234
|
+
try:
|
|
235
|
+
self._ingest(self.provider.snapshot())
|
|
236
|
+
except Exception:
|
|
237
|
+
pass
|
|
238
|
+
|
|
239
|
+
# -- mode (Real/Demo) — an explicit user action, never automatic --------- #
|
|
240
|
+
def has_real(self) -> bool:
|
|
241
|
+
"""True when a live kernel plane is attached (an xv6 is running)."""
|
|
242
|
+
return bool(self._real and self._real[0] is not None)
|
|
243
|
+
|
|
244
|
+
def attach_real(self, provider, vm=None, fs=None) -> None:
|
|
245
|
+
"""Wire the live bridge as the Real plane (called when the topology starts). If we're
|
|
246
|
+
currently in Real mode, switch to it now so an already-open Lab goes live."""
|
|
247
|
+
self._real = (provider, vm if vm is not None else getattr(provider, "vm", None),
|
|
248
|
+
fs if fs is not None else getattr(provider, "fs", None))
|
|
249
|
+
if self.mode == "real":
|
|
250
|
+
self.provider, self.vm, self.fs = self._real
|
|
251
|
+
self.refresh()
|
|
252
|
+
|
|
253
|
+
def set_mode(self, mode: str) -> None:
|
|
254
|
+
"""Switch the active data plane. This is the ONLY place the source changes, and only when
|
|
255
|
+
the user asks — nothing auto-falls-back from real to demo. Clears the timelines so demo
|
|
256
|
+
and real samples never blend on the Gantt."""
|
|
257
|
+
if mode not in ("real", "demo") or mode == self.mode:
|
|
258
|
+
return
|
|
259
|
+
self.mode = mode
|
|
260
|
+
prov, vm, fs = self._plane(mode)
|
|
261
|
+
self.provider, self.vm, self.fs = prov, vm, fs
|
|
262
|
+
self.timeline = SchedTimeline()
|
|
263
|
+
self.cpu_timelines = {}
|
|
264
|
+
self.latest = None
|
|
265
|
+
if prov is not None:
|
|
266
|
+
try:
|
|
267
|
+
self._ingest(prov.snapshot())
|
|
268
|
+
except Exception:
|
|
269
|
+
pass
|
|
270
|
+
|
|
271
|
+
def _plane(self, mode: str) -> tuple:
|
|
272
|
+
if mode == "real":
|
|
273
|
+
return self._real if self.has_real() else (None, None, None)
|
|
274
|
+
if self._demo is None: # build the demo plane on first switch to it
|
|
275
|
+
self._demo = (DemoScheduler(), DemoVm(), DemoDisk())
|
|
276
|
+
return self._demo
|
|
277
|
+
|
|
278
|
+
# -- reads --------------------------------------------------------------- #
|
|
279
|
+
@property
|
|
280
|
+
def timeslice(self) -> int:
|
|
281
|
+
return int(getattr(self.provider, "timeslice", 1) or 1)
|
|
282
|
+
|
|
283
|
+
def refresh(self) -> Snapshot | None:
|
|
284
|
+
if self.provider is None: # Real mode with no running kernel -> no data
|
|
285
|
+
return None
|
|
286
|
+
self._ingest(self.provider.snapshot())
|
|
287
|
+
return self.latest
|
|
288
|
+
|
|
289
|
+
def step(self) -> Snapshot | None:
|
|
290
|
+
if self.provider is None:
|
|
291
|
+
return None
|
|
292
|
+
self._ingest(self.provider.step())
|
|
293
|
+
return self.latest
|
|
294
|
+
|
|
295
|
+
def _ingest(self, snap: Snapshot | None) -> None:
|
|
296
|
+
# An empty process list means the read FAILED (init+sh always exist), not that the
|
|
297
|
+
# kernel has no processes — so keep the last good snapshot instead of blanking the UI.
|
|
298
|
+
if snap is None or not getattr(snap, "procs", None):
|
|
299
|
+
return
|
|
300
|
+
self.latest = snap
|
|
301
|
+
self.timeline.add(snap)
|
|
302
|
+
# per-CPU timelines for the SMP Gantt strips; fall back to one strip (cpu 0) when the
|
|
303
|
+
# kernel reports no CPU lines (single-CPU or older build).
|
|
304
|
+
cpus = snap.cpus or ({0: snap.running_pid} if snap.running_pid is not None else {})
|
|
305
|
+
for ci, pid in cpus.items():
|
|
306
|
+
name = next((p.name for p in snap.procs if p.pid == pid), "")
|
|
307
|
+
self.cpu_timelines.setdefault(ci, SchedTimeline()).add_run(pid, snap.ticks, name)
|
|
308
|
+
self._emit(self.watcher.observe(snap))
|
|
309
|
+
|
|
310
|
+
def _emit(self, events) -> None:
|
|
311
|
+
"""Record events; notify a listener (proactive Coach) only for genuine teachable
|
|
312
|
+
moments — NOT the student's own control changes, which they already know about."""
|
|
313
|
+
if not events:
|
|
314
|
+
return
|
|
315
|
+
self._events.extend(events)
|
|
316
|
+
if self.on_event and any(e.kind != "control" for e in events):
|
|
317
|
+
try:
|
|
318
|
+
self.on_event(self)
|
|
319
|
+
except Exception:
|
|
320
|
+
pass
|
|
321
|
+
|
|
322
|
+
def pending_events(self) -> bool:
|
|
323
|
+
return bool(self._events)
|
|
324
|
+
|
|
325
|
+
def scheduling_flags(self) -> dict:
|
|
326
|
+
"""Currently-active scheduling conditions, for the scheduler-face badges:
|
|
327
|
+
{"starvation": {pids}, "cpu_monopoly": {pids}, "zombie_leak": {pids}}. Read-only view of
|
|
328
|
+
the watcher — does NOT drain the Coach event queue."""
|
|
329
|
+
return {kind: self.watcher.active(kind)
|
|
330
|
+
for kind in ("starvation", "cpu_monopoly", "zombie_leak")}
|
|
331
|
+
|
|
332
|
+
def shadows(self) -> dict:
|
|
333
|
+
"""The shadow manifest ({name: ShadowStatus}) from the live provider — which student
|
|
334
|
+
shadows are wired and healthy. Empty when the provider (or build) has no shadows."""
|
|
335
|
+
getter = getattr(self.provider, "shadows", None)
|
|
336
|
+
if callable(getter):
|
|
337
|
+
try:
|
|
338
|
+
return getter() or {}
|
|
339
|
+
except Exception:
|
|
340
|
+
return {}
|
|
341
|
+
return {}
|
|
342
|
+
|
|
343
|
+
def policies(self) -> dict:
|
|
344
|
+
"""The kernel's policy roster {id: name} (from the POLICY lines) — drives the UI selector,
|
|
345
|
+
so a policy the kernel ships auto-appears. Empty on an older build / offline."""
|
|
346
|
+
return dict(getattr(self.provider, "kernel_policies", {}) or {})
|
|
347
|
+
|
|
348
|
+
# -- controls ------------------------------------------------------------ #
|
|
349
|
+
def set_timeslice(self, ticks: int) -> None:
|
|
350
|
+
old = self.timeslice
|
|
351
|
+
try:
|
|
352
|
+
self.provider.set_timeslice(ticks)
|
|
353
|
+
except Exception:
|
|
354
|
+
pass
|
|
355
|
+
if int(ticks) != old:
|
|
356
|
+
self._emit([OsEvent(
|
|
357
|
+
"control", f"you changed the time-slice {old} -> {ticks} ticks")])
|
|
358
|
+
|
|
359
|
+
def set_policy(self, policy: str) -> None:
|
|
360
|
+
if policy and policy != self.policy:
|
|
361
|
+
self._emit([OsEvent(
|
|
362
|
+
"control", f"you switched the scheduler policy {self.policy} -> {policy}")])
|
|
363
|
+
self.policy = policy
|
|
364
|
+
setter = getattr(self.provider, "set_policy", None)
|
|
365
|
+
if callable(setter):
|
|
366
|
+
try:
|
|
367
|
+
setter(policy)
|
|
368
|
+
except Exception:
|
|
369
|
+
pass
|
|
370
|
+
|
|
371
|
+
# -- for Coach ----------------------------------------------------------- #
|
|
372
|
+
def drain_events(self) -> list:
|
|
373
|
+
ev, self._events = self._events, []
|
|
374
|
+
return ev
|
|
375
|
+
|
|
376
|
+
# -- for the agent (Explain / Chat context) ------------------------------ #
|
|
377
|
+
def card(self, level: int = 0) -> str:
|
|
378
|
+
meta = {"policy": self.policy, "timeslice": self.timeslice}
|
|
379
|
+
deltas = self._compute_deltas(meta)
|
|
380
|
+
# scheduler card (L0/L1); at L2 append live memory + FS summaries (real ground truth,
|
|
381
|
+
# so paging/file-system questions are grounded in this student's kernel, not training).
|
|
382
|
+
base = state_card(self.latest, self.timeline, meta, deltas, min(level, 1))
|
|
383
|
+
if level >= 2:
|
|
384
|
+
extra = []
|
|
385
|
+
for provider, summarize in ((self.vm, memory_summary), (self.fs, fs_summary)):
|
|
386
|
+
try:
|
|
387
|
+
s = summarize(provider.snapshot())
|
|
388
|
+
if s:
|
|
389
|
+
extra.append(s)
|
|
390
|
+
except Exception:
|
|
391
|
+
pass
|
|
392
|
+
if extra:
|
|
393
|
+
base += "\n\n" + "\n\n".join(extra)
|
|
394
|
+
return base
|
|
395
|
+
|
|
396
|
+
def _compute_deltas(self, meta: dict) -> list[str]:
|
|
397
|
+
"""Short 'what changed since the last time the agent looked' line for the card."""
|
|
398
|
+
out: list[str] = []
|
|
399
|
+
prev = self._prev_card
|
|
400
|
+
if prev:
|
|
401
|
+
if prev.get("policy") != meta["policy"]:
|
|
402
|
+
out.append(f"policy {prev['policy']} -> {meta['policy']}")
|
|
403
|
+
if prev.get("timeslice") != meta["timeslice"]:
|
|
404
|
+
out.append(f"time-slice {prev['timeslice']} -> {meta['timeslice']}")
|
|
405
|
+
sw = self.timeline.switches()
|
|
406
|
+
if sw != prev.get("switches"):
|
|
407
|
+
out.append(f"{sw - prev.get('switches', 0)} more context switch(es)")
|
|
408
|
+
self._prev_card = {"policy": meta["policy"], "timeslice": meta["timeslice"],
|
|
409
|
+
"switches": self.timeline.switches()}
|
|
410
|
+
return out
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
id: basic-lan
|
|
2
|
+
layer: core
|
|
3
|
+
teaches: networking-basics
|
|
4
|
+
spirit: two hosts share a switched LAN and reach off-subnet through a router gateway — hosts wire to the
|
|
5
|
+
switch, the switch wires to the router. Any valid such shape counts.
|
|
6
|
+
summary: 'Build a switched LAN: two hosts on a switch, a router as the gateway.'
|
|
7
|
+
parent: lan/switched
|
|
8
|
+
provides:
|
|
9
|
+
- switched-segment
|
|
10
|
+
- router-gateway
|
|
11
|
+
- service-endpoint
|
|
12
|
+
objectives:
|
|
13
|
+
- id: has-switch
|
|
14
|
+
say: Place a switch on the canvas
|
|
15
|
+
check: exists(switch)
|
|
16
|
+
- id: two-hosts
|
|
17
|
+
say: Place at least two hosts
|
|
18
|
+
check: count(host) >= 2
|
|
19
|
+
- id: has-gateway
|
|
20
|
+
say: Place a router (the gateway)
|
|
21
|
+
check: exists(router)
|
|
22
|
+
- id: hosts-on-switch
|
|
23
|
+
say: Wire the hosts to the switch
|
|
24
|
+
check: link(host, switch)
|
|
25
|
+
- id: switch-to-gateway
|
|
26
|
+
say: Wire the switch to the router
|
|
27
|
+
check: link(switch, router)
|
|
28
|
+
misconceptions:
|
|
29
|
+
- Wiring the two hosts directly together instead of through the switch.
|
|
30
|
+
- Forgetting the router, so the LAN has no gateway off-subnet.
|
|
31
|
+
peers:
|
|
32
|
+
- sdn-reactive
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
id: cache-in-front
|
|
2
|
+
layer: core
|
|
3
|
+
teaches: datastores
|
|
4
|
+
spirit: a cache sits in front of a database to cut read load — both exist and are wired.
|
|
5
|
+
summary: Put a cache in front of a database.
|
|
6
|
+
parent: data
|
|
7
|
+
provides:
|
|
8
|
+
- cache-tier
|
|
9
|
+
- relational-store
|
|
10
|
+
objectives:
|
|
11
|
+
- id: has-cache
|
|
12
|
+
say: Place a cache
|
|
13
|
+
check: exists(cache)
|
|
14
|
+
- id: has-db
|
|
15
|
+
say: Place a database
|
|
16
|
+
check: exists(database)
|
|
17
|
+
- id: fronted
|
|
18
|
+
say: Put the cache in front of the database
|
|
19
|
+
check: path(cache, database)
|
|
20
|
+
misconceptions:
|
|
21
|
+
- A cache not connected to anything it fronts.
|
|
22
|
+
peers:
|
|
23
|
+
- reachability-boundary
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
id: decouple-with-queue
|
|
2
|
+
layer: core
|
|
3
|
+
teaches: messaging-queue
|
|
4
|
+
spirit: a producer and a consumer are decoupled by a queue — the producer publishes to it and the consumer
|
|
5
|
+
reads from it, so neither waits on the other.
|
|
6
|
+
summary: Decouple a web app and a function with a queue.
|
|
7
|
+
parent: messaging
|
|
8
|
+
provides:
|
|
9
|
+
- message-broker
|
|
10
|
+
- web-endpoint
|
|
11
|
+
- serverless-fn
|
|
12
|
+
objectives:
|
|
13
|
+
- id: has-queue
|
|
14
|
+
say: Place a message queue
|
|
15
|
+
check: exists(queue)
|
|
16
|
+
- id: has-producer
|
|
17
|
+
say: Place a web app (the producer)
|
|
18
|
+
check: exists(web_app)
|
|
19
|
+
- id: has-consumer
|
|
20
|
+
say: Place a function (the consumer)
|
|
21
|
+
check: exists(function)
|
|
22
|
+
- id: produces
|
|
23
|
+
say: Connect the producer to the queue
|
|
24
|
+
check: path(web_app, queue)
|
|
25
|
+
- id: consumes
|
|
26
|
+
say: Connect the queue to the consumer
|
|
27
|
+
check: path(queue, function)
|
|
28
|
+
misconceptions:
|
|
29
|
+
- Wiring the producer straight to the consumer (no decoupling).
|
|
30
|
+
peers:
|
|
31
|
+
- serverless-api
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
id: drive-load
|
|
2
|
+
layer: exercise
|
|
3
|
+
teaches: load-testing
|
|
4
|
+
spirit: traffic is actively driven at the system so the student can watch it respond under load — a client
|
|
5
|
+
generates requests toward the entry point.
|
|
6
|
+
summary: Add a client that generates load against the system.
|
|
7
|
+
parent: traffic
|
|
8
|
+
step: Add a host to act as a load generator and point it at the entry point, then run it and watch the
|
|
9
|
+
traffic spread.
|
|
10
|
+
provides:
|
|
11
|
+
- load-generator
|
|
12
|
+
requires:
|
|
13
|
+
- traffic-sink
|
|
14
|
+
objectives:
|
|
15
|
+
- id: has-client
|
|
16
|
+
say: There is a client to generate load
|
|
17
|
+
check: exists(host)
|
|
18
|
+
misconceptions:
|
|
19
|
+
- A system with nothing driving traffic shows nothing under load.
|
|
20
|
+
catalog: false
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
id: fix-the-address
|
|
2
|
+
layer: core
|
|
3
|
+
teaches: networking-basics
|
|
4
|
+
spirit: everything is plugged in and the picture looks perfect, but one host was addressed into the
|
|
5
|
+
wrong subnet, so it is deaf to its neighbours. The student cannot see the bug — they must run the
|
|
6
|
+
network, observe the failure, read the addresses, and repair the one that does not belong. This is
|
|
7
|
+
the mis-configuration fault, where absence is visible but a wrong setting is not.
|
|
8
|
+
summary: 'The silent host: three hosts share a switch, but one cannot talk to the others. Find the bad
|
|
9
|
+
address and fix it.'
|
|
10
|
+
parent: lan/switched
|
|
11
|
+
provides:
|
|
12
|
+
- switched-segment
|
|
13
|
+
objectives:
|
|
14
|
+
# --- L1 / L2: the board is GIVEN — these start green, and say so out loud, because the lesson is
|
|
15
|
+
# that a correct-looking picture is not a working network.
|
|
16
|
+
- id: has-switch
|
|
17
|
+
say: A switch is on the board (pre-built — the wiring is not the problem)
|
|
18
|
+
check: exists(switch)
|
|
19
|
+
- id: three-hosts
|
|
20
|
+
say: Three hosts are on the board (pre-built)
|
|
21
|
+
check: count(host) >= 3
|
|
22
|
+
- id: hosts-on-switch
|
|
23
|
+
say: Every host is cabled to the switch (pre-built — look closer than the cables)
|
|
24
|
+
check: link(host, switch)
|
|
25
|
+
# --- L4: only RUNNING it can reveal a wrong address ---------------------------
|
|
26
|
+
- id: live-reach
|
|
27
|
+
say: 'Live: every host can actually reach every other host (Run to check)'
|
|
28
|
+
kind: behavioral
|
|
29
|
+
# `all` is load-bearing: with the default existential reading, the two HEALTHY hosts pinging each
|
|
30
|
+
# other would tick this green while the mis-addressed one stayed deaf — the mission would pass
|
|
31
|
+
# without the bug being fixed. Self-pairs are dropped, so a host can't ping itself into passing.
|
|
32
|
+
probe: reach(host -> host, all) == ok
|
|
33
|
+
misconceptions:
|
|
34
|
+
- Re-cabling the host. The cable is fine; the address is wrong — a wiring fix cannot repair a subnet
|
|
35
|
+
mistake.
|
|
36
|
+
- Assuming a host that is plugged in is a host that can talk.
|
|
37
|
+
- Reading the topology picture as proof of connectivity.
|
|
38
|
+
peers:
|
|
39
|
+
- basic-lan
|
|
40
|
+
- fix-the-lan
|
|
41
|
+
stage:
|
|
42
|
+
# manual addressing is essential: with auto-IP on, the compiler would quietly REPAIR the very
|
|
43
|
+
# fault we are injecting, and the mission would be unwinnable-by-being-already-won.
|
|
44
|
+
manual_addressing: true
|
|
45
|
+
devices:
|
|
46
|
+
- ref: s1
|
|
47
|
+
type: switch
|
|
48
|
+
x: 380
|
|
49
|
+
y: 260
|
|
50
|
+
- ref: h1
|
|
51
|
+
type: host
|
|
52
|
+
x: 180
|
|
53
|
+
y: 130
|
|
54
|
+
ips:
|
|
55
|
+
s1: 10.0.1.11
|
|
56
|
+
- ref: h2
|
|
57
|
+
type: host
|
|
58
|
+
x: 180
|
|
59
|
+
y: 290
|
|
60
|
+
ips:
|
|
61
|
+
s1: 10.0.1.12
|
|
62
|
+
- ref: h3
|
|
63
|
+
type: host
|
|
64
|
+
x: 180
|
|
65
|
+
y: 450
|
|
66
|
+
ips:
|
|
67
|
+
# THE FAULT — a perfectly valid address, on the wrong network. Nothing on the canvas shows it.
|
|
68
|
+
s1: 192.168.5.13
|
|
69
|
+
links:
|
|
70
|
+
- - h1
|
|
71
|
+
- s1
|
|
72
|
+
- - h2
|
|
73
|
+
- s1
|
|
74
|
+
- - h3
|
|
75
|
+
- s1
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
id: fix-the-lan
|
|
2
|
+
layer: core
|
|
3
|
+
teaches: networking-basics
|
|
4
|
+
spirit: a switched LAN was pre-built but has no gateway off-subnet — the student diagnoses the gap and
|
|
5
|
+
adds a router wired to the switch so the hosts can reach beyond their segment.
|
|
6
|
+
summary: 'Fix the LAN: two hosts sit on a switch, but there is no gateway. Add the router.'
|
|
7
|
+
parent: lan/switched
|
|
8
|
+
objectives:
|
|
9
|
+
- id: has-switch
|
|
10
|
+
say: A switch is on the board (pre-built)
|
|
11
|
+
check: exists(switch)
|
|
12
|
+
- id: two-hosts
|
|
13
|
+
say: Two hosts are on the board (pre-built)
|
|
14
|
+
check: count(host) >= 2
|
|
15
|
+
- id: add-gateway
|
|
16
|
+
say: Add a router as the gateway
|
|
17
|
+
check: exists(router)
|
|
18
|
+
- id: switch-to-gateway
|
|
19
|
+
say: Wire the switch to the router gateway
|
|
20
|
+
check: link(switch, router)
|
|
21
|
+
misconceptions:
|
|
22
|
+
- Adding a second switch instead of a router — a switch doesn't route off-subnet.
|
|
23
|
+
peers:
|
|
24
|
+
- basic-lan
|
|
25
|
+
stage:
|
|
26
|
+
devices:
|
|
27
|
+
- ref: s1
|
|
28
|
+
type: switch
|
|
29
|
+
x: 380
|
|
30
|
+
y: 260
|
|
31
|
+
- ref: h1
|
|
32
|
+
type: host
|
|
33
|
+
x: 200
|
|
34
|
+
y: 160
|
|
35
|
+
- ref: h2
|
|
36
|
+
type: host
|
|
37
|
+
x: 200
|
|
38
|
+
y: 360
|
|
39
|
+
links:
|
|
40
|
+
- - h1
|
|
41
|
+
- s1
|
|
42
|
+
- - h2
|
|
43
|
+
- s1
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
id: inspect-flows
|
|
2
|
+
layer: observe
|
|
3
|
+
teaches: sdn
|
|
4
|
+
spirit: the student opens the switch's flow table and watches entries and counters change as traffic flows
|
|
5
|
+
— seeing the control plane's decisions, not guessing them.
|
|
6
|
+
summary: Open the OpenFlow table and watch it react to traffic.
|
|
7
|
+
parent: observability
|
|
8
|
+
step: Double-click the OpenVSwitch to open its flow table and watch the counters move.
|
|
9
|
+
provides:
|
|
10
|
+
- flow-inspector
|
|
11
|
+
- visualizer
|
|
12
|
+
requires:
|
|
13
|
+
- sdn-fabric
|
|
14
|
+
misconceptions:
|
|
15
|
+
- Believing forwarding is magic instead of controller-installed flows.
|
|
16
|
+
catalog: false
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
id: k8s-autoscale
|
|
2
|
+
layer: core
|
|
3
|
+
teaches: kubernetes
|
|
4
|
+
spirit: a pod runs inside a cluster with an autoscaler attached so it can scale on load.
|
|
5
|
+
summary: Run a pod in a cluster with an autoscaler.
|
|
6
|
+
parent: k8s
|
|
7
|
+
provides:
|
|
8
|
+
- orchestrated-compute
|
|
9
|
+
- pod-workload
|
|
10
|
+
- metrics-source
|
|
11
|
+
objectives:
|
|
12
|
+
- id: has-cluster
|
|
13
|
+
say: Place a Kubernetes cluster
|
|
14
|
+
check: exists(k8s_cluster)
|
|
15
|
+
- id: has-pod
|
|
16
|
+
say: Place a pod
|
|
17
|
+
check: exists(pod)
|
|
18
|
+
- id: has-hpa
|
|
19
|
+
say: Place a pod autoscaler
|
|
20
|
+
check: exists(instance_group)
|
|
21
|
+
- id: pod-in-cluster
|
|
22
|
+
say: The pod lives in the cluster
|
|
23
|
+
check: link(k8s_cluster, pod) or contains_type(k8s_cluster, pod)
|
|
24
|
+
misconceptions:
|
|
25
|
+
- A pod outside any cluster — a pod must live in a cluster.
|
|
26
|
+
peers:
|
|
27
|
+
- load-balanced-web
|