0

Execute task

by
Published today

Execute a pending provisioning task (legacy Network Provisioning API); a task that is not Pending is refused. With wait_timeout_seconds set, wait for it to finish and fail with the task's status if it ends Failed or Cancelled; otherwise return once CloudVision has accepted it. To group tasks under an approval, use Create change control instead.

Script arista_cloudvision Verified

The script

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

3
/**
4
 * Execute task
5
 * Execute a pending provisioning task (legacy Network Provisioning API); a task that is not Pending is refused. With wait_timeout_seconds set, wait for it to finish and fail with the task's status if it ends Failed or Cancelled; otherwise return once CloudVision has accepted it. To group tasks under an approval, use Create change control instead.
6
 */
7
export async function main(
8
  auth: RT.AristaCloudvision,
9
  task_id: string,
10
  wait_timeout_seconds: number = 0
11
) {
12
  const before = await getTask(auth, task_id)
13
  if (before.workOrderUserDefinedStatus !== "Pending") {
14
    throw new Error(
15
      `Task ${task_id} is ${before.workOrderUserDefinedStatus}, not Pending: nothing to execute`
16
    )
17
  }
18
  await cv(auth, "/cvpservice/task/executeTask.do", {
19
    body: { data: [task_id] },
20
  })
21
  const deadline = Date.now() + (wait_timeout_seconds ?? 0) * 1000
22
  let task = await getTask(auth, task_id)
23
  while (
24
    !["Completed", "Failed", "Cancelled"].includes(
25
      task.workOrderUserDefinedStatus
26
    ) &&
27
    Date.now() < deadline
28
  ) {
29
    await new Promise((resolve) => setTimeout(resolve, 5000))
30
    task = await getTask(auth, task_id)
31
  }
32
  const status = task.workOrderUserDefinedStatus
33
  if (status === "Failed" || status === "Cancelled") {
34
    throw new Error(
35
      `Task ${task_id} ${status.toLowerCase()} on ${task.workOrderDetails?.netElementHostName ?? "its device"}: see the task logs in CloudVision${task.ccIdV2 ? ` (change control ${task.ccIdV2})` : ""}`
36
    )
37
  }
38
  if (wait_timeout_seconds && status !== "Completed") {
39
    throw new Error(
40
      `Task ${task_id} still ${status} after ${wait_timeout_seconds} s`
41
    )
42
  }
43
  return task
44
}
45

46
async function getTask(auth: RT.AristaCloudvision, task_id: string) {
47
  const task = await cv(
48
    auth,
49
    `/cvpservice/task/getTaskById.do?taskId=${encodeURIComponent(task_id)}`,
50
    { notFound: `Task ${task_id} not found` }
51
  )
52
  if (!task || task.workOrderId == null)
53
    throw new Error(`Task ${task_id} not found`)
54
  return task
55
}
56

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

123
function findError(value: any): any {
124
  if (!value || typeof value !== "object") return undefined
125
  // Like cvprac, only a truthy errorCode counts, at any depth.
126
  if (!Array.isArray(value) && value.errorCode) return value
127
  for (const item of Object.values(value)) {
128
    const error = findError(item)
129
    if (error) return error
130
  }
131
  return undefined
132
}
133