0

Set device variables

by
Published today

Declaratively set a device's template variables: compute the diff against the current variables and apply only the difference. Dry run by default.

Script aruba_central_onprem Verified

The script

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

3
/**
4
 * Set device variables
5
 * Declaratively set the template variables of a device: reads the current variables, returns the diff and applies only the difference (nothing changes while dry_run is true). mode "merge" only sets the given keys; "replace" also removes keys not given (except _sys_*). For a device with no variables yet, _sys_lan_mac is required.
6
 */
7
export async function main(
8
  auth: RT.ArubaCentralOnprem,
9
  device_serial: string,
10
  variables: { [key: string]: any },
11
  mode: "merge" | "replace" | undefined,
12
  dry_run: boolean = true
13
) {
14
  const dryRun = dry_run !== false
15
  const replace = mode === "replace"
16
  const token = await getAccessToken(auth, dryRun ? "read" : "all")
17
  const base = auth.base_url.replace(/\/+$/, "")
18
  const url = `${base}/configuration/v1/devices/${encodeURIComponent(device_serial)}/template_variables`
19
  const headers = {
20
    Authorization: `Bearer ${token}`,
21
    "Content-Type": "application/json",
22
    Accept: "application/json",
23
  }
24

25
  // 1. Read the current state
26
  const current = await fetch(url, {
27
    headers: { Authorization: headers.Authorization, Accept: headers.Accept },
28
  })
29
  if (!current.ok) {
30
    throw new Error(`${current.status} ${await current.text()}`)
31
  }
32
  const currentBody = (await current.json()) as {
33
    data?: { total?: number; variables?: { [key: string]: any } }
34
  }
35
  const existing: { [key: string]: any } = currentBody?.data?.variables ?? {}
36
  const intended: { [key: string]: any } = variables ?? {}
37

38
  // 2. Compute the difference
39
  const same = (a: any, b: any) => JSON.stringify(a) === JSON.stringify(b)
40
  const added: { [key: string]: any } = {}
41
  const changed: { [key: string]: { from: any; to: any } } = {}
42
  const removed: { [key: string]: any } = {}
43
  for (const [k, v] of Object.entries(intended)) {
44
    if (!(k in existing)) {
45
      added[k] = v
46
    } else if (!same(existing[k], v)) {
47
      changed[k] = { from: existing[k], to: v }
48
    }
49
  }
50
  if (replace) {
51
    for (const [k, v] of Object.entries(existing)) {
52
      if (!(k in intended) && !k.startsWith("_sys_")) {
53
        removed[k] = v
54
      }
55
    }
56
  }
57
  const diff = { added, changed, removed }
58
  const hasChanges =
59
    Object.keys(added).length +
60
      Object.keys(changed).length +
61
      Object.keys(removed).length >
62
    0
63

64
  const creating = Object.keys(existing).length === 0
65
  // _sys_serial and _sys_lan_mac are mandatory variable definitions (CoP "Managing Variable Files")
66
  if (creating && hasChanges && !intended._sys_lan_mac) {
67
    throw new Error(
68
      "device has no variables yet: include _sys_lan_mac (the device MAC address) to create them"
69
    )
70
  }
71

72
  if (!hasChanges || dryRun) {
73
    return { changed: hasChanges, diff, applied: false, dry_run: dryRun }
74
  }
75

76
  // 3. Apply only the difference, always with the device's _sys_* identifiers
77
  const sys = Object.fromEntries(
78
    Object.entries(existing).filter(([k]) => k.startsWith("_sys_"))
79
  )
80
  let method: "POST" | "PUT" | "PATCH"
81
  let payload: { [key: string]: any }
82
  if (creating) {
83
    // No variables yet: create them (PATCH/PUT reject a device without variables)
84
    method = "POST"
85
    payload = { _sys_serial: device_serial, ...intended }
86
  } else if (Object.keys(removed).length > 0) {
87
    // Removing keys needs a full replace
88
    method = "PUT"
89
    payload = { ...sys, ...intended }
90
  } else {
91
    method = "PATCH"
92
    payload = {
93
      ...sys,
94
      ...added,
95
      ...Object.fromEntries(Object.entries(changed).map(([k, c]) => [k, c.to])),
96
    }
97
  }
98

99
  const response = await fetch(url, {
100
    method,
101
    headers,
102
    body: JSON.stringify({
103
      total: Object.keys(payload).length,
104
      variables: payload,
105
    }),
106
  })
107
  if (!response.ok) {
108
    throw new Error(`${response.status} ${await response.text()}`)
109
  }
110

111
  return { changed: true, diff, applied: true, dry_run: false, method }
112
}
113

114
// OAuth grant mechanism documented for Central On-Premises (API Gateway > OAuth grant):
115
// login -> authorization code -> access token. Tokens last 2 hours, so a fresh one is minted per run.
116
async function getAccessToken(
117
  auth: RT.ArubaCentralOnprem,
118
  scope: "read" | "all"
119
): Promise<string> {
120
  const base = auth.base_url.replace(/\/+$/, "")
121

122
  const loginUrl = new URL(`${base}/oauth2/authorize/central/api/login`)
123
  loginUrl.searchParams.append("client_id", auth.client_id)
124
  const login = await fetch(loginUrl, {
125
    method: "POST",
126
    headers: { "Content-Type": "application/json", Accept: "application/json" },
127
    body: JSON.stringify({ username: auth.username, password: auth.password }),
128
  })
129
  if (!login.ok) {
130
    throw new Error(`login: ${login.status} ${await login.text()}`)
131
  }
132
  const cookies = login.headers.get("set-cookie") ?? ""
133
  const csrf = cookies.match(/csrftoken=([^;,\s]+)/)?.[1]
134
  const session = cookies.match(/session=([^;,\s]+)/)?.[1]
135
  if (!csrf || !session) {
136
    throw new Error(
137
      "login: response did not set the csrftoken and session cookies"
138
    )
139
  }
140

141
  const authorizeUrl = new URL(`${base}/oauth2/authorize/central/api`)
142
  authorizeUrl.searchParams.append("client_id", auth.client_id)
143
  authorizeUrl.searchParams.append("response_type", "code")
144
  authorizeUrl.searchParams.append("scope", scope)
145
  const authorize = await fetch(authorizeUrl, {
146
    method: "POST",
147
    headers: {
148
      "Content-Type": "application/json",
149
      Accept: "application/json",
150
      Cookie: `session=${session}`,
151
      "X-CSRF-Token": csrf,
152
    },
153
    body: JSON.stringify({ customer_id: auth.customer_id }),
154
  })
155
  if (!authorize.ok) {
156
    throw new Error(`authorize: ${authorize.status} ${await authorize.text()}`)
157
  }
158
  const { auth_code } = (await authorize.json()) as { auth_code: string }
159

160
  const tokenUrl = new URL(`${base}/oauth2/token`)
161
  tokenUrl.searchParams.append("client_id", auth.client_id)
162
  tokenUrl.searchParams.append("client_secret", auth.client_secret)
163
  tokenUrl.searchParams.append("grant_type", "authorization_code")
164
  tokenUrl.searchParams.append("code", auth_code)
165
  const token = await fetch(tokenUrl, {
166
    method: "POST",
167
    headers: { Accept: "application/json" },
168
  })
169
  if (!token.ok) {
170
    throw new Error(`token: ${token.status} ${await token.text()}`)
171
  }
172
  const { access_token } = (await token.json()) as { access_token: string }
173
  return access_token
174
}
175