1 | |
2 |
|
3 | |
4 | * Create change control |
5 | * 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. |
6 | */ |
7 | export async function main( |
8 | auth: RT.AristaCloudvision, |
9 | name: string, |
10 | task_ids: string[], |
11 | run_in: "series" | "parallel" = "series", |
12 | notes: string = "", |
13 | change_control_id: string = "" |
14 | ) { |
15 | if (!task_ids?.length) throw new Error("task_ids is empty") |
16 | const id = change_control_id || crypto.randomUUID() |
17 | const stages: Record<string, any> = { |
18 | root: { name: "root", rows: { values: [] as { values: string[] }[] } }, |
19 | } |
20 | task_ids.forEach((task, index) => { |
21 | const stageId = `stage${index}` |
22 | stages[stageId] = { |
23 | name: `Task ${task}`, |
24 | action: { |
25 | name: "task", |
26 | timeout: 3000, |
27 | args: { values: { TaskID: String(task) } }, |
28 | }, |
29 | } |
30 | }) |
31 | const stageIds = task_ids.map((_, index) => `stage${index}`) |
32 | stages.root.rows.values = |
33 | run_in === "parallel" |
34 | ? [{ values: stageIds }] |
35 | : stageIds.map((stageId) => ({ values: [stageId] })) |
36 |
|
37 | const result = await cv( |
38 | auth, |
39 | "/api/resources/changecontrol/v1/ChangeControlConfig", |
40 | { |
41 | body: { |
42 | key: { id }, |
43 | change: { |
44 | name, |
45 | rootStageId: "root", |
46 | stages: { values: stages }, |
47 | ...(notes ? { notes } : {}), |
48 | }, |
49 | }, |
50 | } |
51 | ) |
52 | return { change_control_id: id, ...result } |
53 | } |
54 |
|
55 | |
56 | async function cv( |
57 | auth: RT.AristaCloudvision, |
58 | path: string, |
59 | options: { body?: unknown; notFound?: string; optional?: boolean } = {} |
60 | ): Promise<any> { |
61 | const url = new URL(path, auth.base_url) |
62 | const response = await fetch(url, { |
63 | method: options.body === undefined ? "GET" : "POST", |
64 | headers: { |
65 | Authorization: `Bearer ${auth.token}`, |
66 | ...(options.body === undefined |
67 | ? {} |
68 | : { "Content-Type": "application/json" }), |
69 | Accept: "application/json", |
70 | }, |
71 | body: options.body === undefined ? undefined : JSON.stringify(options.body), |
72 | }) |
73 | const text = await response.text() |
74 | if ( |
75 | response.status === 401 || |
76 | response.status === 403 || |
77 | (!response.ok && text.includes("User is unauthorized")) |
78 | ) { |
79 | throw new Error( |
80 | `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}` |
81 | ) |
82 | } |
83 | const data = |
84 | text.trim() === "" |
85 | ? {} |
86 | : /^\s*[[{]/.test(text) |
87 | ? JSON.parse(text) |
88 | : undefined |
89 | const error = findError(data) |
90 | if (response.ok && data !== undefined && !error) return data |
91 | const detail = error |
92 | ? `${error.errorCode} ${error.errorMessage ?? (error.errors ?? []).join("; ")}` |
93 | : `HTTP ${response.status} ${text.slice(0, 500)}` |
94 | |
95 | |
96 | if ( |
97 | response.status === 404 || |
98 | (error && (options.optional || options.notFound)) |
99 | ) { |
100 | if (options.optional) return undefined |
101 | if (options.notFound) throw new Error(`${options.notFound} (${detail})`) |
102 | } |
103 | if (response.ok && !error) { |
104 | throw new Error( |
105 | `CloudVision returned a non-JSON response for ${url.pathname}: check that base_url is the CloudVision address. ${detail}` |
106 | ) |
107 | } |
108 | throw new Error(`CloudVision ${url.pathname} failed: ${detail}`) |
109 | } |
110 |
|
111 | function findError(value: any): any { |
112 | if (!value || typeof value !== "object") return undefined |
113 | |
114 | if (!Array.isArray(value) && value.errorCode) return value |
115 | for (const item of Object.values(value)) { |
116 | const error = findError(item) |
117 | if (error) return error |
118 | } |
119 | return undefined |
120 | } |
121 |
|