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,201 @@
|
|
|
1
|
+
"""Process fingerprints — a behavioral signature per process, from REAL kernel telemetry.
|
|
2
|
+
|
|
3
|
+
A fingerprint is a small feature vector over five axes, each derived from data the Machine Lab
|
|
4
|
+
already collects (no simulation, no LLM judgement):
|
|
5
|
+
|
|
6
|
+
cpu — fraction of samples the proc wanted the CPU (RUNNING/RUNNABLE), from procdump
|
|
7
|
+
io_wait — fraction of samples SLEEPING (blocked on IO / wait / pipe), from procdump
|
|
8
|
+
syscalls — syscall rate, from the syscall trace ring (per-pid TRACE entries)
|
|
9
|
+
faults — page-fault rate, from the trap-taxonomy ring (per-pid pagefault traps)
|
|
10
|
+
forks — fork() rate, from the syscall ring (num == SYS_fork)
|
|
11
|
+
|
|
12
|
+
Two layers sit on top, both pure/deterministic and testable:
|
|
13
|
+
|
|
14
|
+
• classify(fp) — a threshold rule mapping a fingerprint to a behavior CLASS. This is the
|
|
15
|
+
baseline the "classify game" grades a student against (or that a student
|
|
16
|
+
tunes as an assignment).
|
|
17
|
+
• confusion_matrix(...) — scores predicted-vs-true classes. GROUND_TRUTH is the oracle's knowledge
|
|
18
|
+
of what each shipped program really is; it grades, it never classifies.
|
|
19
|
+
"""
|
|
20
|
+
from __future__ import annotations
|
|
21
|
+
|
|
22
|
+
import math
|
|
23
|
+
from dataclasses import dataclass
|
|
24
|
+
|
|
25
|
+
SYS_FORK = 1 # xv6 syscall number for fork()
|
|
26
|
+
TRAP_PAGEFAULT = 1 # trap-taxonomy kind index for a page fault
|
|
27
|
+
|
|
28
|
+
# the fingerprint axes, in radar order
|
|
29
|
+
FEATURE_AXES = ["cpu", "syscalls", "io_wait", "faults", "forks"]
|
|
30
|
+
AXIS_LABEL = {"cpu": "CPU", "syscalls": "syscalls", "io_wait": "IO wait",
|
|
31
|
+
"faults": "faults", "forks": "forks"}
|
|
32
|
+
|
|
33
|
+
# behavior classes a fingerprint can be sorted into
|
|
34
|
+
CLASSES = ["cpu-bound", "io-bound", "memory", "fork-heavy", "mixed"]
|
|
35
|
+
|
|
36
|
+
# the oracle's ground truth — what each launchable program ACTUALLY is. Used only to grade the
|
|
37
|
+
# classify game; never shown to the classifier. (grind forks + does file/pipe work, so it can
|
|
38
|
+
# honestly be confused with io-bound — that overlap is the lesson.)
|
|
39
|
+
GROUND_TRUTH = {
|
|
40
|
+
"spin": "cpu-bound", "busy": "cpu-bound",
|
|
41
|
+
"alloc": "memory", "writer": "io-bound",
|
|
42
|
+
"forktest": "fork-heavy", "grind": "fork-heavy",
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
# saturation constants: rate at which each axis reaches ~0.5 (via x/(x+k)). Tuned so the shipped
|
|
46
|
+
# programs land in distinct regions; documented so a student can reason about them.
|
|
47
|
+
_K_SYS, _K_FLT, _K_FORK = 20.0, 5.0, 2.0
|
|
48
|
+
|
|
49
|
+
# default classifier thresholds — a student assignment can tune these and watch the confusion move.
|
|
50
|
+
DEFAULT_THRESHOLDS = {"fork": 0.4, "flt": 0.3, "io": 0.35, "sys": 0.4, "cpu": 0.5}
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
@dataclass
|
|
54
|
+
class ProcFeatures:
|
|
55
|
+
"""Raw per-process tallies accumulated over a window. All fields come from real dumps."""
|
|
56
|
+
pid: int
|
|
57
|
+
name: str = ""
|
|
58
|
+
samples: int = 0 # procdump samples observed
|
|
59
|
+
run_samples: int = 0 # ...RUNNING or RUNNABLE (wanted the CPU)
|
|
60
|
+
sleep_samples: int = 0 # ...SLEEPING (blocked)
|
|
61
|
+
syscalls: int = 0 # syscall trace events attributed to this pid
|
|
62
|
+
forks: int = 0 # ...of which fork()
|
|
63
|
+
faults: int = 0 # page-fault traps
|
|
64
|
+
window: float = 1.0 # seconds the tallies span (for rates)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def _sat(x: float, k: float) -> float:
|
|
68
|
+
"""Saturating normalizer x/(x+k) -> 0..1: bounded and stable frame to frame."""
|
|
69
|
+
return x / (x + k) if x > 0 else 0.0
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def fingerprint(f: ProcFeatures) -> dict:
|
|
73
|
+
"""A ProcFeatures -> the 0..1 feature vector (the fingerprint)."""
|
|
74
|
+
w = max(f.window, 1e-6)
|
|
75
|
+
s = max(f.samples, 1)
|
|
76
|
+
return {
|
|
77
|
+
"cpu": min(1.0, f.run_samples / s),
|
|
78
|
+
"io_wait": min(1.0, f.sleep_samples / s),
|
|
79
|
+
"syscalls": _sat(f.syscalls / w, _K_SYS),
|
|
80
|
+
"faults": _sat(f.faults / w, _K_FLT),
|
|
81
|
+
"forks": _sat(f.forks / w, _K_FORK),
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def classify(fp: dict, thresholds: dict | None = None) -> str:
|
|
86
|
+
"""Map a fingerprint to a behavior class by a simple, inspectable rule. Order matters: the most
|
|
87
|
+
distinctive signal wins first. Thresholds are tunable (the assignment)."""
|
|
88
|
+
th = {**DEFAULT_THRESHOLDS, **(thresholds or {})}
|
|
89
|
+
if fp["forks"] >= th["fork"]:
|
|
90
|
+
return "fork-heavy"
|
|
91
|
+
if fp["faults"] >= th["flt"]:
|
|
92
|
+
return "memory"
|
|
93
|
+
if fp["io_wait"] >= th["io"] or (fp["syscalls"] >= th["sys"] and fp["cpu"] < th["cpu"]):
|
|
94
|
+
return "io-bound"
|
|
95
|
+
if fp["cpu"] >= th["cpu"]:
|
|
96
|
+
return "cpu-bound"
|
|
97
|
+
return "mixed"
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def similarity(a: dict, b: dict) -> float:
|
|
101
|
+
"""Cosine similarity of two fingerprints (0..1) — 'who behaves like whom' for the board."""
|
|
102
|
+
dot = sum(a[k] * b[k] for k in FEATURE_AXES)
|
|
103
|
+
na = math.sqrt(sum(a[k] ** 2 for k in FEATURE_AXES))
|
|
104
|
+
nb = math.sqrt(sum(b[k] ** 2 for k in FEATURE_AXES))
|
|
105
|
+
return dot / (na * nb) if na > 0 and nb > 0 else 0.0
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def scatter_xy(fp: dict) -> tuple[float, float]:
|
|
109
|
+
"""Project a fingerprint onto the behavior map: x = CPU-bound(1) <-> IO-bound(0),
|
|
110
|
+
y = compute(0) <-> heavy-kernel(1, syscalls+faults+forks). Both 0..1."""
|
|
111
|
+
x = max(0.0, min(1.0, 0.5 + 0.5 * (fp["cpu"] - fp["io_wait"])))
|
|
112
|
+
y = max(0.0, min(1.0, (fp["syscalls"] + fp["faults"] + fp["forks"]) / 3.0))
|
|
113
|
+
return x, y
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def true_class(name: str) -> str | None:
|
|
117
|
+
"""The oracle's ground-truth class for a program name (None if unknown)."""
|
|
118
|
+
return GROUND_TRUTH.get((name or "").strip())
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def confusion_matrix(pairs, classes=None) -> dict:
|
|
122
|
+
"""[(true_class, predicted_class), …] -> {(true, pred): count}. Off-diagonal = confusion."""
|
|
123
|
+
classes = classes or CLASSES
|
|
124
|
+
m = {(t, p): 0 for t in classes for p in classes}
|
|
125
|
+
for true, pred in pairs:
|
|
126
|
+
if (true, pred) in m:
|
|
127
|
+
m[(true, pred)] += 1
|
|
128
|
+
return m
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def accuracy(pairs) -> float:
|
|
132
|
+
"""Fraction of (true, pred) pairs on the diagonal."""
|
|
133
|
+
pairs = list(pairs)
|
|
134
|
+
if not pairs:
|
|
135
|
+
return 0.0
|
|
136
|
+
return sum(1 for t, p in pairs if t == p) / len(pairs)
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
class FingerprintAccumulator:
|
|
140
|
+
"""Builds per-pid ProcFeatures from a stream of observations. Pure bookkeeping — the UI feeds it
|
|
141
|
+
procdump states each poll plus the NEW syscall/trap ring events since the last poll."""
|
|
142
|
+
|
|
143
|
+
def __init__(self) -> None:
|
|
144
|
+
self.feats: dict[int, ProcFeatures] = {}
|
|
145
|
+
|
|
146
|
+
def reset(self) -> None:
|
|
147
|
+
self.feats.clear()
|
|
148
|
+
|
|
149
|
+
def _get(self, pid: int, name: str = "") -> ProcFeatures:
|
|
150
|
+
f = self.feats.get(pid)
|
|
151
|
+
if f is None:
|
|
152
|
+
f = ProcFeatures(pid=pid, name=name)
|
|
153
|
+
self.feats[pid] = f
|
|
154
|
+
if name:
|
|
155
|
+
f.name = name
|
|
156
|
+
return f
|
|
157
|
+
|
|
158
|
+
def observe(self, procs, sc_events=(), trap_events=(), dt: float = 0.5) -> None:
|
|
159
|
+
"""One poll: `procs` are the current [Proc]; `sc_events`/`trap_events` are the NEW ring
|
|
160
|
+
entries since the last poll (SyscallEvent / TrapEvent). `dt` advances every proc's window."""
|
|
161
|
+
for p in procs:
|
|
162
|
+
f = self._get(p.pid, p.name)
|
|
163
|
+
f.samples += 1
|
|
164
|
+
if p.state in ("running", "runnable"):
|
|
165
|
+
f.run_samples += 1
|
|
166
|
+
elif p.state == "sleeping":
|
|
167
|
+
f.sleep_samples += 1
|
|
168
|
+
f.window += dt
|
|
169
|
+
for e in sc_events:
|
|
170
|
+
f = self.feats.get(e.pid)
|
|
171
|
+
if f is not None:
|
|
172
|
+
f.syscalls += 1
|
|
173
|
+
if e.num == SYS_FORK:
|
|
174
|
+
f.forks += 1
|
|
175
|
+
for e in trap_events:
|
|
176
|
+
f = self.feats.get(e.pid)
|
|
177
|
+
if f is not None and e.kind == TRAP_PAGEFAULT:
|
|
178
|
+
f.faults += 1
|
|
179
|
+
|
|
180
|
+
def fingerprints(self, min_samples: int = 3) -> dict:
|
|
181
|
+
"""{pid: fingerprint} for procs with enough samples to be meaningful (else 'settling')."""
|
|
182
|
+
return {pid: fingerprint(f) for pid, f in self.feats.items() if f.samples >= min_samples}
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
def demo_features() -> list:
|
|
186
|
+
"""Canned but realistic fingerprints for the shipped programs, so the panel + game work offline
|
|
187
|
+
(and drive the pure tests). Tallies chosen to match GROUND_TRUTH under the default classifier."""
|
|
188
|
+
return [
|
|
189
|
+
ProcFeatures(pid=4, name="spin", samples=20, run_samples=20, sleep_samples=0,
|
|
190
|
+
syscalls=1, forks=0, faults=0, window=10),
|
|
191
|
+
ProcFeatures(pid=5, name="busy", samples=20, run_samples=20, sleep_samples=0,
|
|
192
|
+
syscalls=2, forks=0, faults=0, window=10),
|
|
193
|
+
ProcFeatures(pid=6, name="alloc", samples=20, run_samples=14, sleep_samples=6,
|
|
194
|
+
syscalls=50, forks=0, faults=48, window=10),
|
|
195
|
+
ProcFeatures(pid=7, name="writer", samples=20, run_samples=6, sleep_samples=14,
|
|
196
|
+
syscalls=200, forks=0, faults=0, window=10),
|
|
197
|
+
ProcFeatures(pid=8, name="grind", samples=20, run_samples=10, sleep_samples=10,
|
|
198
|
+
syscalls=180, forks=40, faults=5, window=10),
|
|
199
|
+
ProcFeatures(pid=9, name="forktest", samples=20, run_samples=12, sleep_samples=8,
|
|
200
|
+
syscalls=60, forks=50, faults=0, window=10),
|
|
201
|
+
]
|
gini/domain/firewall.py
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
"""Firewall rules -> gRouter ACL deploy commands (the firewall face of the Router Lab).
|
|
2
|
+
|
|
3
|
+
A GINI Firewall is a gRouter with an ACL data-plane module. A rule `deny <cidr>` (or a bare
|
|
4
|
+
CIDR) becomes `gpipe add acl <cidr>` on the real router; deploying clears the pipeline then
|
|
5
|
+
adds each rule in order — the same live-deploy path as the Service Function Chain, just a
|
|
6
|
+
firewall-shaped front-end. Pure/text-only, so it's unit-tested without Docker.
|
|
7
|
+
"""
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import re
|
|
11
|
+
|
|
12
|
+
_CIDR = re.compile(r"^\d{1,3}(?:\.\d{1,3}){3}(?:/\d{1,2})?$")
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def parse_rules(text: str) -> list[tuple[str, str]]:
|
|
16
|
+
"""[(action, target)] from a rules block — one `deny <cidr>` (or bare CIDR) per line.
|
|
17
|
+
Blank lines and `#` comments are ignored; unrecognised lines are skipped."""
|
|
18
|
+
out: list[tuple[str, str]] = []
|
|
19
|
+
for ln in (text or "").splitlines():
|
|
20
|
+
s = ln.strip()
|
|
21
|
+
if not s or s.startswith("#"):
|
|
22
|
+
continue
|
|
23
|
+
toks = s.split()
|
|
24
|
+
if toks[0].lower() in ("deny", "drop", "block") and len(toks) >= 2 and _CIDR.match(toks[1]):
|
|
25
|
+
out.append(("deny", toks[1]))
|
|
26
|
+
elif _CIDR.match(toks[0]):
|
|
27
|
+
out.append(("deny", toks[0]))
|
|
28
|
+
return out
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def deploy_commands(text: str) -> list[str]:
|
|
32
|
+
"""The `gpipe` arg-lines that program this firewall's ACL into the running gRouter:
|
|
33
|
+
`clear`, then `add acl <cidr>` per deny rule, in order."""
|
|
34
|
+
return ["clear"] + [f"add acl {target}" for _action, target in parse_rules(text)]
|
gini/domain/flowlog.py
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
"""Flow event log — accumulate the controller's flow-rule installs/expiries over time.
|
|
2
|
+
|
|
3
|
+
The live flow table is only a snapshot: a reactive controller installs exact-match rules for
|
|
4
|
+
learned conversations and they expire after an idle timeout, so at any instant you see only a
|
|
5
|
+
few. To show the FULL picture of what the controller programmed, `FlowLog` diffs successive
|
|
6
|
+
snapshots (the dashboard polls `openflow entry all` every ~2.5s): a rule present now but not
|
|
7
|
+
before is an "installed" event; one present before but not now is an "expired" event. This
|
|
8
|
+
captures every rule that lived at least one poll — no backend change needed.
|
|
9
|
+
|
|
10
|
+
Rules are identified by their match+action (the flow's identity), since the numeric slot
|
|
11
|
+
index gets reused. Pure/duck-typed (rows just need `match_summary()`/`action_summary()` and
|
|
12
|
+
`.packets`), so it's unit-tested without Qt or Docker.
|
|
13
|
+
"""
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
import time
|
|
17
|
+
from dataclasses import dataclass
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@dataclass
|
|
21
|
+
class FlowEvent:
|
|
22
|
+
kind: str # "installed" | "expired"
|
|
23
|
+
match: str
|
|
24
|
+
action: str
|
|
25
|
+
packets: int | None # at install: ~0; at expire: the final count the rule handled
|
|
26
|
+
when: str # HH:MM:SS
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class FlowLog:
|
|
30
|
+
def __init__(self, cap: int = 300) -> None:
|
|
31
|
+
self._prev: dict[str, dict] = {} # key -> {match, action, packets}
|
|
32
|
+
self.events: list[FlowEvent] = [] # oldest first
|
|
33
|
+
self.cap = cap
|
|
34
|
+
|
|
35
|
+
def update(self, rows, now: str | None = None) -> list[FlowEvent]:
|
|
36
|
+
"""Fold a fresh snapshot in; return the events newly detected this poll."""
|
|
37
|
+
ts = now or time.strftime("%H:%M:%S")
|
|
38
|
+
cur: dict[str, dict] = {}
|
|
39
|
+
for f in rows or []:
|
|
40
|
+
match, action = f.match_summary(), f.action_summary()
|
|
41
|
+
cur[f"{match} ⇒ {action}"] = {"match": match, "action": action,
|
|
42
|
+
"packets": getattr(f, "packets", None)}
|
|
43
|
+
fresh: list[FlowEvent] = []
|
|
44
|
+
for key, v in cur.items():
|
|
45
|
+
if key not in self._prev:
|
|
46
|
+
fresh.append(FlowEvent("installed", v["match"], v["action"], v["packets"], ts))
|
|
47
|
+
for key, v in self._prev.items():
|
|
48
|
+
if key not in cur: # gone this poll -> expired/removed
|
|
49
|
+
fresh.append(FlowEvent("expired", v["match"], v["action"], v["packets"], ts))
|
|
50
|
+
self._prev = cur
|
|
51
|
+
self.events.extend(fresh)
|
|
52
|
+
if len(self.events) > self.cap:
|
|
53
|
+
self.events = self.events[-self.cap:]
|
|
54
|
+
return fresh
|
|
55
|
+
|
|
56
|
+
def recent(self, n: int = 200) -> list[FlowEvent]:
|
|
57
|
+
"""Most-recent events first (for display)."""
|
|
58
|
+
return list(reversed(self.events[-n:]))
|
|
59
|
+
|
|
60
|
+
def clear(self) -> None:
|
|
61
|
+
self._prev, self.events = {}, []
|
gini/domain/flowtable.py
ADDED
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
"""OpenFlow flow-table parsing — turn the gRouter's `openflow …` CLI dumps into rows the
|
|
2
|
+
SDN dashboard can render.
|
|
3
|
+
|
|
4
|
+
The OVS element is the C gRouter in `--openflow` mode; its CLI (reachable at runtime via
|
|
5
|
+
`element_query(ovs, "openflow …")`) prints the live flow table:
|
|
6
|
+
|
|
7
|
+
• `openflow entry all` -> match fields + actions per active entry
|
|
8
|
+
• `openflow stats entry all` -> match + packet/byte counts + duration per active entry
|
|
9
|
+
|
|
10
|
+
Both share the block format (delimited by `=====\nEntry N\n=====`, match fields one tab in,
|
|
11
|
+
actions two tabs in). This module parses either, and `flows(entry_dump, stats_dump)` merges
|
|
12
|
+
them by index into `FlowEntry` rows. Pure/text-only, so it's fully unit-tested without Docker.
|
|
13
|
+
"""
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
import re
|
|
17
|
+
from dataclasses import dataclass, field
|
|
18
|
+
|
|
19
|
+
_ENTRY_RE = re.compile(r"^Entry\s+(\d+)\s*$")
|
|
20
|
+
_OFPP = {"OFPP_FLOOD": "flood", "OFPP_CONTROLLER": "controller", "OFPP_NORMAL": "normal",
|
|
21
|
+
"OFPP_ALL": "all", "OFPP_IN_PORT": "in_port", "OFPP_LOCAL": "local",
|
|
22
|
+
"OFPP_TABLE": "table", "OFPP_NONE": "none"}
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@dataclass
|
|
26
|
+
class FlowEntry:
|
|
27
|
+
index: int
|
|
28
|
+
match: dict = field(default_factory=dict) # e.g. {"Input port": "1", "IP protocol": "TCP"}
|
|
29
|
+
actions: list = field(default_factory=list) # e.g. ["output:2", "flood"]
|
|
30
|
+
priority: int | None = None
|
|
31
|
+
packets: int | None = None
|
|
32
|
+
bytes: int | None = None
|
|
33
|
+
duration: int | None = None
|
|
34
|
+
idle_timeout: int | None = None
|
|
35
|
+
hard_timeout: int | None = None
|
|
36
|
+
|
|
37
|
+
def match_summary(self) -> str:
|
|
38
|
+
m = self.match
|
|
39
|
+
parts = []
|
|
40
|
+
if m.get("Input port"):
|
|
41
|
+
parts.append("in:" + m["Input port"])
|
|
42
|
+
src, dst = m.get("IP source address"), m.get("IP destination address")
|
|
43
|
+
smac, dmac = (m.get("Ethernet source MAC address"),
|
|
44
|
+
m.get("Ethernet destination MAC address"))
|
|
45
|
+
if src or dst:
|
|
46
|
+
parts.append(f"{src or '*'}→{dst or '*'}")
|
|
47
|
+
elif smac or dmac:
|
|
48
|
+
parts.append(f"{smac or '*'}→{dmac or '*'}")
|
|
49
|
+
kind = m.get("IP protocol") or m.get("Ethernet frame type")
|
|
50
|
+
if kind:
|
|
51
|
+
parts.append(kind.lower())
|
|
52
|
+
dport = m.get("TCP destination port") or m.get("UDP destination port")
|
|
53
|
+
if dport:
|
|
54
|
+
parts.append("dport " + dport)
|
|
55
|
+
return " ".join(parts) or "any (wildcard)"
|
|
56
|
+
|
|
57
|
+
def action_summary(self) -> str:
|
|
58
|
+
return ", ".join(self.actions) or "—"
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def _int(v):
|
|
62
|
+
try:
|
|
63
|
+
return int(v)
|
|
64
|
+
except (TypeError, ValueError):
|
|
65
|
+
return None
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def _split_blocks(text: str):
|
|
69
|
+
"""Yield (index, [lines]) per `Entry N` block, keeping raw lines (with tabs)."""
|
|
70
|
+
idx, buf = None, []
|
|
71
|
+
for raw in (text or "").splitlines():
|
|
72
|
+
line = raw.rstrip("\n")
|
|
73
|
+
if set(line.strip()) == {"="} and line.strip(): # a `=====` divider
|
|
74
|
+
continue
|
|
75
|
+
m = _ENTRY_RE.match(line.strip())
|
|
76
|
+
if m:
|
|
77
|
+
if idx is not None:
|
|
78
|
+
yield idx, buf
|
|
79
|
+
idx, buf = int(m.group(1)), []
|
|
80
|
+
elif idx is not None:
|
|
81
|
+
buf.append(line)
|
|
82
|
+
if idx is not None:
|
|
83
|
+
yield idx, buf
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def _port_name(v: str) -> str:
|
|
87
|
+
v = v.strip()
|
|
88
|
+
if v in _OFPP:
|
|
89
|
+
return _OFPP[v]
|
|
90
|
+
if v.startswith("OFPP_"):
|
|
91
|
+
return v[5:].lower()
|
|
92
|
+
return f"output:{v}"
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def parse(text: str) -> list[FlowEntry]:
|
|
96
|
+
"""Parse one dump (`entry all` or `stats entry all`) into active FlowEntry rows."""
|
|
97
|
+
out: list[FlowEntry] = []
|
|
98
|
+
for index, lines in _split_blocks(text):
|
|
99
|
+
if any(l.strip() == "Entry inactive" for l in lines):
|
|
100
|
+
continue
|
|
101
|
+
e = FlowEntry(index=index)
|
|
102
|
+
section = None
|
|
103
|
+
for line in lines:
|
|
104
|
+
depth = len(line) - len(line.lstrip("\t"))
|
|
105
|
+
s = line.strip()
|
|
106
|
+
if not s:
|
|
107
|
+
continue
|
|
108
|
+
if depth == 0:
|
|
109
|
+
if s == "Match:":
|
|
110
|
+
section = "match"; continue
|
|
111
|
+
if s == "Actions:":
|
|
112
|
+
section = "actions"; continue
|
|
113
|
+
section = None # a top-level field ends the block
|
|
114
|
+
key, _, val = s.partition(":")
|
|
115
|
+
key, val = key.strip(), val.strip()
|
|
116
|
+
if key == "Priority":
|
|
117
|
+
e.priority = _int(val)
|
|
118
|
+
elif key == "Packet count":
|
|
119
|
+
e.packets = _int(val)
|
|
120
|
+
elif key == "Byte count":
|
|
121
|
+
e.bytes = _int(val)
|
|
122
|
+
elif key == "Duration (seconds)":
|
|
123
|
+
e.duration = _int(val)
|
|
124
|
+
elif key == "Last matched timeout (seconds)":
|
|
125
|
+
e.idle_timeout = _int(val)
|
|
126
|
+
elif key == "Last modified timeout (seconds)":
|
|
127
|
+
e.hard_timeout = _int(val)
|
|
128
|
+
elif depth == 1 and section == "match":
|
|
129
|
+
key, sep, val = s.partition(":")
|
|
130
|
+
if sep:
|
|
131
|
+
e.match[key.strip()] = val.strip()
|
|
132
|
+
elif depth >= 2 and section == "actions":
|
|
133
|
+
key, sep, val = s.partition(":")
|
|
134
|
+
if sep and key.strip() == "Output port":
|
|
135
|
+
e.actions.append(_port_name(val))
|
|
136
|
+
out.append(e)
|
|
137
|
+
return out
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def merge(entries: list, stats: list) -> list[FlowEntry]:
|
|
141
|
+
"""Merge an `entry all` parse (match+actions) with a `stats entry all` parse
|
|
142
|
+
(counters), keyed by entry index. Missing fields are filled from whichever dump has them."""
|
|
143
|
+
by_idx: dict[int, FlowEntry] = {e.index: e for e in entries}
|
|
144
|
+
for s in stats:
|
|
145
|
+
e = by_idx.get(s.index)
|
|
146
|
+
if e is None:
|
|
147
|
+
by_idx[s.index] = s
|
|
148
|
+
continue
|
|
149
|
+
e.match = e.match or s.match
|
|
150
|
+
e.actions = e.actions or s.actions
|
|
151
|
+
for attr in ("priority", "packets", "bytes", "duration",
|
|
152
|
+
"idle_timeout", "hard_timeout"):
|
|
153
|
+
if getattr(e, attr) is None and getattr(s, attr) is not None:
|
|
154
|
+
setattr(e, attr, getattr(s, attr))
|
|
155
|
+
return [by_idx[i] for i in sorted(by_idx)]
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def parse_table_stats(text: str) -> dict:
|
|
159
|
+
"""Parse `openflow stats table` into the table-level counters that prove the switch is
|
|
160
|
+
being programmed: active flows, packets looked up, and packets that matched a flow."""
|
|
161
|
+
out: dict[str, int] = {}
|
|
162
|
+
keymap = {
|
|
163
|
+
"Number of active entries": "active",
|
|
164
|
+
"Number of packets looked up in tables": "lookups",
|
|
165
|
+
"Number of packets that hit tables": "matched",
|
|
166
|
+
"Maximum number of supported entries": "max",
|
|
167
|
+
}
|
|
168
|
+
for line in (text or "").splitlines():
|
|
169
|
+
k, sep, v = line.strip().partition(":")
|
|
170
|
+
if sep and k.strip() in keymap:
|
|
171
|
+
n = _int(v.strip())
|
|
172
|
+
if n is not None:
|
|
173
|
+
out[keymap[k.strip()]] = n
|
|
174
|
+
return out
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def flows(entry_dump: str, stats_dump: str = "") -> list[FlowEntry]:
|
|
178
|
+
"""Convenience: parse both dumps and merge into the display rows."""
|
|
179
|
+
return merge(parse(entry_dump), parse(stats_dump) if stats_dump else [])
|