···
29
29
```sh
30
30
pvesh create /cluster/mapping/dir --id {vmid}-compose --map node={node},path=/root/compose/{vmid}/
31
31
32
32
-
pvesh create /nodes/pve/qemu/109/config --virtiofs{fs-nr} dirid={vmid}-compose
32
32
+
pvesh create /nodes/pve/qemu/{vmid}/config --virtiofs{fs-nr} dirid={vmid}-compose
33
33
```
34
34
35
35
···
14
14
// directory_mappings(host, credentials)
15
15
16
16
// directory_mapping_by_id(host, credentials, "mar-music-lib")
17
17
-
directory_mapping_by_id(host, credentials, "awdadwa")
17
17
+
// directory_mapping_by_id(host, credentials, "awdadwa")
18
18
+
19
19
+
// vm_config(host, credentials, "primergy", 108)
20
20
+
21
21
+
nodes(host, credentials)
18
22
|> echo
23
23
+
}
24
24
+
25
25
+
fn nodes(host: String, credentials: proxmox.Credentials) {
26
26
+
let req = proxmox.nodes(host:, credentials:)
27
27
+
28
28
+
use resp <- result.try(
29
29
+
send(req)
30
30
+
|> result.map_error(string.inspect),
31
31
+
)
32
32
+
33
33
+
proxmox.nodes_response(resp)
34
34
+
|> result.map_error(string.inspect)
35
35
+
}
36
36
+
37
37
+
fn vm_config(
38
38
+
host: String,
39
39
+
credentials: proxmox.Credentials,
40
40
+
node: String,
41
41
+
vmid: Int,
42
42
+
) {
43
43
+
let req = proxmox.vm_config_by_id(host:, credentials:, node:, vmid:)
44
44
+
45
45
+
use resp <- result.try(
46
46
+
send(req)
47
47
+
|> result.map_error(string.inspect),
48
48
+
)
49
49
+
50
50
+
proxmox.vm_config_by_id_response(resp)
51
51
+
|> result.map_error(string.inspect)
19
52
}
20
53
21
54
fn send(req) {
···
3
3
import gleam/http
4
4
import gleam/http/request
5
5
import gleam/http/response
6
6
+
import gleam/int
6
7
import gleam/json
7
8
import gleam/list
8
9
import gleam/option
9
10
import gleam/result
10
11
import gleam/string
11
12
12
12
-
const api_base = "/api2/json/"
13
13
+
const api_base: String = "/api2/json/"
13
14
14
15
// AUTH -------------------------------------------------------------------------
15
16
···
48
49
GotUnexpectedResponse(Int, String)
49
50
Unauthorized
50
51
FailedToDecode(json.DecodeError)
51
51
-
NotFound
52
52
+
NotFound(String)
52
53
UnexpectedData(String)
53
54
ProxmoxInternalError(String)
55
55
+
MissingPermissions(String)
54
56
}
55
57
56
58
// REQUESTS ---------------------------------------------------------------------
···
62
64
case resp.status {
63
65
200 -> callback(resp.body)
64
66
401 -> Error(Unauthorized)
67
67
+
403 -> {
68
68
+
case decode_message(resp.body) {
69
69
+
"Permission check failed " <> message -> {
70
70
+
let message = string.trim(message)
71
71
+
Error(MissingPermissions(message))
72
72
+
}
73
73
+
message -> Error(MissingPermissions(message))
74
74
+
}
75
75
+
}
65
76
// proxmox will return response code 500 instead of 404
66
77
500 ->
67
78
// so we try to parse the data, which will be `null` on 404
68
79
case json.parse(resp.body, decode_data(decode.optional(decode.string))) {
69
69
-
Ok(option.None) -> Error(NotFound)
80
80
+
Ok(option.None) -> {
81
81
+
Error(NotFound(decode_message(resp.body)))
82
82
+
}
70
83
Ok(option.Some(data)) -> Error(UnexpectedData(data))
71
84
Error(_) -> Error(ProxmoxInternalError(resp.body))
72
85
}
···
74
87
}
75
88
}
76
89
90
90
+
fn decode_message(body: String) -> String {
91
91
+
json.parse(body, decode.at(["message"], decode.string))
92
92
+
|> result.unwrap("")
93
93
+
}
94
94
+
77
95
fn decode_data(decoder: decode.Decoder(a)) -> decode.Decoder(a) {
78
96
decode.at(["data"], decoder)
79
97
}
···
87
105
/// use path <- decode.then(map_item_decoder(map, "path"))
88
106
/// ```
89
107
///
90
90
-
fn map_item_decoder(map: dict.Dict(String, String), key: String) {
108
108
+
fn map_item_decoder(
109
109
+
map: dict.Dict(String, String),
110
110
+
key: String,
111
111
+
) -> decode.Decoder(String) {
91
112
case dict.get(map, key) {
92
113
Error(_) -> decode.failure("", "Value for key: " <> key)
93
114
Ok(value) -> decode.success(value)
···
103
124
/// use path <- decode.then(map_item_decoder(map, "path"))
104
125
/// ```
105
126
///
106
106
-
fn map_decoder() {
127
127
+
fn map_decoder() -> decode.Decoder(dict.Dict(String, String)) {
107
128
use content <- decode.then(decode.list(decode.string))
108
129
109
130
case content {
···
174
195
pub fn directory_mapping_by_id_response(
175
196
response: response.Response(String),
176
197
id: String,
177
177
-
) {
198
198
+
) -> Result(DirectoryMapping, ApiError) {
178
199
handle_response(response, fn(body) {
179
200
json.parse(body, decode_data(directory_mapping_decoder(option.Some(id))))
180
201
|> result.map_error(FailedToDecode)
···
242
263
}
243
264
}
244
265
}
266
266
+
267
267
+
// VM CONFIG --------------------------------------------------------------------
268
268
+
// /node/{node}/qemu/{vmid}/config ----------------------------------------------
269
269
+
270
270
+
pub type VmConfig {
271
271
+
Qemu(
272
272
+
digest: String,
273
273
+
tags: List(String),
274
274
+
cpu_type: String,
275
275
+
cpu_sockets: Int,
276
276
+
cpu_cores: Int,
277
277
+
memory_mib: Int,
278
278
+
virtiofs: List(VirtioFs),
279
279
+
)
280
280
+
}
281
281
+
282
282
+
fn vm_config_decoder() -> decode.Decoder(VmConfig) {
283
283
+
use digest <- decode.field("digest", decode.string)
284
284
+
use tags <- decode.field("tags", decode_tags())
285
285
+
use cpu_type <- decode.field("cpu", decode.string)
286
286
+
use cpu_sockets <- decode.field("sockets", decode.int)
287
287
+
use cpu_cores <- decode.field("cores", decode.int)
288
288
+
use memory_mib <- decode.field("memory", decode_more_int())
289
289
+
290
290
+
// decode all fields into a dict
291
291
+
use fields <- decode.then(decode.dict(decode.string, decode_more_string()))
292
292
+
293
293
+
let virtiofs =
294
294
+
dict.to_list(fields)
295
295
+
|> list.filter_map(fn(field) {
296
296
+
case field {
297
297
+
// grab virtiofs
298
298
+
#("virtiofs" <> rest, id) -> {
299
299
+
case int.parse(rest) {
300
300
+
Ok(nr) -> VirtioFs(nr:, fs_id: id) |> Ok
301
301
+
Error(_) -> Error(Nil)
302
302
+
}
303
303
+
}
304
304
+
_ -> Error(Nil)
305
305
+
}
306
306
+
})
307
307
+
308
308
+
decode.success(Qemu(
309
309
+
digest:,
310
310
+
tags:,
311
311
+
cpu_type:,
312
312
+
cpu_sockets:,
313
313
+
cpu_cores:,
314
314
+
memory_mib:,
315
315
+
virtiofs:,
316
316
+
))
317
317
+
}
318
318
+
319
319
+
fn decode_more_string() -> decode.Decoder(String) {
320
320
+
decode.one_of(decode.string, [
321
321
+
decode.int |> decode.map(int.to_string),
322
322
+
])
323
323
+
}
324
324
+
325
325
+
fn decode_more_int() -> decode.Decoder(Int) {
326
326
+
decode.one_of(decode.int, [
327
327
+
decode.string
328
328
+
|> decode.then(fn(num) {
329
329
+
case int.parse(num) {
330
330
+
Ok(num) -> decode.success(num)
331
331
+
Error(_) -> decode.failure(0, "Int")
332
332
+
}
333
333
+
}),
334
334
+
])
335
335
+
}
336
336
+
337
337
+
fn decode_tags() -> decode.Decoder(List(String)) {
338
338
+
use tags <- decode.then(decode.string)
339
339
+
decode.success(string.split(tags, ";"))
340
340
+
}
341
341
+
342
342
+
pub type VirtioFs {
343
343
+
VirtioFs(nr: Int, fs_id: String)
344
344
+
}
345
345
+
346
346
+
/// /nodes/{node}/qemu/{vmid}/config
347
347
+
///
348
348
+
/// Required permissions
349
349
+
/// `VM.Audit` for `/vms/{vmid}`
350
350
+
///
351
351
+
pub fn vm_config_by_id(
352
352
+
host host: String,
353
353
+
credentials credentials: Credentials,
354
354
+
node node: String,
355
355
+
vmid vmid: Int,
356
356
+
) -> request.Request(String) {
357
357
+
base_request(host, credentials)
358
358
+
|> request.set_method(http.Get)
359
359
+
|> request.set_path(
360
360
+
api_base <> "nodes/" <> node <> "/qemu/" <> int.to_string(vmid) <> "/config",
361
361
+
)
362
362
+
}
363
363
+
364
364
+
pub fn vm_config_by_id_response(
365
365
+
response: response.Response(String),
366
366
+
) -> Result(VmConfig, ApiError) {
367
367
+
handle_response(response, fn(body) {
368
368
+
json.parse(body, decode_data(vm_config_decoder()))
369
369
+
|> result.map_error(FailedToDecode)
370
370
+
})
371
371
+
}
372
372
+
373
373
+
// NODES ------------------------------------------------------------------------
374
374
+
// /cluster/config/nodes --------------------------------------------------------
375
375
+
376
376
+
/// {
377
377
+
/// "data": [
378
378
+
/// {
379
379
+
/// "nodeid": "1",
380
380
+
/// "ring0_addr": "192.168.188.50",
381
381
+
/// "name": "primergy",
382
382
+
/// "quorum_votes": "1",
383
383
+
/// "node": "primergy"
384
384
+
/// },
385
385
+
/// {
386
386
+
/// "name": "pve",
387
387
+
/// "nodeid": "2",
388
388
+
/// "ring0_addr": "192.168.188.40",
389
389
+
/// "node": "pve",
390
390
+
/// "quorum_votes": "1"
391
391
+
/// }
392
392
+
/// ]
393
393
+
/// }
394
394
+
pub type Node {
395
395
+
Node(
396
396
+
id: Int,
397
397
+
name: String,
398
398
+
node: String,
399
399
+
quorum_votes: Int,
400
400
+
ring0_addr: String,
401
401
+
)
402
402
+
}
403
403
+
404
404
+
fn node_decoder() -> decode.Decoder(Node) {
405
405
+
use id <- decode.field("nodeid", decode_more_int())
406
406
+
use name <- decode.field("name", decode.string)
407
407
+
use node <- decode.field("node", decode.string)
408
408
+
use quorum_votes <- decode.field("quorum_votes", decode_more_int())
409
409
+
use ring0_addr <- decode.field("ring0_addr", decode.string)
410
410
+
decode.success(Node(id:, name:, node:, quorum_votes:, ring0_addr:))
411
411
+
}
412
412
+
413
413
+
/// /cluster/config/nodes
414
414
+
///
415
415
+
/// Required permissions
416
416
+
/// `Sys.Audit`
417
417
+
///
418
418
+
pub fn nodes(
419
419
+
host host: String,
420
420
+
credentials credentials: Credentials,
421
421
+
) -> request.Request(String) {
422
422
+
base_request(host, credentials)
423
423
+
|> request.set_method(http.Get)
424
424
+
|> request.set_path(api_base <> "/cluster/config/nodes/")
425
425
+
}
426
426
+
427
427
+
pub fn nodes_response(
428
428
+
response: response.Response(String),
429
429
+
) -> Result(List(Node), ApiError) {
430
430
+
handle_response(response, fn(body) {
431
431
+
json.parse(body, decode_data(decode.list(node_decoder())))
432
432
+
|> result.map_error(FailedToDecode)
433
433
+
})
434
434
+
}