0

Apply configlets

by
Published today

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.

Script arista_cloudvision Verified

The script

Submitted by hugo989 Typescript (fetch-only)
Verified 4 hours ago
1
//native
2

3
/**
4
 * Apply configlets
5
 * 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.
6
 */
7
export async function main(
8
  auth: RT.AristaCloudvision,
9
  target_type: "device" | "container",
10
  target: string,
11
  configlet_names: string[]
12
) {
13
  if (!configlet_names?.length) throw new Error("configlet_names is empty")
14
  const wanted = await Promise.all(
15
    [...new Set(configlet_names)].map(async (name) => {
16
      const configlet = await cv(
17
        auth,
18
        `/cvpservice/configlet/getConfigletByName.do?name=${encodeURIComponent(name)}`,
19
        { notFound: `Configlet ${name} not found` }
20
      )
21
      return { name: configlet.name ?? name, key: configlet.key as string }
22
    })
23
  )
24

25
  const names: string[] = []
26
  const keys: string[] = []
27
  const builderNames: string[] = []
28
  const builderKeys: string[] = []
29
  let action: Record<string, unknown>
30
  if (target_type === "device") {
31
    const device = await findDevice(auth, target)
32
    const current = await cv(
33
      auth,
34
      `/cvpservice/provisioning/getConfigletsByNetElementId.do?netElementId=${encodeURIComponent(device.systemMacAddress)}&startIndex=0&endIndex=0`
35
    )
36
    for (const c of current.configletList ?? []) {
37
      names.push(c.name)
38
      keys.push(c.key)
39
    }
40
    action = {
41
      info: `Windmill: Configlet Assign: to Device ${device.fqdn}`,
42
      infoPreview: `<b>Configlet Assign:</b> to Device ${device.fqdn}`,
43
      toId: device.systemMacAddress,
44
      toIdType: "netelement",
45
      toName: device.fqdn,
46
      nodeIpAddress: device.ipAddress,
47
      nodeTargetIpAddress: device.ipAddress,
48
    }
49
  } else {
50
    const container = await findContainer(auth, target)
51
    const current = await cv(
52
      auth,
53
      `/cvpservice/provisioning/getConfigletsByContainerId.do?containerId=${encodeURIComponent(container.key)}&startIndex=0&endIndex=0`
54
    )
55
    for (const c of current.configletList ?? []) {
56
      // As cvprac does: static configlets in configletList, builders in the
57
      // builder lists, generated ones left for CloudVision to derive.
58
      if (c.type === "Static") {
59
        names.push(c.name)
60
        keys.push(c.key)
61
      } else if (c.type === "Builder") {
62
        builderNames.push(c.name)
63
        builderKeys.push(c.key)
64
      }
65
    }
66
    action = {
67
      info: `Windmill: Configlet Assign: to Container ${container.name}`,
68
      infoPreview: `<b>Configlet Assign:</b> to Container ${container.name}`,
69
      toId: container.key,
70
      toIdType: "container",
71
      toName: container.name,
72
      nodeIpAddress: "",
73
      nodeTargetIpAddress: "",
74
    }
75
  }
76

77
  const added = wanted.filter((c) => !keys.includes(c.key))
78
  if (!added.length) {
79
    return { status: "unchanged", taskIds: [], applied: names }
80
  }
81
  for (const c of added) {
82
    names.push(c.name)
83
    keys.push(c.key)
84
  }
85
  await cv(
86
    auth,
87
    "/cvpservice/provisioning/addTempAction.do?format=topology&queryParam=&nodeId=root",
88
    {
89
      body: {
90
        data: [
91
          {
92
            note: "",
93
            action: "associate",
94
            nodeType: "configlet",
95
            nodeId: "",
96
            configletList: keys,
97
            configletNamesList: names,
98
            ignoreConfigletNamesList: [],
99
            ignoreConfigletList: [],
100
            configletBuilderList: builderKeys,
101
            configletBuilderNamesList: builderNames,
102
            ignoreConfigletBuilderList: [],
103
            ignoreConfigletBuilderNamesList: [],
104
            fromId: "",
105
            nodeName: "",
106
            fromName: "",
107
            childTasks: [],
108
            parentTask: "",
109
            ...action,
110
          },
111
        ],
112
      },
113
    }
114
  )
115
  const saved = await cv(auth, "/cvpservice/provisioning/v2/saveTopology.do", {
116
    body: [],
117
  })
118
  return {
119
    status: saved.data?.status ?? "saved",
120
    taskIds: saved.data?.taskIds ?? [],
121
    applied: names,
122
  }
123
}
124

