//native
/**
* Apply configlets
* Apply provisioning configlets to a device or a container (legacy Network Provisioning API, as cvprac does) and save, which creates the pending tasks returned in taskIds. A device is found by serial number, hostname, FQDN or system MAC address; a container by exact name. Configlets already applied there are kept and left in place.
*/
export async function main(
auth: RT.AristaCloudvision,
target_type: "device" | "container",
target: string,
configlet_names: string[]
) {
if (!configlet_names?.length) throw new Error("configlet_names is empty")
const wanted = await Promise.all(
[...new Set(configlet_names)].map(async (name) => {
const configlet = await cv(
auth,
`/cvpservice/configlet/getConfigletByName.do?name=${encodeURIComponent(name)}`,
{ notFound: `Configlet ${name} not found` }
)
return { name: configlet.name ?? name, key: configlet.key as string }
})
)
const names: string[] = []
const keys: string[] = []
const builderNames: string[] = []
const builderKeys: string[] = []
let action: Record<string, unknown>
if (target_type === "device") {
const device = await findDevice(auth, target)
const current = await cv(
auth,
`/cvpservice/provisioning/getConfigletsByNetElementId.do?netElementId=${encodeURIComponent(device.systemMacAddress)}&startIndex=0&endIndex=0`
)
for (const c of current.configletList ?? []) {
names.push(c.name)
keys.push(c.key)
}
action = {
info: `Windmill: Configlet Assign: to Device ${device.fqdn}`,
infoPreview: `<b>Configlet Assign:</b> to Device ${device.fqdn}`,
toId: device.systemMacAddress,
toIdType: "netelement",
toName: device.fqdn,
nodeIpAddress: device.ipAddress,
nodeTargetIpAddress: device.ipAddress,
}
} else {
const container = await findContainer(auth, target)
const current = await cv(
auth,
`/cvpservice/provisioning/getConfigletsByContainerId.do?containerId=${encodeURIComponent(container.key)}&startIndex=0&endIndex=0`
)
for (const c of current.configletList ?? []) {
// As cvprac does: static configlets in configletList, builders in the
// builder lists, generated ones left for CloudVision to derive.
if (c.type === "Static") {
names.push(c.name)
keys.push(c.key)
} else if (c.type === "Builder") {
builderNames.push(c.name)
builderKeys.push(c.key)
}
}
action = {
info: `Windmill: Configlet Assign: to Container ${container.name}`,
infoPreview: `<b>Configlet Assign:</b> to Container ${container.name}`,
toId: container.key,
toIdType: "container",
toName: container.name,
nodeIpAddress: "",
nodeTargetIpAddress: "",
}
}
const added = wanted.filter((c) => !keys.includes(c.key))
if (!added.length) {
return { status: "unchanged", taskIds: [], applied: names }
}
for (const c of added) {
names.push(c.name)
keys.push(c.key)
}
await cv(
auth,
"/cvpservice/provisioning/addTempAction.do?format=topology&queryParam=&nodeId=root",
{
body: {
data: [
{
note: "",
action: "associate",
nodeType: "configlet",
nodeId: "",
configletList: keys,
configletNamesList: names,
ignoreConfigletNamesList: [],
ignoreConfigletList: [],
configletBuilderList: builderKeys,
configletBuilderNamesList: builderNames,
ignoreConfigletBuilderList: [],
ignoreConfigletBuilderNamesList: [],
fromId: "",
nodeName: "",
fromName: "",
childTasks: [],
parentTask: "",
...action,
},
],
},
}
)
const saved = await cv(auth, "/cvpservice/provisioning/v2/saveTopology.do", {
body: [],
})
return {
status: saved.data?.status ?? "saved",
taskIds: saved.data?.taskIds ?? [],
applied: names,
}
}
async function findDevice(auth: RT.AristaCloudvision, target: string) {
const devices = await cv(
auth,
"/cvpservice/inventory/devices?provisioned=true"
)
const t = target.toLowerCase()
const device = (Array.isArray(devices) ? devices : []).find((d: any) =>
[d.serialNumber, d.hostname, d.fqdn, d.systemMacAddress].some(
(v) => typeof v === "string" && v.toLowerCase() === t
)
)
if (!device) {
throw new Error(
`Device ${target} not found among the provisioned devices in CloudVision (match on serial number, hostname, FQDN or system MAC address)`
)
}
return device
}
async function findContainer(auth: RT.AristaCloudvision, name: string) {
const result = await cv(
auth,
`/cvpservice/provisioning/searchTopology.do?queryParam=${encodeURIComponent(name)}&startIndex=0&endIndex=0`
)
const container = (result.containerList ?? []).find(
(c: any) => c.name === name
)
if (!container) throw new Error(`Container ${name} not found in CloudVision`)
return container
}
// Calls CloudVision and turns its failures into readable errors. Legacy
// endpoints report errors as HTTP 200 with an errorCode field.
async function cv(
auth: RT.AristaCloudvision,
path: string,
options: { body?: unknown; notFound?: string; optional?: boolean } = {}
): Promise<any> {
const url = new URL(path, auth.base_url)
// cvprac sends legacy endpoints to /cvpservice on CVaaS and /web on
// on-premises CVP. Its is_cvaas flag is the resource's deployment field;
// when that is unset, a host under arista.io is taken as CVaaS.
const cvaas = auth.deployment
? auth.deployment === "cvaas"
: /(^|\.)arista\.io\.?$/i.test(url.hostname)
if (!cvaas && url.pathname.startsWith("/cvpservice/")) {
url.pathname = url.pathname.replace(/^\/cvpservice\//, "/web/")
}
const response = await fetch(url, {
method: options.body === undefined ? "GET" : "POST",
headers: {
Authorization: `Bearer ${auth.token}`,
...(options.body === undefined
? {}
: { "Content-Type": "application/json" }),
Accept: "application/json",
},
body: options.body === undefined ? undefined : JSON.stringify(options.body),
})
const text = await response.text()
if (
response.status === 401 ||
response.status === 403 ||
(!response.ok && text.includes("User is unauthorized"))
) {
throw new Error(
`CloudVision refused the token (HTTP ${response.status}): check that the service-account token is valid, not expired, and that its role allows this call. ${text}`
)
}
const data =
text.trim() === ""
? {}
: /^\s*[[{]/.test(text)
? JSON.parse(text)
: undefined
const error = findError(data)
if (response.ok && data !== undefined && !error) return data
const detail = error
? `${error.errorCode} ${error.errorMessage ?? (error.errors ?? []).join("; ")}`
: `HTTP ${response.status} ${text.slice(0, 500)}`
// Legacy lookups answer an unknown name or ID with an errorCode (cvprac
// treats any error from getTaskById and getImageBundleByName that way).
if (
response.status === 404 ||
(error && (options.optional || options.notFound))
) {
if (options.optional) return undefined
if (options.notFound) throw new Error(`${options.notFound} (${detail})`)
}
if (response.ok && !error) {
throw new Error(
`CloudVision returned a non-JSON response for ${url.pathname}: check that base_url is the CloudVision address. ${detail}`
)
}
throw new Error(`CloudVision ${url.pathname} failed: ${detail}`)
}
function findError(value: any): any {
if (!value || typeof value !== "object") return undefined
// Like cvprac, only a truthy errorCode counts, at any depth.
if (!Array.isArray(value) && value.errorCode) return value
for (const item of Object.values(value)) {
const error = findError(item)
if (error) return error
}
return undefined
}
Submitted by hugo989 5 hours ago