//native
/**
* Upsert endpoint
* 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.
*/
export async function main(
auth: RT.CiscoIse,
mac: string,
identity_group: string | undefined,
description: string | undefined,
dry_run: boolean = true
) {
const base = new URL(auth.base_url)
if (auth.ers_port) base.port = String(auth.ers_port)
const headers = {
Authorization: `Basic ${btoa(`${auth.username}:${auth.password}`)}`,
"Content-Type": "application/json",
Accept: "application/json",
}
const ers = async (method: string, path: string, body?: unknown) => {
const response = await fetch(new URL(path, base), {
method,
headers,
body: body === undefined ? undefined : JSON.stringify(body),
})
if (!response.ok) {
throw new Error(`${response.status} ${await response.text()}`)
}
return response
}
// Find an object id by filter, matching client-side and following pages, so the lookup
// stays correct on ISE versions that ignore the filter.
const find = async (
path: string,
filter: string,
match: (r: any) => boolean
) => {
for (let page = 1; ; page++) {
const { SearchResult } = await (
await ers(
"GET",
`${path}?size=100&page=${page}&filter=${encodeURIComponent(filter)}`
)
).json()
const hit = (SearchResult.resources ?? []).find(match)
if (hit) return hit.id as string
if (!SearchResult.nextPage) return null
}
}
// ISE stores MACs as AA:BB:CC:DD:EE:FF; accept any separator or case
const hex = mac.replace(/[-:.\s]/g, "").toUpperCase()
if (!/^[0-9A-F]{12}$/.test(hex))
throw new Error(`Invalid MAC address: ${mac}`)
const macAddress = hex.match(/../g)!.join(":")
// Desired state, only for the fields the caller set
const desired: { [key: string]: any } = {}
if (identity_group) {
const groupId = await find(
"/ers/config/endpointgroup",
`name.EQ.${identity_group}`,
(r) => r.name === identity_group
)
if (!groupId) {
throw new Error(`Endpoint identity group not found: ${identity_group}`)
}
desired.groupId = groupId
desired.staticGroupAssignment = true
}
if (description) desired.description = description
// Current state (endpoint list items are named by MAC)
const id = await find(
"/ers/config/endpoint",
`mac.EQ.${macAddress}`,
(r) => String(r.name).replace(/[-:.]/g, "").toUpperCase() === hex
)
const current = id
? (await (await ers("GET", `/ers/config/endpoint/${id}`)).json())
.ERSEndPoint
: null
const diff = Object.entries(desired)
.filter(([field, value]) => current?.[field] !== value)
.map(([field, value]) => ({
field,
current: current?.[field] ?? null,
desired: value,
}))
if (!current) {
if (!desired.groupId) {
throw new Error(
"Endpoint does not exist: identity_group is required to create it"
)
}
const result = {
changed: true,
action: "create",
id: null as string | null,
diff,
applied: false,
}
if (dry_run !== false) return result
const response = await ers("POST", "/ers/config/endpoint", {
ERSEndPoint: {
name: macAddress,
mac: macAddress,
staticProfileAssignment: false,
...desired,
},
})
result.id = response.headers.get("Location")?.split("/").pop() ?? null
result.applied = true
return result
}
const result = {
changed: diff.length > 0,
action: diff.length ? "update" : "none",
id: current.id as string,
diff,
applied: false,
}
if (dry_run !== false || diff.length === 0) return result
// Send the identifiers and the managed fields only, as Cisco's ansible-ise endpoint module
// does (ciscoisesdk drops the fields it isn't given)
await ers("PUT", `/ers/config/endpoint/${current.id}`, {
ERSEndPoint: {
id: current.id,
name: current.name,
mac: current.mac ?? macAddress,
...desired,
},
})
result.applied = true
return result
}
Submitted by hugo989 6 hours ago