1 | |
2 |
|
3 | |
4 | * Upsert endpoint |
5 | * Declaratively set an endpoint's identity group and description by MAC: reads the current endpoint, returns the difference and, when dry_run is false, creates the endpoint or updates the fields that differ. Fields left empty are not managed. Creating a new endpoint requires identity_group. |
6 | */ |
7 | export async function main( |
8 | auth: RT.CiscoIse, |
9 | mac: string, |
10 | identity_group: string | undefined, |
11 | description: string | undefined, |
12 | dry_run: boolean = true |
13 | ) { |
14 | const base = new URL(auth.base_url) |
15 | if (auth.ers_port) base.port = String(auth.ers_port) |
16 | const headers = { |
17 | Authorization: `Basic ${btoa(`${auth.username}:${auth.password}`)}`, |
18 | "Content-Type": "application/json", |
19 | Accept: "application/json", |
20 | } |
21 | const ers = async (method: string, path: string, body?: unknown) => { |
22 | const response = await fetch(new URL(path, base), { |
23 | method, |
24 | headers, |
25 | body: body === undefined ? undefined : JSON.stringify(body), |
26 | }) |
27 | if (!response.ok) { |
28 | throw new Error(`${response.status} ${await response.text()}`) |
29 | } |
30 | return response |
31 | } |
32 | |
33 | |
34 | const find = async ( |
35 | path: string, |
36 | filter: string, |
37 | match: (r: any) => boolean |
38 | ) => { |
39 | for (let page = 1; ; page++) { |
40 | const { SearchResult } = await ( |
41 | await ers( |
42 | "GET", |
43 | `${path}?size=100&page=${page}&filter=${encodeURIComponent(filter)}` |
44 | ) |
45 | ).json() |
46 | const hit = (SearchResult.resources ?? []).find(match) |
47 | if (hit) return hit.id as string |
48 | if (!SearchResult.nextPage) return null |
49 | } |
50 | } |
51 |
|
52 | |
53 | const hex = mac.replace(/[-:.\s]/g, "").toUpperCase() |
54 | if (!/^[0-9A-F]{12}$/.test(hex)) |
55 | throw new Error(`Invalid MAC address: ${mac}`) |
56 | const macAddress = hex.match(/../g)!.join(":") |
57 |
|
58 | |
59 | const desired: { [key: string]: any } = {} |
60 | if (identity_group) { |
61 | const groupId = await find( |
62 | "/ers/config/endpointgroup", |
63 | `name.EQ.${identity_group}`, |
64 | (r) => r.name === identity_group |
65 | ) |
66 | if (!groupId) { |
67 | throw new Error(`Endpoint identity group not found: ${identity_group}`) |
68 | } |
69 | desired.groupId = groupId |
70 | desired.staticGroupAssignment = true |
71 | } |
72 | if (description) desired.description = description |
73 |
|
74 | |
75 | const id = await find( |
76 | "/ers/config/endpoint", |
77 | `mac.EQ.${macAddress}`, |
78 | (r) => String(r.name).replace(/[-:.]/g, "").toUpperCase() === hex |
79 | ) |
80 | const current = id |
81 | ? (await (await ers("GET", `/ers/config/endpoint/${id}`)).json()) |
82 | .ERSEndPoint |
83 | : null |
84 |
|
85 | const diff = Object.entries(desired) |
86 | .filter(([field, value]) => current?.[field] !== value) |
87 | .map(([field, value]) => ({ |
88 | field, |
89 | current: current?.[field] ?? null, |
90 | desired: value, |
91 | })) |
92 |
|
93 | if (!current) { |
94 | if (!desired.groupId) { |
95 | throw new Error( |
96 | "Endpoint does not exist: identity_group is required to create it" |
97 | ) |
98 | } |
99 | const result = { |
100 | changed: true, |
101 | action: "create", |
102 | id: null as string | null, |
103 | diff, |
104 | applied: false, |
105 | } |
106 | if (dry_run !== false) return result |
107 | const response = await ers("POST", "/ers/config/endpoint", { |
108 | ERSEndPoint: { |
109 | name: macAddress, |
110 | mac: macAddress, |
111 | staticProfileAssignment: false, |
112 | ...desired, |
113 | }, |
114 | }) |
115 | result.id = response.headers.get("Location")?.split("/").pop() ?? null |
116 | result.applied = true |
117 | return result |
118 | } |
119 |
|
120 | const result = { |
121 | changed: diff.length > 0, |
122 | action: diff.length ? "update" : "none", |
123 | id: current.id as string, |
124 | diff, |
125 | applied: false, |
126 | } |
127 | if (dry_run !== false || diff.length === 0) return result |
128 | |
129 | |
130 | await ers("PUT", `/ers/config/endpoint/${current.id}`, { |
131 | ERSEndPoint: { |
132 | id: current.id, |
133 | name: current.name, |
134 | mac: current.mac ?? macAddress, |
135 | ...desired, |
136 | }, |
137 | }) |
138 | result.applied = true |
139 | return result |
140 | } |
141 |
|