1 | |
2 |
|
3 | |
4 | * Move devices to group |
5 | * Move up to 50 devices (by serial number) to a configuration group. preserve_config_overrides ["AOS_CX"] keeps AOS-CX device configuration when moving to a UI group. |
6 | */ |
7 | export async function main( |
8 | auth: RT.ArubaCentralOnprem, |
9 | group: string, |
10 | serials: string[], |
11 | preserve_config_overrides: "AOS_CX"[] | undefined |
12 | ) { |
13 | if (!serials?.length) { |
14 | throw new Error("serials must contain at least one device serial") |
15 | } |
16 | if (serials.length > 50) { |
17 | throw new Error("the API moves at most 50 devices per request") |
18 | } |
19 |
|
20 | const token = await getAccessToken(auth, "all") |
21 | const base = auth.base_url.replace(/\/+$/, "") |
22 |
|
23 | const response = await fetch(`${base}/configuration/v1/devices/move`, { |
24 | method: "POST", |
25 | headers: { |
26 | Authorization: `Bearer ${token}`, |
27 | "Content-Type": "application/json", |
28 | Accept: "application/json", |
29 | }, |
30 | body: JSON.stringify({ |
31 | group, |
32 | serials, |
33 | preserve_config_overrides: preserve_config_overrides?.length |
34 | ? preserve_config_overrides |
35 | : undefined, |
36 | }), |
37 | }) |
38 | if (!response.ok) { |
39 | throw new Error(`${response.status} ${await response.text()}`) |
40 | } |
41 |
|
42 | const text = await response.text() |
43 | return text ? JSON.parse(text) : { success: true, group, serials } |
44 | } |
45 |
|
46 | |
47 | |
48 | async function getAccessToken( |
49 | auth: RT.ArubaCentralOnprem, |
50 | scope: "read" | "all" |
51 | ): Promise<string> { |
52 | const base = auth.base_url.replace(/\/+$/, "") |
53 |
|
54 | const loginUrl = new URL(`${base}/oauth2/authorize/central/api/login`) |
55 | loginUrl.searchParams.append("client_id", auth.client_id) |
56 | const login = await fetch(loginUrl, { |
57 | method: "POST", |
58 | headers: { "Content-Type": "application/json", Accept: "application/json" }, |
59 | body: JSON.stringify({ username: auth.username, password: auth.password }), |
60 | }) |
61 | if (!login.ok) { |
62 | throw new Error(`login: ${login.status} ${await login.text()}`) |
63 | } |
64 | const cookies = login.headers.get("set-cookie") ?? "" |
65 | const csrf = cookies.match(/csrftoken=([^;,\s]+)/)?.[1] |
66 | const session = cookies.match(/session=([^;,\s]+)/)?.[1] |
67 | if (!csrf || !session) { |
68 | throw new Error( |
69 | "login: response did not set the csrftoken and session cookies" |
70 | ) |
71 | } |
72 |
|
73 | const authorizeUrl = new URL(`${base}/oauth2/authorize/central/api`) |
74 | authorizeUrl.searchParams.append("client_id", auth.client_id) |
75 | authorizeUrl.searchParams.append("response_type", "code") |
76 | authorizeUrl.searchParams.append("scope", scope) |
77 | const authorize = await fetch(authorizeUrl, { |
78 | method: "POST", |
79 | headers: { |
80 | "Content-Type": "application/json", |
81 | Accept: "application/json", |
82 | Cookie: `session=${session}`, |
83 | "X-CSRF-Token": csrf, |
84 | }, |
85 | body: JSON.stringify({ customer_id: auth.customer_id }), |
86 | }) |
87 | if (!authorize.ok) { |
88 | throw new Error(`authorize: ${authorize.status} ${await authorize.text()}`) |
89 | } |
90 | const { auth_code } = (await authorize.json()) as { auth_code: string } |
91 |
|
92 | const tokenUrl = new URL(`${base}/oauth2/token`) |
93 | tokenUrl.searchParams.append("client_id", auth.client_id) |
94 | tokenUrl.searchParams.append("client_secret", auth.client_secret) |
95 | tokenUrl.searchParams.append("grant_type", "authorization_code") |
96 | tokenUrl.searchParams.append("code", auth_code) |
97 | const token = await fetch(tokenUrl, { |
98 | method: "POST", |
99 | headers: { Accept: "application/json" }, |
100 | }) |
101 | if (!token.ok) { |
102 | throw new Error(`token: ${token.status} ${await token.text()}`) |
103 | } |
104 | const { access_token } = (await token.json()) as { access_token: string } |
105 | return access_token |
106 | } |
107 |
|