//native
/**
* Create change control
* Create a change control that runs pending tasks (resource API changecontrol.v1), one after the other or all in parallel. It still needs approval and execution. change_control_id is generated when empty and returned.
*/
export async function main(
auth: RT.AristaCloudvision,
name: string,
task_ids: string[],
run_in: "series" | "parallel" = "series",
notes: string = "",
change_control_id: string = ""
) {
if (!task_ids?.length) throw new Error("task_ids is empty")
const id = change_control_id || crypto.randomUUID()
const stages: Record<string, any> = {
root: { name: "root", rows: { values: [] as { values: string[] }[] } },
}
task_ids.forEach((task, index) => {
const stageId = `stage${index}`
stages[stageId] = {
name: `Task ${task}`,
action: {
name: "task",
timeout: 3000,
args: { values: { TaskID: String(task) } },
},
}
})
const stageIds = task_ids.map((_, index) => `stage${index}`)
stages.root.rows.values =
run_in === "parallel"
? [{ values: stageIds }]
: stageIds.map((stageId) => ({ values: [stageId] }))
const result = await cv(
auth,
"/api/resources/changecontrol/v1/ChangeControlConfig",
{
body: {
key: { id },
change: {
name,
rootStageId: "root",
stages: { values: stages },
...(notes ? { notes } : {}),
},
},
}
)
return { change_control_id: id, ...result }
}
// Calls CloudVision and turns its failures into readable errors.
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)
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