//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