@takodotid/circuit 0.41.0 → 0.42.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.
Files changed (44) hide show
  1. package/dist/chunks/index-6wb41q67.js +62 -0
  2. package/dist/chunks/{index-k38bby43.js → index-yzw86rc8.js} +4 -62
  3. package/dist/chunks/{server-ggertnce.js → server-cxr9ynxc.js} +1 -1
  4. package/dist/cli/index.js +121 -47
  5. package/dist/helpers/index.js +14 -9
  6. package/dist/types/core/cabling.d.ts +0 -6
  7. package/dist/types/core/define.d.ts +1 -1
  8. package/dist/types/core/validator.d.ts +2 -0
  9. package/dist/types/helpers/cables.d.ts +14 -11
  10. package/dist/types/helpers/exchange.d.ts +7 -1
  11. package/dist/types/helpers/upstreams.d.ts +11 -3
  12. package/dist/types/platforms/derive.d.ts +1 -1
  13. package/dist/types/platforms/fortios/routing.d.ts +2 -2
  14. package/dist/types/platforms/junos/bgp.d.ts +2 -2
  15. package/dist/types/platforms/routeros/render.d.ts +4 -0
  16. package/dist/types/schema/device.d.ts +4 -4
  17. package/dist/types/schema/interfaces.d.ts +1 -1
  18. package/dist/types/schema/routing.d.ts +43 -10
  19. package/package.json +1 -1
  20. package/src/cli/index.ts +26 -29
  21. package/src/core/cabling.ts +0 -30
  22. package/src/core/define.ts +3 -1
  23. package/src/core/validator.ts +60 -0
  24. package/src/helpers/cables.ts +20 -15
  25. package/src/helpers/exchange.ts +9 -3
  26. package/src/helpers/upstreams.ts +9 -4
  27. package/src/platforms/derive.ts +23 -1
  28. package/src/platforms/fortios/routing.ts +2 -2
  29. package/src/platforms/junos/bgp.ts +2 -2
  30. package/src/platforms/routeros/TRAPS.md +4 -0
  31. package/src/platforms/routeros/export.ts +1 -0
  32. package/src/platforms/routeros/index.ts +11 -2
  33. package/src/platforms/routeros/plan.ts +3 -0
  34. package/src/platforms/routeros/render.ts +28 -3
  35. package/src/schema/device.ts +4 -3
  36. package/src/schema/interfaces.ts +1 -1
  37. package/src/schema/routing.ts +46 -10
  38. package/templates/colocation/checks.ts +14 -11
  39. package/templates/colocation/edge-01.ts +2 -2
  40. package/templates/colocation/tenancy.ts +50 -71
  41. package/templates/colocation/tenants/acme.ts +10 -10
  42. package/templates/colocation/tenants/index.ts +2 -1
  43. package/templates/colocation/tor-01.ts +2 -2
  44. package/templates/project/AGENTS.md +15 -15
@@ -1,94 +1,73 @@
1
- // What a tenant is, and what each device carries for it. A tenant's file says what is the tenant's own; its VLANs and names follow from its name and number.
1
+ // What a tenant is, and what each device carries for it. A tenant's file writes everything its devices carry: its VLANs, the router's addresses for it, its ports. These functions only gather them for one device.
2
2
 
3
- import type { AclRule, Interface, IP, Port, Prefix, Vlan } from "@takodotid/circuit";
3
+ import type { AclRule, Interface, InterfaceAddress, Port, Prefix, Vlan } from "@takodotid/circuit";
4
4
  import { Address } from "@takodotid/circuit";
5
5
 
