1 | |
2 |
|
3 | |
4 | * Create or update configlet |
5 | * Create a static provisioning configlet, or replace the config of the one with that name (legacy Network Provisioning API). Updating a configlet that is applied somewhere makes CloudVision create pending tasks for the affected devices; the task IDs CloudVision reports are returned in taskIds (check List tasks if it is empty). |
6 | */ |
7 | export async function main( |
8 | auth: RT.AristaCloudvision, |
9 | name: string, |
10 | config: string |
11 | ) { |
12 | const lookup = `/cvpservice/configlet/getConfigletByName.do?name=${encodeURIComponent(name)}` |
13 | const existing = await cv(auth, lookup, { optional: true }) |
14 | if (!existing) { |
15 | await cv(auth, "/cvpservice/configlet/addConfiglet.do", { |
16 | body: { name, config }, |
17 | }) |
18 | const created = await cv(auth, lookup) |
19 | return { action: "created", key: created.key, name, taskIds: [] } |
20 | } |
21 | if (existing.config === config) { |
22 | return { action: "unchanged", key: existing.key, name, taskIds: [] } |
23 | } |
24 | const result = await cv(auth, "/cvpservice/configlet/updateConfiglet.do", { |
25 | body: { config, key: existing.key, name, waitForTaskIds: true }, |
26 | }) |
27 | return { |
28 | action: "updated", |
29 | key: existing.key, |
30 | name, |
31 | taskIds: result.taskIds ?? [], |
32 | response: result, |
33 | } |
34 | } |
35 |
|
36 | |
37 | |
38 | async function cv( |
39 | auth: RT.AristaCloudvision, |
40 | path: string, |
41 | options: { body?: unknown; notFound?: string; optional?: boolean } = {} |
42 | ): Promise<any> { |
43 | const url = new URL(path, auth.base_url) |
44 | |
45 | |
46 | |
47 | const cvaas = auth.deployment |
48 | ? auth.deployment === "cvaas" |
49 | : /(^|\.)arista\.io\.?$/i.test(url.hostname) |
50 | if (!cvaas && url.pathname.startsWith("/cvpservice/")) { |
51 | url.pathname = url.pathname.replace(/^\/cvpservice\//, "/web/") |
52 | } |
53 | const response = await fetch(url, { |
54 | method: options.body === undefined ? "GET" : "POST", |
55 | headers: { |
56 | Authorization: `Bearer ${auth.token}`, |
57 | ...(options.body === undefined |
58 | ? {} |
59 | : { "Content-Type": "application/json" }), |
60 | Accept: "application/json", |
61 | }, |
62 | body: options.body === undefined ? undefined : JSON.stringify(options.body), |
63 | }) |
64 | const text = await response.text() |
65 | if ( |
66 | response.status === 401 || |
67 | response.status === 403 || |
68 | (!response.ok && text.includes("User is unauthorized")) |
69 | ) { |
70 | throw new Error( |
71 | `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}` |
72 | ) |
73 | } |
74 | const data = |
75 | text.trim() === "" |
76 | ? {} |
77 | : /^\s*[[{]/.test(text) |
78 | ? JSON.parse(text) |
79 | : undefined |
80 | const error = findError(data) |
81 | if (response.ok && data !== undefined && !error) return data |
82 | const detail = error |
83 | ? `${error.errorCode} ${error.errorMessage ?? (error.errors ?? []).join("; ")}` |
84 | : `HTTP ${response.status} ${text.slice(0, 500)}` |
85 | |
86 | |
87 | if ( |
88 | response.status === 404 || |
89 | (error && (options.optional || options.notFound)) |
90 | ) { |
91 | if (options.optional) return undefined |
92 | if (options.notFound) throw new Error(`${options.notFound} (${detail})`) |
93 | } |
94 | if (response.ok && !error) { |
95 | throw new Error( |
96 | `CloudVision returned a non-JSON response for ${url.pathname}: check that base_url is the CloudVision address. ${detail}` |
97 | ) |
98 | } |
99 | throw new Error(`CloudVision ${url.pathname} failed: ${detail}`) |
100 | } |
101 |
|
102 | function findError(value: any): any { |
103 | if (!value || typeof value !== "object") return undefined |
104 | |
105 | if (!Array.isArray(value) && value.errorCode) return value |
106 | for (const item of Object.values(value)) { |
107 | const error = findError(item) |
108 | if (error) return error |
109 | } |
110 | return undefined |
111 | } |
112 |
|