0

Upsert address object

by
Published today

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.

Script palo_alto_panos Verified

The script

Submitted by hugo989 Typescript (fetch-only)
Verified 6 hours ago
1
//native
2

3
/**
4
 * Upsert address object
5
 * 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.
6
 */
7
export async function main(
8
  auth: RT.PaloAltoPanos,
9
  name: string,
10
  type: "ip-netmask" | "ip-range" | "ip-wildcard" | "fqdn",
11
  value: string,
12
  location: "vsys" | "device-group" | "shared" | undefined,
13
  vsys: string | undefined,
14
  device_group: string | undefined,
15
  description: string | undefined,
16
  tags: string[] | undefined,
17
  dry_run: boolean = true
18
) {
19
  const version = await restApiVersion(auth)
20
  const url = new URL(`/restapi/${version}/Objects/Addresses`, auth.base_url)
21
  const attrs = setLocation(url, location, vsys, device_group)
22

23
  // Read the current state: list the location and find the object by name.
24
  const current = ((await rest(auth, url, "GET")).result?.entry ?? []).find(
25
    (e: any) => e["@name"] === name
26
  )
27

28
  const desired: { [key: string]: any } = { [type]: value }
29
  if (description) desired.description = description
30
  if (tags?.length) desired.tag = { member: tags }
31

32
  const diff: { [key: string]: { from: any; to: any } } = {}
33
  for (const [key, to] of Object.entries(desired)) {
34
    const from = current?.[key] ?? null
35
    if (canon(from) !== canon(to)) diff[key] = { from, to }
36
  }
37
  // An object has exactly one address type: switching type removes the old one.
38
  for (const other of ADDRESS_TYPES) {
39
    if (other !== type && current?.[other] != null) {
40
      diff[other] = { from: current[other], to: null }
41
    }
42
  }
43

44
  const changed = Object.keys(diff).length > 0
45
  const operation = !current ? "create" : changed ? "update" : "none"
46
  if (!changed || dry_run !== false) {
47
    return {
48
      changed,
49
      operation,
50
      diff,
51
      applied: false,
52
      dry_run: dry_run !== false,
53
    }
54
  }
55

56
  // The docs build a PUT body from the GET response, modified as needed: keep
57
  // everything the action does not manage, attributes included.
58
  const entry: { [key: string]: any } = {}
59
  for (const [key, v] of Object.entries(current ?? {})) {
60
    if (!ADDRESS_TYPES.includes(key)) entry[key] = v
61
  }
62
  Object.assign(entry, { "@name": name, ...attrs }, desired)
63

64
  url.searchParams.set("name", name)
65
  await rest(auth, url, current ? "PUT" : "POST", { entry: [entry] })
66
  return { changed, operation, diff, applied: true, dry_run: false }
67
}
68

69
const ADDRESS_TYPES = ["ip-netmask", "ip-range", "ip-wildcard", "fqdn"]
70

71
// Order-insensitive comparison of member lists (a single member may come back as a string).
72
function canon(v: any): string {
73
  if (Array.isArray(v)) return JSON.stringify(v.map(canon).sort())
74
  if (v && typeof v === "object") {
75
    return JSON.stringify(
76
      Object.keys(v)
77
        .sort()
78
        .map((k) => [
79
          k,
80
          canon(k === "member" && !Array.isArray(v[k]) ? [v[k]] : v[k]),
81
        ])
82
    )
83
  }
84
  return JSON.stringify(v ?? null)
85
}
86

87
async function rest(auth: RT.PaloAltoPanos, url: URL, method: string, body?: any) {
88
  const response = await fetch(url, {
89
    method,
90
    headers: {
91
      "X-PAN-KEY": auth.api_key,
92
      ...(body ? { "Content-Type": "application/json" } : {}),
93
      Accept: "application/json",
94
    },
95
    body: body ? JSON.stringify(body) : undefined,
96
  })
97
  const text = await response.text()
98
  if (!response.ok) {
99
    throw new Error(`${response.status} ${text}`)
100
  }
101
  const data = JSON.parse(text)
102
  if (data["@status"] !== "success") {
103
    throw new Error(`${response.status} ${text}`)
104
  }
105
  return data
106
}
107

108
function setLocation(
109
  url: URL,
110
  location: string | undefined,
111
  vsys: string | undefined,
112
  device_group: string | undefined
113
) {
114
  const loc = location || "vsys"
115
  url.searchParams.set("location", loc)
116
  if (loc === "vsys") {
117
    url.searchParams.set("vsys", vsys || "vsys1")
118
    return { "@location": loc, "@vsys": vsys || "vsys1" }
119
  }
120
  if (loc === "device-group") {
121
    if (!device_group) {
122
      throw new Error("device_group is required when location is device-group")
123
    }
124
    url.searchParams.set("device-group", device_group)
125
    return { "@location": loc, "@device-group": device_group }
126
  }
127
  return { "@location": loc }
128
}
129

130
async function restApiVersion(auth: RT.PaloAltoPanos) {
131
  if (auth.rest_api_version) return auth.rest_api_version
132
  const url = new URL("/api/", auth.base_url)
133
  url.searchParams.set("type", "op")
134
  url.searchParams.set("cmd", "<show><system><info></info></system></show>")
135
  const response = await fetch(url, { headers: { "X-PAN-KEY": auth.api_key } })
136
  const text = await response.text()
137
  if (!response.ok || text.includes('status="error"')) {
138
    throw new Error(`${response.status} ${text}`)
139
  }
140
  const match = text.match(/<sw-version>(\d+)\.(\d+)/)
141
  if (!match) {
142
    throw new Error(
143
      `Could not read sw-version from show system info; set rest_api_version on the resource. ${text}`
144
    )
145
  }
146
  return `v${match[1]}.${match[2]}`
147
}
148