@takodotid/circuit 0.24.0 → 0.26.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 +3 -3
- package/dist/chunks/{index-n4zpd25n.js → index-22sbskdx.js} +22 -6
- package/dist/chunks/{server-ckdy25w2.js → server-1ftkqkx5.js} +1 -1
- package/dist/cli/index.js +4621 -324
- package/dist/presets/index.js +5 -0
- package/dist/types/adapters/devices/catalog.d.ts +28 -0
- package/dist/types/adapters/devices/fortios/firewall.d.ts +21 -0
- package/dist/types/adapters/devices/fortios/index.d.ts +55 -0
- package/dist/types/adapters/devices/fortios/models.d.ts +14 -0
- package/dist/types/adapters/devices/fortios/names.d.ts +32 -0
- package/dist/types/adapters/devices/fortios/plan.d.ts +26 -0
- package/dist/types/adapters/devices/fortios/render.d.ts +36 -0
- package/dist/types/adapters/devices/fortios/routing.d.ts +32 -0
- package/dist/types/adapters/devices/fortios/tree.d.ts +33 -0
- package/dist/types/adapters/devices/junos/bgp.d.ts +11 -0
- package/dist/types/adapters/devices/junos/index.d.ts +39 -0
- package/dist/types/adapters/devices/junos/models.d.ts +14 -0
- package/dist/types/adapters/devices/junos/render.d.ts +52 -0
- package/dist/types/core/graph.d.ts +2 -0
- package/dist/types/presets/index.d.ts +1 -1
- package/package.json +7 -2
- package/src/adapters/devices/catalog.ts +5 -2
- package/src/adapters/devices/fortios/TRAPS.md +40 -0
- package/src/adapters/devices/fortios/firewall.ts +352 -0
- package/src/adapters/devices/fortios/index.ts +800 -0
- package/src/adapters/devices/fortios/models.ts +7 -0
- package/src/adapters/devices/fortios/names.ts +78 -0
- package/src/adapters/devices/fortios/plan.ts +840 -0
- package/src/adapters/devices/fortios/render.ts +734 -0
- package/src/adapters/devices/fortios/routing.ts +444 -0
- package/src/adapters/devices/fortios/tree.ts +121 -0
- package/src/adapters/devices/index.ts +3 -1
- package/src/adapters/devices/junos/TRAPS.md +31 -0
- package/src/adapters/devices/junos/bgp.ts +263 -0
- package/src/adapters/devices/junos/index.ts +784 -0
- package/src/adapters/devices/junos/models.ts +7 -0
- package/src/adapters/devices/junos/render.ts +868 -0
- package/src/core/across.ts +6 -3
- package/src/core/graph.ts +3 -1
- package/src/core/validate.ts +4 -3
- package/src/presets/index.ts +7 -1
- package/templates/colocation/checks.ts +8 -1
package/dist/presets/index.js
CHANGED
|
@@ -332,6 +332,7 @@ function internetExchange(exchange, members) {
|
|
|
332
332
|
const findings = [];
|
|
333
333
|
const error = (message, device) => findings.push({ level: "error", ...device ? { device } : {}, message });
|
|
334
334
|
const taken = new Map((exchange.reserved ?? []).map((address) => [address, "reserved"]));
|
|
335
|
+
const portHolders = new Map;
|
|
335
336
|
members.forEach((member, index) => {
|
|
336
337
|
for (const other of members.slice(index + 1)) {
|
|
337
338
|
if (other.name === member.name)
|
|
@@ -353,6 +354,10 @@ function internetExchange(exchange, members) {
|
|
|
353
354
|
taken.set(address, member.name);
|
|
354
355
|
}
|
|
355
356
|
for (const [deviceName, portName] of Object.entries(member.ports)) {
|
|
357
|
+
const holder = portHolders.get(`${deviceName} ${portName}`);
|
|
358
|
+
if (holder)
|
|
359
|
+
error(`port ${portName} is given to both ${holder} and ${member.name}`, deviceName);
|
|
360
|
+
portHolders.set(`${deviceName} ${portName}`, member.name);
|
|
356
361
|
const device = devices.find((candidate) => candidate.name === deviceName);
|
|
357
362
|
const port = device?.ports?.[portName];
|
|
358
363
|
const carried = port ? vlansOf(port) : [];
|
|
@@ -39,6 +39,34 @@ export declare const catalog: {
|
|
|
39
39
|
};
|
|
40
40
|
};
|
|
41
41
|
};
|
|
42
|
+
readonly junos: {
|
|
43
|
+
readonly "QFX5100-48S-6Q": {
|
|
44
|
+
readonly ports: {
|
|
45
|
+
readonly "40g": 6;
|
|
46
|
+
readonly "10g": 48;
|
|
47
|
+
readonly "1g": 2;
|
|
48
|
+
};
|
|
49
|
+
readonly spelling: {
|
|
50
|
+
readonly "40g": "et-0/0/{n+47}";
|
|
51
|
+
readonly "10g": "xe-0/0/{n-1}";
|
|
52
|
+
readonly "1g": "em{n-1}";
|
|
53
|
+
};
|
|
54
|
+
};
|
|
55
|
+
};
|
|
56
|
+
readonly fortios: {
|
|
57
|
+
readonly "FG-3700D": {
|
|
58
|
+
readonly ports: {
|
|
59
|
+
readonly "40g": 4;
|
|
60
|
+
readonly "10g": 28;
|
|
61
|
+
readonly "1g": 2;
|
|
62
|
+
};
|
|
63
|
+
readonly spelling: {
|
|
64
|
+
readonly "40g": "port{n}";
|
|
65
|
+
readonly "10g": "port{n+4}";
|
|
66
|
+
readonly "1g": "mgmt{n}";
|
|
67
|
+
};
|
|
68
|
+
};
|
|
69
|
+
};
|
|
42
70
|
};
|
|
43
71
|
export type Catalog = typeof catalog;
|
|
44
72
|
export type PlatformName = keyof Catalog;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { AclRule, Device, Family, FilterMatch, FilterRule, NatRule, Ports, Protocol } from "../../../schema";
|
|
2
|
+
import { Block } from "./tree";
|
|
3
|
+
/** IP protocol numbers, for a protocol the platform names only by number. */
|
|
4
|
+
export declare const PROTOCOL_NUMBER: Record<Protocol, number>;
|
|
5
|
+
/** NAT rules of both directions, which central NAT needs turned on. */
|
|
6
|
+
export declare function natRules(device: Device): NatRule<string, string>[];
|
|
7
|
+
/** Which families a rule applies to: its own, its addresses', or both. */
|
|
8
|
+
export declare function familiesOf(device: Device, match: FilterMatch<string, string> | undefined): Family[];
|
|
9
|
+
/** A port or ports as the platform writes them in a service: `22`, `1000-2000`. */
|
|
10
|
+
export declare function portList(ports: Ports | undefined): string[];
|
|
11
|
+
/** The service a rule matches, by name, with what it holds. `ALL` when the rule names no protocol and no port; ports without a protocol are TCP's and UDP's. */
|
|
12
|
+
export declare function serviceOf(match: FilterMatch<string, string> | undefined, family: Family): {
|
|
13
|
+
name: string;
|
|
14
|
+
block?: Block;
|
|
15
|
+
};
|
|
16
|
+
/** A rule the platform keeps without being told: replies to a session it let through, and packets that belong to one, pass on their own. */
|
|
17
|
+
export declare function isImplied(rule: FilterRule<string, string>): boolean;
|
|
18
|
+
/** Packet filtering, who reaches the device, NAT and stateless ACLs. */
|
|
19
|
+
export declare function firewall(root: Block, device: Device): void;
|
|
20
|
+
/** Whether a port's ACL drops before it accepts: an accept before a drop is an exception the platform's ACLs cannot hold. */
|
|
21
|
+
export declare function acceptsBeforeDrop(rules: readonly AclRule<string>[]): boolean;
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import type { Client } from "ssh2";
|
|
2
|
+
import type { Device, Family } from "../../../schema";
|
|
3
|
+
import type { ApplyOptions, DeviceAdapter, Plan } from "../types";
|
|
4
|
+
/** What a session reads and writes: an SSH shell, or anything else that streams the CLI. */
|
|
5
|
+
export type Channel = {
|
|
6
|
+
write(data: string): unknown;
|
|
7
|
+
on(event: "data", listener: (chunk: Buffer) => void): unknown;
|
|
8
|
+
on(event: "close", listener: () => void): unknown;
|
|
9
|
+
removeListener(event: "data", listener: (chunk: Buffer) => void): unknown;
|
|
10
|
+
removeListener(event: "close", listener: () => void): unknown;
|
|
11
|
+
};
|
|
12
|
+
/** A CLI over a channel: one line at a time, and what the device printed until its prompt came back. */
|
|
13
|
+
export declare class Cli {
|
|
14
|
+
private readonly channel;
|
|
15
|
+
private readonly newline;
|
|
16
|
+
/** The prompt that ended the last answer, which shows where the session is: `(port1)` inside an entry. */
|
|
17
|
+
prompt: string;
|
|
18
|
+
constructor(channel: Channel, newline?: string);
|
|
19
|
+
/** Wait for the prompt, after logging in. */
|
|
20
|
+
ready(timeout?: number): Promise<void>;
|
|
21
|
+
/** Send one line and return what the device printed after it, without the echo or the prompt. */
|
|
22
|
+
run(line: string, timeout?: number): Promise<string>;
|
|
23
|
+
/** Answer a question the device asked. */
|
|
24
|
+
answer(reply: "y" | "n", timeout?: number): Promise<string>;
|
|
25
|
+
/** What the device prints until its prompt or a question. A session the device ended, or that went silent, fails. */
|
|
26
|
+
private collect;
|
|
27
|
+
}
|
|
28
|
+
export declare function login(device: Device): Promise<Client>;
|
|
29
|
+
/** Refuse a device on a release this adapter was not proved on, before anything is read or sent. */
|
|
30
|
+
export declare function checkVersion(cli: Cli, device: Device): Promise<void>;
|
|
31
|
+
/** Secrets the device prints: a password or an SNMPv3 key as a cipher, a certificate's key encrypted, and the SNMP community in clear. None leave the read. */
|
|
32
|
+
export declare function scrub(text: string): string;
|
|
33
|
+
/** Every section Circuit manages, as the device holds it, credentials scrubbed. */
|
|
34
|
+
export declare function readConfig(cli: Cli, device: Device): Promise<string>;
|
|
35
|
+
export declare function read(device: Device): Promise<string>;
|
|
36
|
+
/**
|
|
37
|
+
* A line with each secret's value in place, escaped inside its quotes. A certificate's key from a variable is PEM encoded as one line of base64, and is sent as the PEM it holds. A file saved with Windows line endings is sent with plain ones: the CLI takes each carriage return as a line of its own, and refuses the key, `Invalid private key`. A `?`, which the CLI takes as a call for help, is typed literally.
|
|
38
|
+
*/
|
|
39
|
+
export declare function withSecrets(line: string): string;
|
|
40
|
+
/** How a change that could cut access is guarded: the device reloads its saved configuration after `seconds` unless `prove` shows access survived. */
|
|
41
|
+
export type Guard = {
|
|
42
|
+
seconds: number;
|
|
43
|
+
prove: () => Promise<void>;
|
|
44
|
+
};
|
|
45
|
+
/**
|
|
46
|
+
* Send a plan line by line, each applied as it arrives. The device is first set to revert what is not saved, so a refused line or a fresh login that fails reloads its saved configuration. Once access is proved the change is saved, and changes save themselves again.
|
|
47
|
+
*/
|
|
48
|
+
export declare function configure(cli: Cli, plan: Plan, guard: Guard): Promise<void>;
|
|
49
|
+
export declare function apply(device: Device, _network: readonly Device[], plan: Plan, options: ApplyOptions): Promise<void>;
|
|
50
|
+
/** The lines that replace a prefix list's entries with what a registry returned: every entry is purged, then each prefix added, matching it and anything more specific. */
|
|
51
|
+
export declare function fillLines(set: string, family: Family, prefixes: string[]): string[];
|
|
52
|
+
/** Replace a prefix list `refresh` fetched. It cannot cut access, so it is sent without a guard, and saved as it goes. `purge` asks to confirm, and is answered yes. */
|
|
53
|
+
export declare function fill(device: Device, _network: readonly Device[], set: string, family: Family, prefixes: string[]): Promise<void>;
|
|
54
|
+
export declare function unsupported(device: Device): string[];
|
|
55
|
+
export declare const fortios: DeviceAdapter;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export declare const models: {
|
|
2
|
+
readonly "FG-3700D": {
|
|
3
|
+
readonly ports: {
|
|
4
|
+
readonly "40g": 4;
|
|
5
|
+
readonly "10g": 28;
|
|
6
|
+
readonly "1g": 2;
|
|
7
|
+
};
|
|
8
|
+
readonly spelling: {
|
|
9
|
+
readonly "40g": "port{n}";
|
|
10
|
+
readonly "10g": "port{n+4}";
|
|
11
|
+
readonly "1g": "mgmt{n}";
|
|
12
|
+
};
|
|
13
|
+
};
|
|
14
|
+
};
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import type { Device, Family } from "../../../schema";
|
|
2
|
+
type Addressed = {
|
|
3
|
+
addresses?: readonly (string | {
|
|
4
|
+
address: string;
|
|
5
|
+
peer?: string;
|
|
6
|
+
})[];
|
|
7
|
+
vrf?: string;
|
|
8
|
+
lag?: string;
|
|
9
|
+
};
|
|
10
|
+
/** The name the platform gives a port or an interface: a port by its spelling, any other interface by its key. */
|
|
11
|
+
export declare function nameOf(device: Device, name: string): string;
|
|
12
|
+
/** A port's or an interface's settings, by its key. */
|
|
13
|
+
export declare function settingsOf(device: Device, name: string): Addressed | undefined;
|
|
14
|
+
/** Every address of a port or an interface. */
|
|
15
|
+
export declare function addressesOf(settings: Addressed | undefined): string[];
|
|
16
|
+
/** An address or prefix with its length, a bare address taken as a host. */
|
|
17
|
+
export declare const withLength: (value: string) => string;
|
|
18
|
+
/** The family of an address or prefix. */
|
|
19
|
+
export declare const familyOfPrefix: (value: string) => Family;
|
|
20
|
+
/** `192.0.2.0 255.255.255.0`, how the platform writes an IPv4 prefix. */
|
|
21
|
+
export declare const subnet: (prefix: string) => string;
|
|
22
|
+
/** The canonical form of a prefix: its network address and length, a bare address taken as a host. */
|
|
23
|
+
export declare const network: (prefix: string) => string;
|
|
24
|
+
/** The number of a VRF on the platform: its place among the device's VRFs, the default table being 0. */
|
|
25
|
+
export declare function vrfNumber(device: Device, vrf: string | undefined): number;
|
|
26
|
+
/** The interface whose own network holds an address, the most specific when several do, in one routing table. */
|
|
27
|
+
export declare function interfaceFor(device: Device, address: string, vrf?: string): string | undefined;
|
|
28
|
+
/** The interface that holds an address as its own. */
|
|
29
|
+
export declare function holderOf(device: Device, address: string): string | undefined;
|
|
30
|
+
/** Every port and interface with an address, by key, where the device's services answer. */
|
|
31
|
+
export declare function addressed(device: Device): string[];
|
|
32
|
+
export {};
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { ApplyOptions, Plan } from "../types";
|
|
2
|
+
import { Block } from "./tree";
|
|
3
|
+
/** How the plan treats the entries of one table. */
|
|
4
|
+
type Table = {
|
|
5
|
+
/** Entries the factory holds. The plan neither reads nor removes them. */
|
|
6
|
+
factory?: readonly string[];
|
|
7
|
+
/** The device keeps the entries in an order of their own, which `move` changes. */
|
|
8
|
+
ordered?: true;
|
|
9
|
+
/** An entry is known by what it holds, not by its number. */
|
|
10
|
+
byContent?: true;
|
|
11
|
+
/** Settings the device gives an entry it creates. They are unset when the render does not hold them. */
|
|
12
|
+
created?: (entry: Block) => readonly string[];
|
|
13
|
+
/** Settings an entry keeps for life. A change deletes the entry and creates it again. */
|
|
14
|
+
fixed?: readonly string[];
|
|
15
|
+
};
|
|
16
|
+
/** Tables whose entries need more than a plain comparison, by section. */
|
|
17
|
+
export declare const TABLES: Record<string, Table>;
|
|
18
|
+
/** The configuration as the plan reads it: factory entries and the device's own settings left out. */
|
|
19
|
+
export declare function clean(text: string): string;
|
|
20
|
+
/**
|
|
21
|
+
* What changes, by section, in the order the render lists them. Entries that go are deleted after everything else, in the reverse order, so nothing is deleted while another section still names it.
|
|
22
|
+
*
|
|
23
|
+
* A secret is sent when its entry is new, or with `secrets` set; it cannot be read back to compare.
|
|
24
|
+
*/
|
|
25
|
+
export declare function plan(desired: string, current: string, options: Pick<ApplyOptions, "secrets">): Plan;
|
|
26
|
+
export {};
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type { Device, Helper, Severity } from "../../../schema";
|
|
2
|
+
/** Where the pre-login banner's text is kept. */
|
|
3
|
+
export declare const BANNER = "system replacemsg admin \"pre_admin-disclaimer-text\"";
|
|
4
|
+
/** Every section Circuit manages, in the order a change goes in: what others name comes first. */
|
|
5
|
+
export declare const SECTIONS: readonly ["vpn certificate local", "vpn certificate ca", "system global", "system replacemsg admin \"pre_admin-disclaimer-text\"", "system settings", "system accprofile", "system admin", "system interface", "system gre-tunnel", "system vxlan", "system dns", "system dns-server", "system ntp", "system snmp sysinfo", "system snmp community", "system snmp user", "log memory setting", "log memory filter", "log disk setting", ...string[], "system sflow", "system netflow", "system dhcp server", "router static", "router static6", "router prefix-list", "router prefix-list6", "router community-list", "router route-map", "router ospf", "router ospf6", "router bgp", "system session-helper", "firewall address", "firewall address6", "firewall addrgrp", "firewall addrgrp6", "firewall service custom", "firewall ippool", "firewall vip", "firewall policy", "firewall policy6", "firewall local-in-policy", "firewall local-in-policy6", "firewall central-snat-map", "firewall acl", "firewall acl6"];
|
|
6
|
+
export type Section = (typeof SECTIONS)[number];
|
|
7
|
+
/** Sections `show` prints without what matters, which `show full-configuration` prints: a certificate's text. */
|
|
8
|
+
export declare const FULL: readonly string[];
|
|
9
|
+
/** The zone number for an IANA zone. UTC when the config names none. */
|
|
10
|
+
export declare function zoneOf(timezone?: string): string | undefined;
|
|
11
|
+
/** What each role may do: the factory profile for an admin, and a profile Circuit adds for the others. */
|
|
12
|
+
export declare const PROFILE: {
|
|
13
|
+
readonly admin: "super_admin";
|
|
14
|
+
readonly operator: "circuit-operator";
|
|
15
|
+
readonly "read-only": "circuit-read-only";
|
|
16
|
+
};
|
|
17
|
+
/** The most public keys a user holds. */
|
|
18
|
+
export declare const SSH_KEYS = 3;
|
|
19
|
+
/** The most resolvers the device and a DHCP server hold, per family. */
|
|
20
|
+
export declare const RESOLVERS: {
|
|
21
|
+
readonly device: 2;
|
|
22
|
+
readonly dhcp: 4;
|
|
23
|
+
};
|
|
24
|
+
/** The most syslog collectors the device sends to. */
|
|
25
|
+
export declare const COLLECTORS = 4;
|
|
26
|
+
/** The most VRFs the device holds besides its default table. */
|
|
27
|
+
export declare const VRFS = 31;
|
|
28
|
+
/** A severity as the platform names it. */
|
|
29
|
+
export declare const LEVEL: Record<Severity, string>;
|
|
30
|
+
/** The helpers the factory runs, by the name a config gives them: protocol and port of each. */
|
|
31
|
+
export declare const HELPERS: Partial<Record<Helper, readonly [string, number, number][]>>;
|
|
32
|
+
/** Speed by the platform's spelling, for each class a port's cage takes. */
|
|
33
|
+
export declare const SPEEDS: Record<string, Partial<Record<string, string>>>;
|
|
34
|
+
export declare function render(device: Device): string;
|
|
35
|
+
/** The one port or LAG that carries a VLAN tagged, which its VLAN interface sits on. `unsupported()` refuses any other case. */
|
|
36
|
+
export declare function vlanParent(device: Device, vlan: string): string | undefined;
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import type { Device, Family, Neighbor, NeighborSettings, PolicyRule } from "../../../schema";
|
|
2
|
+
import { Block } from "./tree";
|
|
3
|
+
type AnyRule = PolicyRule<string, string>;
|
|
4
|
+
type AnyNeighbor = Neighbor<string, string>;
|
|
5
|
+
/** The route map a session takes when its config states none, so it learns or announces nothing. */
|
|
6
|
+
export declare const REJECT_ALL = "circuit-reject-all";
|
|
7
|
+
/** The start of the comment on a prefix list `refresh` fills. The plan keeps the entries the device holds in one. */
|
|
8
|
+
export declare const FILLED = "Filled by circuit refresh";
|
|
9
|
+
/** The table a prefix list of one family sits in. */
|
|
10
|
+
export declare const PREFIX_LIST: {
|
|
11
|
+
readonly ipv4: "router prefix-list";
|
|
12
|
+
readonly ipv6: "router prefix-list6";
|
|
13
|
+
};
|
|
14
|
+
/** The longest prefix of each family. */
|
|
15
|
+
export declare const BITS: {
|
|
16
|
+
readonly ipv4: 32;
|
|
17
|
+
readonly ipv6: 128;
|
|
18
|
+
};
|
|
19
|
+
/** A neighbor's settings with its group's beneath them. */
|
|
20
|
+
export declare function neighborSettings(device: Device, neighbor: AnyNeighbor): NeighborSettings<string>;
|
|
21
|
+
/** The families a session carries: its own, or the family of its address. */
|
|
22
|
+
export declare function familiesOfNeighbor(device: Device, neighbor: AnyNeighbor): Family[];
|
|
23
|
+
/** Whether OSPF or BGP detects a dead neighbor with BFD, which the device then runs. */
|
|
24
|
+
export declare function usesBfd(device: Device): boolean;
|
|
25
|
+
export declare function routing(root: Block, device: Device): void;
|
|
26
|
+
/** The family a rule is limited to, by its match: a family, a prefix, or a prefix set of one family. */
|
|
27
|
+
export declare function familyOfRule(device: Device, rule: AnyRule): Family | undefined;
|
|
28
|
+
/** A policy's rules with each call replaced by the rules it calls, in order. A called policy that decides nothing hands back to the caller. */
|
|
29
|
+
export declare function flatten(device: Device, policy: string, seen?: string[]): AnyRule[];
|
|
30
|
+
/** Routing the platform cannot express, or that this adapter does not yet write. */
|
|
31
|
+
export declare function routingGaps(device: Device): string[];
|
|
32
|
+
export {};
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/** A setting the device prints as `unset <key>`, a key it holds empty on purpose, such as an ICMP service's type. */
|
|
2
|
+
export declare const UNSET = "\0unset";
|
|
3
|
+
/** One `edit` of a table: its name as written, and what it holds. */
|
|
4
|
+
export type Entry = {
|
|
5
|
+
id: string;
|
|
6
|
+
block: Block;
|
|
7
|
+
};
|
|
8
|
+
export declare class Block {
|
|
9
|
+
readonly settings: Map<string, string>;
|
|
10
|
+
readonly blocks: Map<string, Block>;
|
|
11
|
+
readonly entries: Map<string, Entry>;
|
|
12
|
+
/** Set a key. A value of several words is written as given. */
|
|
13
|
+
set(key: string, value: string | number): this;
|
|
14
|
+
/** The nested `config` block with this header, created when absent. */
|
|
15
|
+
block(header: string): Block;
|
|
16
|
+
/** The table entry with this name, created when absent. A name is quoted unless it is a number, or the table prints it bare, such as an OSPF area or an IPv6 prefix. */
|
|
17
|
+
entry(id: string | number, plain?: boolean): Block;
|
|
18
|
+
isEmpty(): boolean;
|
|
19
|
+
}
|
|
20
|
+
/** A string as the CLI takes it, in double quotes. */
|
|
21
|
+
export declare function quote(text: string): string;
|
|
22
|
+
/** Each of several strings quoted, as a list setting takes them. */
|
|
23
|
+
export declare const quoteAll: (texts: readonly string[]) => string;
|
|
24
|
+
/** A value's words; a quoted string is one word with its quotes. */
|
|
25
|
+
export declare function wordsOf(value: string): string[];
|
|
26
|
+
/** A word as the device means it, without quotes or escapes. */
|
|
27
|
+
export declare function bare(word: string): string;
|
|
28
|
+
/** The lines that build a block, at an indent, in the form `show` prints and the CLI takes. */
|
|
29
|
+
export declare function linesOf(block: Block, indent?: string): string[];
|
|
30
|
+
/** The whole configuration: each top-level block as `config <section>` to `end`. */
|
|
31
|
+
export declare function print(root: Block): string;
|
|
32
|
+
/** The tree of what `show` printed. Lines that are not configuration, such as the version header, are skipped. */
|
|
33
|
+
export declare function parse(text: string): Block;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { Device, Neighbor, NeighborSettings } from "../../../schema";
|
|
2
|
+
import { type Statements } from "./render";
|
|
3
|
+
type AnyNeighbor = Neighbor<string, string>;
|
|
4
|
+
/** The policy a session uses when it states none, so that it learns or announces nothing. */
|
|
5
|
+
export declare const REJECT_ALL = "circuit-reject-all";
|
|
6
|
+
/** A comment in the render naming a path whose statements `refresh` fills, so the plan keeps what the device holds there. */
|
|
7
|
+
export declare const GENERATED = "# generated ";
|
|
8
|
+
/** A neighbor's settings with its group's beneath them. */
|
|
9
|
+
export declare function settingsOf(device: Device, neighbor: AnyNeighbor): NeighborSettings<string>;
|
|
10
|
+
export declare function renderBgp(out: Statements, device: Device): void;
|
|
11
|
+
export {};
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import type { Client, ClientChannel } from "ssh2";
|
|
2
|
+
import type { Device, Family } from "../../../schema";
|
|
3
|
+
import type { ApplyOptions, DeviceAdapter, Plan } from "../types";
|
|
4
|
+
/** A statement's words; a quoted value is one word with its quotes. */
|
|
5
|
+
export declare function wordsOf(statement: string): string[];
|
|
6
|
+
/** The device prints a password as a hash, an SNMP key as a reversible cipher, and a community as itself. None leave the read. */
|
|
7
|
+
export declare function scrub(text: string): string;
|
|
8
|
+
/** The configuration's statements from what `show configuration | display set` printed. */
|
|
9
|
+
export declare function clean(output: string): string;
|
|
10
|
+
/**
|
|
11
|
+
* What changes, by section.
|
|
12
|
+
*
|
|
13
|
+
* A statement the render holds and the device does not is set. One the device holds and the render does not is deleted at the shortest path that holds nothing the render wants, so a removed port goes as a whole and a changed value goes alone. A hidden value ends a path by its name alone; a hidden name in a list, such as a community, stays `<redacted>` and the session names each entry when it sends the plan. A statement the device holds deactivated is activated when the render wants it.
|
|
14
|
+
*
|
|
15
|
+
* A policy, a filter or an import or export chain is ordered, so when anything in one changes it is deleted and set again whole, in the render's order. A path the render marks as generated, such as a prefix list `refresh` fills, keeps what the device holds.
|
|
16
|
+
*/
|
|
17
|
+
export declare function plan(desired: string, current: string, options: Pick<ApplyOptions, "secrets">): Plan;
|
|
18
|
+
export declare function login(device: Device): Promise<Client>;
|
|
19
|
+
export declare function read(device: Device): Promise<string>;
|
|
20
|
+
/** An interactive CLI with paging off. Root lands in a shell and starts the CLI from there. */
|
|
21
|
+
export declare function openCli(channel: ClientChannel): Promise<void>;
|
|
22
|
+
/** How a commit that could cut access is guarded: it rolls back after `minutes` unless `prove` shows access survived. */
|
|
23
|
+
export type Guard = {
|
|
24
|
+
minutes: number;
|
|
25
|
+
prove: () => Promise<void>;
|
|
26
|
+
};
|
|
27
|
+
/**
|
|
28
|
+
* Load the plan into an exclusive candidate and check it. With a guard, commit it to roll back after `minutes`, prove access, then confirm; without one, commit it outright.
|
|
29
|
+
*
|
|
30
|
+
* Deletions go before the statements that are set, so a path deleted and set again in one plan ends up set.
|
|
31
|
+
*/
|
|
32
|
+
export declare function configure(channel: ClientChannel, plan: Plan, guard?: Guard): Promise<void>;
|
|
33
|
+
export declare function apply(device: Device, _network: readonly Device[], plan: Plan, options: ApplyOptions): Promise<void>;
|
|
34
|
+
export declare function unsupported(device: Device): string[];
|
|
35
|
+
/** The plan that replaces a prefix list's entries with what a registry returned. */
|
|
36
|
+
export declare function fillPlan(set: string, prefixes: string[]): Plan;
|
|
37
|
+
/** Replace a prefix list `refresh` fetched. It cannot cut access, so it is committed without a rollback. */
|
|
38
|
+
export declare function fill(device: Device, _network: readonly Device[], set: string, _family: Family, prefixes: string[]): Promise<void>;
|
|
39
|
+
export declare const junos: DeviceAdapter;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export declare const models: {
|
|
2
|
+
readonly "QFX5100-48S-6Q": {
|
|
3
|
+
readonly ports: {
|
|
4
|
+
readonly "40g": 6;
|
|
5
|
+
readonly "10g": 48;
|
|
6
|
+
readonly "1g": 2;
|
|
7
|
+
};
|
|
8
|
+
readonly spelling: {
|
|
9
|
+
readonly "40g": "et-0/0/{n+47}";
|
|
10
|
+
readonly "10g": "xe-0/0/{n-1}";
|
|
11
|
+
readonly "1g": "em{n-1}";
|
|
12
|
+
};
|
|
13
|
+
};
|
|
14
|
+
};
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import type { Device, Interface, Port, Vrrp } from "../../../schema";
|
|
2
|
+
type AnyPort = Port<string, string, string, string>;
|
|
3
|
+
type AnyInterface = Interface<string, string>;
|
|
4
|
+
type LagInterface = Extract<AnyInterface, {
|
|
5
|
+
type: "lag";
|
|
6
|
+
}>;
|
|
7
|
+
type Switched = AnyPort | LagInterface;
|
|
8
|
+
/** The statement each kind of public key is written under, by the key's own prefix. */
|
|
9
|
+
export declare const KEY_STATEMENT: Record<string, string>;
|
|
10
|
+
/** Detection under a second: three missed packets 300 ms apart. */
|
|
11
|
+
export declare const BFD_INTERVAL = 300;
|
|
12
|
+
/** Quote a value unless it is a plain word. Quoting more than the platform would is harmless: the comparison ignores quotes. */
|
|
13
|
+
export declare function quote(value: string): string;
|
|
14
|
+
/** Collects `set` statements, and comments the plan reads. */
|
|
15
|
+
export declare class Statements {
|
|
16
|
+
private readonly lines;
|
|
17
|
+
private readonly notes;
|
|
18
|
+
set(...words: (string | number)[]): void;
|
|
19
|
+
note(comment: string): void;
|
|
20
|
+
/** The statements as the device prints them: each once, and none that another one extends, because the platform prints only the longest. */
|
|
21
|
+
text(): string;
|
|
22
|
+
}
|
|
23
|
+
/** What the platform calls a port, a LAG, a VLAN interface or a loopback. */
|
|
24
|
+
export declare class Names {
|
|
25
|
+
private readonly device;
|
|
26
|
+
readonly lags: Map<string, number>;
|
|
27
|
+
readonly loopbacks: Map<string, number>;
|
|
28
|
+
readonly tunnels: Map<string, number>;
|
|
29
|
+
constructor(device: Device);
|
|
30
|
+
/** A physical port. A 10G cage holding a 1G optic is `ge-`, not `xe-`. */
|
|
31
|
+
port(port: string): string;
|
|
32
|
+
/** The physical interface a port or a LAG is: `xe-0/0/1`, `ae1`. */
|
|
33
|
+
physical(name: string): string;
|
|
34
|
+
/** The logical interface that holds addresses: `xe-0/0/1.0`, `ae1.0`, `irb.10`, `lo0.0`. */
|
|
35
|
+
unit(name: string): string;
|
|
36
|
+
/** The physical ports behind a port or a LAG. */
|
|
37
|
+
members(name: string): string[];
|
|
38
|
+
}
|
|
39
|
+
/** The address a VRRP group sits on: the one whose network holds the virtual address. */
|
|
40
|
+
export declare function vrrpAddress(addresses: readonly string[], group: Vrrp): string | undefined;
|
|
41
|
+
/**
|
|
42
|
+
* The storm-control profile a port or LAG names. A profile holds one threshold for every kind it limits, so a port's kinds must share one in percent; `unsupported()` refuses any other.
|
|
43
|
+
*/
|
|
44
|
+
export declare function stormProfile(settings: Switched): {
|
|
45
|
+
name: string;
|
|
46
|
+
percent: number;
|
|
47
|
+
left: string[];
|
|
48
|
+
} | undefined;
|
|
49
|
+
export declare function render(device: Device): string;
|
|
50
|
+
/** The size a port's frames may reach for an IP MTU: the Ethernet header, and a VLAN tag when the port switches. */
|
|
51
|
+
export declare const frameSize: (mtu: number, switched: boolean) => number;
|
|
52
|
+
export {};
|
|
@@ -60,6 +60,8 @@ export type Holder = {
|
|
|
60
60
|
/** The far end of a point-to-point address. */
|
|
61
61
|
peer?: string;
|
|
62
62
|
vrrp: NonNullable<Routed<string>["vrrp"]>;
|
|
63
|
+
/** The VRF the address is in. The default table when absent. */
|
|
64
|
+
vrf?: string;
|
|
63
65
|
};
|
|
64
66
|
export declare const deviceId: (name: string) => string;
|
|
65
67
|
/** An address as a key, so two spellings of one address compare equal. Undefined when it is not an address. */
|
|
@@ -171,7 +171,7 @@ export type InternetExchange<V extends string> = {
|
|
|
171
171
|
* Member ports and a check for an internet exchange's fabric.
|
|
172
172
|
*
|
|
173
173
|
* - `ports(device)`: every member's port on one switch. A member port is in the peering VLAN and nothing else, takes no part in spanning tree or LLDP, and lets only a trickle of broadcast, multicast and unknown unicast through.
|
|
174
|
-
* - `check`: members never share a name, an AS or
|
|
174
|
+
* - `check`: members never share a name, an AS, an address or a port; every address is inside the peering LAN and not reserved; every member port carries the peering VLAN alone.
|
|
175
175
|
*/
|
|
176
176
|
export declare function internetExchange<const V extends string>(exchange: InternetExchange<V>, members: readonly ExchangeMember[]): {
|
|
177
177
|
ports: (device: string) => Record<string, Port<V, string, string, string>>;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@takodotid/circuit",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.26.0",
|
|
4
4
|
"description": "Declarative, vendor-neutral network configuration: describe each device in TypeScript, and Circuit makes the device match. Alpha.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"network",
|
|
@@ -13,7 +13,12 @@
|
|
|
13
13
|
"mikrotik",
|
|
14
14
|
"vrp",
|
|
15
15
|
"huawei",
|
|
16
|
-
"raisecom"
|
|
16
|
+
"raisecom",
|
|
17
|
+
"junos",
|
|
18
|
+
"juniper",
|
|
19
|
+
"fortios",
|
|
20
|
+
"fortinet",
|
|
21
|
+
"fortigate"
|
|
17
22
|
],
|
|
18
23
|
"homepage": "https://circuit.tako.id",
|
|
19
24
|
"bugs": {
|
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
// Every model each platform supports. Types read from here, so a model or port name that does not exist fails to compile.
|
|
2
2
|
|
|
3
|
+
import { models as fortios } from "./fortios/models";
|
|
4
|
+
import { models as junos } from "./junos/models";
|
|
3
5
|
import { models as raisecom } from "./raisecom-ros/models";
|
|
4
6
|
import { models as routeros } from "./routeros/models";
|
|
5
7
|
import { models as vrp } from "./vrp/models";
|
|
6
8
|
|
|
7
|
-
export const catalog = { routeros, vrp, "raisecom-ros": raisecom } as const;
|
|
9
|
+
export const catalog = { routeros, vrp, "raisecom-ros": raisecom, junos, fortios } as const;
|
|
8
10
|
|
|
9
11
|
export type Catalog = typeof catalog;
|
|
10
12
|
export type PlatformName = keyof Catalog;
|
|
@@ -34,5 +36,6 @@ export function spell(platform: string, model: string, port: string): string {
|
|
|
34
36
|
const template = parts && spec?.spelling[parts[1]!];
|
|
35
37
|
if (!template) throw new Error(`${model} has no port ${port}`);
|
|
36
38
|
|
|
37
|
-
|
|
39
|
+
// A platform that counts from 0, or numbers one class after another, shifts the position: `{n-1}`, `{n+47}`.
|
|
40
|
+
return template.replace(/\{n([+-]\d+)?\}/, (_, shift?: string) => String(Number(parts[2]) + Number(shift ?? 0)));
|
|
38
41
|
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# FortiOS traps
|
|
2
|
+
|
|
3
|
+
Behaviour that looks like something else. Each one has been hit on a FortiGate 3700D running FortiOS 6.2.2, without a licence.
|
|
4
|
+
|
|
5
|
+
- A change applies at `next` or `end`, and with `cfg-save automatic`, the factory setting, it is saved at once. `abort` discards the changes of the one object being edited, and only there: inside a table it is `Unknown action 0`, and the entries already left with `next` are applied.
|
|
6
|
+
- `cfg-save revert` keeps changes running but unsaved, and `cfg-revert-timeout` seconds after a change the device announces `System will reload` and reloads its saved configuration. The reload restarts the configuration, not the hardware, and takes about two minutes. `execute cfg reload` does the same at once, without asking, and says `no config to be reloaded` when nothing is unsaved. `execute cfg save` saves; setting `cfg-save automatic` again saves too.
|
|
7
|
+
- `cfg-save revert` set while saving is automatic is saved itself. After a reload the device is still in revert mode, and from then on every change made by hand is undone after the timeout until someone saves it or sets `cfg-save automatic`. Circuit sets it back at the end of every apply.
|
|
8
|
+
- `execute config-transaction start N` opens a candidate that `commit` applies and `abort` discards, but Circuit does not use it on 6.2.2. Inside one, deleting an entry does not release what its lists name, so deleting a policy and then the address it named is refused: `The entry is used by other 1 entries`. A transaction whose session is cut off keeps every object it touched locked until its N minutes run out: `Can not config the object since it's being configured by another transaction`. Editing a `firewall policy6` entry after `unset central-nat` ends the session at `edit`. Deleting the user you are logged in as ends the session with `cmdb_txn_cache_data ... failed` and breaks every later transaction until the device restarts. `show` prints a new static route or policy with the comment of the last new one, which the commit corrects. OSPF's `config redistribute` is refused, `config child object 'redistribute' is not handled in transaction`, and a certificate added in one cannot be named until the commit. None of these happen outside a transaction.
|
|
9
|
+
- The CLI pages long output with `--More--`, and erases the prompt with spaces and a carriage return once it is answered.
|
|
10
|
+
- A `?` typed anywhere shows help at once and is never sent. Ctrl-V before it types it literally.
|
|
11
|
+
- The idle timeout, `admintimeout`, is 5 minutes by default; the session then returns to `login:`.
|
|
12
|
+
- With `pre-login-banner` on, the console shows the banner and waits on `(Press 'a' to accept):` before `login:`. Circuit answers `a`.
|
|
13
|
+
- `grep` after a pipe takes no `-E`, and a command ending in `| grep` reports `Command fail. Return code -27` after printing its matches.
|
|
14
|
+
- `show` prints a setting only when it differs from its default, and prints keys in its own order, not the order they were set. A new aggregate is given `device-identification enable` and `lldp-transmission enable`, a new VLAN interface `device-identification enable` and `role lan`, without being asked. Circuit unsets them.
|
|
15
|
+
- A port joins an aggregate only while nothing names it, such as a static route, `entry not found in datasource`, and its speed cannot change while it is in one, not even back to `auto`: `Interface speed cannot be set for aggregated interfaces`. Circuit frees ports first: what names a port that joins is deleted and created again after, a port whose speed changes leaves its aggregate and joins again, and an aggregate that goes, or is left with no port, goes before anything else with what names it.
|
|
16
|
+
- `unset secondary-IP` hides the list of secondary addresses and drops it. NTP's `unset type` hides its servers but keeps them: `set type custom` brings them back. NTP refuses to delete its last server while `ntpsync` is on, and to empty the interfaces it serves on while `server-mode` is on, `There aren't any interfaces with NTP server enabled`; `unset server-mode` hides that list and lets go of the interfaces in it. The plan deletes a nested list before the setting that hides it, except `ntpsync`, which goes first.
|
|
17
|
+
- A DHCP server prints `dns-server1` only while `dns-service` is `specify`, the default, which `show` does not print. The factory server on `mgmt1` uses `default`, so the render states `specify` before its resolvers.
|
|
18
|
+
- The timezone is a number of the platform's own, such as `53` for Jakarta, Bangkok and Hanoi. The default is `00`, Eniwetok, at UTC+12; UTC is `80`.
|
|
19
|
+
- Global `alias` is set again at every start, whatever was saved.
|
|
20
|
+
- `log memory setting` prints its `status` whether on or off, and `unset status` turns memory logging on.
|
|
21
|
+
- Two interfaces that are up cannot hold overlapping subnets, `Subnets overlap between ...`, so an address moving from one to another must leave the first before it joins the second.
|
|
22
|
+
- A static route identical to another is refused, `Duplicated entry`. An IPv4 route's destination is not printed when it is the default route; neither is IPv6's.
|
|
23
|
+
- A deny policy is given `logtraffic disable`, and its action is not printed, since deny is the default. `send-deny-packet` exists only while a policy denies: set before `action deny` or unset after `action accept`, it is `command parse error`. The render states `action deny`, and the plan undoes what goes before it sets what comes.
|
|
24
|
+
- `show` prints `unset` in two places: an ICMP service's `unset icmptype`, and a prefix list rule's `unset ge` and `unset le` for a bound it does not hold.
|
|
25
|
+
- An SNMPv3 user authenticates with MD5 or SHA1 only, and has no list of sources: only a community has hosts. Circuit decides who reaches every service with local-in policies instead.
|
|
26
|
+
- The console ignores a user's trusted hosts.
|
|
27
|
+
- A tunnel's interface takes an IPv4 address only as a host, with the far end in `remote-ip`: `set ip 10.0.0.1 255.255.255.252` on one is dropped without a word.
|
|
28
|
+
- A GRE or VXLAN tunnel creates an interface of its own, named after it, which goes when the tunnel goes. VXLAN has no local address: it leaves from the address of the interface it names.
|
|
29
|
+
- A static route has no VRF of its own. It lands in the VRF of the interface it leaves through, as `get router info routing-table all` shows, so a blackhole route cannot go in a VRF. OSPF and BGP have no VRF setting.
|
|
30
|
+
- The stateless ACL, `firewall acl` and `acl6`, only drops: it has no action.
|
|
31
|
+
- A route map stops at the first rule that matches, with no way to go on to the next, and its rules permit unless told to deny.
|
|
32
|
+
- `show vpn certificate local` prints a certificate without its text or key. `show full-configuration` prints both, the key encrypted.
|
|
33
|
+
- A private key the device cannot read is answered `Invalid private key, password may be required`, with no `Command fail`, and the session goes on, as it is for one whose lines end in CR LF: the CLI takes each CR as a line of its own; the certificate fails only at `end`, `Attribute 'private-key' MUST be set`.
|
|
34
|
+
- A GRE or VXLAN interface takes no flow sampler, and says so without `Command fail`: `Netflow sampler isn't supported on Tunnel interface..`, and the same for sFlow and for VXLAN.
|
|
35
|
+
- A port in an aggregate has no `icmp-send-redirect`, `command parse error`, and loses it when it joins one.
|
|
36
|
+
- `purge` empties a table after asking `(y/n)`.
|
|
37
|
+
- A loopback has no MTU of its own and runs no VRRP. An interface's name has at most 15 characters.
|
|
38
|
+
- There is no WireGuard and no RPKI, `command parse error`. A route map has no large community and a BGP neighbor no role: neither is among their settings. SNMPv3 offers no SHA256.
|
|
39
|
+
- `execute backup config flash <comment>` keeps a revision on the device, and `execute restore config flash <id>` restores one.
|
|
40
|
+
- The ports are `port1` to `port4` at 40G and `port5` to `port32` at 10G, then `mgmt1` and `mgmt2`. The factory dedicates both management ports to management and gives them addresses `192.168.1.99` and `192.168.2.99`.
|