0

Approve change control

by
Published today

Approve a change control (resource API changecontrol.v1). The approval is tied to the change control's current version, read just before, so a change control edited since is not approved by mistake.

Script arista_cloudvision Verified

The script

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

3
/**
4
 * Approve change control
5
 * Approve a change control (resource API changecontrol.v1). The approval is tied to the change control's current version, read just before, so a change control edited since is not approved by mistake.
6
 */
7
export async function main(
8
  auth: RT.AristaCloudvision,
9
  change_control_id: string,
10
  notes: string = ""
11
) {
12
  const current = await cv(
13
    auth,
14
    `/api/resources/changecontrol/v1/ChangeControl?key.id=${encodeURIComponent(change_control_id)}`,
15
    { notFound: `Change control ${change_control_id} not found` }
16
  )
17
  const version = current.value?.change?.time
18
  if (!version) {
19
    throw new Error(
20
      `Change control ${change_control_id} has no change time to approve against`
21
    )
22
  }
23
  return await cv(auth, "/api/resources/changecontrol/v1/ApproveConfig", {
24
    body: {
25
      key: { id: change_control_id },
26
      approve: { value: true, notes: notes || "" },
27
      version,
28
    },
29
  })
30
}
31

32
// Calls CloudVision and turns its failures into readable errors.
33
async function cv(
34
  auth: RT.AristaCloudvision,
35
  path: string,
36
  options: { body?: unknown; notFound?: string; optional?: boolean } = {}
37
): Promise<any> {
38
  const url = new URL(path, auth.base_url)
39
  const response = await fetch(url, {
40
    method: options.body === undefined ? "GET" : "POST",
41
    headers: {
42
      Authorization: `Bearer ${auth.token}`,
43
      ...(options.body === undefined
44
        ? {}
45
        : { "Content-Type": "application/json" }),
46
      Accept: "application/json",
47
    },
48
    body: options.body === undefined ? undefined : JSON.stringify(options.body),
49
  })
50
  const text = await response.text()
51
  if (
52
    response.status === 401 ||
53
    response.status === 403 ||
54
    (!response.ok && text.includes("User is unauthorized"))
55
  ) {
56
    throw new Error(
57
      `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}`
58
    )
59
  }
60
  const data =
61
    text.trim() === ""
62
      ? {}
63
      : /^\s*[[{]/.test(text)
64
        ? JSON.parse(text)
65
        : undefined
66
  const error = findError(data)
67
  if (response.ok && data !== undefined && !error) return data
68
  const detail = error
69
    ? `${error.errorCode} ${error.errorMessage ?? (error.errors ?? []).join("; ")}`
70
    : `HTTP ${response.status} ${text.slice(0, 500)}`
71
  // Legacy lookups answer an unknown name or ID with an errorCode (cvprac
72
  // treats any error from getTaskById and getImageBundleByName that way).
73
  if (
74
    response.status === 404 ||
75
    (error && (options.optional || options.notFound))
76
  ) {
77
    if (options.optional) return undefined
78
    if (options.notFound) throw new Error(`${options.notFound} (${detail})`)
79
  }
80
  if (response.ok && !error) {
81
    throw new Error(
82
      `CloudVision returned a non-JSON response for ${url.pathname}: check that base_url is the CloudVision address. ${detail}`
83
    )
84
  }
85
  throw new Error(`CloudVision ${url.pathname} failed: ${detail}`)
86
}
87

88
function findError(value: any): any {
89
  if (!value || typeof value !== "object") return undefined
90
  // Like cvprac, only a truthy errorCode counts, at any depth.
91
  if (!Array.isArray(value) && value.errorCode) return value
92
  for (const item of Object.values(value)) {
93
    const error = findError(item)
94
    if (error) return error
95
  }
96
  return undefined
97
}
98