6
- /** A tenant's networks. `internet` carries its servers' public addresses and reaches the router. `private` connects its own servers to each other and stays on the switches. */
7
- type TenantNetwork = "internet" | "private";
8
-
9
- export type Tenant<N extends string = string> = {
10
- /** Lowercase, and never reused. Names everything the tenant has. */
11
- name: N;
12
- /** A number no other tenant has. Its internet VLAN is 1000 + number, its private VLAN 2000 + number. */
13
- number: number;
14
- /** Public blocks routed to the tenant. `gateway` is the router's address inside the block, which the tenant's servers use as their gateway. */
15
- blocks: readonly { prefix: Prefix; gateway: IP }[];
6
+ /** A VLAN with its name. */
7
+ export type NamedVlan = Vlan & { name: string };
8
+
9
+ export type Tenant = {
10
+ /** Lowercase, and never reused. */
11
+ name: string;
12
+ /** The tenant's VLAN to the internet. The router has it, with the tenant's gateways, and so does every switch, to carry it up to the router. */
13
+ internet: {
14
+ vlan: NamedVlan;
15
+ /** The router's addresses on it, each the gateway the tenant's servers use inside one of its blocks, such as `198.51.100.9/29`. */
16
+ gateways: readonly InterfaceAddress[];
17
+ };
18
+ /** A VLAN between the tenant's own servers. It is only on a switch where one of the tenant's ports uses it, and never on the router. */
19
+ private?: { vlan: NamedVlan };
16
20
  /** Rules for traffic toward the tenant's IPv4 blocks from the transit and the exchange, such as a cap on UDP. Everything reaches the tenant when absent. */
17
21
  acl?: readonly (Omit<AclRule<never>, "match"> & { match?: Omit<NonNullable<AclRule<never>["match"]>, "vlan" | "dst"> })[];
18
- /** The tenant's ports, by switch, then by port. A port in `private` gives the tenant a private network on that switch. */
19
- ports: Readonly<
20
- Record<string, Readonly<Record<string, { description: string; access?: TenantNetwork; trunk?: readonly TenantNetwork[] }>>>
21
- >;
22
+ /** The tenant's ports, by switch, then by port, written as any port is. */
23
+ ports: Readonly<Record<string, Readonly<Record<string, Port<string, string, string, string>>>>>;
22
24
  };
23
25
 
24
- /** Declare a tenant. */
25
- export const tenant = <const N extends string>(spec: Tenant<N>): Tenant<N> => spec;
26
-
27
- const internetVlan = <N extends string>(t: Tenant<N>) => `${t.name}-internet` as const;
28
- const privateVlan = <N extends string>(t: Tenant<N>) => `${t.name}-private` as const;
26
+ /** A VLAN as `vlans` takes it, keyed by its name. */
27
+ const entry = ({ name, ...vlan }: NamedVlan): [string, Vlan] => [name, vlan];
29
28
 
30
- /** Every tenant's internet VLAN. The router has them all, and so does each switch, to carry them up to the router. */
31
- export const internetVlans = <N extends string>(tenants: readonly Tenant<N>[]) =>
32
- Object.fromEntries(tenants.map((t) => [internetVlan(t), { id: 1000 + t.number, description: `Tenant ${t.name} internet` }])) as Record<
33
- `${N}-internet`,
34
- Vlan
35
- >;
29
+ /** The tenant's blocks: the network of each gateway. */
30
+ export const blocksOf = (t: Tenant): Prefix[] => t.internet.gateways.map((gateway) => Address.prefixOf(gateway));
36
31
 
37
- /** The names of every tenant's internet VLAN, for a trunk toward the router. */
38
- export const internetVlanNames = <N extends string>(tenants: readonly Tenant<N>[]) => tenants.map(internetVlan);
32
+ /** Every tenant's internet VLAN, for the router. */
33
+ export const routerVlans = (tenants: readonly Tenant[]): Record<string, Vlan> =>
34
+ Object.fromEntries(tenants.map((t) => entry(t.internet.vlan)));
39
35
 
40
- /** The private VLANs one switch has: those of tenants with a port on it in their private network. */
41
- export const privateVlans = <N extends string>(tenants: readonly Tenant<N>[], device: string) =>
36
+ /** The VLANs one switch carries: every tenant's internet VLAN, and the private VLAN of each tenant with a port on it that uses it. */
37
+ export const switchVlans = (tenants: readonly Tenant[], device: string): Record<string, Vlan> =>
42
38
  Object.fromEntries(
43
- tenants
44
- .filter((t) =>
45
- Object.values(t.ports[device] ?? {}).some((port) => port.access === "private" || port.trunk?.includes("private"))
46
- )
47
- .map((t) => [privateVlan(t), { id: 2000 + t.number, description: `Tenant ${t.name} private` }])
48
- ) as Record<`${N}-private`, Vlan>;
49
-
50
- /** The tenants' ports on one switch. */
51
- export function portsOn<N extends string>(tenants: readonly Tenant<N>[], device: string) {
52
- const ports: Record<string, Port<`${N}-internet` | `${N}-private`, string, string, string>> = {};
53
-
54
- for (const t of tenants) {
55
- const vlanOf = (network: TenantNetwork) => (network === "internet" ? internetVlan(t) : privateVlan(t));
39
+ tenants.flatMap((t) => {
40
+ const ports = Object.values(t.ports[device] ?? {});
41
+ const usesPrivate =
42
+ t.private &&
43
+ ports.some((port) => port.access_vlan === t.private!.vlan.name || port.trunk_vlans?.includes(t.private!.vlan.name));
44
+ return [entry(t.internet.vlan), ...(usesPrivate ? [entry(t.private!.vlan)] : [])];
45
+ })
46
+ );
56
47
 
57
- for (const [name, { access, trunk, description }] of Object.entries(t.ports[device] ?? {})) {
58
- ports[name] = {
59
- description,
60
- ...(access ? { access_vlan: vlanOf(access) } : {}),
61
- ...(trunk ? { trunk_vlans: trunk.map(vlanOf) } : {}),
62
- };
63
- }
64
- }
48
+ /** The names of every tenant's internet VLAN, for a trunk toward the router. */
49
+ export const internetVlanNames = (tenants: readonly Tenant[]) => tenants.map((t) => t.internet.vlan.name);
65
50
 
66
- return ports;
67
- }
51
+ /** The tenants' ports on one switch. */
52
+ export const portsOn = (tenants: readonly Tenant[], device: string): Record<string, Port<string, string, string, string>> =>
53
+ Object.assign({}, ...tenants.map((t) => t.ports[device] ?? {}));
68
54
 
