@takodotid/circuit 0.34.2 → 0.35.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/README.md +13 -14
- package/dist/chunks/index-20kcjxjg.js +174 -0
- package/dist/chunks/index-2r4bbhac.js +977 -0
- package/dist/chunks/index-n7w3tg5y.js +149 -0
- package/dist/chunks/new-w3zys5y4.js +172 -0
- package/dist/chunks/server-gzsf5me5.js +259 -0
- package/dist/cli/index.js +11347 -9018
- package/dist/helpers/index.js +594 -0
- package/dist/index.js +9 -28
- package/dist/types/core/{addresses.d.ts → address-space.d.ts} +23 -7
- package/dist/types/core/address.d.ts +44 -0
- package/dist/types/core/cabling.d.ts +41 -0
- package/dist/types/core/cross-checks.d.ts +28 -0
- package/dist/types/core/define.d.ts +25 -3
- package/dist/types/core/project-files.d.ts +12 -0
- package/dist/types/core/schema-docs.d.ts +13 -4
- package/dist/types/core/secrets.d.ts +62 -21
- package/dist/types/core/{graph.d.ts → topology.d.ts} +32 -12
- package/dist/types/core/utils.d.ts +8 -0
- package/dist/types/core/validator.d.ts +19 -0
- package/dist/types/helpers/bgp.d.ts +97 -0
- package/dist/types/helpers/checks.d.ts +32 -0
- package/dist/types/helpers/exchange.d.ts +49 -0
- package/dist/types/helpers/filters.d.ts +51 -0
- package/dist/types/helpers/index.d.ts +10 -0
- package/dist/types/helpers/route-server.d.ts +52 -0
- package/dist/types/index.d.ts +5 -3
- package/dist/types/integrations/librenms.d.ts +60 -0
- package/dist/types/integrations/netbox.d.ts +89 -0
- package/dist/types/integrations/peeringdb.d.ts +24 -0
- package/dist/types/integrations/prometheus.d.ts +24 -0
- package/dist/types/integrations/registries.d.ts +8 -0
- package/dist/types/platforms/bgp-wait.d.ts +18 -0
- package/dist/types/platforms/catalog.d.ts +109 -0
- package/dist/types/platforms/derive.d.ts +17 -0
- package/dist/types/platforms/fortios/firewall.d.ts +45 -0
- package/dist/types/platforms/fortios/index.d.ts +134 -0
- package/dist/types/platforms/fortios/names.d.ts +52 -0
- package/dist/types/platforms/fortios/plan.d.ts +104 -0
- package/dist/types/platforms/fortios/render.d.ts +75 -0
- package/dist/types/platforms/fortios/routing.d.ts +56 -0
- package/dist/types/platforms/fortios/tree.d.ts +37 -0
- package/dist/types/platforms/index.d.ts +13 -0
- package/dist/types/platforms/junos/bgp.d.ts +44 -0
- package/dist/types/platforms/junos/index.d.ts +114 -0
- package/dist/types/platforms/junos/render.d.ts +154 -0
- package/dist/types/platforms/line-diff.d.ts +67 -0
- package/dist/types/platforms/platform.d.ts +120 -0
- package/dist/types/platforms/raisecom-ros/index.d.ts +71 -0
- package/dist/types/platforms/raisecom-ros/render.d.ts +41 -0
- package/dist/types/platforms/routeros/export.d.ts +59 -0
- package/dist/types/platforms/routeros/index.d.ts +96 -0
- package/dist/types/{adapters/devices → platforms}/routeros/models.d.ts +4 -0
- package/dist/types/platforms/routeros/plan.d.ts +54 -0
- package/dist/types/platforms/routeros/render.d.ts +240 -0
- package/dist/types/platforms/vrp/index.d.ts +83 -0
- package/dist/types/platforms/vrp/render.d.ts +96 -0
- package/dist/types/schema/common.d.ts +12 -6
- package/dist/types/schema/index.d.ts +1 -0
- package/dist/types/schema/interfaces.d.ts +14 -6
- package/dist/types/schema/policy.d.ts +18 -10
- package/dist/types/schema/routing.d.ts +14 -1
- package/dist/types/schema/system.d.ts +16 -6
- package/dist/types/schema/time-zones.d.ts +2 -0
- package/dist/types/transport/console.d.ts +79 -23
- package/dist/types/transport/path-check.d.ts +32 -0
- package/dist/types/transport/ssh.d.ts +17 -15
- package/package.json +17 -11
- package/src/cli/graph-format.ts +91 -0
- package/src/cli/index.ts +1074 -484
- package/src/cli/new.ts +208 -175
- package/src/cli/rename.ts +97 -91
- package/src/core/address-space.ts +320 -0
- package/src/core/address.ts +202 -0
- package/src/core/cabling.ts +154 -0
- package/src/core/cross-checks.ts +364 -0
- package/src/core/define.ts +26 -3
- package/src/core/project-files.ts +35 -0
- package/src/core/schema-docs.ts +138 -130
- package/src/core/secrets.ts +170 -76
- package/src/core/topology.ts +656 -0
- package/src/core/utils.ts +37 -0
- package/src/core/validator.ts +407 -0
- package/src/helpers/bgp.ts +301 -0
- package/src/helpers/checks.ts +178 -0
- package/src/helpers/exchange.ts +117 -0
- package/src/helpers/filters.ts +107 -0
- package/src/helpers/index.ts +12 -0
- package/src/helpers/route-server.ts +212 -0
- package/src/index.ts +6 -3
- package/src/integrations/librenms.ts +291 -0
- package/src/integrations/netbox.ts +620 -0
- package/src/integrations/peeringdb.ts +173 -0
- package/src/integrations/prometheus.ts +90 -0
- package/src/integrations/registries.ts +25 -0
- package/src/platforms/bgp-wait.ts +41 -0
- package/src/platforms/catalog.ts +69 -0
- package/src/platforms/derive.ts +117 -0
- package/src/{adapters/devices → platforms}/fortios/TRAPS.md +6 -0
- package/src/platforms/fortios/firewall.ts +377 -0
- package/src/platforms/fortios/index.ts +1060 -0
- package/src/platforms/fortios/names.ts +130 -0
- package/src/platforms/fortios/plan.ts +929 -0
- package/src/platforms/fortios/render.ts +779 -0
- package/src/platforms/fortios/routing.ts +476 -0
- package/src/platforms/fortios/tree.ts +129 -0
- package/src/platforms/index.ts +36 -0
- package/src/{adapters/devices → platforms}/junos/TRAPS.md +8 -0
- package/src/platforms/junos/bgp.ts +293 -0
- package/src/platforms/junos/index.ts +1006 -0
- package/src/platforms/junos/render.ts +967 -0
- package/src/platforms/line-diff.ts +244 -0
- package/src/platforms/platform.ts +122 -0
- package/src/platforms/raisecom-ros/TRAPS.md +40 -0
- package/src/platforms/raisecom-ros/index.ts +731 -0
- package/src/platforms/raisecom-ros/render.ts +328 -0
- package/src/{adapters/devices → platforms}/routeros/TRAPS.md +16 -2
- package/src/platforms/routeros/export.ts +342 -0
- package/src/platforms/routeros/index.ts +636 -0
- package/src/{adapters/devices → platforms}/routeros/models.ts +2 -0
- package/src/platforms/routeros/plan.ts +444 -0
- package/src/platforms/routeros/render.ts +1483 -0
- package/src/platforms/vrp/TRAPS.md +45 -0
- package/src/platforms/vrp/index.ts +652 -0
- package/src/platforms/vrp/render.ts +615 -0
- package/src/schema/common.ts +15 -6
- package/src/schema/index.ts +1 -0
- package/src/schema/interfaces.ts +15 -5
- package/src/schema/policy.ts +22 -10
- package/src/schema/routing.ts +16 -1
- package/src/schema/system.ts +16 -6
- package/src/schema/time-zones.ts +482 -0
- package/src/studio/data.ts +172 -168
- package/src/studio/git.ts +95 -90
- package/src/studio/public/app.js +161 -150
- package/src/studio/public/compare.js +219 -212
- package/src/studio/public/diff.js +77 -75
- package/src/studio/public/dom.js +118 -108
- package/src/studio/public/evaluate.js +584 -545
- package/src/studio/public/export.js +120 -118
- package/src/studio/public/graph.js +1343 -1266
- package/src/studio/public/net.js +115 -109
- package/src/studio/public/tables.js +1135 -1083
- package/src/studio/public/tools.js +561 -529
- package/src/studio/server.ts +202 -196
- package/src/transport/console.ts +363 -271
- package/src/transport/path-check.ts +91 -0
- package/src/transport/ssh.ts +98 -94
- package/templates/colocation/checks.ts +11 -6
- package/templates/colocation/edge-01.ts +9 -9
- package/templates/colocation/routing.ts +2 -2
- package/templates/colocation/tenancy.ts +3 -3
- package/templates/colocation/tor-01.ts +3 -3
- package/templates/edge-router/checks.ts +4 -4
- package/templates/edge-router/edge-01.ts +5 -5
- package/templates/edge-router/routing.ts +2 -2
- package/templates/project/AGENTS.md +5 -1
- package/templates/single-site/checks.ts +2 -2
- package/dist/chunks/index-47debt16.js +0 -956
- package/dist/chunks/index-v8zgr7wm.js +0 -128
- package/dist/chunks/new-ny89s7y7.js +0 -166
- package/dist/chunks/server-tjbb3rqc.js +0 -251
- package/dist/presets/index.js +0 -394
- package/dist/types/adapters/devices/catalog.d.ts +0 -86
- package/dist/types/adapters/devices/derive.d.ts +0 -7
- package/dist/types/adapters/devices/fortios/firewall.d.ts +0 -23
- package/dist/types/adapters/devices/fortios/index.d.ts +0 -58
- package/dist/types/adapters/devices/fortios/names.d.ts +0 -48
- package/dist/types/adapters/devices/fortios/plan.d.ts +0 -28
- package/dist/types/adapters/devices/fortios/render.d.ts +0 -38
- package/dist/types/adapters/devices/fortios/routing.d.ts +0 -32
- package/dist/types/adapters/devices/fortios/tree.d.ts +0 -33
- package/dist/types/adapters/devices/index.d.ts +0 -3
- package/dist/types/adapters/devices/junos/bgp.d.ts +0 -11
- package/dist/types/adapters/devices/junos/index.d.ts +0 -42
- package/dist/types/adapters/devices/junos/render.d.ts +0 -52
- package/dist/types/adapters/devices/lines.d.ts +0 -56
- package/dist/types/adapters/devices/raisecom-ros/index.d.ts +0 -28
- package/dist/types/adapters/devices/routeros/export.d.ts +0 -39
- package/dist/types/adapters/devices/routeros/index.d.ts +0 -30
- package/dist/types/adapters/devices/routeros/plan.d.ts +0 -4
- package/dist/types/adapters/devices/routeros/render.d.ts +0 -100
- package/dist/types/adapters/devices/types.d.ts +0 -40
- package/dist/types/adapters/devices/vrp/index.d.ts +0 -27
- package/dist/types/adapters/registries/index.d.ts +0 -3
- package/dist/types/adapters/registries/librenms.d.ts +0 -15
- package/dist/types/adapters/registries/peeringdb.d.ts +0 -16
- package/dist/types/adapters/registries/prometheus.d.ts +0 -16
- package/dist/types/core/across.d.ts +0 -4
- package/dist/types/core/addr.d.ts +0 -34
- package/dist/types/core/files.d.ts +0 -6
- package/dist/types/core/merge.d.ts +0 -4
- package/dist/types/core/time.d.ts +0 -2
- package/dist/types/core/validate.d.ts +0 -2
- package/dist/types/presets/index.d.ts +0 -183
- package/src/adapters/devices/catalog.ts +0 -41
- package/src/adapters/devices/derive.ts +0 -59
- package/src/adapters/devices/fortios/firewall.ts +0 -362
- package/src/adapters/devices/fortios/index.ts +0 -923
- package/src/adapters/devices/fortios/names.ts +0 -115
- package/src/adapters/devices/fortios/plan.ts +0 -888
- package/src/adapters/devices/fortios/render.ts +0 -768
- package/src/adapters/devices/fortios/routing.ts +0 -444
- package/src/adapters/devices/fortios/tree.ts +0 -121
- package/src/adapters/devices/index.ts +0 -9
- package/src/adapters/devices/junos/bgp.ts +0 -269
- package/src/adapters/devices/junos/index.ts +0 -862
- package/src/adapters/devices/junos/render.ts +0 -872
- package/src/adapters/devices/lines.ts +0 -212
- package/src/adapters/devices/raisecom-ros/TRAPS.md +0 -25
- package/src/adapters/devices/raisecom-ros/index.ts +0 -693
- package/src/adapters/devices/routeros/export.ts +0 -321
- package/src/adapters/devices/routeros/index.ts +0 -346
- package/src/adapters/devices/routeros/plan.ts +0 -398
- package/src/adapters/devices/routeros/render.ts +0 -1490
- package/src/adapters/devices/types.ts +0 -38
- package/src/adapters/devices/vrp/TRAPS.md +0 -26
- package/src/adapters/devices/vrp/index.ts +0 -865
- package/src/adapters/registries/index.ts +0 -19
- package/src/adapters/registries/librenms.ts +0 -92
- package/src/adapters/registries/peeringdb.ts +0 -169
- package/src/adapters/registries/prometheus.ts +0 -71
- package/src/cli/graph.ts +0 -89
- package/src/core/across.ts +0 -350
- package/src/core/addr.ts +0 -174
- package/src/core/addresses.ts +0 -316
- package/src/core/files.ts +0 -32
- package/src/core/graph.ts +0 -640
- package/src/core/merge.ts +0 -19
- package/src/core/time.ts +0 -15
- package/src/core/validate.ts +0 -348
- package/src/presets/index.ts +0 -579
- /package/dist/types/{adapters/devices → platforms}/fortios/models.d.ts +0 -0
- /package/dist/types/{adapters/devices → platforms}/junos/models.d.ts +0 -0
- /package/dist/types/{adapters/devices → platforms}/raisecom-ros/models.d.ts +0 -0
- /package/dist/types/{adapters/devices → platforms}/vrp/models.d.ts +0 -0
- /package/src/{adapters/devices → platforms}/fortios/models.ts +0 -0
- /package/src/{adapters/devices → platforms}/junos/models.ts +0 -0
- /package/src/{adapters/devices → platforms}/raisecom-ros/models.ts +0 -0
- /package/src/{adapters/devices → platforms}/vrp/models.ts +0 -0
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
// Convergence for platforms whose configuration is a list of lines in nested blocks, such as `interface X` holding its settings. A line the render does not hold is undone, so the device ends up holding exactly the render.
|
|
2
|
+
|
|
3
|
+
import type { Plan, Step } from "./platform";
|
|
4
|
+
|
|
5
|
+
export type Dialect = {
|
|
6
|
+
/** A line at the margin that opens a block, for platforms that do not indent a block's lines. */
|
|
7
|
+
block: RegExp;
|
|
8
|
+
/** Whether a block's lines are indented under it. When they are, any line followed by deeper lines opens a block. */
|
|
9
|
+
indented: boolean;
|
|
10
|
+
/** A line at the margin that closes every open block. */
|
|
11
|
+
terminator: string;
|
|
12
|
+
/** Lines that describe the device rather than configure it, or that the platform prints whatever is set. */
|
|
13
|
+
ignore: (line: string) => boolean;
|
|
14
|
+
/** The command that removes a line. */
|
|
15
|
+
undo: (line: string) => string;
|
|
16
|
+
/** Leaves one level of block. */
|
|
17
|
+
leave: string;
|
|
18
|
+
/** Lines sent before the undos of their block, because the platform refuses an undo until they are in place. */
|
|
19
|
+
first?: RegExp;
|
|
20
|
+
/** A line in the form both sides compare in, for example a secret the device prints as a cipher. */
|
|
21
|
+
canon?: (line: string) => string;
|
|
22
|
+
/** Where a block is sent among the others, lowest first, so that nothing is referenced before it exists. In the order they differ when absent. */
|
|
23
|
+
order?: (path: string[]) => number;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
/** One line, with the blocks it sits in, outermost first. */
|
|
27
|
+
export type Entry = { path: string[]; line: string };
|
|
28
|
+
|
|
29
|
+
/** What changes inside one block. */
|
|
30
|
+
export type Group = { path: string[]; remove: string[]; add: string[] };
|
|
31
|
+
|
|
32
|
+
/** Commands a platform needs around one group, such as releasing a binding before a change and restoring it after. */
|
|
33
|
+
export type Around = (group: Group) => { before: string[]; after: string[] };
|
|
34
|
+
|
|
35
|
+
export class LineDiff {
|
|
36
|
+
/** What a switch prints in place of a secret: the cipher of a password, a community or an SNMPv3 key, and a Raisecom community in clear. Never an algorithm list such as `ssh server cipher`. */
|
|
37
|
+
private static readonly STORED_CREDENTIAL =
|
|
38
|
+
/((?:password|community read|-mode \S+) (?:irreversible-)?cipher|^snmp-server community)( +)\S+/gm;
|
|
39
|
+
|
|
40
|
+
/** How deep a line is indented. */
|
|
41
|
+
private static indentOf(raw: string): number {
|
|
42
|
+
return raw.length - raw.trimStart().length;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
static entries(text: string, dialect: Dialect): Entry[] {
|
|
46
|
+
const lines = text
|
|
47
|
+
.replace(/\r/g, "")
|
|
48
|
+
.split("\n")
|
|
49
|
+
.filter((raw) => raw.trim() !== "");
|
|
50
|
+
const result: Entry[] = [];
|
|
51
|
+
const open: { indent: number; header: string }[] = [];
|
|
52
|
+
|
|
53
|
+
for (let i = 0; i < lines.length; i++) {
|
|
54
|
+
const raw = lines[i]!;
|
|
55
|
+
const indent = LineDiff.indentOf(raw);
|
|
56
|
+
const line = raw.trim().replace(/\s+/g, " ");
|
|
57
|
+
|
|
58
|
+
if (line === dialect.terminator) {
|
|
59
|
+
if (indent === 0) open.length = 0;
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
if (dialect.ignore(line)) continue;
|
|
64
|
+
|
|
65
|
+
if (dialect.indented) {
|
|
66
|
+
while (open.length && open.at(-1)!.indent >= indent) open.pop();
|
|
67
|
+
} else if (indent === 0 && dialect.block.test(line)) {
|
|
68
|
+
open.length = 0;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
result.push({ path: open.map((block) => block.header), line });
|
|
72
|
+
|
|
73
|
+
const isContent = (candidate: string) => candidate.trim() !== dialect.terminator && !dialect.ignore(candidate.trim());
|
|
74
|
+
const nextLine = lines.slice(i + 1).find(isContent);
|
|
75
|
+
|
|
76
|
+
const opensBlock = dialect.indented
|
|
77
|
+
? nextLine !== undefined && LineDiff.indentOf(nextLine) > indent
|
|
78
|
+
: indent === 0 && dialect.block.test(line);
|
|
79
|
+
|
|
80
|
+
if (opensBlock) open.push({ indent, header: line });
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return result;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** The part of a line that names what it sets: `rule 5`, a free-text keyword, or the line up to its value. Two lines with one head are one setting with two values. */
|
|
87
|
+
static head(line: string): string {
|
|
88
|
+
const rule = /^rule \d+/.exec(line);
|
|
89
|
+
if (rule) return rule[0];
|
|
90
|
+
|
|
91
|
+
if (/^(description|name) /.test(line)) return line.split(" ")[0]!;
|
|
92
|
+
|
|
93
|
+
const words = line.split(" ");
|
|
94
|
+
return words.slice(0, Math.max(1, words.length - 1)).join(" ");
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* What differs, by block.
|
|
99
|
+
*
|
|
100
|
+
* A device line replaced by a desired line with the same head is not undone; sending the new one is enough. A block the render does not hold goes with its header, and its lines with it.
|
|
101
|
+
*/
|
|
102
|
+
static compare(desired: string, current: string, dialect: Dialect): Group[] {
|
|
103
|
+
const canon = dialect.canon ?? ((line: string) => line);
|
|
104
|
+
const keyOf = (entry: Entry) => [...entry.path, canon(entry.line)].join("\u0000");
|
|
105
|
+
const pathKey = (path: string[]) => path.join("\u0000");
|
|
106
|
+
|
|
107
|
+
const wanted = LineDiff.entries(desired, dialect);
|
|
108
|
+
const existing = LineDiff.entries(current, dialect);
|
|
109
|
+
const wantedKeys = new Set(wanted.map(keyOf));
|
|
110
|
+
const existingKeys = new Set(existing.map(keyOf));
|
|
111
|
+
const wantedBlocks = new Set(wanted.map((entry) => pathKey([...entry.path, entry.line])));
|
|
112
|
+
|
|
113
|
+
const groups = new Map<string, Group>();
|
|
114
|
+
const groupFor = (path: string[]) => {
|
|
115
|
+
const key = pathKey(path);
|
|
116
|
+
if (!groups.has(key)) groups.set(key, { path, remove: [], add: [] });
|
|
117
|
+
return groups.get(key)!;
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
for (const entry of wanted) {
|
|
121
|
+
if (existingKeys.has(keyOf(entry))) continue;
|
|
122
|
+
|
|
123
|
+
// A header goes with its block's own step, never on its own.
|
|
124
|
+
const ownBlock = pathKey([...entry.path, entry.line]);
|
|
125
|
+
const opensBlock = wanted.some((other) => {
|
|
126
|
+
const deeper = other.path.length > entry.path.length;
|
|
127
|
+
return deeper && pathKey(other.path.slice(0, entry.path.length + 1)) === ownBlock;
|
|
128
|
+
});
|
|
129
|
+
if (opensBlock) continue;
|
|
130
|
+
|
|
131
|
+
groupFor(entry.path).add.push(entry.line);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// Undone in reverse order, so a line is undone before the line it depends on.
|
|
135
|
+
for (const entry of [...existing].reverse()) {
|
|
136
|
+
if (wantedKeys.has(keyOf(entry))) continue;
|
|
137
|
+
|
|
138
|
+
// Inside a block the render does not hold: undoing the header removes it.
|
|
139
|
+
const insideRemovedBlock = entry.path.some((_, depth) => {
|
|
140
|
+
const enclosing = pathKey(entry.path.slice(0, depth + 1));
|
|
141
|
+
return !wantedBlocks.has(enclosing);
|
|
142
|
+
});
|
|
143
|
+
if (insideRemovedBlock) continue;
|
|
144
|
+
|
|
145
|
+
const replaced = wanted.some((other) => {
|
|
146
|
+
const sameBlock = pathKey(other.path) === pathKey(entry.path);
|
|
147
|
+
const isNew = !existingKeys.has(keyOf(other));
|
|
148
|
+
return sameBlock && isNew && LineDiff.head(other.line) === LineDiff.head(entry.line);
|
|
149
|
+
});
|
|
150
|
+
if (!replaced) groupFor(entry.path).remove.push(entry.line);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
return [...groups.values()].filter((group) => group.add.length || group.remove.length);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Send every line that holds a secret again, for `apply --secrets`. A device prints a secret as a cipher, so a line whose secret changed compares equal and is otherwise never sent.
|
|
158
|
+
*/
|
|
159
|
+
static withSecrets(groups: Group[], desired: string, dialect: Dialect): Group[] {
|
|
160
|
+
const pathKey = (path: string[]) => path.join("\u0000");
|
|
161
|
+
const result = groups.map((group) => ({ ...group, add: [...group.add] }));
|
|
162
|
+
|
|
163
|
+
for (const entry of LineDiff.entries(desired, dialect)) {
|
|
164
|
+
if (!entry.line.includes("<secret:")) continue;
|
|
165
|
+
|
|
166
|
+
let group = result.find((candidate) => pathKey(candidate.path) === pathKey(entry.path));
|
|
167
|
+
if (!group) {
|
|
168
|
+
group = { path: entry.path, remove: [], add: [] };
|
|
169
|
+
result.push(group);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
if (!group.add.includes(entry.line)) group.add.push(entry.line);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
return result;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Replace a changed VLAN list line with only what changed.
|
|
180
|
+
*
|
|
181
|
+
* A platform keeps its VLANs as one line, such as `vlan batch 10 20 to 30`. Undoing the old line would delete VLANs still in use, so the plan adds the new IDs and removes the gone ones instead.
|
|
182
|
+
*/
|
|
183
|
+
static vlanListChange(
|
|
184
|
+
groups: Group[],
|
|
185
|
+
prefix: string,
|
|
186
|
+
parse: (line: string) => number[],
|
|
187
|
+
format: (ids: number[]) => string[],
|
|
188
|
+
removal: (ids: number[]) => string[]
|
|
189
|
+
): void {
|
|
190
|
+
const top = groups.find((group) => group.path.length === 0);
|
|
191
|
+
if (!top) return;
|
|
192
|
+
|
|
193
|
+
const old = top.remove.find((line) => line.startsWith(prefix));
|
|
194
|
+
const fresh = top.add.find((line) => line.startsWith(prefix));
|
|
195
|
+
if (!old && !fresh) return;
|
|
196
|
+
|
|
197
|
+
const before = old ? parse(old) : [];
|
|
198
|
+
const after = fresh ? parse(fresh) : [];
|
|
199
|
+
|
|
200
|
+
top.remove = top.remove.filter((line) => line !== old);
|
|
201
|
+
top.add = top.add.filter((line) => line !== fresh);
|
|
202
|
+
|
|
203
|
+
const added = after.filter((id) => !before.includes(id));
|
|
204
|
+
const removed = before.filter((id) => !after.includes(id));
|
|
205
|
+
|
|
206
|
+
// New VLANs exist before anything uses them; gone ones go after.
|
|
207
|
+
if (added.length) top.add.unshift(...format(added));
|
|
208
|
+
if (removed.length) top.remove.push(...removal(removed));
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
private static nothingAround(): ReturnType<Around> {
|
|
212
|
+
return { before: [], after: [] };
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/** The steps for the groups: enter each block of the path, undo, add, then leave each level. */
|
|
216
|
+
static steps(groups: Group[], dialect: Dialect, around: Around = LineDiff.nothingAround): Plan["steps"] {
|
|
217
|
+
const rank = dialect.order ?? (() => 0);
|
|
218
|
+
const ordered = [...groups].sort((a, b) => rank(a.path) - rank(b.path));
|
|
219
|
+
|
|
220
|
+
return ordered.map((group): Step => {
|
|
221
|
+
const { before, after } = around(group);
|
|
222
|
+
|
|
223
|
+
const early = group.add.filter((line) => dialect.first?.test(line));
|
|
224
|
+
const late = group.add.filter((line) => !early.includes(line));
|
|
225
|
+
|
|
226
|
+
// A block header sent on its own, such as an empty `bfd`, enters its view; it is left at once so the next line is not sent into it.
|
|
227
|
+
const added = late.flatMap((line) => (dialect.block.test(line) ? [line, dialect.leave] : [line]));
|
|
228
|
+
|
|
229
|
+
const inner = [...early, ...group.remove.map(dialect.undo), ...added];
|
|
230
|
+
const leave = group.path.map(() => dialect.leave);
|
|
231
|
+
|
|
232
|
+
return {
|
|
233
|
+
title: group.path.join(" > ") || "(top level)",
|
|
234
|
+
show: [...group.remove.map((line) => `- ${line}`), ...group.add.map((line) => `+ ${line}`)],
|
|
235
|
+
send: [...before, ...group.path, ...inner, ...leave, ...after],
|
|
236
|
+
};
|
|
237
|
+
});
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/** A device prints stored credentials as ciphers. They never leave the read. */
|
|
241
|
+
static scrub(text: string): string {
|
|
242
|
+
return text.replace(LineDiff.STORED_CREDENTIAL, "$1$2<redacted>");
|
|
243
|
+
}
|
|
244
|
+
}
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import type { Neighbor } from "../core/cabling";
|
|
2
|
+
import type { Device, Family } from "../schema";
|
|
3
|
+
import type { ConsoleDialect } from "../transport/console";
|
|
4
|
+
import type { WaitLimits } from "./bgp-wait";
|
|
5
|
+
import type { PlatformName } from "./catalog";
|
|
6
|
+
|
|
7
|
+
/** One part of a plan: what a reviewer reads, and what is sent. */
|
|
8
|
+
export type Step = { title: string; show: string[]; send: string[] };
|
|
9
|
+
|
|
10
|
+
/** The commands that turn what a device runs into what its config says. `problems` blocks an apply: something the adapter cannot converge. */
|
|
11
|
+
export type Plan = { steps: Step[]; problems: string[] };
|
|
12
|
+
|
|
13
|
+
export type ApplyOptions = {
|
|
14
|
+
/** Also send every secret, so a rotated value reaches the device. Secrets cannot be read back, so they are otherwise only sent when the object holding them is new. */
|
|
15
|
+
secrets: boolean;
|
|
16
|
+
/** Minutes before a platform that can restore itself does so, if the run does not prove access survived. */
|
|
17
|
+
rollback: number;
|
|
18
|
+
/** Reach the device through its serial console, `connection.console`, instead of SSH to its management address. */
|
|
19
|
+
console?: boolean;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
/** How a device is read. */
|
|
23
|
+
export type ReadOptions = Pick<ApplyOptions, "console">;
|
|
24
|
+
|
|
25
|
+
/** A login on a device for commands that only read, as `circuit show` runs them. */
|
|
26
|
+
export type ReadSession = {
|
|
27
|
+
/**
|
|
28
|
+
* Run one command and return what it printed, without its echo or the prompt.
|
|
29
|
+
*
|
|
30
|
+
* At `timeout` milliseconds, a command run on its own over SSH is left, and what it printed so far is returned. In a shell or on a console, where it would hold up every command after it, the run fails instead.
|
|
31
|
+
*/
|
|
32
|
+
run(command: string, timeout?: number): Promise<string>;
|
|
33
|
+
/** Log out. */
|
|
34
|
+
close(): void;
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
/** How the checks after an apply reach the device, and how long they wait. */
|
|
38
|
+
export type HealthOptions = ReadOptions & {
|
|
39
|
+
/** How long BGP sessions established before the apply have to be established again. `BgpWait.AFTER_APPLY` when absent. */
|
|
40
|
+
bgp?: WaitLimits;
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
/** The checks after an apply, holding what they compare against, which was read before the plan was sent. */
|
|
44
|
+
export type HealthCheck = {
|
|
45
|
+
/** Run the checks. Gives one line per problem, each saying what to do; none when the device is healthy. */
|
|
46
|
+
run(): Promise<string[]>;
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
/** A BGP neighbor in the config and its live session, as `circuit bgp` shows it. A neighbor with no session is not established. */
|
|
50
|
+
export type BgpSession = {
|
|
51
|
+
/** The neighbor's name in the config. */
|
|
52
|
+
neighbor: string;
|
|
53
|
+
/** The session's remote address, or `-` when there is no session. */
|
|
54
|
+
remote: string;
|
|
55
|
+
/** Whether the session is established. */
|
|
56
|
+
established: boolean;
|
|
57
|
+
/** How many prefixes it received, as the device prints it, or `-`. */
|
|
58
|
+
prefixes: string;
|
|
59
|
+
/** How long it has been up, as the device prints it, or `no session`. */
|
|
60
|
+
uptime: string;
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
/** One BGP log line: when, as the device printed it, such as `2026-10-24 03:12:44`, and the rest of the line. */
|
|
64
|
+
export type BgpLogLine = { at: string; message: string };
|
|
65
|
+
|
|
66
|
+
/** The operating system a platform runs, and the versions it is proved on. */
|
|
67
|
+
export type OsSupport = {
|
|
68
|
+
/** Its name in full, such as `MikroTik RouterOS`. */
|
|
69
|
+
name: string;
|
|
70
|
+
/** The versions the adapter renders for, such as `7`. */
|
|
71
|
+
versions: string;
|
|
72
|
+
/** The device and version it was proved on, such as `CCR2216-1G-12XS-2XQ, RouterOS 7.23`. */
|
|
73
|
+
provedOn: string;
|
|
74
|
+
/** Whether a read or an apply refuses a device on another version. */
|
|
75
|
+
checked: boolean;
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
/** The switch rules a render makes from a device's hardware ACLs, counted. */
|
|
79
|
+
export type SwitchRules = {
|
|
80
|
+
/** How many rules there are. */
|
|
81
|
+
count: number;
|
|
82
|
+
/** How many distinct port ranges they use, such as `0-1023`. */
|
|
83
|
+
ranges: number;
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
export abstract class Platform {
|
|
87
|
+
/** The name a device's `platform` field takes. */
|
|
88
|
+
abstract readonly name: PlatformName;
|
|
89
|
+
abstract readonly os: OsSupport;
|
|
90
|
+
/** File extension of a saved snapshot, for syntax highlighting. */
|
|
91
|
+
abstract readonly extension: string;
|
|
92
|
+
/** How the platform's serial console asks for a login and shows its prompt. Present when the platform can be read and applied through a console, `--console`, and opened with `circuit console`. */
|
|
93
|
+
readonly console: ConsoleDialect | undefined = undefined;
|
|
94
|
+
/** Fields this platform cannot express. Validation fails on any of them, so nothing is skipped silently. */
|
|
95
|
+
abstract unsupported(device: Device): string[];
|
|
96
|
+
/** The switch rules the render makes from the device's hardware ACLs, so `validate` can hold them to the model's limits. Absent on a platform whose rules are not counted. */
|
|
97
|
+
switchRules?(device: Device): SwitchRules;
|
|
98
|
+
/** The whole configuration the device should run. Secrets appear as markers, never values. */
|
|
99
|
+
abstract render(device: Device): string;
|
|
100
|
+
/** What the device runs now, secrets removed, in the form `render` produces where the platform allows. */
|
|
101
|
+
abstract read(device: Device, network: readonly Device[], options?: ReadOptions): Promise<string>;
|
|
102
|
+
/** The LLDP neighbors the device sees, by Circuit's port names, for `snapshot` to save. Absent on a platform whose LLDP command and its output are not proved on a device yet. */
|
|
103
|
+
neighbors?(device: Device, network: readonly Device[], options?: ReadOptions): Promise<Neighbor[]>;
|
|
104
|
+
/** What it takes to go from `current` to `desired`. */
|
|
105
|
+
abstract plan(desired: string, current: string, options: ApplyOptions): Plan;
|
|
106
|
+
/** Replace the contents of a prefix set fetched from a registry. Absent on a platform that cannot hold one. */
|
|
107
|
+
fill?(device: Device, network: readonly Device[], set: string, family: Family, prefixes: string[]): Promise<void>;
|
|
108
|
+
/** Send a plan, keeping the device reachable or restoring it. */
|
|
109
|
+
abstract apply(device: Device, network: readonly Device[], plan: Plan, options: ApplyOptions): Promise<void>;
|
|
110
|
+
/** Whether a command only reads on this platform, so `circuit show` may send it. Anything not known to only read is refused. */
|
|
111
|
+
abstract readOnly(command: string): boolean;
|
|
112
|
+
/** Log in for commands that only read, over SSH or on the console, the way the platform's read logs in. */
|
|
113
|
+
abstract session(device: Device, options?: ReadOptions): Promise<ReadSession>;
|
|
114
|
+
/** Each BGP neighbor in the config, in its order, with its live session, read over a `session()`. Absent on a platform `circuit bgp` does not support yet. */
|
|
115
|
+
bgpSessions?(device: Device, session: ReadSession): Promise<BgpSession[]>;
|
|
116
|
+
/** The device's BGP log lines from the last `minutes`, by the device's own clock, read over a `session()`. */
|
|
117
|
+
bgpLog?(session: ReadSession, minutes: number): Promise<BgpLogLine[]>;
|
|
118
|
+
/**
|
|
119
|
+
* Checks that the device is healthy after an apply, not only that it took the commands. Called before the plan is sent, to read what the checks compare against; the CLI runs them after the apply and the snapshot. Absent on a platform with no checks yet.
|
|
120
|
+
*/
|
|
121
|
+
health?(device: Device, plan: Plan, options?: HealthOptions): Promise<HealthCheck>;
|
|
122
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# Raisecom ROS traps
|
|
2
|
+
|
|
3
|
+
Behaviour that looks like something else. Each one has been hit on a RAX721 running ROS 6.5.2.
|
|
4
|
+
|
|
5
|
+
- Commands apply as they arrive and persist only on `write`.
|
|
6
|
+
- The SSH server offers only an `ssh-rsa` host key. The adapter allows it for this platform alone; key exchange, cipher and MAC still negotiate modern ones.
|
|
7
|
+
- `show running-config` stops about halfway with no error unless `terminal page-break disable` is sent first and the read waits several seconds of silence.
|
|
8
|
+
- A `?` sent by a script is executed. Once it applied an address with a /8 mask.
|
|
9
|
+
- An access-list cannot change while any service uses it: `ACL 1000 is in use`. Every service that uses it is unbound first and bound again after. `no ssh2 access-list` needs the list number.
|
|
10
|
+
- `portswitch`, `switchport trunk allowed vlan` and `switchport mode` ask for `y` and do nothing without it, except when they need no confirming: `switchport mode trunk` on a port leaving access mode answers `Set successfully` at once, and a `y` sent after it is refused, `Error input`.
|
|
11
|
+
- A VLAN is removed with `no vlan <id>`, not `no create vlan`.
|
|
12
|
+
- `no storm-control mode` is refused with `Error input`. The modes are `kbps`, the default, which prints nothing, and `pps`; leaving `pps` is setting `kbps`.
|
|
13
|
+
- A port's speed or duplex set to something else and back to auto prints `speed auto` and `duplex auto` from then on, though auto is the default. `no duplex auto` is refused with `Error input`.
|
|
14
|
+
- Tab after a word the CLI does not know answers `Error input`, as if the line were sent. Like `?`, it is not a safe way to explore.
|
|
15
|
+
- The factory account `raisecom` cannot be removed and its service type cannot change. Only its password is ours.
|
|
16
|
+
- Factory SNMP communities survive hardening, and `private` can write. The render removes both.
|
|
17
|
+
- Several commands are not the usual spelling: `terminal page-break disable`, `telnet-server disable`, `ip rarp server disable`.
|
|
18
|
+
- An access-list description takes one word. A space is refused with `Error input`.
|
|
19
|
+
- The out-of-band port `fastethernet 1/0/1` cannot be shut down and is not in the catalog.
|
|
20
|
+
- A LAG is `interface port-channel N`. LACP is its default and prints nothing; a static LAG prints `mode manual`. The LAG takes layer 2 settings only after `portswitch`, and a port joins with `portswitch` then `port-channel N`.
|
|
21
|
+
- An SNMPv3 user prints as localized keys, `authkey sha <hex> privkey aes128 <hex>`, which authenticate as well as the passphrase. The read redacts them. An access group prints a `notify internet` it was not given. A user's group is removed with `no snmp-server group user <user> usm`.
|
|
22
|
+
- A VRRP group is enabled once it has an address and preempts unless `no vrrp N preempt`. `no vrrp N ip <address>` removes the whole group.
|
|
23
|
+
- OSPF settings for an interface sit on the interface, `ip ospf cost`, `ip ospf network ptp`, `ip ospf passive-interface enable`; passive is undone with `... disable`, not `no`.
|
|
24
|
+
- `create vlan <list> active` has every VLAN on one line. A change creates only the new VLANs and removes each one that is gone with `no vlan <id>`.
|
|
25
|
+
- An access-list rule is not replaced by one with the same number: the device answers `Set unsuccessfully.` and keeps the old rule. The old one is removed first. `no rule N` answers slowly, after a second or more.
|
|
26
|
+
- `show running-config` can stop partway even with paging off and six seconds of silence, with no error: on 2026-10-23 three reads of an RAX721 in a row gave plans of 62, 0 and 158 commands against an unchanged switch. A dump that does not end at the prompt is read again, and after three the read fails rather than planning against a part.
|
|
27
|
+
- On a lossy path a TCP retransmit can stall `show running-config` for several seconds mid-dump, and a read that stopped at the first long silence gave up after three tries (seen with 5 to 25% loss, #86). The read now waits for the dump to end at the prompt, up to 30 seconds of silence and 150 seconds in all.
|
|
28
|
+
- `show lldp remote` shortens port names (`TGE1/3/3`, `100GE1/1/4`) and pads its columns, so a system name with a space cannot be told apart. The read uses `show lldp remote detail`, which spells each port in full; a long value there wraps mid-word onto a line that starts with spaces.
|
|
29
|
+
- `hostname <name>` is refused in configuration mode (`Error input`) and taken only at the `#` prompt; the plan leaves configuration mode to send it. It prints as `hostname <name>` among the `enable_mode` lines. An unset hostname prints nothing, and the switch then sends `Raisecom` as its LLDP system name.
|
|
30
|
+
- A Junos neighbor sends its port as a number (`PortIdSubtype: local`, `PortId: 520`), its SNMP index, and its description as `PortDesc`. The read keeps the number; matching it to a `link` needs the Junos side to send the port's name.
|
|
31
|
+
- `snmp-server access-list 1001` adds a list beside `1000` rather than replacing it, and SNMP then holds both. The plan binds the new list first and then sends `no snmp-server access-list 1000`, so SNMP is never left without one. SNMP's own sources are access-list 1001.
|
|
32
|
+
- `logging host <ip>` takes a level by name (`warnings`) or number and a port as `udpport <n>`, level first: `logging host <ip> udpport <n> warnings` is refused with `Error input`, `port` and `udp-port` too. It prints the level as a number and leaves out level 6 and port 514: `logging host 192.0.2.50 4 udpport 5140`. A second `logging host` for the same address changes it in place; `no logging host <ip>` removes it whatever its level and port.
|
|
33
|
+
- Port security takes only a switched port: `switchport port-security` on a port without `portswitch` is refused with `Error input`. It takes a maximum of 1 to 1024; 1024 is its default and prints nothing. Its action at the limit is `protect`, drop, and learned addresses age out after 5 minutes. `no switchport port-security maximum` takes no number.
|
|
34
|
+
- A MAC access-list is `access-list 3000` and up. A rule's ethertype is hex without `0x` (`0x86DD` is refused with `The EtherType is INVALID!`, and `2054` is 0x2054, not ARP); `0800` and `0806` print as `ip` and `arp`, any other as `86dd ffff`. A port binds it with `filter ingress access-list N`, which answers nothing, also on a port without `portswitch`.
|
|
35
|
+
- A MAC access-list a port filters with cannot change: `ACL 3000 is in use, and it can not be modified or destroyed`. Each port has its own list, 3001 and up by its place in the catalog, and the plan unbinds the port, changes the list and binds it again.
|
|
36
|
+
- `no portswitch` asks for `y` like `portswitch`; a port the plan switched stays switched when it goes back to unused, which the read cannot see.
|
|
37
|
+
- OSPF BFD is `ip ospf bfd` on the VLAN interface, printed as sent and undone with `no ip ospf bfd`.
|
|
38
|
+
- A static route is watched by a BFD session: `bfd N bind peer-ip <next hop> interface vlan <V>` opens the session's view, where `session enable` starts it. It prints under `!command in bfd_template_mode`, with `session enable` indented, and goes with `no bfd N`. The route takes it as `ip route <prefix> <mask> <next hop> track bfd N` and prints `track bfd-session N`; `no ip route <prefix> <mask> <next hop>` removes the route whatever it tracks. A route takes a session number that does not exist yet. Only one session binds to a peer: a second one's `session enable` answers `Error input`, so the old session goes before the new one is made. Proved on tako-mmr-01, 2026-10-24.
|
|
39
|
+
- BFD timers are global, `bfd detect-multiplier N`, `bfd send-interval N` and `bfd receive-interval N`, but `send-interval` took 10 and refused 300 and 99999, so its unit and range are not known and Circuit refuses `routing.bfd` here.
|
|
40
|
+
- The caret under `Error input` marks how far the CLI took a line, so sending a whole line with a made-up ending shows whether a keyword exists, without the `?` or Tab that run the line.
|