Edits history of script submission #24029 for ' Upsert address object (palo_alto_panos)'

  • bunnative
    One script reply has been approved by the moderators
    Ap­pro­ved
    //native
    
    /**
     * Upsert address object
     * Declaratively create or update an address object in a firewall vsys, a Panorama device group or the shared location (PAN-OS REST API). Reads the current object, returns the difference and, unless dry_run is true (the default), applies only that difference to the candidate configuration. Empty description or tags leave the current values unchanged. Changes take effect after a commit.
     */
    export async function main(
      auth: RT.PaloAltoPanos,
      name: string,
      type: "ip-netmask" | "ip-range" | "ip-wildcard" | "fqdn",
      value: string,
      location: "vsys" | "device-group" | "shared" | undefined,
      vsys: string | undefined,
      device_group: string | undefined,
      description: string | undefined,
      tags: string[] | undefined,
      dry_run: boolean = true
    ) {
      const version = await restApiVersion(auth)
      const url = new URL(`/restapi/${version}/Objects/Addresses`, auth.base_url)
      const attrs = setLocation(url, location, vsys, device_group)
    
      // Read the current state: list the location and find the object by name.
      const current = ((await rest(auth, url, "GET")).result?.entry ?? []).find(
        (e: any) => e["@name"] === name
      )
    
      const desired: { [key: string]: any } = { [type]: value }
      if (description) desired.description = description
      if (tags?.length) desired.tag = { member: tags }
    
      const diff: { [key: string]: { from: any; to: any } } = {}
      for (const [key, to] of Object.entries(desired)) {
        const from = current?.[key] ?? null
        if (canon(from) !== canon(to)) diff[key] = { from, to }
      }
      // An object has exactly one address type: switching type removes the old one.
      for (const other of ADDRESS_TYPES) {
        if (other !== type && current?.[other] != null) {
          diff[other] = { from: current[other], to: null }
        }
      }
    
      const changed = Object.keys(diff).length > 0
      const operation = !current ? "create" : changed ? "update" : "none"
      if (!changed || dry_run !== false) {
        return {
          changed,
          operation,
          diff,
          applied: false,
          dry_run: dry_run !== false,
        }
      }
    
      // The docs build a PUT body from the GET response, modified as needed: keep
      // everything the action does not manage, attributes included.
      const entry: { [key: string]: any } = {}
      for (const [key, v] of Object.entries(current ?? {})) {
        if (!ADDRESS_TYPES.includes(key)) entry[key] = v
      }
      Object.assign(entry, { "@name": name, ...attrs }, desired)
    
      url.searchParams.set("name", name)
      await rest(auth, url, current ? "PUT" : "POST", { entry: [entry] })
      return { changed, operation, diff, applied: true, dry_run: false }
    }
    
    const ADDRESS_TYPES = ["ip-netmask", "ip-range", "ip-wildcard", "fqdn"]
    
    // Order-insensitive comparison of member lists (a single member may come back as a string).
    function canon(v: any): string {
      if (Array.isArray(v)) return JSON.stringify(v.map(canon).sort())
      if (v && typeof v === "object") {
        return JSON.stringify(
          Object.keys(v)
            .sort()
            .map((k) => [
              k,
              canon(k === "member" && !Array.isArray(v[k]) ? [v[k]] : v[k]),
            ])
        )
      }
      return JSON.stringify(v ?? null)
    }
    
    async function rest(auth: RT.PaloAltoPanos, url: URL, method: string, body?: any) {
      const response = await fetch(url, {
        method,
        headers: {
          "X-PAN-KEY": auth.api_key,
          ...(body ? { "Content-Type": "application/json" } : {}),
          Accept: "application/json",
        },
        body: body ? JSON.stringify(body) : undefined,
      })
      const text = await response.text()
      if (!response.ok) {
        throw new Error(`${response.status} ${text}`)
      }
      const data = JSON.parse(text)
      if (data["@status"] !== "success") {
        throw new Error(`${response.status} ${text}`)
      }
      return data
    }
    
    function setLocation(
      url: URL,
      location: string | undefined,
      vsys: string | undefined,
      device_group: string | undefined
    ) {
      const loc = location || "vsys"
      url.searchParams.set("location", loc)
      if (loc === "vsys") {
        url.searchParams.set("vsys", vsys || "vsys1")
        return { "@location": loc, "@vsys": vsys || "vsys1" }
      }
      if (loc === "device-group") {
        if (!device_group) {
          throw new Error("device_group is required when location is device-group")
        }
        url.searchParams.set("device-group", device_group)
        return { "@location": loc, "@device-group": device_group }
      }
      return { "@location": loc }
    }
    
    async function restApiVersion(auth: RT.PaloAltoPanos) {
      if (auth.rest_api_version) return auth.rest_api_version
      const url = new URL("/api/", auth.base_url)
      url.searchParams.set("type", "op")
      url.searchParams.set("cmd", "<show><system><info></info></system></show>")
      const response = await fetch(url, { headers: { "X-PAN-KEY": auth.api_key } })
      const text = await response.text()
      if (!response.ok || text.includes('status="error"')) {
        throw new Error(`${response.status} ${text}`)
      }
      const match = text.match(/<sw-version>(\d+)\.(\d+)/)
      if (!match) {
        throw new Error(
          `Could not read sw-version from show system info; set rest_api_version on the resource. ${text}`
        )
      }
      return `v${match[1]}.${match[2]}`
    }
    

    Submitted by hugo989 7 hours ago