1 | |
2 |
|
3 | |
4 | * Execute change control |
5 | * Start an approved change control (resource API changecontrol.v1). One that has already run is refused; one already running is not started again. With wait_timeout_seconds set, wait for it to complete and fail with CloudVision's error if it did not succeed; otherwise return once it is started. |
6 | */ |
7 | export async function main( |
8 | auth: RT.AristaCloudvision, |
9 | change_control_id: string, |
10 | notes: string = "", |
11 | wait_timeout_seconds: number = 0 |
12 | ) { |
13 | const path = `/api/resources/changecontrol/v1/ChangeControl?key.id=${encodeURIComponent(change_control_id)}` |
14 | const notFound = `Change control ${change_control_id} not found` |
15 | const before = await cv(auth, path, { notFound }) |
16 | if (!before.value?.approve?.value) { |
17 | throw new Error(`Change control ${change_control_id} is not approved`) |
18 | } |
19 | if (before.value.status === "CHANGE_CONTROL_STATUS_COMPLETED") { |
20 | throw new Error( |
21 | `Change control ${change_control_id} has already run${before.value.error ? ` and failed: ${before.value.error}` : ""}` |
22 | ) |
23 | } |
24 | |
25 | if (before.value.status !== "CHANGE_CONTROL_STATUS_RUNNING") { |
26 | await cv(auth, "/api/resources/changecontrol/v1/ChangeControlConfig", { |
27 | body: { |
28 | key: { id: change_control_id }, |
29 | start: { value: true, notes: notes || "" }, |
30 | }, |
31 | }) |
32 | } |
33 | if (!wait_timeout_seconds) return (await cv(auth, path, { notFound })).value |
34 |
|
35 | const deadline = Date.now() + wait_timeout_seconds * 1000 |
36 | let cc = (await cv(auth, path, { notFound })).value |
37 | while ( |
38 | cc.status !== "CHANGE_CONTROL_STATUS_COMPLETED" && |
39 | Date.now() < deadline |
40 | ) { |
41 | await new Promise((resolve) => setTimeout(resolve, 5000)) |
42 | cc = (await cv(auth, path, { notFound })).value |
43 | } |
44 | if (cc.status !== "CHANGE_CONTROL_STATUS_COMPLETED") { |
45 | throw new Error( |
46 | `Change control ${change_control_id} still ${cc.status} after ${wait_timeout_seconds} s` |
47 | ) |
48 | } |
49 | if (cc.error || cc.completionReason === "COMPLETION_REASON_FAILED") { |
50 | throw new Error( |
51 | `Change control ${change_control_id} failed: ${cc.error || cc.completionReason}` |
52 | ) |
53 | } |
54 | return cc |
55 | } |
56 |
|
57 | |
58 | async function cv( |
59 | auth: RT.AristaCloudvision, |
60 | path: string, |
61 | options: { body?: unknown; notFound?: string; optional?: boolean } = {} |
62 | ): Promise<any> { |
63 | const url = new URL(path, auth.base_url) |
64 | const response = await fetch(url, { |
65 | method: options.body === undefined ? "GET" : "POST", |
66 | headers: { |
67 | Authorization: `Bearer ${auth.token}`, |
68 | ...(options.body === undefined |
69 | ? {} |
70 | : { "Content-Type": "application/json" }), |
71 | Accept: "application/json", |
72 | }, |
73 | body: options.body === undefined ? undefined : JSON.stringify(options.body), |
74 | }) |
75 | const text = await response.text() |
76 | if ( |
77 | response.status === 401 || |
78 | response.status === 403 || |
79 | (!response.ok && text.includes("User is unauthorized")) |
80 | ) { |
81 | throw new Error( |
82 | `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}` |
83 | ) |
84 | } |
85 | const data = |
86 | text.trim() === "" |
87 | ? {} |
88 | : /^\s*[[{]/.test(text) |
89 | ? JSON.parse(text) |
90 | : undefined |
91 | const error = findError(data) |
92 | if (response.ok && data !== undefined && !error) return data |
93 | const detail = error |
94 | ? `${error.errorCode} ${error.errorMessage ?? (error.errors ?? []).join("; ")}` |
95 | : `HTTP ${response.status} ${text.slice(0, 500)}` |
96 | |
97 | |
98 | if ( |
99 | response.status === 404 || |
100 | (error && (options.optional || options.notFound)) |
101 | ) { |
102 | if (options.optional) return undefined |
103 | if (options.notFound) throw new Error(`${options.notFound} (${detail})`) |
104 | } |
105 | if (response.ok && !error) { |
106 | throw new Error( |
107 | `CloudVision returned a non-JSON response for ${url.pathname}: check that base_url is the CloudVision address. ${detail}` |
108 | ) |
109 | } |
110 | throw new Error(`CloudVision ${url.pathname} failed: ${detail}`) |
111 | } |
112 |
|
113 | function findError(value: any): any { |
114 | if (!value || typeof value !== "object") return undefined |
115 | |
116 | if (!Array.isArray(value) && value.errorCode) return value |
117 | for (const item of Object.values(value)) { |
118 | const error = findError(item) |
119 | if (error) return error |
120 | } |
121 | return undefined |
122 | } |
123 |
|