Edits history of script submission #24163 for ' Remove configlets (arista_cloudvision)'

  • bunnative
    One script reply has been approved by the moderators
    Ap­pro­ved
    //native
    
    /**
     * Remove configlets
     * Remove provisioning configlets applied directly to a device or a container (legacy Network Provisioning API, as cvprac does) and save, which creates the pending tasks returned in taskIds. A device is found by serial number, hostname, FQDN or system MAC address; a container by exact name. Names not in the target's configlet list are reported in not_applied and left alone.
     */
    export async function main(
      auth: RT.AristaCloudvision,
      target_type: "device" | "container",
      target: string,
      configlet_names: string[]
    ) {
      if (!configlet_names?.length) throw new Error("configlet_names is empty")
    
      let current: any
      let action: Record<string, unknown>
      if (target_type === "device") {
        const device = await findDevice(auth, target)
        current = await cv(
          auth,
          `/cvpservice/provisioning/getConfigletsByNetElementId.do?netElementId=${encodeURIComponent(device.systemMacAddress)}&startIndex=0&endIndex=0`
        )
        action = {
          info: `Windmill Configlet Remove: from Device ${device.fqdn}`,
          infoPreview: `<b>Configlet Remove:</b> from Device ${device.fqdn}`,
          toId: device.systemMacAddress,
          toIdType: "netelement",
          toName: device.fqdn,
          nodeIpAddress: device.ipAddress,
          nodeTargetIpAddress: device.ipAddress,
        }
      } else {
        const container = await findContainer(auth, target)
        current = await cv(
          auth,
          `/cvpservice/provisioning/getConfigletsByContainerId.do?containerId=${encodeURIComponent(container.key)}&startIndex=0&endIndex=0`
        )
        action = {
          info: `Windmill Configlet Remove: from Container ${container.name}`,
          infoPreview: `<b>Configlet Remove:</b> from Container ${container.name}`,
          toId: container.key,
          toIdType: "container",
          toName: container.name,
          nodeIpAddress: "",
          nodeTargetIpAddress: "",
        }
      }
    
      const applied: { name: string; key: string }[] = current.configletList ?? []
      const keep = applied.filter((c) => !configlet_names.includes(c.name))
      const removed = applied.filter((c) => configlet_names.includes(c.name))
      const notApplied = configlet_names.filter(
        (n) => !applied.some((c) => c.name === n)
      )
      if (!removed.length) {
        return { status: "unchanged", taskIds: [], not_applied: notApplied }
      }
      await cv(
        auth,
        "/cvpservice/provisioning/addTempAction.do?format=topology&queryParam=&nodeId=root",
        {
          body: {
            data: [
              {
                note: "",
                action: "associate",
                nodeType: "configlet",
                nodeId: "",
                configletList: keep.map((c) => c.key),
                configletNamesList: keep.map((c) => c.name),
                ignoreConfigletNamesList: removed.map((c) => c.name),
                ignoreConfigletList: removed.map((c) => c.key),
                configletBuilderList: [],
                configletBuilderNamesList: [],
                ignoreConfigletBuilderList: [],
                ignoreConfigletBuilderNamesList: [],
                fromId: "",
                nodeName: "",
                fromName: "",
                childTasks: [],
                parentTask: "",
                ...action,
              },
            ],
          },
        }
      )
      const saved = await cv(auth, "/cvpservice/provisioning/v2/saveTopology.do", {
        body: [],
      })
      return {
        status: saved.data?.status ?? "saved",
        taskIds: saved.data?.taskIds ?? [],
        removed: removed.map((c) => c.name),
        not_applied: notApplied,
      }
    }
    
    async function findDevice(auth: RT.AristaCloudvision, target: string) {
      const devices = await cv(
        auth,
        "/cvpservice/inventory/devices?provisioned=true"
      )
      const t = target.toLowerCase()
      const device = (Array.isArray(devices) ? devices : []).find((d: any) =>
        [d.serialNumber, d.hostname, d.fqdn, d.systemMacAddress].some(
          (v) => typeof v === "string" && v.toLowerCase() === t
        )
      )
      if (!device) {
        throw new Error(
          `Device ${target} not found among the provisioned devices in CloudVision (match on serial number, hostname, FQDN or system MAC address)`
        )
      }
      return device
    }
    
    async function findContainer(auth: RT.AristaCloudvision, name: string) {
      const result = await cv(
        auth,
        `/cvpservice/provisioning/searchTopology.do?queryParam=${encodeURIComponent(name)}&startIndex=0&endIndex=0`
      )
      const container = (result.containerList ?? []).find(
        (c: any) => c.name === name
      )
      if (!container) throw new Error(`Container ${name} not found in CloudVision`)
      return container
    }
    
    // Calls CloudVision and turns its failures into readable errors. Legacy
    // endpoints report errors as HTTP 200 with an errorCode field.
    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)
      // cvprac sends legacy endpoints to /cvpservice on CVaaS and /web on
      // on-premises CVP. Its is_cvaas flag is the resource's deployment field;
      // when that is unset, a host under arista.io is taken as CVaaS.
      const cvaas = auth.deployment
        ? auth.deployment === "cvaas"
        : /(^|\.)arista\.io\.?$/i.test(url.hostname)
      if (!cvaas && url.pathname.startsWith("/cvpservice/")) {
        url.pathname = url.pathname.replace(/^\/cvpservice\//, "/web/")
      }
      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