@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.
- package/dist/chunks/index-6wb41q67.js +62 -0
- package/dist/chunks/{index-k38bby43.js → index-yzw86rc8.js} +4 -62
- package/dist/chunks/{server-ggertnce.js → server-cxr9ynxc.js} +1 -1
- package/dist/cli/index.js +121 -47
- package/dist/helpers/index.js +14 -9
- package/dist/types/core/cabling.d.ts +0 -6
- package/dist/types/core/define.d.ts +1 -1
- package/dist/types/core/validator.d.ts +2 -0
- package/dist/types/helpers/cables.d.ts +14 -11
- package/dist/types/helpers/exchange.d.ts +7 -1
- package/dist/types/helpers/upstreams.d.ts +11 -3
- package/dist/types/platforms/derive.d.ts +1 -1
- package/dist/types/platforms/fortios/routing.d.ts +2 -2
- package/dist/types/platforms/junos/bgp.d.ts +2 -2
- package/dist/types/platforms/routeros/render.d.ts +4 -0
- package/dist/types/schema/device.d.ts +4 -4
- package/dist/types/schema/interfaces.d.ts +1 -1
- package/dist/types/schema/routing.d.ts +43 -10
- package/package.json +1 -1
- package/src/cli/index.ts +26 -29
- package/src/core/cabling.ts +0 -30
- package/src/core/define.ts +3 -1
- package/src/core/validator.ts +60 -0
- package/src/helpers/cables.ts +20 -15
- package/src/helpers/exchange.ts +9 -3
- package/src/helpers/upstreams.ts +9 -4
- package/src/platforms/derive.ts +23 -1
- package/src/platforms/fortios/routing.ts +2 -2
- package/src/platforms/junos/bgp.ts +2 -2
- package/src/platforms/routeros/TRAPS.md +4 -0
- package/src/platforms/routeros/export.ts +1 -0
- package/src/platforms/routeros/index.ts +11 -2
- package/src/platforms/routeros/plan.ts +3 -0
- package/src/platforms/routeros/render.ts +28 -3
- package/src/schema/device.ts +4 -3
- package/src/schema/interfaces.ts +1 -1
- package/src/schema/routing.ts +46 -10
- package/templates/colocation/checks.ts +14 -11
- package/templates/colocation/edge-01.ts +2 -2
- package/templates/colocation/tenancy.ts +50 -71
- package/templates/colocation/tenants/acme.ts +10 -10
- package/templates/colocation/tenants/index.ts +2 -1
- package/templates/colocation/tor-01.ts +2 -2
- 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
|
|
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,
|
|
3
|
+
import type { AclRule, Interface, InterfaceAddress, Port, Prefix, Vlan } from "@takodotid/circuit";
|
|
4
4
|
import { Address } from "@takodotid/circuit";
|
|
5
5
|
|
|
6
|
-
/** A
|
|
7
|
-
type
|
|
8
|
-
|
|
9
|
-
export type Tenant
|
|
10
|
-
/** Lowercase, and never reused.
|
|
11
|
-
name:
|
|
12
|
-
/**
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
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
|
|
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
|
-
/**
|
|
25
|
-
|
|
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
|
-
/**
|
|
31
|
-
export const
|
|
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
|
-
/**
|
|
38
|
-
export const
|
|
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
|
|
41
|
-
export const
|
|
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
|
-
.
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
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
|
-
|
|
58
|
-
|
|
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
|
-
|
|
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
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
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
|
|
90
|
-
.filter((
|
|
91
|
-
.map((
|
|
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 {
|
|
3
|
+
import type { Tenant } from "../tenancy";
|
|
4
4
|
|
|
5
|
-
export default
|
|
5
|
+
export default {
|
|
6
6
|
name: "acme",
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
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",
|
|
16
|
-
"10g-2": { description: "acme server 2",
|
|
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]
|
|
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,
|
|
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" } },
|
|
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
|
|
20
|
-
|
|
|
21
|
-
| `npx circuit validate`
|
|
22
|
-
| `npx circuit diff [device]`
|
|
23
|
-
| `npx circuit build <device>`
|
|
24
|
-
| `npx circuit snapshot [device]`
|
|
25
|
-
| `npx circuit show <device> '<command>'`
|
|
26
|
-
| `npx circuit bgp [device]
|
|
27
|
-
| `npx circuit console <device> '<line>'`
|
|
28
|
-
| `npx circuit apply <device>`
|
|
29
|
-
| `npx circuit apply <device> --confirm`
|
|
30
|
-
| `npx circuit secrets`
|
|
31
|
-
| `npx circuit rename <device> <new-name>`
|
|
32
|
-
| `npx circuit addresses`
|
|
33
|
-
| `npx circuit graph --layer=all --format=json`
|
|
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
|
|