0

Remove configlets

by
Published today

Remove provisioning configlets applied directly 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. Names not in the target's configlet list are reported in not_applied and left alone.

Script arista_cloudvision Verified

The script

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

3
/**
4
 * Remove configlets
5
 * Remove provisioning configlets applied directly 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. Names not in the target's configlet list are reported in not_applied and left alone.
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

15
  let current: any
16
  let action: Record<string, unknown>
17
  if (target_type === "device") {
18
    const device = await findDevice(auth, target)
19
    current = await cv(
20
      auth,
21
      `/cvpservice/provisioning/getConfigletsByNetElementId.do?netElementId=${encodeURIComponent(device.systemMacAddress)}&startIndex=0&endIndex=0`
22
    )
23
    action = {
24
      info: `Windmill Configlet Remove: from Device ${device.fqdn}`,
25
      infoPreview: `<b>Configlet Remove:</b> from Device ${device.fqdn}`,
26
      toId: device.systemMacAddress,
27
      toIdType: "netelement",
28
      toName: device.fqdn,
29
      nodeIpAddress: device.ipAddress,
30
      nodeTargetIpAddress: device.ipAddress,
31
    }
32
  } else {
33
    const container = await findContainer(auth, target)
34
    current = await cv(
35
      auth,
36
      `/cvpservice/provisioning/getConfigletsByContainerId.do?containerId=${encodeURIComponent(container.key)}&startIndex=0&endIndex=0`
37
    )
38
    action = {
39
      info: `Windmill Configlet Remove: from Container ${container.name}`,
40
      infoPreview: `<b>Configlet Remove:</b> from Container ${container.name}`,
41
      toId: container.key,
42
      toIdType: "container",
43
      toName: container.name,
44
      nodeIpAddress: "",
45
      nodeTargetIpAddress: "",
46
    }
47
  }
48

49
  const applied: { name: string; key: string }[] = current.configletList ?? []
50
  const keep = applied.filter((c) => !configlet_names.includes(c.name))
51
  const removed = applied.filter((c) => configlet_names.includes(c.name))
52
  const notApplied = configlet_names.filter(
53
    (n) => !applied.some((c) => c.name === n)
54
  )
55
  if (!removed.length) {
56
    return { status: "unchanged", taskIds: [], not_applied: notApplied }
57
  }
58
  await cv(
59
    auth,
60
    "/cvpservice/provisioning/addTempAction.do?format=topology&queryParam=&nodeId=root",
61
    {
62
      body: {
63
        data: [
64
          {
65
            note: "",
66
            action: "associate",
67
            nodeType: "configlet",
68
            nodeId: "",
69
            configletList: keep.map((c) => c.key),
70
            configletNamesList: keep.map((c) => c.name),
71
            ignoreConfigletNamesList: removed.map((c) => c.name),
72
            ignoreConfigletList: removed.map((c) => c.key),
73
            configletBuilderList: [],
74
            configletBuilderNamesList: [],
75
            ignoreConfigletBuilderList: [],
76
            ignoreConfigletBuilderNamesList: [],
77
            fromId: "",
78
            nodeName: "",
79
            fromName: "",
80
            childTasks: [],
81
            parentTask: "",
82
            ...action,
83
          },
84
        ],
85
      },
86
    }
87
  )
88
  const saved = await cv(auth, "/cvpservice/provisioning/v2/saveTopology.do", {
89
    body: [],
90
  })
91
  return {
92
    status: saved.data?.status ?? "saved",
93
    taskIds: saved.data?.taskIds ?? [],
94
    removed: removed.map((c) => c.name),
95
    not_applied: notApplied,
96
  }
97
}
98

99
async function findDevice(auth: RT.AristaCloudvision, target: string) {
100
  const devices = await cv(
101
    auth,
102
    "/cvpservice/inventory/devices?provisioned=true"
103
  )
104
  const t = target.toLowerCase()
105
  const device = (Array.isArray(devices) ? devices : []).find((d: any) =>
106
    [d.serialNumber, d.hostname, d.fqdn, d.systemMacAddress].some(
107
      (v) => typeof v === "string" && v.toLowerCase() === t
108
    )
109
  )
110
  if (!device) {
111
    throw new Error(
112
      `Device ${target} not found among the provisioned devices in CloudVision (match on serial number, hostname, FQDN or system MAC address)`
113
    )
114
  }
115
  return device
116
}
117

118
async function findContainer(auth: RT.AristaCloudvision, name: string) {
119
  const result = await cv(
120
    auth,
121
    `/cvpservice/provisioning/searchTopology.do?queryParam=${encodeURIComponent(name)}&startIndex=0&endIndex=0`
122
  )
123
  const container = (result.containerList ?? []).find(
124
    (c: any) => c.name === name
125
  )
126
  if (!container) throw new Error(`Container ${name} not found in CloudVision`)
127
  return container
128
}
129

130
// Calls CloudVision and turns its failures into readable errors. Legacy
131
// endpoints report errors as HTTP 200 with an errorCode field.
132
async function cv(
133
  auth: RT.AristaCloudvision,
134
  path: string,
135
  options: { body?: unknown; notFound?: string; optional?: boolean } = {}
136
): Promise<any> {
137
  const url = new URL(path, auth.base_url)
138
  // cvprac sends legacy endpoints to /cvpservice on CVaaS and /web on
139
  // on-premises CVP. Its is_cvaas flag is the resource's deployment field;
140
  // when that is unset, a host under arista.io is taken as CVaaS.
141
  const cvaas = auth.deployment
142
    ? auth.deployment === "cvaas"
143
    : /(^|\.)arista\.io\.?$/i.test(url.hostname)
144
  if (!cvaas && url.pathname.startsWith("/cvpservice/")) {
145
    url.pathname = url.pathname.replace(/^\/cvpservice\//, "/web/")
146
  }
147
  const response = await fetch(url, {
148
    method: options.body === undefined ? "GET" : "POST",
149
    headers: {
150
      Authorization: `Bearer ${auth.token}`,
151
      ...(options.body === undefined
152
        ? {}
153
        : { "Content-Type": "application/json" }),
154
      Accept: "application/json",
155
    },
156
    body: options.body === undefined ? undefined : JSON.stringify(options.body),
157
  })
158
  const text = await response.text()
159
  if (
160
    response.status === 401 ||
161
    response.status === 403 ||
162
    (!response.ok && text.includes("User is unauthorized"))
163
  ) {
164
    throw new Error(
165
      `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}`
166
    )
167
  }
168
  const data =
169
    text.trim() === ""
170
      ? {}
171
      : /^\s*[[{]/.test(text)
172
        ? JSON.parse(text)
173
        : undefined
174
  const error = findError(data)
175
  if (response.ok && data !== undefined && !error) return data
176
  const detail = error
177
    ? `${error.errorCode} ${error.errorMessage ?? (error.errors ?? []).join("; ")}`
178
    : `HTTP ${response.status} ${text.slice(0, 500)}`
179
  // Legacy lookups answer an unknown name or ID with an errorCode (cvprac
180
  // treats any error from getTaskById and getImageBundleByName that way).
181
  if (
182
    response.status === 404 ||
183
    (error && (options.optional || options.notFound))
184
  ) {
185
    if (options.optional) return undefined
186
    if (options.notFound) throw new Error(`${options.notFound} (${detail})`)
187
  }
188
  if (response.ok && !error) {
189
    throw new Error(
190
      `CloudVision returned a non-JSON response for ${url.pathname}: check that base_url is the CloudVision address. ${detail}`
191
    )
192
  }
193
  throw new Error(`CloudVision ${url.pathname} failed: ${detail}`)
194
}
195

196
function findError(value: any): any {
197
  if (!value || typeof value !== "object") return undefined
198
  // Like cvprac, only a truthy errorCode counts, at any depth.
199
  if (!Array.isArray(value) && value.errorCode) return value
200
  for (const item of Object.values(value)) {
201
    const error = findError(item)
202
    if (error) return error
203
  }
204
  return undefined
205
}
206