69
55
  /** The router's interface on each tenant's internet VLAN, holding the tenant's gateways. */
70
- export function gateways<N extends string>(tenants: readonly Tenant<N>[]) {
71
- const interfaces = {} as Record<`${N}-internet`, Interface<`${N}-internet`, never>>;
72
-
73
- for (const t of tenants) {
74
- interfaces[internetVlan(t)] = {
75
- type: "vlan",
76
- vlan: internetVlan(t),
77
- description: `Tenant ${t.name}`,
78
- addresses: t.blocks.map(({ prefix, gateway }): Prefix => `${gateway}/${Address.split(prefix).len}`),
79
- };
80
- }
81
-
82
- return interfaces;
83
- }
56
+ export const gateways = (tenants: readonly Tenant[]): Record<string, Interface<string, never>> =>
57
+ Object.fromEntries(
58
+ tenants.map((t) => [
59
+ t.internet.vlan.name,
60
+ { type: "vlan", vlan: t.internet.vlan.name, description: `Tenant ${t.name}`, addresses: t.internet.gateways },
61
+ ])
62
+ );
84
63
 
85
64
  /** Each tenant's `acl` rules on one VLAN from outside, one per rule and IPv4 block, each limited to traffic toward that block. */
86
65
  export const edgeRules = <V extends string>(tenants: readonly Tenant[], vlan: V): AclRule<V>[] =>
