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
gini/domain/devices.py
ADDED
|
@@ -0,0 +1,747 @@
|
|
|
1
|
+
"""GINI device / element taxonomy.
|
|
2
|
+
|
|
3
|
+
A single registry describes every kind of element a user can place on the canvas,
|
|
4
|
+
spanning classic computer-networking devices and the new cloud-computing primitives.
|
|
5
|
+
This is pure data with no Qt dependency so it can be driven by the UI, the compiler,
|
|
6
|
+
the persistence layer, and the AI agent layer alike.
|
|
7
|
+
"""
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from dataclasses import dataclass, field
|
|
11
|
+
from enum import Enum
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class Category(str, Enum):
|
|
15
|
+
"""Palette sections, in the order they appear.
|
|
16
|
+
|
|
17
|
+
MACHINES exists because the single most valuable comparison GINI can make was invisible: the
|
|
18
|
+
things that RUN YOUR CODE were scattered across three sections (Machine and Instance under
|
|
19
|
+
"Compute", Container under "Containers & Kubernetes", xv6 under "xv6"), so a student hunting
|
|
20
|
+
for "something to run this on" had to look in three places — and never saw that they were
|
|
21
|
+
looking at one family. They are: a container, a container with your image, a cloud VM, a real
|
|
22
|
+
microVM with its own kernel (Kata), and a real kernel on QEMU (xv6). Listed lightest-to-heaviest
|
|
23
|
+
(see PALETTE_RANK), the section itself teaches the isolation/weight tradeoff before the student
|
|
24
|
+
reads a word — and the startup-time stamp and cost meter then make it measurable.
|
|
25
|
+
"""
|
|
26
|
+
NETWORKING = "Networking"
|
|
27
|
+
SDN = "Software-Defined Networking"
|
|
28
|
+
MACHINES = "Machines"
|
|
29
|
+
XV6 = "xv6 Peripherals" # attach only to an xv6 Machine (no networking)
|
|
30
|
+
CONTAINERS = "Containers & Kubernetes"
|
|
31
|
+
CLOUD_NETWORK = "Cloud Networking"
|
|
32
|
+
STORAGE = "Storage & Data"
|
|
33
|
+
STREAMING = "Streaming & Messaging"
|
|
34
|
+
OBSERVABILITY = "Observability"
|
|
35
|
+
WORKLOAD = "Workload & Testing"
|
|
36
|
+
SOURCE = "Sources" # stimulus riders — inject inputs into a donor element
|
|
37
|
+
SINK = "Sinks" # observer riders — read outputs off a donor element
|
|
38
|
+
SERVERLESS = "Serverless"
|
|
39
|
+
EXTERNAL = "External"
|
|
40
|
+
OS_ZOO = "OS Zoo" # play with real historical OSes (emulated, embedded noVNC)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
# Within the Machines section, order is the ISOLATION LADDER — lightest first. This is the lesson:
|
|
44
|
+
# they all run your code, and they differ in how much of a machine they actually are.
|
|
45
|
+
PALETTE_RANK: dict[str, int] = {
|
|
46
|
+
"host": 1, # a Linux container on the fabric
|
|
47
|
+
"desktop": 2, # …the same, but it also runs a graphical desktop (headful)
|
|
48
|
+
"container": 3, # …the same, but you supply the image
|
|
49
|
+
"instance": 4, # a cloud VM (as the cloud presents it)
|
|
50
|
+
"kinstance": 5, # a REAL microVM — its own kernel (Kata)
|
|
51
|
+
"xv6": 6, # a real teaching kernel on QEMU-RISC-V
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
# Color-category keys; the theme maps each to a concrete accent color per theme.
|
|
56
|
+
class Accent(str, Enum):
|
|
57
|
+
BLUE = "blue" # routing / L3
|
|
58
|
+
GREEN = "green" # switching / L2
|
|
59
|
+
PURPLE = "purple" # compute / hosts
|
|
60
|
+
TEAL = "teal" # sdn
|
|
61
|
+
CYAN = "cyan" # containers
|
|
62
|
+
INDIGO = "indigo" # cloud networking
|
|
63
|
+
AMBER = "amber" # storage
|
|
64
|
+
PINK = "pink" # serverless
|
|
65
|
+
SLATE = "slate" # external / generic
|
|
66
|
+
ORANGE = "orange" # observability / monitoring
|
|
67
|
+
RED = "red" # workload / testing
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
@dataclass(frozen=True)
|
|
71
|
+
class DeviceType:
|
|
72
|
+
key: str
|
|
73
|
+
label: str
|
|
74
|
+
category: Category
|
|
75
|
+
icon: str
|
|
76
|
+
accent: Accent
|
|
77
|
+
description: str
|
|
78
|
+
backend_kind: str | None = None # compiler element, e.g. "vr", "vs", "vm"
|
|
79
|
+
is_container: bool = False # can visually/logically contain children
|
|
80
|
+
default_properties: dict[str, str] = field(default_factory=dict)
|
|
81
|
+
# properties that should render as a dropdown in the inspector: name -> choices
|
|
82
|
+
property_choices: dict[str, tuple[str, ...]] = field(default_factory=dict)
|
|
83
|
+
# Properties the element REPORTS rather than accepts: shown in the inspector but
|
|
84
|
+
# not editable, because the value is observed from the real world (e.g. a GINI32
|
|
85
|
+
# board's radio channel, which APSTA forces to match the uplink).
|
|
86
|
+
readonly_properties: tuple[str, ...] = ()
|
|
87
|
+
max_links: int | None = None # None = unlimited
|
|
88
|
+
hidden: bool = False # kept in the registry but off the palette
|
|
89
|
+
# Is this a CLOUD element (a managed service you rent) rather than a networking primitive you
|
|
90
|
+
# build? Stated explicitly, not inferred from the palette category — otherwise reorganising a UI
|
|
91
|
+
# section silently changes what the AI is told about an element, which is how a Message Queue
|
|
92
|
+
# ends up "not a cloud thing" because someone moved it next to Pub/Sub.
|
|
93
|
+
is_cloud: bool | None = None # None = fall back to the category default
|
|
94
|
+
# --- rider elements (Sources / Sinks) ------------------------------------ #
|
|
95
|
+
# A rider has NO container of its own: it runs as a process INSIDE a donor element (a Machine,
|
|
96
|
+
# Router, OVS…) via the donor's runtime. On the canvas it hangs off its donor by a dotted
|
|
97
|
+
# *attach* edge, not a network cable. `role` is stated, not inferred from the palette section.
|
|
98
|
+
rider: bool = False # True = runs on a donor, spawns no container
|
|
99
|
+
role: str = "" # "source" (injects input) | "sink" (reads output)
|
|
100
|
+
attaches_to: tuple[str, ...] = () # donor type_keys this rider may ride
|
|
101
|
+
driver: str = "" # how it runs: "docker-exec" | "grouter-cli" | "qemu-serial"
|
|
102
|
+
|
|
103
|
+
@property
|
|
104
|
+
def cloud(self) -> bool:
|
|
105
|
+
if self.is_cloud is not None:
|
|
106
|
+
return self.is_cloud
|
|
107
|
+
return self.category in (
|
|
108
|
+
Category.CONTAINERS,
|
|
109
|
+
Category.CLOUD_NETWORK,
|
|
110
|
+
Category.STORAGE,
|
|
111
|
+
Category.STREAMING,
|
|
112
|
+
Category.OBSERVABILITY,
|
|
113
|
+
Category.WORKLOAD,
|
|
114
|
+
Category.SERVERLESS,
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
# --------------------------------------------------------------------------- #
|
|
119
|
+
# The registry
|
|
120
|
+
# --------------------------------------------------------------------------- #
|
|
121
|
+
|
|
122
|
+
_DEVICES: list[DeviceType] = [
|
|
123
|
+
# ---- Classic networking -------------------------------------------------
|
|
124
|
+
DeviceType(
|
|
125
|
+
"router", "Router", Category.NETWORKING, "router", Accent.BLUE,
|
|
126
|
+
"Virtual router (Layer-3 forwarding between subnets).",
|
|
127
|
+
backend_kind="vr",
|
|
128
|
+
default_properties={"Name": "", "Forwarding": "true"},
|
|
129
|
+
),
|
|
130
|
+
DeviceType(
|
|
131
|
+
"switch", "Switch", Category.NETWORKING, "switch", Accent.GREEN,
|
|
132
|
+
"Layer-2 learning switch.",
|
|
133
|
+
backend_kind="vs",
|
|
134
|
+
default_properties={"Name": "", "Priority": "100", "MAC": ""},
|
|
135
|
+
),
|
|
136
|
+
DeviceType(
|
|
137
|
+
"hub", "Hub", Category.NETWORKING, "hub", Accent.GREEN,
|
|
138
|
+
"Layer-1 repeater hub (broadcasts to all ports).",
|
|
139
|
+
backend_kind="vs",
|
|
140
|
+
default_properties={"Name": ""},
|
|
141
|
+
),
|
|
142
|
+
DeviceType(
|
|
143
|
+
"host", "Machine", Category.MACHINES, "host", Accent.PURPLE,
|
|
144
|
+
"Virtual host / end machine — a Linux container on the fabric.",
|
|
145
|
+
backend_kind="vm",
|
|
146
|
+
# Toolkit = which image this host is built from. LEAN (Alpine) is the default and the one
|
|
147
|
+
# to prefer: it has everything a student actually types (ip, ping, traceroute, tcpdump,
|
|
148
|
+
# dig, curl, nc, socat, iperf3, nmap) and is ~10x smaller, so topologies build and boot
|
|
149
|
+
# far faster on a modest laptop. Switch to FULL only for the experiments that need the
|
|
150
|
+
# heavy servers — bind9 (DNS), postfix (mail), ettercap/dsniff (spoofing), haproxy.
|
|
151
|
+
# NOTE: unrelated to the element's SIZE tier, which sets CPU and cost, not contents.
|
|
152
|
+
default_properties={"Name": "", "OS": "linux", "Interfaces": "1", "Toolkit": "lean"},
|
|
153
|
+
property_choices={"Toolkit": ("lean", "full", "security")},
|
|
154
|
+
is_cloud=False, # stated, not inferred from the palette section
|
|
155
|
+
),
|
|
156
|
+
DeviceType(
|
|
157
|
+
"desktop", "Desktop", Category.MACHINES, "host", Accent.PINK,
|
|
158
|
+
"A HEADFUL machine — a real Linux host on the fabric (pingable, routable, all the usual "
|
|
159
|
+
"networking tools) that also runs a light graphical desktop (fluxbox, a file manager, a "
|
|
160
|
+
"terminal, and the Dillo browser). Double-click to open its screen in an embedded window "
|
|
161
|
+
"over noVNC. Use it when you want a GUI in the topology — e.g. browse a web server another "
|
|
162
|
+
"machine is serving. Heavier than a plain Machine (it carries an X stack), so reach for it "
|
|
163
|
+
"when you actually want the desktop.",
|
|
164
|
+
backend_kind="vm",
|
|
165
|
+
default_properties={"Name": "", "OS": "linux", "Interfaces": "1"},
|
|
166
|
+
is_cloud=False,
|
|
167
|
+
),
|
|
168
|
+
DeviceType(
|
|
169
|
+
"xv6", "xv6 Machine", Category.MACHINES, "host", Accent.RED,
|
|
170
|
+
"A real teaching kernel: xv6 (MIT 6.1810) running on QEMU-RISC-V. Not a container — a "
|
|
171
|
+
"genuine OS you can watch and steer. Double-click it to open the Machine Lab: observe the "
|
|
172
|
+
"scheduler, process table, CPU registers, memory and kernel stack live, and slow the "
|
|
173
|
+
"time-slice to watch context switches. Runs standalone; xv6 has no networking, so instead "
|
|
174
|
+
"of network links you attach peripherals — a Terminal and a Storage Volume.",
|
|
175
|
+
backend_kind="xv6",
|
|
176
|
+
# vCPU count is the Size tier (S/M/L/XL -> -smp harts, capped at 2); no separate CPUs
|
|
177
|
+
# property (it was vestigial and contradicted the Size).
|
|
178
|
+
default_properties={"Name": "", "Timeslice": "1"},
|
|
179
|
+
property_choices={"Timeslice": ("1", "5", "10", "100")},
|
|
180
|
+
is_cloud=False, # stated, not inferred from the palette section
|
|
181
|
+
),
|
|
182
|
+
# --- xv6 peripherals (software devices attached to the xv6 Machine) -------
|
|
183
|
+
DeviceType(
|
|
184
|
+
"terminal", "Terminal", Category.XV6, "dashboard", Accent.RED,
|
|
185
|
+
"A console for an xv6 Machine — one shell terminal (a screen and keyboard in one, like a "
|
|
186
|
+
"real tty). Connect it to an xv6 Machine and double-click to open it: type xv6 commands "
|
|
187
|
+
"(ls, cat, echo, spin 10 &, …) and watch their output inline. Up-arrow recalls history; "
|
|
188
|
+
"`help` lists what you can run.",
|
|
189
|
+
default_properties={"Name": ""},
|
|
190
|
+
max_links=1,
|
|
191
|
+
),
|
|
192
|
+
DeviceType(
|
|
193
|
+
"storage_volume", "Storage Volume", Category.XV6, "database", Accent.RED,
|
|
194
|
+
"The xv6 disk. Connect it to an xv6 Machine and double-click to open the Storage view — "
|
|
195
|
+
"the on-disk layout, inodes, buffer cache and write-ahead log. (xv6 has a single custom "
|
|
196
|
+
"file system; alternate file systems are an advanced student project.)",
|
|
197
|
+
default_properties={"Name": "", "File system": "xv6fs"},
|
|
198
|
+
property_choices={"File system": ("xv6fs",)},
|
|
199
|
+
max_links=1,
|
|
200
|
+
),
|
|
201
|
+
DeviceType(
|
|
202
|
+
"firewall", "Firewall", Category.NETWORKING, "firewall", Accent.BLUE,
|
|
203
|
+
"Packet-filtering firewall node.",
|
|
204
|
+
backend_kind="vr",
|
|
205
|
+
default_properties={"Name": "", "Policy": "default-deny"},
|
|
206
|
+
),
|
|
207
|
+
DeviceType(
|
|
208
|
+
"vnf", "VNF (Service Function)", Category.NETWORKING, "controller", Accent.TEAL,
|
|
209
|
+
"A Virtualized Network Function: a container that runs a network function (firewall, "
|
|
210
|
+
"IDS, cache, shaper) and is inserted INLINE in the forwarding path — wire it between "
|
|
211
|
+
"two elements and traffic flows through it. Pick the function in 'Kind'; give its "
|
|
212
|
+
"config in 'Rules' (e.g. firewall: 'deny 10.0.3.0/24'; block: '10.0.3.5'). Chain "
|
|
213
|
+
"several in series (host → firewall → IDS → NAT) for a Service Function Chain (SFC).",
|
|
214
|
+
default_properties={"Name": "", "Kind": "firewall", "Rules": "deny 10.0.3.0/24"},
|
|
215
|
+
property_choices={"Kind": ("firewall", "block", "ids", "cache", "shaper")},
|
|
216
|
+
),
|
|
217
|
+
DeviceType(
|
|
218
|
+
"wap", "Access Point", Category.NETWORKING, "wifi", Accent.GREEN,
|
|
219
|
+
"Wireless access point / mobile gateway.",
|
|
220
|
+
default_properties={"Name": "", "SSID": "gini"},
|
|
221
|
+
),
|
|
222
|
+
DeviceType(
|
|
223
|
+
"cloud", "Internet", Category.EXTERNAL, "cloud", Accent.SLATE,
|
|
224
|
+
"External network / the Internet.",
|
|
225
|
+
# DNS lives HERE rather than on the things that use it, because name resolution
|
|
226
|
+
# is a property of "the outside world is reachable" — which is precisely what
|
|
227
|
+
# this element represents. Remove the element and DNS goes with it, which is the
|
|
228
|
+
# honest behaviour: there is nothing left to resolve names against.
|
|
229
|
+
# Handed to real devices on a GINI32 board's hotspot via DHCP; editable because
|
|
230
|
+
# some campus networks block public resolvers.
|
|
231
|
+
default_properties={"Name": "Internet", "DNS": "8.8.8.8"},
|
|
232
|
+
),
|
|
233
|
+
# A real ESP32 board (GINI32) running the gBridge firmware: the one element on
|
|
234
|
+
# the palette that is not emulated at all. It stands for a physical radio on
|
|
235
|
+
# your desk that carries real devices — a phone, a Raspberry Pi, a sensor —
|
|
236
|
+
# into the drawn topology. The board finds the lab through the `gbridge` relay
|
|
237
|
+
# and is handed its fabric address from here, so the canvas stays the source of
|
|
238
|
+
# truth. BoardID must match the id flashed into the board (serial: `set id`).
|
|
239
|
+
DeviceType(
|
|
240
|
+
"gini32", "GINI32 Board", Category.EXTERNAL, "gini32", Accent.GREEN,
|
|
241
|
+
"A real ESP32 gateway board: carries physical devices into the emulated "
|
|
242
|
+
"topology over Wi-Fi (Ethernet-in-UDP via the gbridge relay).",
|
|
243
|
+
# BoardID names a PHYSICAL object, so it is never auto-generated: it is the
|
|
244
|
+
# sticker on the board (`gini32 provision --id gini-5`). Empty by default so
|
|
245
|
+
# two boards cannot silently collide on a shared default — an unset id is a
|
|
246
|
+
# visible error rather than one board vanishing from the relay's table.
|
|
247
|
+
# Everything else is per-RUN and assigned by the canvas: blank PhysicalSubnet
|
|
248
|
+
# and ApSSID mean "allocate me one". Channel is REPORTED, not set — in APSTA
|
|
249
|
+
# the hotspot is forced onto the uplink's channel.
|
|
250
|
+
default_properties={"Name": "", "BoardID": "", "Mode": "routed",
|
|
251
|
+
"ApSSID": "", "ApPassword": "gini12345",
|
|
252
|
+
"PhysicalSubnet": "", "Channel": ""},
|
|
253
|
+
property_choices={"Mode": ("routed", "nat")},
|
|
254
|
+
readonly_properties=("Channel",),
|
|
255
|
+
max_links=1,
|
|
256
|
+
is_cloud=False,
|
|
257
|
+
),
|
|
258
|
+
|
|
259
|
+
# ---- Software-defined networking ----------------------------------------
|
|
260
|
+
DeviceType(
|
|
261
|
+
"ovs", "OpenVSwitch", Category.SDN, "ovs", Accent.TEAL,
|
|
262
|
+
"Open vSwitch programmable bridge.",
|
|
263
|
+
backend_kind="vs",
|
|
264
|
+
default_properties={"Name": "", "Datapath": "", "Protocol": "OpenFlow13"},
|
|
265
|
+
),
|
|
266
|
+
DeviceType(
|
|
267
|
+
"controller", "OpenFlow Controller", Category.SDN, "controller", Accent.TEAL,
|
|
268
|
+
"SDN controller managing OpenFlow switches.",
|
|
269
|
+
default_properties={"Name": "", "Port": "6633",
|
|
270
|
+
"App": "gini.samples.switch"},
|
|
271
|
+
# the POX app that gives the switch its personality. The gini.samples.*
|
|
272
|
+
# apps are GINI's own (they clear the Flow Switch's match-all -> NORMAL
|
|
273
|
+
# default first, so the controller actually sees packet-ins); the
|
|
274
|
+
# forwarding.* / misc.* apps are stock POX. Runs in the controller container.
|
|
275
|
+
property_choices={"App": ("gini.samples.switch", "gini.samples.packet_loss",
|
|
276
|
+
"gini.samples.port_knock", "gini.samples.l4_lb",
|
|
277
|
+
"gini.samples.ids", "gini.samples.redirect",
|
|
278
|
+
"forwarding.l2_learning", "forwarding.hub",
|
|
279
|
+
"misc.of_tutorial")},
|
|
280
|
+
),
|
|
281
|
+
|
|
282
|
+
# ---- Containers & Kubernetes --------------------------------------------
|
|
283
|
+
DeviceType(
|
|
284
|
+
"container", "Container", Category.MACHINES, "container", Accent.CYAN,
|
|
285
|
+
"A single Docker/OCI container.",
|
|
286
|
+
backend_kind="vm",
|
|
287
|
+
default_properties={"Name": "", "Image": "alpine:latest", "Command": ""},
|
|
288
|
+
is_cloud=True, # stated, not inferred from the palette section
|
|
289
|
+
),
|
|
290
|
+
DeviceType(
|
|
291
|
+
"pod", "Pod", Category.CONTAINERS, "pod", Accent.CYAN,
|
|
292
|
+
"A Kubernetes workload — a Deployment of an image, run as N pod replicas. "
|
|
293
|
+
"Connect it to a K8s Cluster to deploy it there.",
|
|
294
|
+
is_container=True,
|
|
295
|
+
default_properties={"Name": "", "Image": "nginxdemos/hello:latest",
|
|
296
|
+
"Replicas": "2", "Port": "80"},
|
|
297
|
+
),
|
|
298
|
+
DeviceType(
|
|
299
|
+
"k8s_node", "K8s Node", Category.CONTAINERS, "k8s_node", Accent.CYAN,
|
|
300
|
+
"A Kubernetes worker node (a k3s agent). v1 clusters are single-node; nodes are "
|
|
301
|
+
"shown for the model — multi-node scheduling is a follow-on.",
|
|
302
|
+
default_properties={"Name": "", "Role": "worker"},
|
|
303
|
+
# hidden from the palette for v1: a single-node cluster makes a separate Node
|
|
304
|
+
# element confusing next to 'K8s Cluster'. The type is retained so older saved
|
|
305
|
+
# projects still load and the compiler role keeps working. Re-expose with
|
|
306
|
+
# multi-node scheduling.
|
|
307
|
+
hidden=True,
|
|
308
|
+
),
|
|
309
|
+
DeviceType(
|
|
310
|
+
"k8s_cluster", "K8s Cluster", Category.CONTAINERS, "k8s_cluster", Accent.CYAN,
|
|
311
|
+
"A real Kubernetes cluster (k3s in a container). Connect Pods to deploy them; add "
|
|
312
|
+
"a Pod Autoscaler (HPA) on a Pod to scale its replicas.",
|
|
313
|
+
is_container=True,
|
|
314
|
+
default_properties={"Name": "", "Version": "1.30"},
|
|
315
|
+
),
|
|
316
|
+
DeviceType(
|
|
317
|
+
"registry", "Container Registry", Category.CONTAINERS, "registry", Accent.CYAN,
|
|
318
|
+
"Image registry serving container images.",
|
|
319
|
+
default_properties={"Name": "", "Endpoint": ""},
|
|
320
|
+
),
|
|
321
|
+
|
|
322
|
+
# ---- Cloud networking (VPC / SG / LB) -----------------------------------
|
|
323
|
+
DeviceType(
|
|
324
|
+
"vpc", "VPC", Category.CLOUD_NETWORK, "vpc", Accent.INDIGO,
|
|
325
|
+
"Virtual private cloud — an isolated cloud network.",
|
|
326
|
+
is_container=True,
|
|
327
|
+
default_properties={"Name": "", "CIDR": "10.0.0.0/16", "Region": "us-east-1"},
|
|
328
|
+
),
|
|
329
|
+
DeviceType(
|
|
330
|
+
"cloud_subnet", "Cloud Subnet", Category.CLOUD_NETWORK, "cloud_subnet", Accent.INDIGO,
|
|
331
|
+
"A subnet inside a VPC. Drop elements in it. A *public* subnet's members reach the "
|
|
332
|
+
"internet (and their consoles are reachable); a *private* subnet's members stay "
|
|
333
|
+
"inside the VPC only — reachable by other VPC members, but with no internet.",
|
|
334
|
+
is_container=True,
|
|
335
|
+
default_properties={"Name": "", "CIDR": "10.0.1.0/24", "Tier": "private"},
|
|
336
|
+
property_choices={"Tier": ("private", "public")},
|
|
337
|
+
),
|
|
338
|
+
DeviceType(
|
|
339
|
+
"security_group", "Security Group", Category.CLOUD_NETWORK, "security_group", Accent.INDIGO,
|
|
340
|
+
"A stateful, default-deny firewall. Connect it to the workloads/datastores it "
|
|
341
|
+
"protects, then list inbound rules in Ingress (one per line): '<port> from <source>', "
|
|
342
|
+
"where source is a CIDR, 'anywhere', or another Security Group's name — e.g. "
|
|
343
|
+
"'80 from anywhere' or '5432 from app-sg'. Only listed ports open; outbound is allowed.",
|
|
344
|
+
default_properties={"Name": "", "Ingress": "80 from anywhere", "Egress": "allow-all"},
|
|
345
|
+
),
|
|
346
|
+
DeviceType(
|
|
347
|
+
"gateway", "Gateway", Category.CLOUD_NETWORK, "gateway", Accent.INDIGO,
|
|
348
|
+
"Internet / NAT gateway connecting a VPC to the outside.",
|
|
349
|
+
default_properties={"Name": "", "Type": "internet"},
|
|
350
|
+
),
|
|
351
|
+
DeviceType(
|
|
352
|
+
"load_balancer", "Load Balancer", Category.CLOUD_NETWORK, "load_balancer", Accent.INDIGO,
|
|
353
|
+
"Distributes traffic across backend targets.",
|
|
354
|
+
default_properties={"Name": "", "Scheme": "round-robin", "Listener": "80"},
|
|
355
|
+
property_choices={"Scheme": ("round-robin", "least_conn", "ip_hash")},
|
|
356
|
+
),
|
|
357
|
+
|
|
358
|
+
# ---- Compute & autoscaling ----------------------------------------------
|
|
359
|
+
DeviceType(
|
|
360
|
+
"instance", "Instance", Category.MACHINES, "instance", Accent.PURPLE,
|
|
361
|
+
"Cloud compute instance (VM).",
|
|
362
|
+
backend_kind="vm",
|
|
363
|
+
default_properties={"Name": "", "Type": "t3.micro", "Image": "ubuntu-22.04"},
|
|
364
|
+
is_cloud=True, # stated, not inferred from the palette section
|
|
365
|
+
),
|
|
366
|
+
DeviceType(
|
|
367
|
+
"kinstance", "Kata Instance (VM)", Category.MACHINES, "instance", Accent.PURPLE,
|
|
368
|
+
"A VM-isolated workload (Kata Containers): your container runs inside a lightweight "
|
|
369
|
+
"microVM with its own guest kernel — stronger isolation than a normal container, at "
|
|
370
|
+
"the cost of boot time, memory and I/O overhead. Use it to compare VM-vs-container "
|
|
371
|
+
"trade-offs. Needs a Kata-enabled GINI server backend (Settings - Backend).",
|
|
372
|
+
backend_kind="vm",
|
|
373
|
+
default_properties={"Name": "", "Image": "ubuntu:22.04", "Command": ""},
|
|
374
|
+
is_cloud=True, # stated, not inferred from the palette section
|
|
375
|
+
),
|
|
376
|
+
DeviceType(
|
|
377
|
+
"instance_group", "Pod Autoscaler (HPA)", Category.CONTAINERS, "instance_group", Accent.CYAN,
|
|
378
|
+
"A Kubernetes Horizontal Pod Autoscaler. Connect it to a Pod to scale that "
|
|
379
|
+
"Deployment's replicas between Min and Max to hold a target CPU%. (This is the "
|
|
380
|
+
"HPA — different from the Cluster Autoscaler, which adds Nodes.)",
|
|
381
|
+
is_container=True,
|
|
382
|
+
default_properties={"Name": "", "Min": "1", "Max": "5", "TargetCPU": "60"},
|
|
383
|
+
),
|
|
384
|
+
DeviceType(
|
|
385
|
+
"region", "Region / Zone", Category.CLOUD_NETWORK, "region", Accent.PURPLE,
|
|
386
|
+
"A cloud region or availability zone boundary.",
|
|
387
|
+
is_container=True,
|
|
388
|
+
default_properties={"Name": "us-east-1", "Zones": "a,b,c"},
|
|
389
|
+
),
|
|
390
|
+
|
|
391
|
+
# ---- Storage & data ------------------------------------------------------
|
|
392
|
+
DeviceType(
|
|
393
|
+
"object_store", "Object Storage", Category.STORAGE, "object_store", Accent.AMBER,
|
|
394
|
+
"Object storage bucket (S3-style).",
|
|
395
|
+
default_properties={"Name": "", "Versioning": "off"},
|
|
396
|
+
),
|
|
397
|
+
DeviceType(
|
|
398
|
+
"block_volume", "Block Volume", Category.STORAGE, "block_volume", Accent.AMBER,
|
|
399
|
+
"Attachable block storage volume.",
|
|
400
|
+
default_properties={"Name": "", "SizeGB": "20"},
|
|
401
|
+
),
|
|
402
|
+
DeviceType(
|
|
403
|
+
"database", "Managed Database", Category.STORAGE, "database", Accent.AMBER,
|
|
404
|
+
"Managed relational / NoSQL database.",
|
|
405
|
+
default_properties={"Name": "", "Engine": "postgres", "Replicas": "0"},
|
|
406
|
+
),
|
|
407
|
+
|
|
408
|
+
# ---- Serverless ----------------------------------------------------------
|
|
409
|
+
DeviceType(
|
|
410
|
+
"function", "Function", Category.SERVERLESS, "function", Accent.PINK,
|
|
411
|
+
"A serverless function (FaaS). Runs your handler on demand in a shared runtime — "
|
|
412
|
+
"no server to manage, scales per request, billed per invocation. Reachable over "
|
|
413
|
+
"HTTP at /<name>; front it with an API Gateway and drive it with a Load Generator.",
|
|
414
|
+
default_properties={"Name": "", "Runtime": "python3.12", "Handler": "echo",
|
|
415
|
+
"Code": ""},
|
|
416
|
+
property_choices={"Handler": ("echo", "transform", "slow", "fail", "counter",
|
|
417
|
+
"custom")},
|
|
418
|
+
),
|
|
419
|
+
DeviceType(
|
|
420
|
+
"api_gateway", "API Gateway", Category.SERVERLESS, "api_gateway", Accent.PINK,
|
|
421
|
+
"The front door for your functions — a real Traefik edge router that maps a URL "
|
|
422
|
+
"path to each connected Function (/<name>). Connect it to Functions and it routes "
|
|
423
|
+
"automatically; open its dashboard to watch requests.",
|
|
424
|
+
default_properties={"Name": "", "Stage": "prod"},
|
|
425
|
+
),
|
|
426
|
+
DeviceType(
|
|
427
|
+
"queue", "Message Queue", Category.STREAMING, "queue", Accent.PINK,
|
|
428
|
+
"Managed message queue / event bus.",
|
|
429
|
+
default_properties={"Name": "", "Type": "fifo"},
|
|
430
|
+
),
|
|
431
|
+
|
|
432
|
+
# ---- Edge & traffic (proxies / web) -------------------------------------
|
|
433
|
+
DeviceType(
|
|
434
|
+
"proxy", "Reverse Proxy", Category.CLOUD_NETWORK, "proxy", Accent.INDIGO,
|
|
435
|
+
"Traefik reverse proxy / edge router with a live dashboard.",
|
|
436
|
+
default_properties={"Name": "", "Dashboard": "on"},
|
|
437
|
+
),
|
|
438
|
+
DeviceType(
|
|
439
|
+
"web_app", "Web App", Category.CONTAINERS, "web_app", Accent.PURPLE,
|
|
440
|
+
"A small demo web backend that reports which instance served the request.",
|
|
441
|
+
default_properties={"Name": ""},
|
|
442
|
+
),
|
|
443
|
+
|
|
444
|
+
# ---- Streaming & messaging ----------------------------------------------
|
|
445
|
+
DeviceType(
|
|
446
|
+
"stream", "Event Stream", Category.STREAMING, "stream", Accent.CYAN,
|
|
447
|
+
"Kafka-compatible event streaming log (Redpanda).",
|
|
448
|
+
default_properties={"Name": "", "Partitions": "1"},
|
|
449
|
+
),
|
|
450
|
+
DeviceType(
|
|
451
|
+
"messaging", "Pub/Sub", Category.STREAMING, "messaging", Accent.CYAN,
|
|
452
|
+
"Lightweight NATS publish/subscribe messaging server.",
|
|
453
|
+
default_properties={"Name": ""},
|
|
454
|
+
),
|
|
455
|
+
|
|
456
|
+
# ---- Storage: cache & NoSQL ---------------------------------------------
|
|
457
|
+
DeviceType(
|
|
458
|
+
"cache", "Cache", Category.STORAGE, "cache", Accent.AMBER,
|
|
459
|
+
"Redis in-memory key/value cache and store.",
|
|
460
|
+
default_properties={"Name": ""},
|
|
461
|
+
),
|
|
462
|
+
DeviceType(
|
|
463
|
+
"nosql", "NoSQL Database", Category.STORAGE, "nosql", Accent.AMBER,
|
|
464
|
+
"MongoDB document database.",
|
|
465
|
+
default_properties={"Name": "", "Database": "app"},
|
|
466
|
+
),
|
|
467
|
+
|
|
468
|
+
# ---- Observability ------------------------------------------------------
|
|
469
|
+
# Observability elements are SINKS (they observe outputs) — they live in the Sinks section now,
|
|
470
|
+
# but unlike the rider sinks they are real services with their own container (rider=False).
|
|
471
|
+
DeviceType(
|
|
472
|
+
"metrics", "Metrics", Category.SINK, "metrics", Accent.ORANGE,
|
|
473
|
+
"Prometheus metrics collection + PromQL query UI. A Sink: it scrapes and stores the numbers "
|
|
474
|
+
"your services emit. Its own container (not a rider) — wire it to the targets it scrapes.",
|
|
475
|
+
role="sink", is_cloud=True,
|
|
476
|
+
default_properties={"Name": ""},
|
|
477
|
+
),
|
|
478
|
+
DeviceType(
|
|
479
|
+
"dashboard", "Dashboards", Category.SINK, "dashboard", Accent.ORANGE,
|
|
480
|
+
"Grafana dashboards over metrics and logs. A Sink that visualizes what Metrics collected — "
|
|
481
|
+
"wire it to a Metrics source. Its own container (not a rider).",
|
|
482
|
+
role="sink", is_cloud=True,
|
|
483
|
+
default_properties={"Name": ""},
|
|
484
|
+
),
|
|
485
|
+
DeviceType(
|
|
486
|
+
"tracing", "Tracing", Category.SINK, "tracing", Accent.ORANGE,
|
|
487
|
+
"Jaeger distributed tracing (request timelines across services). A Sink: it observes request "
|
|
488
|
+
"flow across services. Its own container (not a rider).",
|
|
489
|
+
role="sink", is_cloud=True,
|
|
490
|
+
default_properties={"Name": ""},
|
|
491
|
+
),
|
|
492
|
+
|
|
493
|
+
# ---- Load generation (a heavyweight Source: its own container) ----------
|
|
494
|
+
DeviceType(
|
|
495
|
+
"load_generator", "Load Generator", Category.SOURCE, "load_generator", Accent.RED,
|
|
496
|
+
"Fortio HTTP/gRPC load generator with a web UI to launch experiments. A Source that injects "
|
|
497
|
+
"sustained traffic — heavier than the HTTP Probe rider, with its own container. Wire it to "
|
|
498
|
+
"the backend/gateway you want to load.",
|
|
499
|
+
role="source", is_cloud=True,
|
|
500
|
+
default_properties={"Name": "", "QPS": "100", "Connections": "8"},
|
|
501
|
+
),
|
|
502
|
+
|
|
503
|
+
# ---- Sources (stimulus riders — run ON a donor, no container of their own) ----
|
|
504
|
+
DeviceType(
|
|
505
|
+
"ping_probe", "Ping Probe", Category.SOURCE, "load_generator", Accent.RED,
|
|
506
|
+
"A ping (ICMP) stimulus. Attach it to a Machine or Router — its donor — and give it a "
|
|
507
|
+
"Target; double-click to start pinging INSIDE the donor (live RTT / loss in its Live tab), "
|
|
508
|
+
"double-click again to stop. It rides the donor (dotted attach edge, not a cable). "
|
|
509
|
+
"Count 0 = ping continuously until stopped; Count N = send N and stop.",
|
|
510
|
+
rider=True, role="source", driver="docker-exec",
|
|
511
|
+
attaches_to=("host", "router", "instance", "container"),
|
|
512
|
+
default_properties={"Name": "", "Target": "", "Count": "0"},
|
|
513
|
+
),
|
|
514
|
+
DeviceType(
|
|
515
|
+
"http_probe", "HTTP Probe", Category.SOURCE, "load_generator", Accent.RED,
|
|
516
|
+
"An HTTP request stimulus (curl). Attach it to a Machine / Router donor and give it a "
|
|
517
|
+
"Target and Path; double-click to start requesting from inside the donor (live 2xx + "
|
|
518
|
+
"latency), double-click again to stop. Count 0 = request continuously; Count N = do N. "
|
|
519
|
+
"Rides the donor — no container of its own. For heavy sustained load use the Load Generator.",
|
|
520
|
+
rider=True, role="source", driver="docker-exec",
|
|
521
|
+
attaches_to=("host", "router", "instance", "container"),
|
|
522
|
+
default_properties={"Name": "", "Target": "", "Path": "/", "Count": "0"},
|
|
523
|
+
),
|
|
524
|
+
|
|
525
|
+
# ---- Sinks (observer riders — run ON a donor, render its output) ----
|
|
526
|
+
DeviceType(
|
|
527
|
+
"packet_view", "Packet View", Category.SINK, "tracing", Accent.ORANGE,
|
|
528
|
+
"A live packet capture (tcpdump). Attach it to a Machine or Router — its donor; double-click "
|
|
529
|
+
"to start sniffing the donor's interface (live packet stream in its Live tab), double-click "
|
|
530
|
+
"again to stop. Count 0 = capture until stopped; Count N = stop after N packets. Captures "
|
|
531
|
+
"the GINI overlay (gini0) by default — set Interface to eth0/any for the Docker bridge. "
|
|
532
|
+
"Rides the donor — no container of its own.",
|
|
533
|
+
rider=True, role="sink", driver="docker-exec",
|
|
534
|
+
attaches_to=("host", "router", "ovs", "instance", "container"),
|
|
535
|
+
default_properties={"Name": "", "Interface": "gini0", "Filter": "", "Count": "0"},
|
|
536
|
+
),
|
|
537
|
+
DeviceType(
|
|
538
|
+
"dns_probe", "DNS Probe", Category.SOURCE, "load_generator", Accent.RED,
|
|
539
|
+
"A name-resolution stimulus. Attach it to a Machine/Router donor, put the hostname to "
|
|
540
|
+
"resolve in Target (e.g. 'M2' or 'web'), and double-click to start; it resolves the name "
|
|
541
|
+
"from inside the donor — over the DRAWN network (gini0), because GINI writes peer names into "
|
|
542
|
+
"/etc/hosts — and reports the answer + resolve rate. Count 0 = query continuously; Count N = "
|
|
543
|
+
"do N. Rides the donor.",
|
|
544
|
+
rider=True, role="source", driver="docker-exec",
|
|
545
|
+
attaches_to=("host", "router", "instance", "container"),
|
|
546
|
+
default_properties={"Name": "", "Target": "", "Count": "0"},
|
|
547
|
+
),
|
|
548
|
+
DeviceType(
|
|
549
|
+
"traceroute_probe", "Traceroute", Category.SOURCE, "load_generator", Accent.RED,
|
|
550
|
+
"A path-discovery stimulus (traceroute). Attach it to a Machine/Router donor and give it a "
|
|
551
|
+
"Target; it traces the hops to the target from inside the donor and reports the hop count. "
|
|
552
|
+
"Rides the donor — a good pair with a Packet View to watch each hop.",
|
|
553
|
+
rider=True, role="source", driver="docker-exec",
|
|
554
|
+
attaches_to=("host", "router", "instance", "container"),
|
|
555
|
+
default_properties={"Name": "", "Target": ""},
|
|
556
|
+
),
|
|
557
|
+
DeviceType(
|
|
558
|
+
"iperf_client", "iPerf Client", Category.SOURCE, "load_generator", Accent.RED,
|
|
559
|
+
"A throughput generator (iperf3 -c). Attach it to a Machine donor and point Target at a "
|
|
560
|
+
"donor running an iPerf Server; it drives traffic and reports the measured bandwidth. "
|
|
561
|
+
"Seconds sets the test length; Bitrate caps the rate (e.g. 100M) so it doesn't saturate the "
|
|
562
|
+
"software fabric — set '0' for unlimited. Rides the donor.",
|
|
563
|
+
rider=True, role="source", driver="docker-exec",
|
|
564
|
+
attaches_to=("host", "instance", "container"),
|
|
565
|
+
default_properties={"Name": "", "Target": "", "Seconds": "10", "Bitrate": "100M"},
|
|
566
|
+
),
|
|
567
|
+
DeviceType(
|
|
568
|
+
"iperf_server", "iPerf Server", Category.SINK, "tracing", Accent.ORANGE,
|
|
569
|
+
"A throughput endpoint (iperf3 -s). Attach it to a Machine donor; it listens for an iPerf "
|
|
570
|
+
"Client and reports the bandwidth it receives. Runs until stopped. Rides the donor.",
|
|
571
|
+
rider=True, role="sink", driver="docker-exec",
|
|
572
|
+
attaches_to=("host", "instance", "container"),
|
|
573
|
+
default_properties={"Name": ""},
|
|
574
|
+
),
|
|
575
|
+
DeviceType(
|
|
576
|
+
"iface_stats", "Interface Stats", Category.SINK, "metrics", Accent.ORANGE,
|
|
577
|
+
"A live interface counter (reads /proc/net/dev). Attach it to a Machine/Router donor; it "
|
|
578
|
+
"streams rx/tx packet and byte counts so you can watch traffic volume. Rides the donor.",
|
|
579
|
+
rider=True, role="sink", driver="docker-exec",
|
|
580
|
+
attaches_to=("host", "router", "instance", "container"),
|
|
581
|
+
default_properties={"Name": ""},
|
|
582
|
+
),
|
|
583
|
+
|
|
584
|
+
# ---- xv6 Sources (OS course) — ride the xv6 Machine over its console -----
|
|
585
|
+
DeviceType(
|
|
586
|
+
"xv6_shell", "Shell Probe", Category.SOURCE, "load_generator", Accent.RED,
|
|
587
|
+
"Launch a custom command into an xv6 Machine. Attach it to an xv6 Machine, put the command "
|
|
588
|
+
"in Command (e.g. 'ls', 'echo hi', 'cat README'); it types the command into the kernel's "
|
|
589
|
+
"console and streams the output back. The OS-course counterpart of the HTTP Probe.",
|
|
590
|
+
rider=True, role="source", driver="qemu-serial",
|
|
591
|
+
attaches_to=("xv6",),
|
|
592
|
+
default_properties={"Name": "", "Command": ""},
|
|
593
|
+
),
|
|
594
|
+
DeviceType(
|
|
595
|
+
"xv6_workload", "Workload", Category.SOURCE, "load_generator", Accent.RED,
|
|
596
|
+
"Spawn a program on an xv6 Machine to drive the scheduler (the OS-course load generator). "
|
|
597
|
+
"Attach it to an xv6 Machine; set Program (e.g. 'spin', 'forktest', 'usertests') and Args. "
|
|
598
|
+
"Background = run with '&' so several can compete. Watch the effect in the Machine Lab.",
|
|
599
|
+
rider=True, role="source", driver="qemu-serial",
|
|
600
|
+
attaches_to=("xv6",),
|
|
601
|
+
default_properties={"Name": "", "Program": "spin", "Args": "", "Background": "true"},
|
|
602
|
+
property_choices={"Background": ("true", "false")},
|
|
603
|
+
),
|
|
604
|
+
|
|
605
|
+
# ---- OS Zoo — real historical OSes under emulation, embedded via noVNC ------
|
|
606
|
+
DeviceType(
|
|
607
|
+
"freedos", "FreeDOS", Category.OS_ZOO, "host", Accent.ORANGE,
|
|
608
|
+
"The open, still-maintained MS-DOS-compatible OS — the command-line PC of the DOS era. "
|
|
609
|
+
"Boots out of the box; double-click to use it in an embedded screen.",
|
|
610
|
+
default_properties={"Name": "", "Persist": "false"},
|
|
611
|
+
property_choices={"Persist": ("false", "true")},
|
|
612
|
+
),
|
|
613
|
+
DeviceType(
|
|
614
|
+
"kolibri", "KolibriOS", Category.OS_ZOO, "host", Accent.ORANGE,
|
|
615
|
+
"A tiny GUI operating system written entirely in assembly — the whole thing boots from a "
|
|
616
|
+
"single 1.44 MB floppy to a full graphical desktop in seconds, even under emulation. The "
|
|
617
|
+
"fast one to reach for.",
|
|
618
|
+
default_properties={"Name": "", "Persist": "false"},
|
|
619
|
+
property_choices={"Persist": ("false", "true")},
|
|
620
|
+
),
|
|
621
|
+
DeviceType(
|
|
622
|
+
"menuet", "MenuetOS", Category.OS_ZOO, "host", Accent.ORANGE,
|
|
623
|
+
"The assembly GUI OS that KolibriOS forked from — a complete graphical desktop with apps "
|
|
624
|
+
"on a single 1.44 MB floppy (the 32-bit build is open source). Boots in seconds under "
|
|
625
|
+
"emulation, just like KolibriOS.",
|
|
626
|
+
default_properties={"Name": "", "Persist": "false"},
|
|
627
|
+
property_choices={"Persist": ("false", "true")},
|
|
628
|
+
),
|
|
629
|
+
DeviceType(
|
|
630
|
+
"msdos", "MS-DOS 6.22", Category.OS_ZOO, "host", Accent.PURPLE,
|
|
631
|
+
"The real Microsoft MS-DOS 6.22, booted from a disk image under QEMU (so it's the genuine "
|
|
632
|
+
"article — `VER` reports MS-DOS, not a clone). Drag it on and Run — GINI downloads a public "
|
|
633
|
+
"pre-installed MS-DOS 6.22 disk on first boot (GINI ships nothing proprietary) and boots to "
|
|
634
|
+
"the C:\\> prompt. Put it next to FreeDOS to compare the original with the open re-creation. "
|
|
635
|
+
"The Image URL is pre-filled and editable. Ephemeral unless Persist is on.",
|
|
636
|
+
default_properties={
|
|
637
|
+
"Name": "", "Emulator": "qemu", "Arch": "x86",
|
|
638
|
+
"Image": "https://archive.org/download/pre-installed-ms-dos-622-disk-image/"
|
|
639
|
+
"Installed%20MS-DOS%206.22.img",
|
|
640
|
+
"Persist": "false"},
|
|
641
|
+
property_choices={"Persist": ("false", "true")},
|
|
642
|
+
),
|
|
643
|
+
DeviceType(
|
|
644
|
+
"mac7", "Mac System 7", Category.OS_ZOO, "host", Accent.PURPLE,
|
|
645
|
+
"Classic Macintosh System 7 on an emulated 68k Mac (Basilisk II). Just drag it on and Run — "
|
|
646
|
+
"GINI downloads a Quadra ROM and a bootable System 7.5.3 disk from a public archive on first "
|
|
647
|
+
"boot (GINI ships nothing proprietary) and boots to the Mac desktop. The Image/Rom URLs are "
|
|
648
|
+
"pre-filled; change them to point at files you prefer. Ephemeral unless Persist is on.",
|
|
649
|
+
default_properties={
|
|
650
|
+
"Name": "", "Emulator": "basilisk", "Arch": "68k",
|
|
651
|
+
"Image": "https://archive.org/download/system-753/System753.dsk",
|
|
652
|
+
"Rom": "https://archive.org/download/mac_rom_archive_-_as_of_8-19-2011/"
|
|
653
|
+
"mac_rom_archive_-_as_of_8-19-2011.zip/"
|
|
654
|
+
"F1ACAD13%20-%20Quadra%20610%2C650%2Cmaybe%20800.ROM",
|
|
655
|
+
"Persist": "false"},
|
|
656
|
+
property_choices={"Persist": ("false", "true")},
|
|
657
|
+
),
|
|
658
|
+
DeviceType(
|
|
659
|
+
"win31", "Windows 3.11", Category.OS_ZOO, "host", Accent.PURPLE,
|
|
660
|
+
"Windows for Workgroups 3.11 under DOSBox — the fast vintage-Windows path. Just drag it on "
|
|
661
|
+
"and Run — GINI downloads a public, pre-installed Windows 3.11, mounts it as C:, and starts "
|
|
662
|
+
"Windows (GINI ships nothing proprietary). The Image URL is pre-filled; change it to point "
|
|
663
|
+
"at a folder or zip you prefer. Ephemeral unless Persist is on.",
|
|
664
|
+
default_properties={
|
|
665
|
+
"Name": "", "Emulator": "dosbox", "Arch": "x86",
|
|
666
|
+
"Image": "https://archive.org/download/win3_stock/win311-stock.zip",
|
|
667
|
+
"Persist": "false"},
|
|
668
|
+
property_choices={"Persist": ("false", "true")},
|
|
669
|
+
),
|
|
670
|
+
DeviceType(
|
|
671
|
+
"oszoo_byo", "Classic OS (your image)", Category.OS_ZOO, "host", Accent.SLATE,
|
|
672
|
+
"Bring your own OS: run a proprietary classic OS that GINI can't legally ship. Pick the "
|
|
673
|
+
"emulator — QEMU (Windows 95/98, or any bootable x86 disk), DOSBox (DOS and Windows 3.x, "
|
|
674
|
+
"the fast path), or Basilisk II (classic 68k Mac — System 7 / Mac OS 8). Set 'Image' to a "
|
|
675
|
+
"disk image (or folder for DOSBox) you own, and 'Rom' to a Macintosh ROM for Basilisk. "
|
|
676
|
+
"GINI provides the emulator and hosts nothing — you supply the image.",
|
|
677
|
+
default_properties={"Name": "", "Emulator": "qemu", "Image": "", "Rom": "", "Arch": "x86"},
|
|
678
|
+
property_choices={"Emulator": ("qemu", "dosbox", "basilisk"),
|
|
679
|
+
"Arch": ("x86", "x86_64", "68k")},
|
|
680
|
+
),
|
|
681
|
+
]
|
|
682
|
+
|
|
683
|
+
# Lookup table
|
|
684
|
+
REGISTRY: dict[str, DeviceType] = {d.key: d for d in _DEVICES}
|
|
685
|
+
|
|
686
|
+
# Auto-name prefixes per element type (R1, S1, M1, …). Curated to be short and
|
|
687
|
+
# collision-free (e.g. Metrics is PROM, not M, so it never clashes with Machine).
|
|
688
|
+
# Users can override the popular ones in Settings → Naming.
|
|
689
|
+
DEFAULT_PREFIXES: dict[str, str] = {
|
|
690
|
+
# networking
|
|
691
|
+
"router": "R", "switch": "S", "hub": "H", "host": "M", "firewall": "FW",
|
|
692
|
+
"wap": "AP", "cloud": "NET", "vnf": "VNF", "gini32": "GB",
|
|
693
|
+
# sdn
|
|
694
|
+
"ovs": "OVS", "controller": "OFC",
|
|
695
|
+
# compute / containers
|
|
696
|
+
"instance": "I", "container": "CT", "web_app": "WA", "pod": "POD",
|
|
697
|
+
"k8s_node": "KN", "k8s_cluster": "K8S", "registry": "REG",
|
|
698
|
+
"instance_group": "HPA", "region": "RGN",
|
|
699
|
+
# cloud networking
|
|
700
|
+
"vpc": "VPC", "cloud_subnet": "CSUB", "security_group": "SG",
|
|
701
|
+
"gateway": "GW", "load_balancer": "LB", "proxy": "PXY",
|
|
702
|
+
# storage & data
|
|
703
|
+
"object_store": "OBJ", "block_volume": "VOL", "database": "DB",
|
|
704
|
+
"cache": "CA", "nosql": "NDB",
|
|
705
|
+
# streaming & messaging
|
|
706
|
+
"stream": "STR", "messaging": "MSG", "queue": "Q",
|
|
707
|
+
# observability
|
|
708
|
+
"metrics": "PROM", "dashboard": "GRAF", "tracing": "JGR",
|
|
709
|
+
# workload / serverless
|
|
710
|
+
"load_generator": "LG", "function": "FN", "api_gateway": "AGW",
|
|
711
|
+
# sources / sinks (riders)
|
|
712
|
+
"ping_probe": "PING", "http_probe": "HTTP", "packet_view": "PCAP",
|
|
713
|
+
"dns_probe": "DNS", "traceroute_probe": "TRACE", "iperf_client": "IPERFC",
|
|
714
|
+
"iperf_server": "IPERFS", "iface_stats": "IFSTAT",
|
|
715
|
+
"xv6_shell": "SH", "xv6_workload": "WL",
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
|
|
719
|
+
def default_prefix(type_key: str) -> str:
|
|
720
|
+
dt = REGISTRY.get(type_key)
|
|
721
|
+
if type_key in DEFAULT_PREFIXES:
|
|
722
|
+
return DEFAULT_PREFIXES[type_key]
|
|
723
|
+
if dt is not None: # fallback: capitals of the label, e.g. VPC
|
|
724
|
+
return "".join(c for c in dt.label if c.isupper()) or dt.label[:2].upper()
|
|
725
|
+
return "N"
|
|
726
|
+
|
|
727
|
+
|
|
728
|
+
def all_devices() -> list[DeviceType]:
|
|
729
|
+
return list(_DEVICES)
|
|
730
|
+
|
|
731
|
+
|
|
732
|
+
def get(key: str) -> DeviceType:
|
|
733
|
+
return REGISTRY[key]
|
|
734
|
+
|
|
735
|
+
|
|
736
|
+
def by_category() -> dict[Category, list[DeviceType]]:
|
|
737
|
+
"""The palette, section by section. Sections come out in Category order; within a section,
|
|
738
|
+
PALETTE_RANK wins (that's what puts Machines in the isolation ladder — container first, real
|
|
739
|
+
kernel last), and anything unranked keeps its registry order."""
|
|
740
|
+
out: dict[Category, list[DeviceType]] = {c: [] for c in Category}
|
|
741
|
+
for d in _DEVICES:
|
|
742
|
+
if d.hidden: # retained in REGISTRY but kept off the palette
|
|
743
|
+
continue
|
|
744
|
+
out[d.category].append(d)
|
|
745
|
+
for items in out.values():
|
|
746
|
+
items.sort(key=lambda d: PALETTE_RANK.get(d.key, 99)) # stable: unranked keep their order
|
|
747
|
+
return {c: items for c, items in out.items() if items}
|