Edits history of script submission #24153 for ' Execute change control (arista_cloudvision)'

  • bunnative
    One script reply has been approved by the moderators
    Ap­pro­ved
    //native
    
    /**
     * Execute change control
     * Start an approved change control (resource API changecontrol.v1). One that has already run is refused; one already running is not started again. With wait_timeout_seconds set, wait for it to complete and fail with CloudVision's error if it did not succeed; otherwise return once it is started.
     */
    export async function main(
      auth: RT.AristaCloudvision,
      change_control_id: string,
      notes: string = "",
      wait_timeout_seconds: number = 0
    ) {
      const path = `/api/resources/changecontrol/v1/ChangeControl?key.id=${encodeURIComponent(change_control_id)}`
      const notFound = `Change control ${change_control_id} not found`
      const before = await cv(auth, path, { notFound })
      if (!before.value?.approve?.value) {
        throw new Error(`Change control ${change_control_id} is not approved`)
      }
      if (before.value.status === "CHANGE_CONTROL_STATUS_COMPLETED") {
        throw new Error(
          `Change control ${change_control_id} has already run${before.value.error ? ` and failed: ${before.value.error}` : ""}`
        )
      }
      // A running change control is not started again, only waited for.
      if (before.value.status !== "CHANGE_CONTROL_STATUS_RUNNING") {
        await cv(auth, "/api/resources/changecontrol/v1/ChangeControlConfig", {
          body: {
            key: { id: change_control_id },
            start: { value: true, notes: notes || "" },
          },
        })
      }
      if (!wait_timeout_seconds) return (await cv(auth, path, { notFound })).value
    
      const deadline = Date.now() + wait_timeout_seconds * 1000
      let cc = (await cv(auth, path, { notFound })).value
      while (
        cc.status !== "CHANGE_CONTROL_STATUS_COMPLETED" &&
        Date.now() < deadline
      ) {
        await new Promise((resolve) => setTimeout(resolve, 5000))
        cc = (await cv(auth, path, { notFound })).value
      }
      if (cc.status !== "CHANGE_CONTROL_STATUS_COMPLETED") {
        throw new Error(
          `Change control ${change_control_id} still ${cc.status} after ${wait_timeout_seconds} s`
        )
      }
      if (cc.error || cc.completionReason === "COMPLETION_REASON_FAILED") {
        throw new Error(
          `Change control ${change_control_id} failed: ${cc.error || cc.completionReason}`
        )
      }
      return cc
    }
    
    // Calls CloudVision and turns its failures into readable errors.
    async function cv(
      auth: RT.AristaCloudvision,
      path: string,
      options: { body?: unknown; notFound?: string; optional?: boolean } = {}
    ): Promise<any> {
      const url = new URL(path, auth.base_url)
      const response = await fetch(url, {
        method: options.body === undefined ? "GET" : "POST",
        headers: {
          Authorization: `Bearer ${auth.token}`,
          ...(options.body === undefined
            ? {}
            : { "Content-Type": "application/json" }),
          Accept: "application/json",
        },
        body: options.body === undefined ? undefined : JSON.stringify(options.body),
      })
      const text = await response.text()
      if (
        response.status === 401 ||
        response.status === 403 ||
        (!response.ok && text.includes("User is unauthorized"))
      ) {
        throw new Error(
          `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}`
        )
      }
      const data =
        text.trim() === ""
          ? {}
          : /^\s*[[{]/.test(text)
            ? JSON.parse(text)
            : undefined
      const error = findError(data)
      if (response.ok && data !== undefined && !error) return data
      const detail = error
        ? `${error.errorCode} ${error.errorMessage ?? (error.errors ?? []).join("; ")}`
        : `HTTP ${response.status} ${text.slice(0, 500)}`
      // Legacy lookups answer an unknown name or ID with an errorCode (cvprac
      // treats any error from getTaskById and getImageBundleByName that way).
      if (
        response.status === 404 ||
        (error && (options.optional || options.notFound))
      ) {
        if (options.optional) return undefined
        if (options.notFound) throw new Error(`${options.notFound} (${detail})`)
      }
      if (response.ok && !error) {
        throw new Error(
          `CloudVision returned a non-JSON response for ${url.pathname}: check that base_url is the CloudVision address. ${detail}`
        )
      }
      throw new Error(`CloudVision ${url.pathname} failed: ${detail}`)
    }
    
    function findError(value: any): any {
      if (!value || typeof value !== "object") return undefined
      // Like cvprac, only a truthy errorCode counts, at any depth.
      if (!Array.isArray(value) && value.errorCode) return value
      for (const item of Object.values(value)) {
        const error = findError(item)
        if (error) return error
      }
      return undefined
    }
    

    Submitted by hugo989 5 hours ago