@isothrum/editor 0.0.0-stage → 0.1.0
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.
- package/LICENSE +202 -0
- package/NOTICE +8 -0
- package/PUBLISHING.md +93 -0
- package/README.md +106 -2
- package/desktop-editor/editor.css +11 -0
- package/desktop-editor/index.html +14 -0
- package/desktop-editor/main.tsx +26 -0
- package/dist/desktop/editor.css +1 -0
- package/dist/desktop/editor.js +181 -0
- package/dist/desktop/fonts/geist-cyrillic-ext-wght-normal-RO72NYA2.woff2 +0 -0
- package/dist/desktop/fonts/geist-cyrillic-wght-normal-CK2XY27K.woff2 +0 -0
- package/dist/desktop/fonts/geist-latin-ext-wght-normal-XEQ56JUF.woff2 +0 -0
- package/dist/desktop/fonts/geist-latin-wght-normal-T72VVCUW.woff2 +0 -0
- package/dist/desktop/fonts/geist-mono-cyrillic-ext-wght-normal-QXKVIKFD.woff2 +0 -0
- package/dist/desktop/fonts/geist-mono-cyrillic-wght-normal-GOIAQAGU.woff2 +0 -0
- package/dist/desktop/fonts/geist-mono-latin-ext-wght-normal-HU3ZAWN6.woff2 +0 -0
- package/dist/desktop/fonts/geist-mono-latin-wght-normal-CJNQKXUG.woff2 +0 -0
- package/dist/desktop/fonts/geist-mono-symbols2-wght-normal-WZJJVLQO.woff2 +0 -0
- package/dist/desktop/fonts/geist-mono-vietnamese-wght-normal-KW5NM6YV.woff2 +0 -0
- package/dist/desktop/fonts/geist-vietnamese-wght-normal-Y2R7JZKV.woff2 +0 -0
- package/dist/desktop/fonts/shantell-sans-cyrillic-ext-wght-normal-ILPQWRJJ.woff2 +0 -0
- package/dist/desktop/fonts/shantell-sans-cyrillic-wght-normal-RKC6NDPM.woff2 +0 -0
- package/dist/desktop/fonts/shantell-sans-latin-ext-wght-normal-SNKHG3LM.woff2 +0 -0
- package/dist/desktop/fonts/shantell-sans-latin-wght-normal-7CHHGC3N.woff2 +0 -0
- package/dist/desktop/fonts/shantell-sans-vietnamese-wght-normal-IPFAQSBA.woff2 +0 -0
- package/dist/desktop/index.html +14 -0
- package/dist/desktop/styles.css +2 -0
- package/dist/desktop/wasm/isothrum_wasm.d.ts +60 -0
- package/dist/desktop/wasm/isothrum_wasm.js +312 -0
- package/dist/desktop/wasm/isothrum_wasm_bg.wasm +0 -0
- package/dist/index.js +7059 -0
- package/dist/index.js.map +7 -0
- package/dist/model.js +745 -0
- package/dist/model.js.map +7 -0
- package/dist/types/components/tool/canvas/cable-edge.d.ts +4 -0
- package/dist/types/components/tool/canvas/contexts.d.ts +12 -0
- package/dist/types/components/tool/canvas/device-icon.d.ts +17 -0
- package/dist/types/components/tool/canvas/inline-text.d.ts +15 -0
- package/dist/types/components/tool/canvas/lan-node.d.ts +8 -0
- package/dist/types/components/tool/canvas/machine-addresses.d.ts +14 -0
- package/dist/types/components/tool/canvas/machine-node.d.ts +4 -0
- package/dist/types/components/tool/canvas/note-node.d.ts +7 -0
- package/dist/types/components/tool/canvas/port-led.d.ts +10 -0
- package/dist/types/components/tool/canvas/router-node.d.ts +8 -0
- package/dist/types/components/tool/canvas/sketch.d.ts +50 -0
- package/dist/types/components/tool/canvas/status-dot.d.ts +5 -0
- package/dist/types/components/tool/canvas/switch-node.d.ts +8 -0
- package/dist/types/components/tool/canvas/use-measured-ports.d.ts +7 -0
- package/dist/types/components/tool/context-menus.d.ts +5 -0
- package/dist/types/components/tool/device-block.d.ts +12 -0
- package/dist/types/components/tool/editor-menu.d.ts +28 -0
- package/dist/types/components/tool/export/copy-button.d.ts +9 -0
- package/dist/types/components/tool/export/export-panel.d.ts +17 -0
- package/dist/types/components/tool/export/target-list.d.ts +15 -0
- package/dist/types/components/tool/floating-window.d.ts +13 -0
- package/dist/types/components/tool/flow-canvas.d.ts +6 -0
- package/dist/types/components/tool/hooks/use-cabling.d.ts +23 -0
- package/dist/types/components/tool/hooks/use-captures.d.ts +9 -0
- package/dist/types/components/tool/hooks/use-clipboard.d.ts +12 -0
- package/dist/types/components/tool/hooks/use-context-menus.d.ts +25 -0
- package/dist/types/components/tool/hooks/use-design.d.ts +10 -0
- package/dist/types/components/tool/hooks/use-desktop.d.ts +50 -0
- package/dist/types/components/tool/hooks/use-devices.d.ts +17 -0
- package/dist/types/components/tool/hooks/use-downloads.d.ts +9 -0
- package/dist/types/components/tool/hooks/use-editor.d.ts +609 -0
- package/dist/types/components/tool/hooks/use-engine.d.ts +9 -0
- package/dist/types/components/tool/hooks/use-history.d.ts +9 -0
- package/dist/types/components/tool/hooks/use-lab-actions.d.ts +25 -0
- package/dist/types/components/tool/hooks/use-lab-run.d.ts +25 -0
- package/dist/types/components/tool/hooks/use-lab.d.ts +18 -0
- package/dist/types/components/tool/hooks/use-lan-carry.d.ts +8 -0
- package/dist/types/components/tool/hooks/use-machine-actions.d.ts +26 -0
- package/dist/types/components/tool/hooks/use-placement.d.ts +27 -0
- package/dist/types/components/tool/hooks/use-project.d.ts +25 -0
- package/dist/types/components/tool/hooks/use-projects.d.ts +34 -0
- package/dist/types/components/tool/hooks/use-shared-link.d.ts +23 -0
- package/dist/types/components/tool/hooks/use-toast.d.ts +2 -0
- package/dist/types/components/tool/hooks/use-tools.d.ts +17 -0
- package/dist/types/components/tool/lab-canvas.d.ts +18 -0
- package/dist/types/components/tool/overlays.d.ts +5 -0
- package/dist/types/components/tool/projects-list.d.ts +9 -0
- package/dist/types/components/tool/text.d.ts +418 -0
- package/dist/types/components/tool/toolbar.d.ts +5 -0
- package/dist/types/components/tool/top-bars.d.ts +10 -0
- package/dist/types/components/tool/windows/conditions-fields.d.ts +6 -0
- package/dist/types/components/tool/windows/editor-windows.d.ts +5 -0
- package/dist/types/components/tool/windows/fields.d.ts +4 -0
- package/dist/types/components/tool/windows/lab-status.d.ts +6 -0
- package/dist/types/components/tool/windows/lab-windows.d.ts +13 -0
- package/dist/types/components/tool/windows/link-controls.d.ts +21 -0
- package/dist/types/components/tool/windows/machine-controls.d.ts +16 -0
- package/dist/types/components/tool/windows/machine-inspector.d.ts +11 -0
- package/dist/types/components/tool/windows/runs-fields.d.ts +18 -0
- package/dist/types/components/tool/windows/selected-windows.d.ts +21 -0
- package/dist/types/components/ui/button.d.ts +8 -0
- package/dist/types/components/ui/dropdown-menu.d.ts +21 -0
- package/dist/types/index.d.ts +7 -0
- package/dist/types/lib/auth.d.ts +38 -0
- package/dist/types/lib/brand.d.ts +19 -0
- package/dist/types/lib/i18n.d.ts +19 -0
- package/dist/types/lib/tool/blocks.d.ts +35 -0
- package/dist/types/lib/tool/cables.d.ts +48 -0
- package/dist/types/lib/tool/clone.d.ts +12 -0
- package/dist/types/lib/tool/conditions.d.ts +31 -0
- package/dist/types/lib/tool/create.d.ts +22 -0
- package/dist/types/lib/tool/desktop.d.ts +63 -0
- package/dist/types/lib/tool/draft.d.ts +20 -0
- package/dist/types/lib/tool/engine.d.ts +33 -0
- package/dist/types/lib/tool/examples.d.ts +11 -0
- package/dist/types/lib/tool/export-targets.d.ts +21 -0
- package/dist/types/lib/tool/facing.d.ts +9 -0
- package/dist/types/lib/tool/geometry.d.ts +36 -0
- package/dist/types/lib/tool/gns3-cables.d.ts +17 -0
- package/dist/types/lib/tool/gns3-import.d.ts +15 -0
- package/dist/types/lib/tool/gns3-parts.d.ts +25 -0
- package/dist/types/lib/tool/issues.d.ts +12 -0
- package/dist/types/lib/tool/layout.d.ts +52 -0
- package/dist/types/lib/tool/live.d.ts +21 -0
- package/dist/types/lib/tool/logos.d.ts +7 -0
- package/dist/types/lib/tool/model.d.ts +173 -0
- package/dist/types/lib/tool/os.d.ts +20 -0
- package/dist/types/lib/tool/pins.d.ts +5 -0
- package/dist/types/lib/tool/placement.d.ts +21 -0
- package/dist/types/lib/tool/ports.d.ts +21 -0
- package/dist/types/lib/tool/presets.d.ts +21 -0
- package/dist/types/lib/tool/projects-api.d.ts +37 -0
- package/dist/types/lib/tool/router-ports.d.ts +22 -0
- package/dist/types/lib/tool/selection.d.ts +19 -0
- package/dist/types/lib/tool/storage.d.ts +25 -0
- package/dist/types/lib/tool/switch-edit.d.ts +17 -0
- package/dist/types/lib/tool/text/de.d.ts +2 -0
- package/dist/types/lib/tool/text/en.d.ts +429 -0
- package/dist/types/lib/tool/text/es.d.ts +2 -0
- package/dist/types/lib/tool/text/fr.d.ts +2 -0
- package/dist/types/lib/tool/text/index.d.ts +419 -0
- package/dist/types/lib/tool/topology.d.ts +2 -0
- package/dist/types/lib/tool/vlans.d.ts +24 -0
- package/dist/types/lib/utils.d.ts +2 -0
- package/dist/types/model.d.ts +13 -0
- package/package.json +88 -4
- package/scripts/build-desktop-editor.mjs +43 -0
- package/scripts/build-lib.mjs +38 -0
- package/src/components/tool/canvas/cable-edge.tsx +112 -0
- package/src/components/tool/canvas/contexts.ts +18 -0
- package/src/components/tool/canvas/device-icon.tsx +105 -0
- package/src/components/tool/canvas/inline-text.tsx +69 -0
- package/src/components/tool/canvas/lan-node.tsx +148 -0
- package/src/components/tool/canvas/machine-addresses.tsx +117 -0
- package/src/components/tool/canvas/machine-node.tsx +114 -0
- package/src/components/tool/canvas/note-node.tsx +109 -0
- package/src/components/tool/canvas/port-led.tsx +21 -0
- package/src/components/tool/canvas/router-node.tsx +114 -0
- package/src/components/tool/canvas/sketch.tsx +130 -0
- package/src/components/tool/canvas/status-dot.tsx +29 -0
- package/src/components/tool/canvas/switch-node.tsx +143 -0
- package/src/components/tool/canvas/use-measured-ports.ts +25 -0
- package/src/components/tool/context-menus.tsx +142 -0
- package/src/components/tool/device-block.tsx +107 -0
- package/src/components/tool/editor-menu.tsx +191 -0
- package/src/components/tool/export/copy-button.tsx +45 -0
- package/src/components/tool/export/export-panel.tsx +127 -0
- package/src/components/tool/export/target-list.tsx +87 -0
- package/src/components/tool/floating-window.tsx +69 -0
- package/src/components/tool/flow-canvas.tsx +138 -0
- package/src/components/tool/hooks/use-cabling.ts +66 -0
- package/src/components/tool/hooks/use-captures.ts +24 -0
- package/src/components/tool/hooks/use-clipboard.ts +54 -0
- package/src/components/tool/hooks/use-context-menus.ts +37 -0
- package/src/components/tool/hooks/use-design.ts +24 -0
- package/src/components/tool/hooks/use-desktop.ts +100 -0
- package/src/components/tool/hooks/use-devices.ts +58 -0
- package/src/components/tool/hooks/use-downloads.ts +42 -0
- package/src/components/tool/hooks/use-editor.ts +114 -0
- package/src/components/tool/hooks/use-engine.ts +29 -0
- package/src/components/tool/hooks/use-history.ts +107 -0
- package/src/components/tool/hooks/use-lab-actions.ts +57 -0
- package/src/components/tool/hooks/use-lab-run.ts +61 -0
- package/src/components/tool/hooks/use-lab.ts +32 -0
- package/src/components/tool/hooks/use-lan-carry.ts +43 -0
- package/src/components/tool/hooks/use-machine-actions.ts +57 -0
- package/src/components/tool/hooks/use-placement.ts +80 -0
- package/src/components/tool/hooks/use-project.ts +112 -0
- package/src/components/tool/hooks/use-projects.ts +85 -0
- package/src/components/tool/hooks/use-shared-link.ts +64 -0
- package/src/components/tool/hooks/use-toast.ts +16 -0
- package/src/components/tool/hooks/use-tools.ts +60 -0
- package/src/components/tool/lab-canvas.tsx +96 -0
- package/src/components/tool/overlays.tsx +104 -0
- package/src/components/tool/projects-list.tsx +96 -0
- package/src/components/tool/text.ts +9 -0
- package/src/components/tool/toolbar.tsx +106 -0
- package/src/components/tool/top-bars.tsx +90 -0
- package/src/components/tool/windows/conditions-fields.tsx +43 -0
- package/src/components/tool/windows/editor-windows.tsx +73 -0
- package/src/components/tool/windows/fields.ts +6 -0
- package/src/components/tool/windows/lab-status.tsx +68 -0
- package/src/components/tool/windows/lab-windows.tsx +130 -0
- package/src/components/tool/windows/link-controls.tsx +69 -0
- package/src/components/tool/windows/machine-controls.tsx +116 -0
- package/src/components/tool/windows/machine-inspector.tsx +83 -0
- package/src/components/tool/windows/runs-fields.tsx +127 -0
- package/src/components/tool/windows/selected-windows.tsx +147 -0
- package/src/components/ui/button.tsx +59 -0
- package/src/components/ui/dropdown-menu.tsx +185 -0
- package/src/index.ts +21 -0
- package/src/lib/auth.tsx +43 -0
- package/src/lib/brand.tsx +46 -0
- package/src/lib/i18n.ts +53 -0
- package/src/lib/tool/blocks.test.ts +64 -0
- package/src/lib/tool/blocks.ts +78 -0
- package/src/lib/tool/cable-wiring.test.ts +74 -0
- package/src/lib/tool/cables.test.ts +75 -0
- package/src/lib/tool/cables.ts +215 -0
- package/src/lib/tool/clone.test.ts +54 -0
- package/src/lib/tool/clone.ts +48 -0
- package/src/lib/tool/conditions.test.ts +170 -0
- package/src/lib/tool/conditions.ts +132 -0
- package/src/lib/tool/create.test.ts +67 -0
- package/src/lib/tool/create.ts +83 -0
- package/src/lib/tool/desktop.test.ts +59 -0
- package/src/lib/tool/desktop.ts +99 -0
- package/src/lib/tool/draft.test.ts +150 -0
- package/src/lib/tool/draft.ts +109 -0
- package/src/lib/tool/engine.test.ts +39 -0
- package/src/lib/tool/engine.ts +58 -0
- package/src/lib/tool/examples.test.ts +35 -0
- package/src/lib/tool/examples.ts +71 -0
- package/src/lib/tool/export-targets.test.ts +47 -0
- package/src/lib/tool/export-targets.ts +84 -0
- package/src/lib/tool/facing.test.ts +72 -0
- package/src/lib/tool/facing.ts +100 -0
- package/src/lib/tool/geometry.test.ts +80 -0
- package/src/lib/tool/geometry.ts +109 -0
- package/src/lib/tool/gns3-cables.test.ts +47 -0
- package/src/lib/tool/gns3-cables.ts +81 -0
- package/src/lib/tool/gns3-import.test.ts +102 -0
- package/src/lib/tool/gns3-import.ts +198 -0
- package/src/lib/tool/gns3-layout.test.ts +62 -0
- package/src/lib/tool/gns3-parts.test.ts +47 -0
- package/src/lib/tool/gns3-parts.ts +68 -0
- package/src/lib/tool/issues.test.ts +35 -0
- package/src/lib/tool/issues.ts +60 -0
- package/src/lib/tool/layout.test.ts +128 -0
- package/src/lib/tool/layout.ts +229 -0
- package/src/lib/tool/live.test.ts +46 -0
- package/src/lib/tool/live.ts +49 -0
- package/src/lib/tool/logos.test.ts +30 -0
- package/src/lib/tool/logos.ts +99 -0
- package/src/lib/tool/model.test.ts +128 -0
- package/src/lib/tool/model.ts +238 -0
- package/src/lib/tool/os.test.ts +45 -0
- package/src/lib/tool/os.ts +57 -0
- package/src/lib/tool/parity.test.ts +145 -0
- package/src/lib/tool/pins.test.ts +24 -0
- package/src/lib/tool/pins.ts +11 -0
- package/src/lib/tool/placement.test.ts +79 -0
- package/src/lib/tool/placement.ts +60 -0
- package/src/lib/tool/ports.test.ts +39 -0
- package/src/lib/tool/ports.ts +48 -0
- package/src/lib/tool/presets.test.ts +50 -0
- package/src/lib/tool/presets.ts +77 -0
- package/src/lib/tool/projects-api.test.ts +59 -0
- package/src/lib/tool/projects-api.ts +47 -0
- package/src/lib/tool/router-ports.test.ts +63 -0
- package/src/lib/tool/router-ports.ts +88 -0
- package/src/lib/tool/selection.test.ts +44 -0
- package/src/lib/tool/selection.ts +34 -0
- package/src/lib/tool/storage.test.ts +123 -0
- package/src/lib/tool/storage.ts +127 -0
- package/src/lib/tool/switch-edit.test.ts +49 -0
- package/src/lib/tool/switch-edit.ts +38 -0
- package/src/lib/tool/text/de.ts +358 -0
- package/src/lib/tool/text/en.ts +371 -0
- package/src/lib/tool/text/es.ts +358 -0
- package/src/lib/tool/text/fr.ts +358 -0
- package/src/lib/tool/text/index.ts +21 -0
- package/src/lib/tool/text/text.test.ts +33 -0
- package/src/lib/tool/topology.test.ts +108 -0
- package/src/lib/tool/topology.ts +116 -0
- package/src/lib/tool/vlans.test.ts +64 -0
- package/src/lib/tool/vlans.ts +79 -0
- package/src/lib/utils.ts +6 -0
- package/src/model.ts +13 -0
- package/src/test/arbitrary.ts +41 -0
- package/src/test/engine.ts +48 -0
- package/src/test/lab.ts +101 -0
- package/styles/globals.css +258 -0
- package/styles/theme.css +192 -0
- package/tsconfig.build.json +12 -0
- package/tsconfig.json +22 -0
- package/wasm/isothrum_wasm.d.ts +60 -0
- package/wasm/isothrum_wasm.js +312 -0
- package/wasm/isothrum_wasm_bg.wasm +0 -0
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import { describe, expect, test } from "vitest";
|
|
2
|
+
import {
|
|
3
|
+
APPLIANCES,
|
|
4
|
+
LAN_H,
|
|
5
|
+
LAN_W,
|
|
6
|
+
MACHINE_H,
|
|
7
|
+
MACHINE_W,
|
|
8
|
+
ROUTER_H,
|
|
9
|
+
ROUTER_W,
|
|
10
|
+
SWITCH_H,
|
|
11
|
+
SWITCH_W,
|
|
12
|
+
applianceInterface,
|
|
13
|
+
centerOf,
|
|
14
|
+
cleanName,
|
|
15
|
+
freeName,
|
|
16
|
+
idleByDefault,
|
|
17
|
+
imageName,
|
|
18
|
+
isAppliance,
|
|
19
|
+
isRealRouter,
|
|
20
|
+
nodeName,
|
|
21
|
+
sizeOf,
|
|
22
|
+
uid,
|
|
23
|
+
unselected,
|
|
24
|
+
} from "./model";
|
|
25
|
+
import { autoRouter, lan, machine, note, realRouter, switchNode } from "@/test/lab";
|
|
26
|
+
|
|
27
|
+
describe("applianceInterface", () => {
|
|
28
|
+
test("numbers IOL interfaces four to a slot", () => {
|
|
29
|
+
expect([1, 3, 4, 5].map((k) => applianceInterface("cisco-iol", k))).toEqual(["Ethernet0/1", "Ethernet0/3", "Ethernet1/0", "Ethernet1/1"]);
|
|
30
|
+
expect(applianceInterface("cisco-iol-l2", 4)).toBe("Ethernet1/0");
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
test("numbers IOSv interfaces on slot 0", () => {
|
|
34
|
+
expect(applianceInterface("cisco-vios", 2)).toBe("GigabitEthernet0/2");
|
|
35
|
+
expect(applianceInterface("cisco-vios-l2", 1)).toBe("GigabitEthernet0/1");
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
test("numbers IOS XE interfaces from 2 (GigabitEthernet1 is management)", () => {
|
|
39
|
+
expect(applianceInterface("cisco-csr1000v", 1)).toBe("GigabitEthernet2");
|
|
40
|
+
expect(applianceInterface("cisco-c8000v", 3)).toBe("GigabitEthernet4");
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
test("numbers Dynamips interfaces two to a slot, the first on slot 0", () => {
|
|
44
|
+
expect([1, 2, 3, 4].map((k) => applianceInterface("cisco-dynamips", k))).toEqual(["FastEthernet0/0", "FastEthernet1/0", "FastEthernet1/1", "FastEthernet2/0"]);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
test("gives every Cisco OS an image except Dynamips, which Isoloom builds", () => {
|
|
48
|
+
expect(APPLIANCES.filter((a) => !a.image).map((a) => a.id)).toEqual(["cisco-dynamips"]);
|
|
49
|
+
});
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
describe("images", () => {
|
|
53
|
+
test("names an image by its last path segment, without tag, in lowercase", () => {
|
|
54
|
+
expect(imageName(" Docker.io/Library/Nginx:1.27 ")).toBe("nginx");
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
test("keeps stock Linux images running idle, not service images", () => {
|
|
58
|
+
expect(["alpine", "x/ubuntu:22", " Debian:12", "nicolaka/netshoot"].map(idleByDefault)).toEqual([true, true, true, true]);
|
|
59
|
+
expect(["nginx", "postgres:16", ""].map(idleByDefault)).toEqual([false, false, false]);
|
|
60
|
+
});
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
describe("kinds of node", () => {
|
|
64
|
+
test("a router you configure is a real router; the automatic one is not", () => {
|
|
65
|
+
expect(isRealRouter(realRouter("frr"))).toBe(true);
|
|
66
|
+
expect(isRealRouter(autoRouter("r"))).toBe(false);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
test("routers and firewalls are appliances; servers are not", () => {
|
|
70
|
+
expect(isAppliance(autoRouter("r"))).toBe(true);
|
|
71
|
+
expect(isAppliance(machine("fw", { icon: "shield" }))).toBe(true);
|
|
72
|
+
expect(isAppliance(machine("web", { icon: "server" }))).toBe(false);
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
test("names a note 'note' and anything else by its name", () => {
|
|
76
|
+
expect(nodeName(note("n1"))).toBe("note");
|
|
77
|
+
expect(nodeName(lan("l1", { name: "office" }))).toBe("office");
|
|
78
|
+
});
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
describe("sizeOf", () => {
|
|
82
|
+
test("sizes a LAN by its own width and height, or the default area", () => {
|
|
83
|
+
expect(sizeOf(lan("l", {}, { w: 500, h: 200 }))).toEqual({ w: 500, h: 200 });
|
|
84
|
+
expect(sizeOf({ ...lan("l"), width: undefined, height: undefined })).toEqual({ w: LAN_W, h: LAN_H });
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
test("sizes switches, real routers and machines by their faceplate or box", () => {
|
|
88
|
+
expect(sizeOf(switchNode("sw"))).toEqual({ w: SWITCH_W, h: SWITCH_H });
|
|
89
|
+
expect(sizeOf(realRouter("frr"))).toEqual({ w: ROUTER_W, h: ROUTER_H });
|
|
90
|
+
expect(sizeOf(autoRouter("r"))).toEqual({ w: MACHINE_W, h: MACHINE_H });
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
test("sizes a note by its own size, 200 by 90 when it has none", () => {
|
|
94
|
+
expect(sizeOf({ ...note("n"), width: undefined, height: undefined })).toEqual({ w: 200, h: 90 });
|
|
95
|
+
});
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
describe("centerOf", () => {
|
|
99
|
+
test("uses the node's width, then its measured size, then the fallback", () => {
|
|
100
|
+
expect(centerOf(lan("l", {}, { x: 10, y: 20, w: 100, h: 50 }))).toEqual({ x: 60, y: 45 });
|
|
101
|
+
expect(centerOf({ ...machine("m"), measured: { width: 40, height: 20 } })).toEqual({ x: 20, y: 10 });
|
|
102
|
+
expect(centerOf(machine("m"), { w: 10, h: 10 })).toEqual({ x: 5, y: 5 });
|
|
103
|
+
});
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
describe("names", () => {
|
|
107
|
+
test("cleans a name into an Isoloom key: lowercase letters, digits and dashes", () => {
|
|
108
|
+
expect(cleanName("Web 1")).toBe("web1");
|
|
109
|
+
expect(cleanName("ÉA_b-c")).toBe("ab-c");
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
test("gives the first number no machine or LAN uses", () => {
|
|
113
|
+
const nodes = [machine("a", { name: "web1" }), lan("b", { name: "web2" }), machine("c", { name: "web4" })];
|
|
114
|
+
expect(freeName("web", nodes)).toBe("web3");
|
|
115
|
+
expect(freeName("db", nodes)).toBe("db1");
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
test("makes ids with their prefix, different each time", () => {
|
|
119
|
+
const [a, b] = [uid("m"), uid("m")];
|
|
120
|
+
expect(a).toMatch(/^m-[a-z0-9]+$/);
|
|
121
|
+
expect(a).not.toBe(b);
|
|
122
|
+
});
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
test("unselected clears the selection of every node", () => {
|
|
126
|
+
const nodes = [{ ...machine("a"), selected: true }, lan("b")];
|
|
127
|
+
expect(unselected(nodes).map((n) => n.selected)).toEqual([false, false]);
|
|
128
|
+
});
|
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
import type { Edge, Node, XYPosition } from "@xyflow/react";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* The lab as drawn on the canvas: machines, LANs, switches and notes are nodes, cables are edges
|
|
5
|
+
* between two ports. `toTopology` translates it into the topology JSON the engine reads
|
|
6
|
+
* (`isothrum_compiler::Topology`), which it compiles to Isoloom's isoloom.yml.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
export type IconId = "container" | "server" | "router" | "shield" | "monitor" | "laptop";
|
|
10
|
+
|
|
11
|
+
/** The Cisco OSes a container node can be (Isoloom's `app.appliance`). */
|
|
12
|
+
export type Appliance = "cisco-iol" | "cisco-iol-l2" | "cisco-vios" | "cisco-vios-l2" | "cisco-csr1000v" | "cisco-c8000v" | "cisco-dynamips";
|
|
13
|
+
|
|
14
|
+
/** Each Cisco OS: its usual vrnetlab image (none for Dynamips: Isoloom builds the emulator). */
|
|
15
|
+
export const APPLIANCES: { id: Appliance; label: string; image: string; switch?: boolean; kvm?: boolean }[] = [
|
|
16
|
+
{ id: "cisco-iol", label: "IOL (IOS on Linux)", image: "vrnetlab/cisco_iol:17.12.01" },
|
|
17
|
+
{ id: "cisco-iol-l2", label: "IOL-L2 (switch)", image: "vrnetlab/cisco_iol:L2-17.12.01", switch: true },
|
|
18
|
+
{ id: "cisco-vios", label: "IOSv", image: "vrnetlab/cisco_vios:15.9.3M6", kvm: true },
|
|
19
|
+
{ id: "cisco-vios-l2", label: "IOSvL2 (switch)", image: "vrnetlab/cisco_viosl2:15.2", switch: true, kvm: true },
|
|
20
|
+
{ id: "cisco-csr1000v", label: "CSR1000v (IOS XE)", image: "vrnetlab/cisco_csr1000v:17.03.08", kvm: true },
|
|
21
|
+
{ id: "cisco-c8000v", label: "Catalyst 8000v (IOS XE)", image: "vrnetlab/cisco_c8000v:17.15.01a", kvm: true },
|
|
22
|
+
{ id: "cisco-dynamips", label: "Dynamips 7200 (classic IOS)", image: "" },
|
|
23
|
+
];
|
|
24
|
+
|
|
25
|
+
/** The OS's name for the `k`th data interface (from 1), as Isoloom numbers them. */
|
|
26
|
+
export function applianceInterface(kind: Appliance, k: number): string {
|
|
27
|
+
switch (kind) {
|
|
28
|
+
case "cisco-iol":
|
|
29
|
+
case "cisco-iol-l2":
|
|
30
|
+
return `Ethernet${String(Math.floor(k / 4))}/${String(k % 4)}`;
|
|
31
|
+
case "cisco-vios":
|
|
32
|
+
case "cisco-vios-l2":
|
|
33
|
+
return `GigabitEthernet0/${String(k)}`;
|
|
34
|
+
case "cisco-csr1000v":
|
|
35
|
+
case "cisco-c8000v":
|
|
36
|
+
return `GigabitEthernet${String(k + 1)}`;
|
|
37
|
+
case "cisco-dynamips":
|
|
38
|
+
return k === 1 ? "FastEthernet0/0" : `FastEthernet${String(Math.floor(k / 2))}/${String(k % 2)}`;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export type MachineData = {
|
|
43
|
+
name: string;
|
|
44
|
+
/** "router": an automatic router, not a machine (its networks reach each other). */
|
|
45
|
+
kind: "container" | "vm" | "router";
|
|
46
|
+
icon: IconId;
|
|
47
|
+
/** Container image (`kind: container`): Isoloom's `app.image`. */
|
|
48
|
+
image: string;
|
|
49
|
+
/** The OS, as Isoloom writes it (`debian@12`, see `OS_CHOICES`) (`kind: vm`): Isoloom's `os`. */
|
|
50
|
+
os: string;
|
|
51
|
+
/** Provisioning steps, one per line (`kind: vm`): Isoloom's `app.steps`. */
|
|
52
|
+
provision: string;
|
|
53
|
+
/** Service ports, comma-separated, each a port and what it speaks when it isn't TCP (`80, 53/udp`): see `readPorts`. */
|
|
54
|
+
ports: string;
|
|
55
|
+
/** The machine the user lands on. */
|
|
56
|
+
access: boolean;
|
|
57
|
+
/**
|
|
58
|
+
* A container with no service of its own (a stock Linux image), kept running idle as a
|
|
59
|
+
* machine to work from. Unset: guessed from the image (see `idleByDefault`).
|
|
60
|
+
*/
|
|
61
|
+
idle?: boolean;
|
|
62
|
+
/** A network OS in its container, wired as its image expects (Cisco IOL, built with vrnetlab). */
|
|
63
|
+
appliance?: Appliance;
|
|
64
|
+
/** The appliance's own configuration (IOS commands), added to the one Isoloom generates. */
|
|
65
|
+
config?: string;
|
|
66
|
+
/** Addresses fixed by hand: LAN name -> last octet. Others take the next free one. */
|
|
67
|
+
pins?: Record<string, number>;
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
/** Stock Linux images: a shell that exits at once, so they need to be kept running idle. */
|
|
71
|
+
const STOCK_IMAGES = ["alpine", "ubuntu", "debian", "kali-rolling", "fedora", "rockylinux", "almalinux", "centos", "busybox", "archlinux", "netshoot"];
|
|
72
|
+
|
|
73
|
+
/** An image's name: its last path segment, without the tag, in lowercase. */
|
|
74
|
+
export function imageName(image: string): string {
|
|
75
|
+
return image.trim().split(":")[0].split("/").pop()?.toLowerCase() ?? "";
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export function idleByDefault(image: string): boolean {
|
|
79
|
+
return STOCK_IMAGES.includes(imageName(image));
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** How a cable or a LAN behaves, as a slow or lossy link: one-way delay and its jitter (ms),
|
|
83
|
+
* loss (%), bandwidth (Mbit/s). Isoloom applies it with `tc netem` (its `tc`). */
|
|
84
|
+
export type LinkConditions = {
|
|
85
|
+
delay?: number;
|
|
86
|
+
jitter?: number;
|
|
87
|
+
loss?: number;
|
|
88
|
+
rate?: number;
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
/** The two ends of a cable: `aPort` on the source machine, `bPort` on the target. */
|
|
92
|
+
export type CableData = {
|
|
93
|
+
aPort: string;
|
|
94
|
+
bPort: string;
|
|
95
|
+
/** How the cable behaves (between two machines; a cable to a LAN takes the LAN's). */
|
|
96
|
+
tc?: LinkConditions;
|
|
97
|
+
/** Drawing only, worked out from what the cable is plugged into (never saved). */
|
|
98
|
+
color?: string;
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
/** A LAN (or VLAN) drawn as an area: machines inside it are on it. */
|
|
102
|
+
export type LanData = {
|
|
103
|
+
name: string;
|
|
104
|
+
cidr: string;
|
|
105
|
+
internet: boolean;
|
|
106
|
+
/** How the LAN behaves (not on a switch's VLAN yet). */
|
|
107
|
+
tc?: LinkConditions;
|
|
108
|
+
/** Only its label is drawn: the LAN, its machines and their addresses are unchanged. */
|
|
109
|
+
hidden?: boolean;
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
/** A VLAN on a switch: `vlan<id>`, with its block. VLANs are lab-wide: `vlan10` is the same
|
|
113
|
+
* network on every switch, and a LAN area named `vlan10` is that VLAN too. */
|
|
114
|
+
type Vlan = {
|
|
115
|
+
id: number;
|
|
116
|
+
cidr: string;
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
/** A switch port: a VLAN (access), "trunk" (every VLAN of the switch), or nothing. */
|
|
120
|
+
export type PortMode = number | "trunk" | null;
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* A switch: access ports, each in a VLAN (or none), and one trunk port that carries every VLAN
|
|
124
|
+
* of the switch. Not a machine: it is the network fabric, so it never appears in isoloom.yml;
|
|
125
|
+
* its VLANs do, as networks, and a machine cabled to a port joins the port's VLAN.
|
|
126
|
+
*/
|
|
127
|
+
type SwitchData = {
|
|
128
|
+
name: string;
|
|
129
|
+
vlans: Vlan[];
|
|
130
|
+
ports: PortMode[];
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
/** A note on the canvas: text, a box or an ellipse. Drawing only: never in isoloom.yml. */
|
|
134
|
+
export type NoteShape = "text" | "rect" | "ellipse";
|
|
135
|
+
export const NOTE_COLORS = ["yellow", "red", "blue", "green", "violet"] as const;
|
|
136
|
+
type NoteData = {
|
|
137
|
+
text: string;
|
|
138
|
+
shape: NoteShape;
|
|
139
|
+
color: (typeof NOTE_COLORS)[number];
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
export type MachineNode = Node<MachineData, "machine">;
|
|
143
|
+
export type LanNode = Node<LanData, "lan">;
|
|
144
|
+
export type SwitchNode = Node<SwitchData, "switch">;
|
|
145
|
+
export type NoteNode = Node<NoteData, "note">;
|
|
146
|
+
export type LabNode = MachineNode | LanNode | SwitchNode | NoteNode;
|
|
147
|
+
export type CableEdge = Edge<CableData, "cable">;
|
|
148
|
+
|
|
149
|
+
export const isMachine = (n: LabNode): n is MachineNode => n.type === "machine";
|
|
150
|
+
/**
|
|
151
|
+
* A router you configure (FRR as a container, or a VM): drawn as a faceplate with interfaces,
|
|
152
|
+
* like a switch. The automatic router (kind "router") has no interfaces to show.
|
|
153
|
+
*/
|
|
154
|
+
export const isRealRouter = (n: LabNode): n is MachineNode => isMachine(n) && n.data.icon === "router" && n.data.kind !== "router";
|
|
155
|
+
export const ROUTER_PORTS = 4;
|
|
156
|
+
export const ROUTER_W = 168;
|
|
157
|
+
export const ROUTER_H = 74;
|
|
158
|
+
export const routerPort = (i: number) => `eth${String(i)}`;
|
|
159
|
+
|
|
160
|
+
/** Routers and firewalls: one connection point, and they snap onto a LAN's edge. */
|
|
161
|
+
export const isAppliance = (n: LabNode): n is MachineNode => isMachine(n) && (n.data.icon === "router" || n.data.icon === "shield");
|
|
162
|
+
export const isLan = (n: LabNode): n is LanNode => n.type === "lan";
|
|
163
|
+
export const isSwitch = (n: LabNode): n is SwitchNode => n.type === "switch";
|
|
164
|
+
export const isNote = (n: LabNode): n is NoteNode => n.type === "note";
|
|
165
|
+
/** A node's name as lists show it (a note has none). */
|
|
166
|
+
export const nodeName = (n: LabNode): string => (isNote(n) ? "note" : n.data.name);
|
|
167
|
+
|
|
168
|
+
export const SWITCH_PORTS = 8;
|
|
169
|
+
/** A switch's faceplate on the canvas (see `SwitchNodeView`). */
|
|
170
|
+
export const SWITCH_W = 306;
|
|
171
|
+
export const SWITCH_H = 74;
|
|
172
|
+
export const TRUNK = "trunk";
|
|
173
|
+
|
|
174
|
+
/** A machine's box on the canvas (see `MachineNodeView`). */
|
|
175
|
+
export const MACHINE_W = 120;
|
|
176
|
+
export const MACHINE_H = 116;
|
|
177
|
+
export const LAN_W = 420;
|
|
178
|
+
export const LAN_H = 300;
|
|
179
|
+
|
|
180
|
+
/** How big a node is drawn: what snapping and "is it inside the LAN" go by. */
|
|
181
|
+
export function sizeOf(n: LabNode): { w: number; h: number } {
|
|
182
|
+
if (n.type === "lan") return { w: n.width ?? LAN_W, h: n.height ?? LAN_H };
|
|
183
|
+
if (n.type === "switch") return { w: SWITCH_W, h: SWITCH_H };
|
|
184
|
+
if (n.type === "note") return { w: n.width ?? 200, h: n.height ?? 90 };
|
|
185
|
+
if (n.data.icon === "router" && n.data.kind !== "router") return { w: ROUTER_W, h: ROUTER_H };
|
|
186
|
+
return { w: MACHINE_W, h: MACHINE_H };
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/** A node's center on the canvas, by its drawn size (measured by React Flow, or `fallback`). */
|
|
190
|
+
export function centerOf(n: LabNode, fallback: { w: number; h: number } = { w: MACHINE_W, h: MACHINE_H }): XYPosition {
|
|
191
|
+
return {
|
|
192
|
+
x: n.position.x + (n.width ?? n.measured?.width ?? fallback.w) / 2,
|
|
193
|
+
y: n.position.y + (n.height ?? n.measured?.height ?? fallback.h) / 2,
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* How a LAN behaves on the canvas, as an Excalidraw frame: it is moved and selected by its label
|
|
199
|
+
* only, so a click or a drag inside it reaches the canvas (placing a device, drawing a selection
|
|
200
|
+
* box). Not saved: applied to every LAN when it is created or loaded.
|
|
201
|
+
*/
|
|
202
|
+
export const LAN_BEHAVIOR = { dragHandle: ".lan-handle", className: "pointer-events-none" } as const;
|
|
203
|
+
|
|
204
|
+
/** Which side of a LAN an appliance sits on, when it is snapped onto one. */
|
|
205
|
+
export type Side = "top" | "bottom" | "left" | "right";
|
|
206
|
+
|
|
207
|
+
/** The canvas's look (see `DesignContext`): hand-drawn, or crisp for a report. */
|
|
208
|
+
export type Design = "sketch" | "business";
|
|
209
|
+
export const DESIGNS: Design[] = ["sketch", "business"];
|
|
210
|
+
|
|
211
|
+
export type Draft = {
|
|
212
|
+
name: string;
|
|
213
|
+
nodes: LabNode[];
|
|
214
|
+
edges: CableEdge[];
|
|
215
|
+
design?: Design;
|
|
216
|
+
};
|
|
217
|
+
|
|
218
|
+
/** Machine names are Isoloom keys: lowercase letters, digits and dashes. */
|
|
219
|
+
export function cleanName(name: string): string {
|
|
220
|
+
return name.toLowerCase().replace(/[^a-z0-9-]/g, "");
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
export function uid(prefix: string): string {
|
|
224
|
+
return `${prefix}-${Date.now().toString(36)}${Math.random().toString(36).slice(2, 7)}`;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/** The first `<prefix><n>` no machine or LAN uses yet. */
|
|
228
|
+
export function freeName(prefix: string, nodes: LabNode[]): string {
|
|
229
|
+
const taken = new Set(nodes.map(nodeName));
|
|
230
|
+
for (let i = 1; ; i++) {
|
|
231
|
+
if (!taken.has(`${prefix}${String(i)}`)) return `${prefix}${String(i)}`;
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/** Every node unselected: what placing or pasting something does to the rest. */
|
|
236
|
+
export function unselected<T extends LabNode | CableEdge>(items: T[]): T[] {
|
|
237
|
+
return items.map((n) => ({ ...n, selected: false }));
|
|
238
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import fc from "fast-check";
|
|
2
|
+
import { describe, expect, test } from "vitest";
|
|
3
|
+
import { DEFAULT_OS, OS_CHOICES, currentMachine, currentOs } from "./os";
|
|
4
|
+
import { machine } from "@/test/lab";
|
|
5
|
+
|
|
6
|
+
describe("currentOs", () => {
|
|
7
|
+
test("writes a stored lab's OS as Isoloom names it now", () => {
|
|
8
|
+
expect(["debian-12", "ubuntu-24.04", "almalinux-9", "kali", "windows-11", "windows-server-2008r2", "centos-7"].map(currentOs)).toEqual([
|
|
9
|
+
"debian@12",
|
|
10
|
+
"ubuntu@24.04",
|
|
11
|
+
"alma@9",
|
|
12
|
+
"kali@rolling",
|
|
13
|
+
"windows@11",
|
|
14
|
+
"windows-server@2008r2",
|
|
15
|
+
"centos@7",
|
|
16
|
+
]);
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
test("keeps an OS already written today's way, and what it can't read", () => {
|
|
20
|
+
for (const os of [...OS_CHOICES, "", "plan9", "debian"]) expect(currentOs(os)).toBe(os);
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
test("never changes an OS twice", () => {
|
|
24
|
+
fc.assert(
|
|
25
|
+
fc.property(fc.string(), (os) => {
|
|
26
|
+
expect(currentOs(currentOs(os))).toBe(currentOs(os));
|
|
27
|
+
}),
|
|
28
|
+
);
|
|
29
|
+
});
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
describe("currentMachine", () => {
|
|
33
|
+
test("renames a stored machine's OS and its Cisco OS, and nothing else", () => {
|
|
34
|
+
const m = machine("sw", { os: "debian-12", appliance: "cisco-viosl2" as never, ports: "22" }).data;
|
|
35
|
+
expect(currentMachine(m)).toEqual({ ...m, os: "debian@12", appliance: "cisco-vios-l2" });
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
test("leaves a machine without a Cisco OS without one", () => {
|
|
39
|
+
expect("appliance" in currentMachine(machine("web").data)).toBe(false);
|
|
40
|
+
});
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
test("a new VM is Debian 12, one of the OSes offered", () => {
|
|
44
|
+
expect(OS_CHOICES).toContain(DEFAULT_OS);
|
|
45
|
+
});
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import type { Appliance } from "./model";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* The OSes a machine can be, as Isoloom writes them (`<family>@<version>`): exactly Isoloom's list,
|
|
5
|
+
* which its engine gives (`choices_json`, checked by the tests). A machine of one of them is its
|
|
6
|
+
* OS, set up by its provisioning steps; each target decides whether it runs as a VM or as a
|
|
7
|
+
* container built from the OS's image.
|
|
8
|
+
*/
|
|
9
|
+
export const OS_CHOICES = [
|
|
10
|
+
"debian@11",
|
|
11
|
+
"debian@12",
|
|
12
|
+
"debian@13",
|
|
13
|
+
"ubuntu@14.04",
|
|
14
|
+
"ubuntu@16.04",
|
|
15
|
+
"ubuntu@18.04",
|
|
16
|
+
"ubuntu@20.04",
|
|
17
|
+
"ubuntu@22.04",
|
|
18
|
+
"ubuntu@24.04",
|
|
19
|
+
"rocky@9",
|
|
20
|
+
"alma@9",
|
|
21
|
+
"alpine@3.19",
|
|
22
|
+
"kali@rolling",
|
|
23
|
+
"fedora@42",
|
|
24
|
+
"centos@7",
|
|
25
|
+
"windows@10",
|
|
26
|
+
"windows@11",
|
|
27
|
+
"windows-server@2008r2",
|
|
28
|
+
"windows-server@2012r2",
|
|
29
|
+
"windows-server@2016",
|
|
30
|
+
"windows-server@2019",
|
|
31
|
+
"windows-server@2022",
|
|
32
|
+
"windows-server@2025",
|
|
33
|
+
];
|
|
34
|
+
|
|
35
|
+
/** The OS a new VM gets. */
|
|
36
|
+
export const DEFAULT_OS = "debian@12";
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* An OS as a lab stored before Isoloom's new format named it (its image-table name: `debian-12`,
|
|
40
|
+
* `almalinux-9`, `kali`, `windows-server-2022`), in today's spelling; any other value as it is.
|
|
41
|
+
*/
|
|
42
|
+
export function currentOs(os: string): string {
|
|
43
|
+
if (os === "kali") return "kali@rolling";
|
|
44
|
+
const m = /^([a-z-]+?)-(\d[\da-z.]*)$/.exec(os);
|
|
45
|
+
if (!m || os.includes("@")) return os;
|
|
46
|
+
const family = m[1] === "almalinux" ? "alma" : m[1];
|
|
47
|
+
return `${family}@${m[2]}`;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** The Cisco OSes stored under the id the editor gave them before Isoloom's (`cisco-viosl2`). */
|
|
51
|
+
const OLD_APPLIANCES: ReadonlyMap<string, Appliance> = new Map([["cisco-viosl2", "cisco-vios-l2"]]);
|
|
52
|
+
|
|
53
|
+
/** A machine as a stored lab or device holds it, in today's spelling: its OS and its Cisco OS. */
|
|
54
|
+
export function currentMachine<M extends { os: string; appliance?: Appliance }>(m: M): M {
|
|
55
|
+
const appliance = m.appliance === undefined ? undefined : (OLD_APPLIANCES.get(m.appliance) ?? m.appliance);
|
|
56
|
+
return { ...m, os: currentOs(m.os), ...(appliance === undefined ? {} : { appliance }) };
|
|
57
|
+
}
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import fc from "fast-check";
|
|
2
|
+
import { describe, expect, test } from "vitest";
|
|
3
|
+
import { shippedEngine, verdict } from "@/test/engine";
|
|
4
|
+
import { conditionsOf, tcOf, type Tc } from "./conditions";
|
|
5
|
+
import { APPLIANCES } from "./model";
|
|
6
|
+
import { OS_CHOICES } from "./os";
|
|
7
|
+
import { PORT_PROTOCOLS, readPorts } from "./ports";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* The editor's model accepts exactly what Isoloom accepts: each list it offers is Isoloom's, and
|
|
11
|
+
* each value it reads or writes is one Isoloom reads the same way, checked against the engine the
|
|
12
|
+
* site ships (Isoloom itself, in WebAssembly).
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
interface Choices {
|
|
16
|
+
os: string[];
|
|
17
|
+
appliances: string[];
|
|
18
|
+
protocols: string[];
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const choices = async () => JSON.parse((await shippedEngine()).choices_json()) as Choices;
|
|
22
|
+
|
|
23
|
+
/** A lab of one container on LAN `lan`, with `tc` on the LAN and `services` on the machine. */
|
|
24
|
+
const lab = (tc?: Tc, services: unknown[] = []) => ({
|
|
25
|
+
name: "parity",
|
|
26
|
+
networks: { lan: { cidr: "10.10.1.0/24", ...(tc ? { tc } : {}) } },
|
|
27
|
+
nodes: { a: { kind: { container: { image: "alpine:3.20" } }, services, networks: { lan: null } } },
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
/** Whether Isoloom finds a problem with `field` of the LAN's `tc`. */
|
|
31
|
+
async function refuses(tc: Tc, field: keyof Tc): Promise<boolean> {
|
|
32
|
+
const v = await verdict(lab(tc));
|
|
33
|
+
return v.problems.some((p) => p.includes(`networks.lan.tc.${field} `));
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
describe("the lists the editor offers are Isoloom's", () => {
|
|
37
|
+
test("every OS, and only those", async () => {
|
|
38
|
+
expect([...OS_CHOICES].sort()).toEqual([...(await choices()).os].sort());
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
test("every network appliance, and only those", async () => {
|
|
42
|
+
expect(APPLIANCES.map((a) => a.id).sort()).toEqual([...(await choices()).appliances].sort());
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
test("every port protocol, and only those", async () => {
|
|
46
|
+
expect([...PORT_PROTOCOLS]).toEqual((await choices()).protocols);
|
|
47
|
+
});
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
/** A number in one of the spellings Rust reads (or doesn't: an empty one, a lone point). */
|
|
51
|
+
const number = (odd: string[]) =>
|
|
52
|
+
fc
|
|
53
|
+
.tuple(fc.constantFrom("", "+", "-"), fc.oneof(fc.nat(100_000).map(String), fc.double({ min: 0, max: 1e6, noNaN: true }).map(String), fc.constantFrom(".5", "5.", "1e3", "2E-2", ...odd)))
|
|
54
|
+
.map(([sign, n]) => `${sign}${n}`);
|
|
55
|
+
|
|
56
|
+
/** A `tc` value: a number, then letters that may or may not be a unit. */
|
|
57
|
+
const measured = fc
|
|
58
|
+
.tuple(number(["", ".", "x1"]), fc.constantFrom("", "ms", "s", "us", "MS", "bit", "kbit", "mbit", "gbit", "bps", "kbps", "mbps", "gbps", "Mbit", "kb", "e", "m s"))
|
|
59
|
+
.map(([n, unit]) => `${n}${unit}`);
|
|
60
|
+
|
|
61
|
+
/** A value Isoloom takes: a number and one of `units`. */
|
|
62
|
+
const taken = (units: string[]) => fc.tuple(number([]), fc.constantFrom(...units)).map(([n, unit]) => `${n}${unit}`);
|
|
63
|
+
|
|
64
|
+
/** Whether two readings hold the same numbers, but for what floating point loses. */
|
|
65
|
+
function same(a: Record<string, number | undefined> | undefined, b: Record<string, number | undefined> | undefined): boolean {
|
|
66
|
+
const keys = new Set([...Object.keys(a ?? {}), ...Object.keys(b ?? {})]);
|
|
67
|
+
return [...keys].every((k) => {
|
|
68
|
+
const [x, y] = [a?.[k], b?.[k]];
|
|
69
|
+
return x !== undefined && y !== undefined && Math.abs(x - y) <= 1e-9 * Math.max(1, Math.abs(x));
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
describe("a link condition is read exactly as Isoloom reads it", () => {
|
|
74
|
+
test("a delay or a rate: Isoloom refuses it if and only if the editor reports it", async () => {
|
|
75
|
+
await fc.assert(
|
|
76
|
+
fc.asyncProperty(measured, fc.constantFrom("delay", "rate"), async (value, field) => {
|
|
77
|
+
const reported = conditionsOf({ [field]: value }).problems.length > 0;
|
|
78
|
+
expect(reported, `${field}: ${value}`).toBe(await refuses({ [field]: value }, field));
|
|
79
|
+
}),
|
|
80
|
+
{ numRuns: 120 },
|
|
81
|
+
);
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
test("a rate without its unit is reported, as Isoloom does", async () => {
|
|
85
|
+
expect(conditionsOf({ rate: "10" }).problems).toEqual([{ field: "rate", value: "10" }]);
|
|
86
|
+
expect(await refuses({ rate: "10" }, "rate")).toBe(true);
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
test("what the editor writes back, Isoloom reads as the same conditions", async () => {
|
|
90
|
+
await fc.assert(
|
|
91
|
+
fc.asyncProperty(taken(["us", "ms", "s"]), taken(["bit", "kbit", "mbit", "gbit", "bps", "kbps", "mbps", "gbps"]), async (delay, rate) => {
|
|
92
|
+
const read = conditionsOf({ delay, rate });
|
|
93
|
+
expect(read.problems).toEqual([]);
|
|
94
|
+
const written = tcOf(read.conditions);
|
|
95
|
+
expect(same(conditionsOf(written).conditions, read.conditions), JSON.stringify({ delay, rate, written })).toBe(true);
|
|
96
|
+
expect((await verdict(lab(written))).problems).toEqual([]);
|
|
97
|
+
}),
|
|
98
|
+
{ numRuns: 40 },
|
|
99
|
+
);
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
test("a loss above 100% or a jitter without a delay is written for Isoloom to report, never changed", async () => {
|
|
103
|
+
expect(tcOf({ loss: 150, jitter: 5 })).toEqual({ loss: 150, jitter: "5ms" });
|
|
104
|
+
const problems = (await verdict(lab(tcOf({ loss: 150, jitter: 5 })))).problems.join("\n");
|
|
105
|
+
expect(problems).toContain("networks.lan.tc.loss");
|
|
106
|
+
expect(problems).toContain("networks.lan.tc.jitter");
|
|
107
|
+
});
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
describe("a machine's ports are read exactly as Isoloom takes them", () => {
|
|
111
|
+
const entry = fc.oneof(
|
|
112
|
+
fc.tuple(fc.integer({ min: -5, max: 70_000 }), fc.constantFrom("", "/tcp", "/udp", "/http", "/https", "/tls", "/TCP", "/sctp", "/")).map(([p, proto]) => `${String(p)}${proto}`),
|
|
113
|
+
fc.constantFrom("http", "80.5", "", "0x50"),
|
|
114
|
+
);
|
|
115
|
+
|
|
116
|
+
test("an entry the editor keeps is a port Isoloom takes; the others are reported", async () => {
|
|
117
|
+
await fc.assert(
|
|
118
|
+
fc.asyncProperty(fc.array(entry, { maxLength: 4 }), async (entries) => {
|
|
119
|
+
const { services, bad } = readPorts(entries.join(", "));
|
|
120
|
+
expect(services.length + bad.length).toBe(entries.filter((e) => e.trim()).length);
|
|
121
|
+
const v = await verdict(lab(undefined, services));
|
|
122
|
+
expect(v.error).toBeNull();
|
|
123
|
+
expect(v.problems.filter((p) => p.includes("machines.a.ports"))).toEqual([]);
|
|
124
|
+
}),
|
|
125
|
+
{ numRuns: 40 },
|
|
126
|
+
);
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
test("a port out of range or a protocol Isoloom doesn't have is refused by the engine too", async () => {
|
|
130
|
+
for (const service of [{ port: 0 }, { port: 70_000 }, { port: 80, protocol: "sctp" }]) {
|
|
131
|
+
expect((await verdict(lab(undefined, [service]))).ok).toBe(false);
|
|
132
|
+
}
|
|
133
|
+
});
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
describe("an OS is one Isoloom knows", () => {
|
|
137
|
+
test("every OS the editor offers compiles, and the earlier spelling doesn't", async () => {
|
|
138
|
+
for (const os of OS_CHOICES) {
|
|
139
|
+
const v = await verdict({ name: "os", networks: { lan: { cidr: "10.10.1.0/24" } }, nodes: { m: { kind: { vm: { os } }, networks: { lan: null } } } });
|
|
140
|
+
expect(v.ok, `${os}: ${v.error ?? ""}`).toBe(true);
|
|
141
|
+
}
|
|
142
|
+
const old = await verdict({ name: "os", nodes: { m: { kind: { vm: { os: "debian-12" } } } } });
|
|
143
|
+
expect(old.error).toContain("unknown OS `debian-12`");
|
|
144
|
+
});
|
|
145
|
+
});
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { describe, expect, test } from "vitest";
|
|
2
|
+
import { pinned } from "./pins";
|
|
3
|
+
|
|
4
|
+
describe("pinned", () => {
|
|
5
|
+
test("pins the last number typed alone", () => {
|
|
6
|
+
expect(pinned(undefined, "office", "10.10.1.10", "20")).toEqual({ office: 20 });
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
test("pins the last number of a whole address typed in the same block", () => {
|
|
10
|
+
expect(pinned({ servers: 7 }, "office", "10.10.1.10", "10.10.1.42")).toEqual({ servers: 7, office: 42 });
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
test("clearing the field unpins that LAN only", () => {
|
|
14
|
+
expect(pinned({ servers: 7, office: 20 }, "office", "10.10.1.20", " ")).toEqual({ servers: 7 });
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
test("refuses what can't be a host's last number", () => {
|
|
18
|
+
for (const typed of ["0", "255", "-3", "abc"]) expect(pinned(undefined, "office", "10.10.1.10", typed)).toBeNull();
|
|
19
|
+
});
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
test("pinned takes the last number of an address in another block", () => {
|
|
23
|
+
expect(pinned(undefined, "office", "10.10.1.10", "192.168.0.9")).toEqual({ office: 9 });
|
|
24
|
+
});
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Fixing a machine's address on a LAN by hand: same block, a new last number (typed alone, or
|
|
3
|
+
* as a whole address); clearing it goes back to the next free one. Null: not a valid address.
|
|
4
|
+
*/
|
|
5
|
+
export function pinned(pins: Record<string, number> | undefined, net: string, ip: string, typed: string): Record<string, number> | null {
|
|
6
|
+
const text = typed.trim();
|
|
7
|
+
if (text === "") return Object.fromEntries(Object.entries(pins ?? {}).filter(([n]) => n !== net));
|
|
8
|
+
const base = ip.slice(0, ip.lastIndexOf(".") + 1);
|
|
9
|
+
const last = Number(text.startsWith(base) ? text.slice(base.length) : text.split(".").pop());
|
|
10
|
+
return Number.isInteger(last) && last > 0 && last < 255 ? { ...pins, [net]: last } : null;
|
|
11
|
+
}
|