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,370 @@
|
|
|
1
|
+
"""GINI Cloud Fabric agent — the cloud telemetry & coordination plane.
|
|
2
|
+
|
|
3
|
+
One small container watches every cloud service in the lab. It reads FABRIC_CONFIG (the
|
|
4
|
+
list of services + how to reach them), and on a timer asks each one for its *native*
|
|
5
|
+
application metrics through a per-type adapter — Redis INFO, the RabbitMQ management API,
|
|
6
|
+
Postgres pg_stat, Traefik / NATS / nginx endpoints. It normalizes everything to one
|
|
7
|
+
schema and serves it at ``/metrics.json`` for gBuilder to poll (and ``/metrics`` in
|
|
8
|
+
Prometheus text so Grafana can graph the app metrics too).
|
|
9
|
+
|
|
10
|
+
Stdlib-only at import time (urllib + raw sockets); psycopg2 is imported lazily inside the
|
|
11
|
+
Postgres adapter, so the parse helpers below are safe to unit-test anywhere.
|
|
12
|
+
|
|
13
|
+
FABRIC_CONFIG = {"services":[
|
|
14
|
+
{"name":"redis1","type":"cache","host":"redis1","port":6379,"creds":{}},
|
|
15
|
+
{"name":"rabbit1","type":"queue","host":"rabbit1","port":15672,
|
|
16
|
+
"creds":{"user":"guest","password":"guest"}}, ... ]}
|
|
17
|
+
"""
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
import json
|
|
21
|
+
import os
|
|
22
|
+
import socket
|
|
23
|
+
import time
|
|
24
|
+
import urllib.error
|
|
25
|
+
import urllib.request
|
|
26
|
+
|
|
27
|
+
POLL = float(os.environ.get("FABRIC_POLL", "2.0"))
|
|
28
|
+
PORT = int(os.environ.get("FABRIC_PORT", "9099"))
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
# --------------------------------------------------------------------------- #
|
|
32
|
+
# Pure parse helpers (unit-tested — no network)
|
|
33
|
+
# --------------------------------------------------------------------------- #
|
|
34
|
+
def parse_prometheus(text: str) -> dict[str, float]:
|
|
35
|
+
"""A Prometheus text exposition -> {metric_name: summed_value}. Labels are ignored
|
|
36
|
+
(samples for the same metric name are summed), which is what we want for totals."""
|
|
37
|
+
out: dict[str, float] = {}
|
|
38
|
+
for line in (text or "").splitlines():
|
|
39
|
+
line = line.strip()
|
|
40
|
+
if not line or line.startswith("#"):
|
|
41
|
+
continue
|
|
42
|
+
# name{labels} value OR name value
|
|
43
|
+
name = line.split("{", 1)[0].split(" ", 1)[0]
|
|
44
|
+
val = line.rsplit(" ", 1)[-1]
|
|
45
|
+
try:
|
|
46
|
+
out[name] = out.get(name, 0.0) + float(val)
|
|
47
|
+
except ValueError:
|
|
48
|
+
continue
|
|
49
|
+
return out
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def parse_redis_info(text: str) -> dict[str, str]:
|
|
53
|
+
"""Redis INFO 'key:value' lines -> dict."""
|
|
54
|
+
out: dict[str, str] = {}
|
|
55
|
+
for line in (text or "").splitlines():
|
|
56
|
+
line = line.strip()
|
|
57
|
+
if not line or line.startswith("#") or ":" not in line:
|
|
58
|
+
continue
|
|
59
|
+
k, _, v = line.partition(":")
|
|
60
|
+
out[k.strip()] = v.strip()
|
|
61
|
+
return out
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def redis_kpis(info: dict) -> list[dict]:
|
|
65
|
+
def num(k, d=0.0):
|
|
66
|
+
try:
|
|
67
|
+
return float(info.get(k, d))
|
|
68
|
+
except (TypeError, ValueError):
|
|
69
|
+
return d
|
|
70
|
+
hits, misses = num("keyspace_hits"), num("keyspace_misses")
|
|
71
|
+
hit_rate = 100.0 * hits / (hits + misses) if (hits + misses) > 0 else 0.0
|
|
72
|
+
return [
|
|
73
|
+
{"label": "ops", "value": round(num("instantaneous_ops_per_sec"), 1), "unit": "/s"},
|
|
74
|
+
{"label": "hit rate", "value": round(hit_rate, 1), "unit": "%"},
|
|
75
|
+
{"label": "clients", "value": int(num("connected_clients")), "unit": ""},
|
|
76
|
+
{"label": "memory", "value": round(num("used_memory") / 1048576, 1), "unit": "MiB"},
|
|
77
|
+
]
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def rabbit_kpis(overview: dict) -> list[dict]:
|
|
81
|
+
qt = overview.get("queue_totals", {}) or {}
|
|
82
|
+
ms = overview.get("message_stats", {}) or {}
|
|
83
|
+
pub = (ms.get("publish_details") or {}).get("rate", 0.0)
|
|
84
|
+
dlv = (ms.get("deliver_get_details") or ms.get("deliver_details") or {}).get("rate", 0.0)
|
|
85
|
+
return [
|
|
86
|
+
{"label": "queued", "value": int(qt.get("messages", 0)), "unit": "msgs"},
|
|
87
|
+
{"label": "publish", "value": round(float(pub), 1), "unit": "/s"},
|
|
88
|
+
{"label": "deliver", "value": round(float(dlv), 1), "unit": "/s"},
|
|
89
|
+
]
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def nats_kpis(varz: dict, rates, ts: float, key: str) -> list[dict]:
|
|
93
|
+
inm = float(varz.get("in_msgs", 0)); outm = float(varz.get("out_msgs", 0))
|
|
94
|
+
return [
|
|
95
|
+
{"label": "conns", "value": int(varz.get("connections", 0)), "unit": ""},
|
|
96
|
+
{"label": "in", "value": round(rates.rate(key + ":in", inm, ts), 1), "unit": "msg/s"},
|
|
97
|
+
{"label": "out", "value": round(rates.rate(key + ":out", outm, ts), 1), "unit": "msg/s"},
|
|
98
|
+
]
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def parse_nginx_status(text: str) -> dict[str, float]:
|
|
102
|
+
"""nginx stub_status -> {active, accepts, handled, requests}. The three counters are
|
|
103
|
+
on the line *after* the 'server accepts handled requests' header."""
|
|
104
|
+
out: dict[str, float] = {}
|
|
105
|
+
lines = (text or "").splitlines()
|
|
106
|
+
for i, line in enumerate(lines):
|
|
107
|
+
if "Active connections:" in line:
|
|
108
|
+
try:
|
|
109
|
+
out["active"] = float(line.split(":")[1].strip().split()[0])
|
|
110
|
+
except (IndexError, ValueError):
|
|
111
|
+
pass
|
|
112
|
+
if "accepts" in line and "handled" in line and "requests" in line:
|
|
113
|
+
nums = [t for t in lines[i + 1].split() if t.isdigit()] if i + 1 < len(lines) else []
|
|
114
|
+
if len(nums) >= 3:
|
|
115
|
+
out["accepts"], out["handled"], out["requests"] = map(float, nums[:3])
|
|
116
|
+
return out
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
# --------------------------------------------------------------------------- #
|
|
120
|
+
# Rate tracker (counter -> per-second), and fetch helpers
|
|
121
|
+
# --------------------------------------------------------------------------- #
|
|
122
|
+
class Rates:
|
|
123
|
+
def __init__(self) -> None:
|
|
124
|
+
self._prev: dict[str, tuple[float, float]] = {}
|
|
125
|
+
|
|
126
|
+
def rate(self, key: str, value: float, ts: float) -> float:
|
|
127
|
+
prev = self._prev.get(key)
|
|
128
|
+
self._prev[key] = (value, ts)
|
|
129
|
+
if not prev:
|
|
130
|
+
return 0.0
|
|
131
|
+
pv, pt = prev
|
|
132
|
+
dt = ts - pt
|
|
133
|
+
return max(0.0, (value - pv) / dt) if dt > 0 else 0.0
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def _http_get(url: str, timeout: float = 3.0, auth: tuple | None = None) -> str | None:
|
|
137
|
+
req = urllib.request.Request(url)
|
|
138
|
+
if auth:
|
|
139
|
+
import base64
|
|
140
|
+
tok = base64.b64encode(f"{auth[0]}:{auth[1]}".encode()).decode()
|
|
141
|
+
req.add_header("Authorization", "Basic " + tok)
|
|
142
|
+
try:
|
|
143
|
+
with urllib.request.urlopen(req, timeout=timeout) as r:
|
|
144
|
+
return r.read().decode("utf-8", "replace")
|
|
145
|
+
except (urllib.error.URLError, OSError, ValueError):
|
|
146
|
+
return None
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def _tcp_up(host: str, port: int, timeout: float = 2.0) -> bool:
|
|
150
|
+
try:
|
|
151
|
+
with socket.create_connection((host, int(port)), timeout=timeout):
|
|
152
|
+
return True
|
|
153
|
+
except OSError:
|
|
154
|
+
return False
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def _redis_info_raw(host: str, port: int, timeout: float = 2.0) -> str | None:
|
|
158
|
+
try:
|
|
159
|
+
with socket.create_connection((host, int(port)), timeout=timeout) as s:
|
|
160
|
+
s.sendall(b"INFO\r\n")
|
|
161
|
+
s.settimeout(timeout)
|
|
162
|
+
chunks = []
|
|
163
|
+
while True:
|
|
164
|
+
b = s.recv(8192)
|
|
165
|
+
if not b:
|
|
166
|
+
break
|
|
167
|
+
chunks.append(b)
|
|
168
|
+
if len(b) < 8192:
|
|
169
|
+
break
|
|
170
|
+
return b"".join(chunks).decode("utf-8", "replace")
|
|
171
|
+
except OSError:
|
|
172
|
+
return None
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
# --------------------------------------------------------------------------- #
|
|
176
|
+
# Adapters: type -> collect(svc, rates, ts) -> {"up", "kpis", [rps], [latency_ms]}
|
|
177
|
+
# --------------------------------------------------------------------------- #
|
|
178
|
+
def collect_cache(svc, rates, ts):
|
|
179
|
+
raw = _redis_info_raw(svc["host"], svc.get("port", 6379))
|
|
180
|
+
if raw is None:
|
|
181
|
+
return {"up": _tcp_up(svc["host"], svc.get("port", 6379)), "kpis": []}
|
|
182
|
+
return {"up": True, "kpis": redis_kpis(parse_redis_info(raw))}
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
def collect_queue(svc, rates, ts):
|
|
186
|
+
c = svc.get("creds", {})
|
|
187
|
+
url = f"http://{svc['host']}:{svc.get('port', 15672)}/api/overview"
|
|
188
|
+
txt = _http_get(url, auth=(c.get("user", "guest"), c.get("password", "guest")))
|
|
189
|
+
if txt is None:
|
|
190
|
+
return {"up": _tcp_up(svc["host"], 5672), "kpis": []}
|
|
191
|
+
try:
|
|
192
|
+
return {"up": True, "kpis": rabbit_kpis(json.loads(txt))}
|
|
193
|
+
except (ValueError, KeyError):
|
|
194
|
+
return {"up": True, "kpis": []}
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
def collect_database(svc, rates, ts):
|
|
198
|
+
host, port = svc["host"], svc.get("port", 5432)
|
|
199
|
+
c = svc.get("creds", {})
|
|
200
|
+
try:
|
|
201
|
+
import psycopg2 # lazy: only this adapter needs it
|
|
202
|
+
conn = psycopg2.connect(host=host, port=port, connect_timeout=2,
|
|
203
|
+
user=c.get("user", "gini"), password=c.get("password", "gini"),
|
|
204
|
+
dbname=c.get("db", "postgres"))
|
|
205
|
+
try:
|
|
206
|
+
cur = conn.cursor()
|
|
207
|
+
cur.execute("SELECT COALESCE(sum(xact_commit),0), COALESCE(sum(numbackends),0) "
|
|
208
|
+
"FROM pg_stat_database")
|
|
209
|
+
commits, conns = cur.fetchone()
|
|
210
|
+
finally:
|
|
211
|
+
conn.close()
|
|
212
|
+
return {"up": True, "kpis": [
|
|
213
|
+
{"label": "TPS", "value": round(rates.rate(svc["name"] + ":tx",
|
|
214
|
+
float(commits), ts), 1), "unit": "/s"},
|
|
215
|
+
{"label": "connections", "value": int(conns), "unit": ""}]}
|
|
216
|
+
except Exception: # noqa: BLE001 — driver missing or unreachable
|
|
217
|
+
return {"up": _tcp_up(host, port), "kpis": []}
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
def collect_proxy(svc, rates, ts):
|
|
221
|
+
"""Traefik exposes Prometheus metrics at :8080/metrics when started with
|
|
222
|
+
--metrics.prometheus. req/s from the request counter, latency from the sum/count."""
|
|
223
|
+
txt = _http_get(f"http://{svc['host']}:{svc.get('port', 8080)}/metrics")
|
|
224
|
+
if txt is None:
|
|
225
|
+
return {"up": _tcp_up(svc["host"], svc.get("port", 8080)), "kpis": []}
|
|
226
|
+
m = parse_prometheus(txt)
|
|
227
|
+
reqs = sum(v for k, v in m.items() if k.endswith("requests_total"))
|
|
228
|
+
rps = rates.rate(svc["name"] + ":req", reqs, ts)
|
|
229
|
+
kpis = [{"label": "req", "value": round(rps, 1), "unit": "/s"}]
|
|
230
|
+
# average latency over the interval = Δ(duration_sum) / Δ(request_count)
|
|
231
|
+
dur = sum(v for k, v in m.items() if k.endswith("request_duration_seconds_sum"))
|
|
232
|
+
cnt = sum(v for k, v in m.items() if k.endswith("request_duration_seconds_count"))
|
|
233
|
+
d_dur = rates.rate(svc["name"] + ":durs", dur, ts)
|
|
234
|
+
d_cnt = rates.rate(svc["name"] + ":durc", cnt, ts)
|
|
235
|
+
out = {"up": True, "rps": round(rps, 1)}
|
|
236
|
+
if d_cnt > 0:
|
|
237
|
+
ms = 1000.0 * d_dur / d_cnt
|
|
238
|
+
kpis.append({"label": "latency", "value": round(ms, 1), "unit": "ms"})
|
|
239
|
+
out["latency_ms"] = round(ms, 1)
|
|
240
|
+
out["kpis"] = kpis
|
|
241
|
+
return out
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
def collect_loadbalancer(svc, rates, ts):
|
|
245
|
+
"""nginx stub_status at /nginx_status (enabled via a generated conf)."""
|
|
246
|
+
txt = _http_get(f"http://{svc['host']}:{svc.get('port', 80)}/nginx_status")
|
|
247
|
+
if txt is None:
|
|
248
|
+
return {"up": _tcp_up(svc["host"], svc.get("port", 80)), "kpis": []}
|
|
249
|
+
st = parse_nginx_status(txt)
|
|
250
|
+
rps = rates.rate(svc["name"] + ":req", st.get("requests", 0.0), ts)
|
|
251
|
+
return {"up": True, "rps": round(rps, 1), "kpis": [
|
|
252
|
+
{"label": "req", "value": round(rps, 1), "unit": "/s"},
|
|
253
|
+
{"label": "active", "value": int(st.get("active", 0)), "unit": ""}]}
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
def collect_nats(svc, rates, ts):
|
|
257
|
+
txt = _http_get(f"http://{svc['host']}:{svc.get('port', 8222)}/varz")
|
|
258
|
+
if txt is None:
|
|
259
|
+
return {"up": _tcp_up(svc["host"], 4222), "kpis": []}
|
|
260
|
+
try:
|
|
261
|
+
return {"up": True, "kpis": nats_kpis(json.loads(txt), rates, ts, svc["name"])}
|
|
262
|
+
except ValueError:
|
|
263
|
+
return {"up": True, "kpis": []}
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
def collect_generic(svc, rates, ts):
|
|
267
|
+
"""Anything without a dedicated adapter: just a reachability check (CPU/mem still
|
|
268
|
+
come from docker stats in gBuilder's Live view)."""
|
|
269
|
+
return {"up": _tcp_up(svc["host"], svc.get("port", 80)), "kpis": []}
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
ADAPTERS = {
|
|
273
|
+
"cache": collect_cache,
|
|
274
|
+
"queue": collect_queue,
|
|
275
|
+
"database": collect_database,
|
|
276
|
+
"proxy": collect_proxy,
|
|
277
|
+
"load_balancer": collect_loadbalancer,
|
|
278
|
+
"messaging": collect_nats,
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
|
|
282
|
+
def snapshot(config: dict, rates: Rates) -> dict:
|
|
283
|
+
ts = time.time()
|
|
284
|
+
services: dict[str, dict] = {}
|
|
285
|
+
up = 0
|
|
286
|
+
total_rps = 0.0
|
|
287
|
+
max_lat = 0.0
|
|
288
|
+
for svc in config.get("services", []):
|
|
289
|
+
fn = ADAPTERS.get(svc.get("type"), collect_generic)
|
|
290
|
+
try:
|
|
291
|
+
data = fn(svc, rates, ts)
|
|
292
|
+
except Exception as e: # noqa: BLE001
|
|
293
|
+
data = {"up": False, "kpis": [], "error": str(e)}
|
|
294
|
+
kpis = data.get("kpis", [])
|
|
295
|
+
services[svc["name"]] = {
|
|
296
|
+
"type": svc.get("type"), "up": bool(data.get("up")),
|
|
297
|
+
"kpis": kpis, "primary": kpis[0] if kpis else None}
|
|
298
|
+
if data.get("up"):
|
|
299
|
+
up += 1
|
|
300
|
+
if data.get("rps"):
|
|
301
|
+
total_rps += float(data["rps"])
|
|
302
|
+
if data.get("latency_ms"):
|
|
303
|
+
max_lat = max(max_lat, float(data["latency_ms"]))
|
|
304
|
+
return {"ts": ts, "services": services,
|
|
305
|
+
"totals": {"services_up": up, "services_total": len(config.get("services", [])),
|
|
306
|
+
"rps": round(total_rps, 1), "latency_ms": round(max_lat, 1)}}
|
|
307
|
+
|
|
308
|
+
|
|
309
|
+
# --------------------------------------------------------------------------- #
|
|
310
|
+
# HTTP server + poll loop
|
|
311
|
+
# --------------------------------------------------------------------------- #
|
|
312
|
+
_LATEST: dict = {"ts": 0, "services": {}, "totals": {}}
|
|
313
|
+
|
|
314
|
+
|
|
315
|
+
def _poll_loop(config: dict) -> None:
|
|
316
|
+
rates = Rates()
|
|
317
|
+
global _LATEST
|
|
318
|
+
while True:
|
|
319
|
+
try:
|
|
320
|
+
_LATEST = snapshot(config, rates)
|
|
321
|
+
except Exception as e: # noqa: BLE001
|
|
322
|
+
_LATEST = {"ts": time.time(), "services": {}, "totals": {}, "error": str(e)}
|
|
323
|
+
time.sleep(POLL)
|
|
324
|
+
|
|
325
|
+
|
|
326
|
+
def _to_prometheus(snap: dict) -> str:
|
|
327
|
+
lines = []
|
|
328
|
+
for name, s in snap.get("services", {}).items():
|
|
329
|
+
lines.append(f'gini_service_up{{service="{name}",type="{s.get("type")}"}} '
|
|
330
|
+
f'{1 if s.get("up") else 0}')
|
|
331
|
+
for k in s.get("kpis", []):
|
|
332
|
+
metric = "gini_" + str(k["label"]).replace(" ", "_")
|
|
333
|
+
lines.append(f'{metric}{{service="{name}"}} {k["value"]}')
|
|
334
|
+
return "\n".join(lines) + "\n"
|
|
335
|
+
|
|
336
|
+
|
|
337
|
+
def main() -> None:
|
|
338
|
+
import threading
|
|
339
|
+
from http.server import BaseHTTPRequestHandler, HTTPServer
|
|
340
|
+
|
|
341
|
+
config = json.loads(os.environ.get("FABRIC_CONFIG", '{"services":[]}'))
|
|
342
|
+
threading.Thread(target=_poll_loop, args=(config,), daemon=True).start()
|
|
343
|
+
|
|
344
|
+
class Handler(BaseHTTPRequestHandler):
|
|
345
|
+
def log_message(self, *_a): # quiet
|
|
346
|
+
pass
|
|
347
|
+
|
|
348
|
+
def do_GET(self):
|
|
349
|
+
if self.path.startswith("/metrics.json"):
|
|
350
|
+
body = json.dumps(_LATEST).encode()
|
|
351
|
+
ctype = "application/json"
|
|
352
|
+
elif self.path.startswith("/metrics"):
|
|
353
|
+
body = _to_prometheus(_LATEST).encode()
|
|
354
|
+
ctype = "text/plain; version=0.0.4"
|
|
355
|
+
else:
|
|
356
|
+
body = b'gini cloud fabric: /metrics.json or /metrics\n'
|
|
357
|
+
ctype = "text/plain"
|
|
358
|
+
self.send_response(200)
|
|
359
|
+
self.send_header("Content-Type", ctype)
|
|
360
|
+
self.send_header("Content-Length", str(len(body)))
|
|
361
|
+
self.end_headers()
|
|
362
|
+
self.wfile.write(body)
|
|
363
|
+
|
|
364
|
+
print(f"[cloudfabric] watching {len(config.get('services', []))} services on :{PORT}",
|
|
365
|
+
flush=True)
|
|
366
|
+
HTTPServer(("0.0.0.0", PORT), Handler).serve_forever()
|
|
367
|
+
|
|
368
|
+
|
|
369
|
+
if __name__ == "__main__":
|
|
370
|
+
main()
|
gini/runtime/console.py
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
"""Interactive console client for one network element.
|
|
2
|
+
|
|
3
|
+
Runs inside the fabric container: python -m dataplane.console R1
|
|
4
|
+
Connects to that element's control socket and relays stdin<->socket, giving the
|
|
5
|
+
student an interactive prompt scoped to *that* router/switch.
|
|
6
|
+
"""
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import os
|
|
10
|
+
import select
|
|
11
|
+
import socket
|
|
12
|
+
import sys
|
|
13
|
+
import time
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def main() -> int: # pragma: no cover - interactive / needs container
|
|
17
|
+
if len(sys.argv) < 2:
|
|
18
|
+
print("usage: python -m dataplane.console <element-name> [command]")
|
|
19
|
+
return 2
|
|
20
|
+
name = sys.argv[1]
|
|
21
|
+
oneshot = " ".join(sys.argv[2:]).strip() if len(sys.argv) > 2 else None
|
|
22
|
+
ctrl_dir = os.environ.get("GINI_CTRL_DIR", "/run/gini")
|
|
23
|
+
path = os.path.join(ctrl_dir, f"{name}.sock")
|
|
24
|
+
s = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
|
|
25
|
+
try:
|
|
26
|
+
s.connect(path)
|
|
27
|
+
except OSError as e:
|
|
28
|
+
print(f"cannot connect to {name} ({path}): {e}")
|
|
29
|
+
return 1
|
|
30
|
+
|
|
31
|
+
if oneshot is not None: # non-interactive: one command, print, exit
|
|
32
|
+
try:
|
|
33
|
+
s.recv(4096) # drain banner + prompt
|
|
34
|
+
s.sendall((oneshot + "\n").encode())
|
|
35
|
+
time.sleep(0.2)
|
|
36
|
+
s.settimeout(0.6)
|
|
37
|
+
data = b""
|
|
38
|
+
while True:
|
|
39
|
+
chunk = s.recv(4096)
|
|
40
|
+
if not chunk:
|
|
41
|
+
break
|
|
42
|
+
data += chunk
|
|
43
|
+
if data.rstrip().endswith(b"gini>"):
|
|
44
|
+
break
|
|
45
|
+
except OSError:
|
|
46
|
+
pass
|
|
47
|
+
print(data.decode(errors="replace").replace("gini>", "").strip())
|
|
48
|
+
return 0
|
|
49
|
+
|
|
50
|
+
print(f"-- GINI console: {name} (type 'help', 'exit') --")
|
|
51
|
+
while True:
|
|
52
|
+
r, _, _ = select.select([s, sys.stdin], [], [])
|
|
53
|
+
if s in r:
|
|
54
|
+
data = s.recv(4096)
|
|
55
|
+
if not data:
|
|
56
|
+
break
|
|
57
|
+
sys.stdout.write(data.decode(errors="replace"))
|
|
58
|
+
sys.stdout.flush()
|
|
59
|
+
if sys.stdin in r:
|
|
60
|
+
line = sys.stdin.readline()
|
|
61
|
+
if not line:
|
|
62
|
+
break
|
|
63
|
+
s.sendall(line.encode())
|
|
64
|
+
return 0
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
if __name__ == "__main__": # pragma: no cover
|
|
68
|
+
raise SystemExit(main())
|
gini/runtime/control.py
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
"""Per-element control socket — the seed of the gRouter/switch console.
|
|
2
|
+
|
|
3
|
+
Each switch and gRouter, when GINI_CTRL_DIR is set, listens on its own UNIX socket
|
|
4
|
+
(<dir>/<name>.sock) speaking a tiny line protocol. `console.py` connects to one of
|
|
5
|
+
these, so a student can "log into" R1 or S1 *individually* even though they share the
|
|
6
|
+
fabric container. This is the first slice of the R3 control protocol.
|
|
7
|
+
"""
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import os
|
|
11
|
+
import socket
|
|
12
|
+
import threading
|
|
13
|
+
from collections.abc import Callable
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class ControlServer(threading.Thread):
|
|
17
|
+
def __init__(self, path: str, handler: Callable[[str], str], banner: str = "") -> None:
|
|
18
|
+
super().__init__(daemon=True)
|
|
19
|
+
self.path = path
|
|
20
|
+
self.handler = handler
|
|
21
|
+
self.banner = banner
|
|
22
|
+
|
|
23
|
+
def run(self) -> None: # pragma: no cover - exercised via integration/in-process test
|
|
24
|
+
try:
|
|
25
|
+
os.unlink(self.path)
|
|
26
|
+
except OSError:
|
|
27
|
+
pass
|
|
28
|
+
srv = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
|
|
29
|
+
srv.bind(self.path)
|
|
30
|
+
srv.listen()
|
|
31
|
+
while True:
|
|
32
|
+
conn, _ = srv.accept()
|
|
33
|
+
threading.Thread(target=self._serve, args=(conn,), daemon=True).start()
|
|
34
|
+
|
|
35
|
+
def _serve(self, conn: socket.socket) -> None:
|
|
36
|
+
conn.sendall((self.banner + "\ngini> ").encode())
|
|
37
|
+
buf = b""
|
|
38
|
+
while True:
|
|
39
|
+
try:
|
|
40
|
+
data = conn.recv(1024)
|
|
41
|
+
except OSError:
|
|
42
|
+
break
|
|
43
|
+
if not data:
|
|
44
|
+
break
|
|
45
|
+
buf += data
|
|
46
|
+
while b"\n" in buf:
|
|
47
|
+
line, buf = buf.split(b"\n", 1)
|
|
48
|
+
cmd = line.decode(errors="replace").strip()
|
|
49
|
+
if cmd in ("exit", "quit"):
|
|
50
|
+
conn.close()
|
|
51
|
+
return
|
|
52
|
+
out = ""
|
|
53
|
+
if cmd:
|
|
54
|
+
try:
|
|
55
|
+
out = self.handler(cmd)
|
|
56
|
+
except Exception as e: # never kill the console on a bad command
|
|
57
|
+
out = f"error: {e}"
|
|
58
|
+
conn.sendall((out + "\ngini> ").encode())
|
|
59
|
+
conn.close()
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def maybe_start(name: str, handler: Callable[[str], str], banner: str = "") -> ControlServer | None:
|
|
63
|
+
"""Start a control server iff GINI_CTRL_DIR is set (i.e., running in the fabric)."""
|
|
64
|
+
ctrl_dir = os.environ.get("GINI_CTRL_DIR")
|
|
65
|
+
if not ctrl_dir:
|
|
66
|
+
return None
|
|
67
|
+
os.makedirs(ctrl_dir, exist_ok=True)
|
|
68
|
+
server = ControlServer(os.path.join(ctrl_dir, f"{name}.sock"), handler, banner)
|
|
69
|
+
server.start()
|
|
70
|
+
return server
|
gini/runtime/frame.py
ADDED
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
"""Ethernet / ARP / IPv4 / ICMP frame helpers.
|
|
2
|
+
|
|
3
|
+
Deliberately tiny and dependency-free (stdlib only) so the same code runs inside a
|
|
4
|
+
slim container and in the no-Docker loopback test. Addresses are strings at the API
|
|
5
|
+
boundary (MAC "aa:bb:..", IP "1.2.3.4"); bytes only inside.
|
|
6
|
+
"""
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import socket
|
|
10
|
+
import struct
|
|
11
|
+
|
|
12
|
+
# ethertypes
|
|
13
|
+
ETH_IP = 0x0800
|
|
14
|
+
ETH_ARP = 0x0806
|
|
15
|
+
BROADCAST = "ff:ff:ff:ff:ff:ff"
|
|
16
|
+
ZERO_MAC = "00:00:00:00:00:00"
|
|
17
|
+
|
|
18
|
+
# ip protocols
|
|
19
|
+
PROTO_ICMP = 1
|
|
20
|
+
|
|
21
|
+
# icmp types
|
|
22
|
+
ICMP_ECHO_REQUEST = 8
|
|
23
|
+
ICMP_ECHO_REPLY = 0
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
# -- address conversion ----------------------------------------------------- #
|
|
27
|
+
def mac_to_bytes(m: str) -> bytes:
|
|
28
|
+
return bytes(int(x, 16) for x in m.split(":"))
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def mac_to_str(b: bytes) -> str:
|
|
32
|
+
return ":".join("%02x" % x for x in b)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def ip_to_bytes(s: str) -> bytes:
|
|
36
|
+
return socket.inet_aton(s)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def ip_to_str(b: bytes) -> str:
|
|
40
|
+
return socket.inet_ntoa(b)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def is_multicast_mac(m: str) -> bool:
|
|
44
|
+
"""Broadcast and multicast both have the low bit of the first octet set."""
|
|
45
|
+
return bool(int(m.split(":")[0], 16) & 1)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
# -- internet checksum ------------------------------------------------------ #
|
|
49
|
+
def checksum(data: bytes) -> int:
|
|
50
|
+
if len(data) % 2:
|
|
51
|
+
data += b"\x00"
|
|
52
|
+
s = sum(int.from_bytes(data[i:i + 2], "big") for i in range(0, len(data), 2))
|
|
53
|
+
s = (s & 0xFFFF) + (s >> 16)
|
|
54
|
+
s = (s & 0xFFFF) + (s >> 16)
|
|
55
|
+
return (~s) & 0xFFFF
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
# -- ethernet --------------------------------------------------------------- #
|
|
59
|
+
def build_eth(dst: str, src: str, etype: int, payload: bytes) -> bytes:
|
|
60
|
+
return mac_to_bytes(dst) + mac_to_bytes(src) + struct.pack("!H", etype) + payload
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def parse_eth(frame: bytes):
|
|
64
|
+
dst = mac_to_str(frame[0:6])
|
|
65
|
+
src = mac_to_str(frame[6:12])
|
|
66
|
+
etype = struct.unpack("!H", frame[12:14])[0]
|
|
67
|
+
return dst, src, etype, frame[14:]
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
# -- arp -------------------------------------------------------------------- #
|
|
71
|
+
def build_arp(op: int, sha: str, spa: str, tha: str, tpa: str) -> bytes:
|
|
72
|
+
return (struct.pack("!HHBBH", 1, ETH_IP, 6, 4, op)
|
|
73
|
+
+ mac_to_bytes(sha) + ip_to_bytes(spa)
|
|
74
|
+
+ mac_to_bytes(tha) + ip_to_bytes(tpa))
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def parse_arp(p: bytes):
|
|
78
|
+
op = struct.unpack("!H", p[6:8])[0]
|
|
79
|
+
sha = mac_to_str(p[8:14])
|
|
80
|
+
spa = ip_to_str(p[14:18])
|
|
81
|
+
tha = mac_to_str(p[18:24])
|
|
82
|
+
tpa = ip_to_str(p[24:28])
|
|
83
|
+
return op, sha, spa, tha, tpa
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
# -- ipv4 ------------------------------------------------------------------- #
|
|
87
|
+
def build_ipv4(src: str, dst: str, proto: int, payload: bytes,
|
|
88
|
+
ttl: int = 64, ident: int = 0) -> bytes:
|
|
89
|
+
total = 20 + len(payload)
|
|
90
|
+
fields = (0x45, 0, total, ident, 0, ttl, proto, 0,
|
|
91
|
+
ip_to_bytes(src), ip_to_bytes(dst))
|
|
92
|
+
hdr = struct.pack("!BBHHHBBH4s4s", *fields)
|
|
93
|
+
csum = checksum(hdr)
|
|
94
|
+
fields = (0x45, 0, total, ident, 0, ttl, proto, csum,
|
|
95
|
+
ip_to_bytes(src), ip_to_bytes(dst))
|
|
96
|
+
hdr = struct.pack("!BBHHHBBH4s4s", *fields)
|
|
97
|
+
return hdr + payload
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def parse_ipv4(p: bytes) -> dict:
|
|
101
|
+
ihl = (p[0] & 0x0F) * 4
|
|
102
|
+
total = struct.unpack("!H", p[2:4])[0]
|
|
103
|
+
return {
|
|
104
|
+
"ihl": ihl,
|
|
105
|
+
"ttl": p[8],
|
|
106
|
+
"proto": p[9],
|
|
107
|
+
"src": ip_to_str(p[12:16]),
|
|
108
|
+
"dst": ip_to_str(p[16:20]),
|
|
109
|
+
"payload": p[ihl:total] if total <= len(p) else p[ihl:],
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def dec_ttl(ippkt: bytes) -> bytes | None:
|
|
114
|
+
"""Decrement TTL and fix the header checksum. None if TTL would hit 0."""
|
|
115
|
+
b = bytearray(ippkt)
|
|
116
|
+
ihl = (b[0] & 0x0F) * 4
|
|
117
|
+
if b[8] <= 1:
|
|
118
|
+
return None
|
|
119
|
+
b[8] -= 1
|
|
120
|
+
b[10] = 0
|
|
121
|
+
b[11] = 0
|
|
122
|
+
c = checksum(bytes(b[:ihl]))
|
|
123
|
+
b[10] = c >> 8
|
|
124
|
+
b[11] = c & 0xFF
|
|
125
|
+
return bytes(b)
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
# -- icmp ------------------------------------------------------------------- #
|
|
129
|
+
def build_icmp(typ: int, ident: int, seq: int, data: bytes = b"gini-r0-spike!!!") -> bytes:
|
|
130
|
+
hdr = struct.pack("!BBHHH", typ, 0, 0, ident, seq)
|
|
131
|
+
csum = checksum(hdr + data)
|
|
132
|
+
hdr = struct.pack("!BBHHH", typ, 0, csum, ident, seq)
|
|
133
|
+
return hdr + data
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def parse_icmp(p: bytes):
|
|
137
|
+
typ, _code, _csum, ident, seq = struct.unpack("!BBHHH", p[:8])
|
|
138
|
+
return typ, ident, seq, p[8:]
|