0

Apply image bundle

by
Published today

Apply an EOS image bundle to a device (legacy Network Provisioning API, as cvprac does) and save, which creates the pending upgrade task returned in taskIds. The device is found by serial number, hostname, FQDN or system MAC address. Nothing runs on the switch until the task is executed, alone or in a change control.

Script arista_cloudvision Verified

The script

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

3
/**
4
 * Apply image bundle
5
 * Apply an EOS image bundle to a device (legacy Network Provisioning API, as cvprac does) and save, which creates the pending upgrade task returned in taskIds. The device is found by serial number, hostname, FQDN or system MAC address. Nothing runs on the switch until the task is executed, alone or in a change control.
6
 */
7
export async function main(
8
  auth: RT.AristaCloudvision,
9
  bundle_name: string,
10
  device: string
11
) {
12
  const bundle = await cv(
13
    auth,
14
    `/cvpservice/image/getImageBundleByName.do?name=${encodeURIComponent(bundle_name)}`,
15
    { notFound: `Image bundle ${bundle_name} not found` }
16
  )
17
  const dev = await findDevice(auth, device)
18
  const info = `Apply image: ${bundle_name} to netelement ${dev.fqdn}`
19
  await cv(
20
    auth,
21
    "/cvpservice/provisioning/addTempAction.do?format=topology&queryParam=&nodeId=root",
22
    {
23
      body: {
24
        data: [
25
          {
26
            info,
27
            infoPreview: info,
28
            note: "",
29
            action: "associate",
30
            nodeType: "imagebundle",
31
            nodeId: bundle.id ?? bundle.key,
32
            toId: dev.systemMacAddress,
33
            toIdType: "netelement",
34
            fromId: "",
35
            nodeName: bundle_name,
36
            fromName: "",
37
            toName: dev.fqdn,
38
            childTasks: [],
39
            parentTask: "",
40
          },
41
        ],
42
      },
43
    }
44
  )
45
  const saved = await cv(auth, "/cvpservice/provisioning/v2/saveTopology.do", {
46
    body: [],
47
  })
48
  return {
49
    status: saved.data?.status ?? "saved",
50
    taskIds: saved.data?.taskIds ?? [],
51
  }
52
}
53

54
async function findDevice(auth: RT.AristaCloudvision, target: string) {
55
  const devices = await cv(
56
    auth,
57
    "/cvpservice/inventory/devices?provisioned=true"
58
  )
59
  const t = target.toLowerCase()
60
  const device = (Array.isArray(devices) ? devices : []).find((d: any) =>
61
    [d.serialNumber, d.hostname, d.fqdn, d.systemMacAddress].some(
62
      (v) => typeof v === "string" && v.toLowerCase() === t
63
    )
64
  )
65
  if (!device) {
66
    throw new Error(
67
      `Device ${target} not found among the provisioned devices in CloudVision (match on serial number, hostname, FQDN or system MAC address)`
68
    )
69
  }
70
  return device
71
}
72

73
// Calls CloudVision and turns its failures into readable errors. Legacy
74
// endpoints report errors as HTTP 200 with an errorCode field.
75
async function cv(
76
  auth: RT.AristaCloudvision,
77
  path: string,
78
  options: { body?: unknown; notFound?: string; optional?: boolean } = {}
79
): Promise<any> {
80
  const url = new URL(path, auth.base_url)
81
  // cvprac sends legacy endpoints to /cvpservice on CVaaS and /web on
82
  // on-premises CVP. Its is_cvaas flag is the resource's deployment field;
83
  // when that is unset, a host under arista.io is taken as CVaaS.
84
  const cvaas = auth.deployment
85
    ? auth.deployment === "cvaas"
86
    : /(^|\.)arista\.io\.?$/i.test(url.hostname)
87
  if (!cvaas && url.pathname.startsWith("/cvpservice/")) {
88
    url.pathname = url.pathname.replace(/^\/cvpservice\//, "/web/")
89
  }
90
  const response = await fetch(url, {
91
    method: options.body === undefined ? "GET" : "POST",
92
    headers: {
93
      Authorization: `Bearer ${auth.token}`,
94
      ...(options.body === undefined
95
        ? {}
96
        : { "Content-Type": "application/json" }),
97
      Accept: "application/json",
98
    },
99
    body: options.body === undefined ? undefined : JSON.stringify(options.body),
100
  })
101
  const text = await response.text()
102
  if (
103
    response.status === 401 ||
104
    response.status === 403 ||
105
    (!response.ok && text.includes("User is unauthorized"))
106
  ) {
107
    throw new Error(
108
      `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}`
109
    )
110
  }
111
  const data =
112
    text.trim() === ""
113
      ? {}
114
      : /^\s*[[{]/.test(text)
115
        ? JSON.parse(text)
116
        : undefined
117
  const error = findError(data)
118
  if (response.ok && data !== undefined && !error) return data
119
  const detail = error
120
    ? `${error.errorCode} ${error.errorMessage ?? (error.errors ?? []).join("; ")}`
121
    : `HTTP ${response.status} ${text.slice(0, 500)}`
122
  // Legacy lookups answer an unknown name or ID with an errorCode (cvprac
123
  // treats any error from getTaskById and getImageBundleByName that way).
124
  if (
125
    response.status === 404 ||
126
    (error && (options.optional || options.notFound))
127
  ) {
128
    if (options.optional) return undefined
129
    if (options.notFound) throw new Error(`${options.notFound} (${detail})`)
130
  }
131
  if (response.ok && !error) {
132
    throw new Error(
133
      `CloudVision returned a non-JSON response for ${url.pathname}: check that base_url is the CloudVision address. ${detail}`
134
    )
135
  }
136
  throw new Error(`CloudVision ${url.pathname} failed: ${detail}`)
137
}
138

139
function findError(value: any): any {
140
  if (!value || typeof value !== "object") return undefined
141
  // Like cvprac, only a truthy errorCode counts, at any depth.
142
  if (!Array.isArray(value) && value.errorCode) return value
143
  for (const item of Object.values(value)) {
144
    const error = findError(item)
145
    if (error) return error
146
  }
147
  return undefined
148
}
149