125
async function findDevice(auth: RT.AristaCloudvision, target: string) {
126
  const devices = await cv(
127
    auth,
128
    "/cvpservice/inventory/devices?provisioned=true"
129
  )
130
  const t = target.toLowerCase()
131
  const device = (Array.isArray(devices) ? devices : []).find((d: any) =>
132
    [d.serialNumber, d.hostname, d.fqdn, d.systemMacAddress].some(
133
      (v) => typeof v === "string" && v.toLowerCase() === t
134
    )
135
  )
136
  if (!device) {
137
    throw new Error(
138
      `Device ${target} not found among the provisioned devices in CloudVision (match on serial number, hostname, FQDN or system MAC address)`
139
    )
140
  }
141
  return device
142
}
143

144
async function findContainer(auth: RT.AristaCloudvision, name: string) {
145
  const result = await cv(
146
    auth,
147
    `/cvpservice/provisioning/searchTopology.do?queryParam=${encodeURIComponent(name)}&startIndex=0&endIndex=0`
148
  )
149
  const container = (result.containerList ?? []).find(
150
    (c: any) => c.name === name
151
  )
152
  if (!container) throw new Error(`Container ${name} not found in CloudVision`)
153
  return container
154
}
155

156
// Calls CloudVision and turns its failures into readable errors. Legacy
157
// endpoints report errors as HTTP 200 with an errorCode field.
158
async function cv(
159
  auth: RT.AristaCloudvision,
160
  path: string,
161
  options: { body?: unknown; notFound?: string; optional?: boolean } = {}
162
): Promise<any> {
163
  const url = new URL(path, auth.base_url)
164
  // cvprac sends legacy endpoints to /cvpservice on CVaaS and /web on
165
  // on-premises CVP. Its is_cvaas flag is the resource's deployment field;
166
  // when that is unset, a host under arista.io is taken as CVaaS.
167
  const cvaas = auth.deployment
168
    ? auth.deployment === "cvaas"
169
    : /(^|\.)arista\.io\.?$/i.test(url.hostname)
170
  if (!cvaas && url.pathname.startsWith("/cvpservice/")) {
171
    url.pathname = url.pathname.replace(/^\/cvpservice\//, "/web/")
172
  }
173
  const response = await fetch(url, {
174
    method: options.body === undefined ? "GET" : "POST",
175
    headers: {
176
      Authorization: `Bearer ${auth.token}`,
177
      ...(options.body === undefined
178
        ? {}
179
        : { "Content-Type": "application/json" }),
180
      Accept: "application/json",
181
    },
182
    body: options.body === undefined ? undefined : JSON.stringify(options.body),
183
  })
184
  const text = await response.text()
185
  if (
186
    response.status === 401 ||
187
    response.status === 403 ||
188
    (!response.ok && text.includes("User is unauthorized"))
189
  ) {
190
    throw new Error(
191
      `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}`
192
    )
193
  }
194
  const data =
195
    text.trim() === ""
196
      ? {}
197
      : /^\s*[[{]/.test(text)
198
        ? JSON.parse(text)
199
        : undefined
200
  const error = findError(data)
201
  if (response.ok && data !== undefined && !error) return data
202
  const detail = error
203
    ? `${error.errorCode} ${error.errorMessage ?? (error.errors ?? []).join("; ")}`
204
    : `HTTP ${response.status} ${text.slice(0, 500)}`
205
  // Legacy lookups answer an unknown name or ID with an errorCode (cvprac
206
  // treats any error from getTaskById and getImageBundleByName that way).
207
  if (
208
    response.status === 404 ||
209
    (error && (options.optional || options.notFound))
210
  ) {
211
    if (options.optional) return undefined
212
    if (options.notFound) throw new Error(`${options.notFound} (${detail})`)
213
  }
214
  if (response.ok && !error) {
215
    throw new Error(
216
      `CloudVision returned a non-JSON response for ${url.pathname}: check that base_url is the CloudVision address. ${detail}`
217
    )
218
  }
219
  throw new Error(`CloudVision ${url.pathname} failed: ${detail}`)
220
}
221

222
function findError(value: any): any {
223
  if (!value || typeof value !== "object") return undefined
224
  // Like cvprac, only a truthy errorCode counts, at any depth.
225
  if (!Array.isArray(value) && value.errorCode) return value
226
  for (const item of Object.values(value)) {
227
    const error = findError(item)
228
    if (error) return error
229
  }
230
  return undefined
231
}
232