87
66
  tenants.flatMap((t) =>
88
67
  (t.acl ?? []).flatMap((rule) =>
89
- t.blocks
90
- .filter(({ prefix }) => Address.family(prefix) === "ipv4")
91
- .map(({ prefix }) => ({
68
+ blocksOf(t)
69
+ .filter((prefix) => Address.family(prefix) === "ipv4")
70
+ .map((prefix) => ({
92
71
  ...rule,
93
72
  ...(rule.description ? { description: `${vlan}: ${rule.description}` } : {}),
94
73
  match: { ...rule.match, vlan, dst: prefix },
@@ -1,19 +1,19 @@
1
1
  // An example tenant. Copy this file for each new tenant, and add it to index.ts.
2
2
 
3
- import { tenant } from "../tenancy";
3
+ import type { Tenant } from "../tenancy";
4
4
 
5
- export default tenant({
5
+ export default {
6
6
  name: "acme",
7
- number: 1,
8
- blocks: [
9
- { prefix: "198.51.100.8/29", gateway: "198.51.100.9" },
10
- { prefix: "2001:db8:100:1::/64", gateway: "2001:db8:100:1::1" },
11
- ],
7
+ internet: {
8
+ vlan: { name: "acme-internet", id: 1001, description: "Tenant acme internet" },
9
+ gateways: ["198.51.100.9/29", "2001:db8:100:1::1/64"],
10
+ },
11
+ private: { vlan: { name: "acme-private", id: 2001, description: "Tenant acme private" } },
12
12
  acl: [{ description: "UDP to acme above 1G", match: { protocol: "udp" }, action: "accept", rate: "1G" }],
13
13
  ports: {
14
14
  "tor-01": {
15
- "10g-1": { description: "acme server 1", trunk: ["internet", "private"] },
16
- "10g-2": { description: "acme server 2", trunk: ["internet", "private"] },
15
+ "10g-1": { description: "acme server 1", trunk_vlans: ["acme-internet", "acme-private"] },
16
+ "10g-2": { description: "acme server 2", trunk_vlans: ["acme-internet", "acme-private"] },
17
17
  },
18
18
  },
19
- });
19
+ } satisfies Tenant;
@@ -1,5 +1,6 @@
1
1
  // Every tenant. A tenant joins by being added here, and leaves by being removed: the next apply takes away everything it had.
2
2
 
3
+ import type { Tenant } from "../tenancy";
3
4
  import acme from "./acme";
4
5
 
5
- export const TENANTS = [acme] as const;
6
+ export const TENANTS: readonly Tenant[] = [acme];
@@ -1,7 +1,7 @@
1
1
  // The top-of-rack switch: the tenants' servers, and one uplink to the router.
2
2
 
3
3
  import { defineDevice, secret, Utils } from "@takodotid/circuit";
4
- import { internetVlanNames, internetVlans, portsOn, privateVlans } from "./tenancy";
4
+ import { internetVlanNames, portsOn, switchVlans } from "./tenancy";
5
5
  import { TENANTS } from "./tenants";
6
6
 
7
7
  export default defineDevice({
@@ -17,7 +17,7 @@ export default defineDevice({
17
17
  lldp: true,
18
18
  stp: {},
19
19
 
20
- vlans: Utils.merge({ mgmt: { id: 99, description: "Management" } }, internetVlans(TENANTS), privateVlans(TENANTS, "tor-01")),
20
+ vlans: Utils.merge({ mgmt: { id: 99, description: "Management" } }, switchVlans(TENANTS, "tor-01")),
21
21
 
22
22
  // Every tenant's ports, from its file in tenants/, beside the switch's own.
23
23
  ports: Utils.merge(portsOn(TENANTS, "tor-01"), {
@@ -16,21 +16,21 @@ What each field means is written as a comment on the field, in `node_modules/@ta
16
16
 
17
17
  ## Commands
18
18
 
19
- | Command | What it does | Changes a device |
20
- | --------------------------------------------- | ----------------------------------------------------------------------------- | ---------------- |
21
- | `npx circuit validate` | Checks every file and every rule in the checks | no |
22
- | `npx circuit diff [device]` | Shows what would change, compared with the last snapshot | no |
23
- | `npx circuit build <device>` | Prints the full configuration the device should run | no |
24
- | `npx circuit snapshot [device]` | Reads what each device runs now into `.circuit/state/` | no, only reads |
25
- | `npx circuit show <device> '<command>'` | Runs commands that only read on the device, secrets hidden | no, only reads |
26
- | `npx circuit bgp [device] [name-regex]` | Each BGP neighbor in the config next to its live session: up or down | no, only reads |
27
- | `npx circuit console <device> '<line>'` | Sends lines to the device's serial console, unchecked | **can** |
28
- | `npx circuit apply <device>` | Reads the device, then shows what would change on it | no |
29
- | `npx circuit apply <device> --confirm` | Sends the change to the device | **yes** |
30
- | `npx circuit secrets` | Lists every secret the config uses, and whether each one can be read | no |
31
- | `npx circuit rename <device> <new-name>` | Shows every line a new device name changes; `--confirm` writes them | no |
32
- | `npx circuit addresses` | Every address the config uses, by prefix, and what is still free | no |
33
- | `npx circuit graph --layer=all --format=json` | The whole network as a graph: cables, VLANs, addresses, BGP, OSPF and tunnels | no |
19
+ | Command | What it does | Changes a device |
20
+ | ---------------------------------------------- | ----------------------------------------------------------------------------- | ---------------- |
21
+ | `npx circuit validate` | Checks every file and every rule in the checks | no |
22
+ | `npx circuit diff [device]` | Shows what would change, compared with the last snapshot | no |
23
+ | `npx circuit build <device>` | Prints the full configuration the device should run | no |
24
+ | `npx circuit snapshot [device]` | Reads what each device runs now into `.circuit/state/` | no, only reads |
25
+ | `npx circuit show <device> '<command>'` | Runs commands that only read on the device, secrets hidden | no, only reads |
26
+ | `npx circuit bgp [device] --neighbors <regex>` | Each BGP neighbor in the config next to its live session: up or down | no, only reads |
27
+ | `npx circuit console <device> '<line>'` | Sends lines to the device's serial console, unchecked | **can** |
28
+ | `npx circuit apply <device>` | Reads the device, then shows what would change on it | no |
29
+ | `npx circuit apply <device> --confirm` | Sends the change to the device | **yes** |
30
+ | `npx circuit secrets` | Lists every secret the config uses, and whether each one can be read | no |
31
+ | `npx circuit rename <device> <new-name>` | Shows every line a new device name changes; `--confirm` writes them | no |
32
+ | `npx circuit addresses` | Every address the config uses, by prefix, and what is still free | no |
33
+ | `npx circuit graph --layer=all --format=json` | The whole network as a graph: cables, VLANs, addresses, BGP, OSPF and tunnels | no |
34
34
 
35
35
  ## Making a change
36
36