0

Create change control

by
Published today

Create a change control that runs pending tasks (resource API changecontrol.v1), one after the other or all in parallel. It still needs approval and execution. change_control_id is generated when empty and returned.

Script arista_cloudvision Verified

The script

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

3
/**
4
 * Create change control
5
 * Create a change control that runs pending tasks (resource API changecontrol.v1), one after the other or all in parallel. It still needs approval and execution. change_control_id is generated when empty and returned.
6
 */
7
export async function main(
8
  auth: RT.AristaCloudvision,
9
  name: string,
10
  task_ids: string[],
11
  run_in: "series" | "parallel" = "series",
12
  notes: string = "",
13
  change_control_id: string = ""
14
) {
15
  if (!task_ids?.length) throw new Error("task_ids is empty")
16
  const id = change_control_id || crypto.randomUUID()
17
  const stages: Record<string, any> = {
18
    root: { name: "root", rows: { values: [] as { values: string[] }[] } },
19
  }
20
  task_ids.forEach((task, index) => {
21
    const stageId = `stage${index}`
22
    stages[stageId] = {
23
      name: `Task ${task}`,
24
      action: {
25
        name: "task",
26
        timeout: 3000,
27
        args: { values: { TaskID: String(task) } },
28
      },
29
    }
30
  })
31
  const stageIds = task_ids.map((_, index) => `stage${index}`)
32
  stages.root.rows.values =
33
    run_in === "parallel"
34
      ? [{ values: stageIds }]
35
      : stageIds.map((stageId) => ({ values: [stageId] }))
36

37
  const result = await cv(
38
    auth,
39
    "/api/resources/changecontrol/v1/ChangeControlConfig",
40
    {
41
      body: {
42
        key: { id },
43
        change: {
44
          name,
45
          rootStageId: "root",
46
          stages: { values: stages },
47
          ...(notes ? { notes } : {}),
48
        },
49
      },
50
    }
51
  )
52
  return { change_control_id: id, ...result }
53
}
54

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

111
function findError(value: any): any {
112
  if (!value || typeof value !== "object") return undefined
113
  // Like cvprac, only a truthy errorCode counts, at any depth.
114
  if (!Array.isArray(value) && value.errorCode) return value
115
  for (const item of Object.values(value)) {
116
    const error = findError(item)
117
    if (error) return error
118
  }
119
  return undefined
120
}
121