Edits history of script submission #24152 for ' Create change control (arista_cloudvision)'

  • bunnative
    One script reply has been approved by the moderators
    Ap­pro­ved
    //native
    
    /**
     * Create change control
     * 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.
     */
    export async function main(
      auth: RT.AristaCloudvision,
      name: string,
      task_ids: string[],
      run_in: "series" | "parallel" = "series",
      notes: string = "",
      change_control_id: string = ""
    ) {
      if (!task_ids?.length) throw new Error("task_ids is empty")
      const id = change_control_id || crypto.randomUUID()
      const stages: Record<string, any> = {
        root: { name: "root", rows: { values: [] as { values: string[] }[] } },
      }
      task_ids.forEach((task, index) => {
        const stageId = `stage${index}`
        stages[stageId] = {
          name: `Task ${task}`,
          action: {
            name: "task",
            timeout: 3000,
            args: { values: { TaskID: String(task) } },
          },
        }
      })
      const stageIds = task_ids.map((_, index) => `stage${index}`)
      stages.root.rows.values =
        run_in === "parallel"
          ? [{ values: stageIds }]
          : stageIds.map((stageId) => ({ values: [stageId] }))
    
      const result = await cv(
        auth,
        "/api/resources/changecontrol/v1/ChangeControlConfig",
        {
          body: {
            key: { id },
            change: {
              name,
              rootStageId: "root",
              stages: { values: stages },
              ...(notes ? { notes } : {}),
            },
          },
        }
      )
      return { change_control_id: id, ...result }
    }
    
    // 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