]> git.proxmox.com Git - pve-common.git/blame - src/PVE/JSONSchema.pm
JSONSchema: add idmap parser and storagepair format
[pve-common.git] / src / PVE / JSONSchema.pm
CommitLineData
e143e9d8
DM
1package PVE::JSONSchema;
2
e143e9d8 3use strict;
c36f332e 4use warnings;
e143e9d8
DM
5use Storable; # for dclone
6use Getopt::Long;
24197a9f
DM
7use Encode::Locale;
8use Encode;
e143e9d8 9use Devel::Cycle -quiet; # todo: remove?
e272bcb7 10use PVE::Tools qw(split_list $IPV6RE $IPV4RE);
e143e9d8
DM
11use PVE::Exception qw(raise);
12use HTTP::Status qw(:constants);
23b56245 13use Net::IP qw(:PROC);
bf27456b 14use Data::Dumper;
e143e9d8
DM
15
16use base 'Exporter';
17
18our @EXPORT_OK = qw(
9bbc4e17 19register_standard_option
e143e9d8
DM
20get_standard_option
21);
22
9bbc4e17 23# Note: This class implements something similar to JSON schema, but it is not 100% complete.
e143e9d8
DM
24# see: http://tools.ietf.org/html/draft-zyp-json-schema-02
25# see: http://json-schema.org/
26
27# the code is similar to the javascript parser from http://code.google.com/p/jsonschema/
28
29my $standard_options = {};
30sub register_standard_option {
31 my ($name, $schema) = @_;
32
9bbc4e17 33 die "standard option '$name' already registered\n"
e143e9d8
DM
34 if $standard_options->{$name};
35
36 $standard_options->{$name} = $schema;
37}
38
39sub get_standard_option {
40 my ($name, $base) = @_;
41
42 my $std = $standard_options->{$name};
3432ae0c 43 die "no such standard option '$name'\n" if !$std;
e143e9d8
DM
44
45 my $res = $base || {};
46
47 foreach my $opt (keys %$std) {
c38ac70f 48 next if defined($res->{$opt});
e143e9d8
DM
49 $res->{$opt} = $std->{$opt};
50 }
51
52 return $res;
53};
54
55register_standard_option('pve-vmid', {
56 description => "The (unique) ID of the VM.",
57 type => 'integer', format => 'pve-vmid',
58 minimum => 1
59});
60
61register_standard_option('pve-node', {
62 description => "The cluster node name.",
63 type => 'string', format => 'pve-node',
64});
65
66register_standard_option('pve-node-list', {
67 description => "List of cluster node names.",
68 type => 'string', format => 'pve-node-list',
69});
70
71register_standard_option('pve-iface', {
72 description => "Network interface name.",
73 type => 'string', format => 'pve-iface',
74 minLength => 2, maxLength => 20,
75});
76
28a2669d 77register_standard_option('pve-storage-id', {
05e787c5
DM
78 description => "The storage identifier.",
79 type => 'string', format => 'pve-storage-id',
9bbc4e17 80});
05e787c5 81
28a2669d 82register_standard_option('pve-config-digest', {
dc5eae7d
DM
83 description => 'Prevent changes if current configuration file has different SHA1 digest. This can be used to prevent concurrent modifications.',
84 type => 'string',
85 optional => 1,
fb3a1b29 86 maxLength => 40, # sha1 hex digest length is 40
dc5eae7d
DM
87});
88
26bcdf92
WB
89register_standard_option('skiplock', {
90 description => "Ignore locks - only root is allowed to use this option.",
91 type => 'boolean',
92 optional => 1,
93});
94
28a2669d 95register_standard_option('extra-args', {
5851be88
WB
96 description => "Extra arguments as array",
97 type => 'array',
98 items => { type => 'string' },
99 optional => 1
100});
101
b21cf575
TL
102register_standard_option('fingerprint-sha256', {
103 description => "Certificate SHA 256 fingerprint.",
104 type => 'string',
105 pattern => '([A-Fa-f0-9]{2}:){31}[A-Fa-f0-9]{2}',
106});
107
ac15655f
DM
108register_standard_option('pve-output-format', {
109 type => 'string',
110 description => 'Output format.',
ac6c61bf 111 enum => [ 'text', 'json', 'json-pretty', 'yaml' ],
ac15655f
DM
112 optional => 1,
113 default => 'text',
114});
115
98d5b8cb
TL
116register_standard_option('pve-snapshot-name', {
117 description => "The name of the snapshot.",
118 type => 'string', format => 'pve-configid',
119 maxLength => 40,
120});
121
e143e9d8
DM
122my $format_list = {};
123
124sub register_format {
125 my ($format, $code) = @_;
126
9bbc4e17 127 die "JSON schema format '$format' already registered\n"
e143e9d8
DM
128 if $format_list->{$format};
129
130 $format_list->{$format} = $code;
131}
132
2421fba1
WB
133sub get_format {
134 my ($format) = @_;
135 return $format_list->{$format};
136}
137
b5212042
DM
138my $renderer_hash = {};
139
140sub register_renderer {
141 my ($name, $code) = @_;
142
143 die "renderer '$name' already registered\n"
144 if $renderer_hash->{$name};
145
146 $renderer_hash->{$name} = $code;
147}
148
149sub get_renderer {
150 my ($name) = @_;
151 return $renderer_hash->{$name};
152}
153
e143e9d8 154# register some common type for pve
8ba7c72b
DM
155
156register_format('string', sub {}); # allow format => 'string-list'
157
c77b4c96
WB
158register_format('urlencoded', \&pve_verify_urlencoded);
159sub pve_verify_urlencoded {
160 my ($text, $noerr) = @_;
161 if ($text !~ /^[-%a-zA-Z0-9_.!~*'()]*$/) {
162 return undef if $noerr;
163 die "invalid urlencoded string: $text\n";
164 }
165 return $text;
166}
167
e143e9d8
DM
168register_format('pve-configid', \&pve_verify_configid);
169sub pve_verify_configid {
170 my ($id, $noerr) = @_;
9bbc4e17 171
e143e9d8
DM
172 if ($id !~ m/^[a-z][a-z0-9_]+$/i) {
173 return undef if $noerr;
9bbc4e17 174 die "invalid configuration ID '$id'\n";
e143e9d8
DM
175 }
176 return $id;
177}
178
05e787c5
DM
179PVE::JSONSchema::register_format('pve-storage-id', \&parse_storage_id);
180sub parse_storage_id {
181 my ($storeid, $noerr) = @_;
182
731950fd
WL
183 return parse_id($storeid, 'storage', $noerr);
184}
185
a8117ff3
WL
186PVE::JSONSchema::register_format('acme-plugin-id', \&parse_acme_plugin_id);
187sub parse_acme_plugin_id {
188 my ($pluginid, $noerr) = @_;
189
190 return parse_id($pluginid, 'ACME plugin', $noerr);
191}
192
731950fd
WL
193sub parse_id {
194 my ($id, $type, $noerr) = @_;
195
196 if ($id !~ m/^[a-z][a-z0-9\-\_\.]*[a-z0-9]$/i) {
05e787c5 197 return undef if $noerr;
731950fd 198 die "$type ID '$id' contains illegal characters\n";
05e787c5 199 }
731950fd 200 return $id;
05e787c5
DM
201}
202
e143e9d8
DM
203register_format('pve-vmid', \&pve_verify_vmid);
204sub pve_verify_vmid {
205 my ($vmid, $noerr) = @_;
206
50ae94c9 207 if ($vmid !~ m/^[1-9][0-9]{2,8}$/) {
e143e9d8
DM
208 return undef if $noerr;
209 die "value does not look like a valid VM ID\n";
210 }
211 return $vmid;
212}
213
214register_format('pve-node', \&pve_verify_node_name);
215sub pve_verify_node_name {
216 my ($node, $noerr) = @_;
217
e6db55c0 218 if ($node !~ m/^([a-zA-Z0-9]([a-zA-Z0-9\-]*[a-zA-Z0-9])?)$/) {
e143e9d8
DM
219 return undef if $noerr;
220 die "value does not look like a valid node name\n";
221 }
222 return $node;
223}
224
18f93ddf
FG
225sub parse_idmap {
226 my ($idmap, $idformat) = @_;
227
228 return undef if !$idmap;
229
230 my $map = {};
231
232 foreach my $entry (PVE::Tools::split_list($idmap)) {
233 if ($entry eq '1') {
234 $map->{identity} = 1;
235 } elsif ($entry =~ m/^([^:]+):([^:]+)$/) {
236 my ($source, $target) = ($1, $2);
237 eval {
238 PVE::JSONSchema::check_format($idformat, $source, '');
239 PVE::JSONSchema::check_format($idformat, $target, '');
240 };
241 die "entry '$entry' contains invalid ID - $@\n"
242 if $@;
243
244 die "duplicate mapping for source '$source'\n"
245 if $map->{entries}->{$source};
246
247 $map->{entries}->{$source} = $target;
248 } else {
249 eval {
250 PVE::JSONSchema::check_format($idformat, $entry);
251 };
252
253 die "entry '$entry' contains invalid ID - $@\n"
254 if $@;
255
256 die "default target ID can only be provided once\n"
257 if $map->{default};
258
259 $map->{default} = $entry;
260 }
261 }
262
263 die "identity mapping cannot be combined with other mappings\n"
264 if $map->{identity} && ($map->{default} || $map->{entries});
265
266 return $map;
267}
268
269register_format('storagepair', \&verify_storagepair);
270sub verify_storagepair {
271 my ($storagepair, $noerr) = @_;
272
273 # note: this only checks a single list entry
274 # when using a storagepair-list map, you need to pass the full
275 # parameter to parse_idmap
276 eval { parse_idmap($storagepair, 'pve-storage-id') };
277 if ($@) {
278 return undef if $noerr;
279 die "$@\n";
280 }
281
282 return $storagepair;
283}
284
14324ea8
CE
285register_format('mac-addr', \&pve_verify_mac_addr);
286sub pve_verify_mac_addr {
287 my ($mac_addr, $noerr) = @_;
288
4fdf30c4
TL
289 # don't allow I/G bit to be set, most of the time it breaks things, see:
290 # https://pve.proxmox.com/pipermail/pve-devel/2019-March/035998.html
a750d596 291 if ($mac_addr !~ m/^[a-f0-9][02468ace](?::[a-f0-9]{2}){5}$/i) {
14324ea8 292 return undef if $noerr;
a750d596 293 die "value does not look like a valid unicast MAC address\n";
14324ea8
CE
294 }
295 return $mac_addr;
a750d596 296
14324ea8 297}
a750d596
SI
298register_standard_option('mac-addr', {
299 type => 'string',
300 description => 'Unicast MAC address.',
4fdf30c4 301 verbose_description => 'A common MAC address with the I/G (Individual/Group) bit not set.',
a750d596
SI
302 format_description => "XX:XX:XX:XX:XX:XX",
303 optional => 1,
304 format => 'mac-addr',
305});
14324ea8 306
e143e9d8
DM
307register_format('ipv4', \&pve_verify_ipv4);
308sub pve_verify_ipv4 {
309 my ($ipv4, $noerr) = @_;
310
ed5880ac
DM
311 if ($ipv4 !~ m/^(?:$IPV4RE)$/) {
312 return undef if $noerr;
313 die "value does not look like a valid IPv4 address\n";
e143e9d8
DM
314 }
315 return $ipv4;
316}
a13c6f08 317
ed5880ac 318register_format('ipv6', \&pve_verify_ipv6);
93276209 319sub pve_verify_ipv6 {
ed5880ac
DM
320 my ($ipv6, $noerr) = @_;
321
322 if ($ipv6 !~ m/^(?:$IPV6RE)$/) {
323 return undef if $noerr;
324 die "value does not look like a valid IPv6 address\n";
325 }
326 return $ipv6;
327}
328
329register_format('ip', \&pve_verify_ip);
330sub pve_verify_ip {
331 my ($ip, $noerr) = @_;
332
333 if ($ip !~ m/^(?:(?:$IPV4RE)|(?:$IPV6RE))$/) {
334 return undef if $noerr;
335 die "value does not look like a valid IP address\n";
336 }
337 return $ip;
338}
339
283ac2ba
DC
340PVE::JSONSchema::register_format('ldap-simple-attr', \&verify_ldap_simple_attr);
341sub verify_ldap_simple_attr {
342 my ($attr, $noerr) = @_;
343
344 if ($attr =~ m/^[a-zA-Z0-9]+$/) {
345 return $attr;
346 }
347
348 die "value '$attr' does not look like a simple ldap attribute name\n" if !$noerr;
349
350 return undef;
351}
352
a13c6f08 353my $ipv4_mask_hash = {
aad3582e 354 '0.0.0.0' => 0,
a13c6f08
DM
355 '128.0.0.0' => 1,
356 '192.0.0.0' => 2,
357 '224.0.0.0' => 3,
358 '240.0.0.0' => 4,
359 '248.0.0.0' => 5,
360 '252.0.0.0' => 6,
361 '254.0.0.0' => 7,
362 '255.0.0.0' => 8,
363 '255.128.0.0' => 9,
364 '255.192.0.0' => 10,
365 '255.224.0.0' => 11,
366 '255.240.0.0' => 12,
367 '255.248.0.0' => 13,
368 '255.252.0.0' => 14,
369 '255.254.0.0' => 15,
370 '255.255.0.0' => 16,
371 '255.255.128.0' => 17,
372 '255.255.192.0' => 18,
373 '255.255.224.0' => 19,
374 '255.255.240.0' => 20,
375 '255.255.248.0' => 21,
376 '255.255.252.0' => 22,
377 '255.255.254.0' => 23,
378 '255.255.255.0' => 24,
379 '255.255.255.128' => 25,
380 '255.255.255.192' => 26,
381 '255.255.255.224' => 27,
382 '255.255.255.240' => 28,
383 '255.255.255.248' => 29,
e43faad9
WB
384 '255.255.255.252' => 30,
385 '255.255.255.254' => 31,
386 '255.255.255.255' => 32,
a13c6f08
DM
387};
388
aad3582e
DC
389sub get_netmask_bits {
390 my ($mask) = @_;
391 return $ipv4_mask_hash->{$mask};
392}
393
e143e9d8
DM
394register_format('ipv4mask', \&pve_verify_ipv4mask);
395sub pve_verify_ipv4mask {
396 my ($mask, $noerr) = @_;
397
a13c6f08 398 if (!defined($ipv4_mask_hash->{$mask})) {
e143e9d8
DM
399 return undef if $noerr;
400 die "value does not look like a valid IP netmask\n";
401 }
402 return $mask;
403}
404
703c1f88
WB
405register_format('CIDRv6', \&pve_verify_cidrv6);
406sub pve_verify_cidrv6 {
e272bcb7
DM
407 my ($cidr, $noerr) = @_;
408
70ea2250 409 if ($cidr =~ m!^(?:$IPV6RE)(?:/(\d+))$! && ($1 > 7) && ($1 <= 128)) {
e272bcb7 410 return $cidr;
703c1f88
WB
411 }
412
413 return undef if $noerr;
414 die "value does not look like a valid IPv6 CIDR network\n";
415}
416
417register_format('CIDRv4', \&pve_verify_cidrv4);
418sub pve_verify_cidrv4 {
419 my ($cidr, $noerr) = @_;
420
0526cc2d 421 if ($cidr =~ m!^(?:$IPV4RE)(?:/(\d+))$! && ($1 > 7) && ($1 <= 32)) {
e272bcb7
DM
422 return $cidr;
423 }
424
425 return undef if $noerr;
703c1f88
WB
426 die "value does not look like a valid IPv4 CIDR network\n";
427}
428
429register_format('CIDR', \&pve_verify_cidr);
430sub pve_verify_cidr {
431 my ($cidr, $noerr) = @_;
432
433 if (!(pve_verify_cidrv4($cidr, 1) ||
434 pve_verify_cidrv6($cidr, 1)))
435 {
436 return undef if $noerr;
437 die "value does not look like a valid CIDR network\n";
438 }
439
440 return $cidr;
441}
442
443register_format('pve-ipv4-config', \&pve_verify_ipv4_config);
444sub pve_verify_ipv4_config {
445 my ($config, $noerr) = @_;
446
447 return $config if $config =~ /^(?:dhcp|manual)$/ ||
448 pve_verify_cidrv4($config, 1);
449 return undef if $noerr;
450 die "value does not look like a valid ipv4 network configuration\n";
451}
452
453register_format('pve-ipv6-config', \&pve_verify_ipv6_config);
454sub pve_verify_ipv6_config {
455 my ($config, $noerr) = @_;
456
457 return $config if $config =~ /^(?:auto|dhcp|manual)$/ ||
458 pve_verify_cidrv6($config, 1);
459 return undef if $noerr;
460 die "value does not look like a valid ipv6 network configuration\n";
e272bcb7
DM
461}
462
e143e9d8
DM
463register_format('email', \&pve_verify_email);
464sub pve_verify_email {
465 my ($email, $noerr) = @_;
466
4cd6dc0a 467 if ($email !~ /^[\w\+\-\~]+(\.[\w\+\-\~]+)*@[a-zA-Z0-9\-]+(\.[a-zA-Z0-9\-]+)*$/) {
e143e9d8
DM
468 return undef if $noerr;
469 die "value does not look like a valid email address\n";
470 }
471 return $email;
472}
473
34ebb226
DM
474register_format('dns-name', \&pve_verify_dns_name);
475sub pve_verify_dns_name {
476 my ($name, $noerr) = @_;
477
ce33e978 478 my $namere = "([a-zA-Z0-9]([a-zA-Z0-9\-]*[a-zA-Z0-9])?)";
34ebb226
DM
479
480 if ($name !~ /^(${namere}\.)*${namere}$/) {
481 return undef if $noerr;
482 die "value does not look like a valid DNS name\n";
483 }
484 return $name;
485}
486
e143e9d8
DM
487# network interface name
488register_format('pve-iface', \&pve_verify_iface);
489sub pve_verify_iface {
490 my ($id, $noerr) = @_;
9bbc4e17 491
e143e9d8
DM
492 if ($id !~ m/^[a-z][a-z0-9_]{1,20}([:\.]\d+)?$/i) {
493 return undef if $noerr;
9bbc4e17 494 die "invalid network interface name '$id'\n";
e143e9d8
DM
495 }
496 return $id;
497}
498
d07b7084
WB
499# general addresses by name or IP
500register_format('address', \&pve_verify_address);
501sub pve_verify_address {
502 my ($addr, $noerr) = @_;
503
504 if (!(pve_verify_ip($addr, 1) ||
505 pve_verify_dns_name($addr, 1)))
506 {
507 return undef if $noerr;
508 die "value does not look like a valid address: $addr\n";
509 }
510 return $addr;
511}
512
b944a22a
WB
513register_format('disk-size', \&pve_verify_disk_size);
514sub pve_verify_disk_size {
515 my ($size, $noerr) = @_;
516 if (!defined(parse_size($size))) {
517 return undef if $noerr;
518 die "value does not look like a valid disk size: $size\n";
519 }
520 return $size;
521}
522
f0a10afc 523register_standard_option('spice-proxy', {
fb3a1b29 524 description => "SPICE proxy server. This can be used by the client to specify the proxy server. All nodes in a cluster runs 'spiceproxy', so it is up to the client to choose one. By default, we return the node where the VM is currently running. As reasonable setting is to use same node you use to connect to the API (This is window.location.hostname for the JS GUI).",
d07b7084 525 type => 'string', format => 'address',
9bbc4e17 526});
f0a10afc
DM
527
528register_standard_option('remote-viewer-config', {
529 description => "Returned values can be directly passed to the 'remote-viewer' application.",
530 additionalProperties => 1,
531 properties => {
532 type => { type => 'string' },
533 password => { type => 'string' },
534 proxy => { type => 'string' },
535 host => { type => 'string' },
536 'tls-port' => { type => 'integer' },
537 },
538});
539
c70c3bbc 540register_format('pve-startup-order', \&pve_verify_startup_order);
b0edd8e6
DM
541sub pve_verify_startup_order {
542 my ($value, $noerr) = @_;
543
544 return $value if pve_parse_startup_order($value);
545
546 return undef if $noerr;
547
548 die "unable to parse startup options\n";
549}
550
2d167ad0
WB
551my %bwlimit_opt = (
552 optional => 1,
553 type => 'number', minimum => '0',
554 format_description => 'LIMIT',
555);
556
557my $bwlimit_format = {
558 default => {
559 %bwlimit_opt,
34e75688 560 description => 'default bandwidth limit in KiB/s',
2d167ad0
WB
561 },
562 restore => {
563 %bwlimit_opt,
34e75688 564 description => 'bandwidth limit in KiB/s for restoring guests from backups',
2d167ad0
WB
565 },
566 migration => {
567 %bwlimit_opt,
34e75688 568 description => 'bandwidth limit in KiB/s for migrating guests (including moving local disks)',
2d167ad0
WB
569 },
570 clone => {
571 %bwlimit_opt,
34e75688 572 description => 'bandwidth limit in KiB/s for cloning disks',
2d167ad0
WB
573 },
574 move => {
575 %bwlimit_opt,
34e75688 576 description => 'bandwidth limit in KiB/s for moving disks',
2d167ad0
WB
577 },
578};
579register_format('bwlimit', $bwlimit_format);
580register_standard_option('bwlimit', {
581 description => "Set bandwidth/io limits various operations.",
582 optional => 1,
583 type => 'string',
584 format => $bwlimit_format,
585});
484b6b39
DC
586
587# used for pve-tag-list in e.g., guest configs
588register_format('pve-tag', \&pve_verify_tag);
589sub pve_verify_tag {
590 my ($value, $noerr) = @_;
591
592 return $value if $value =~ m/^[a-z0-9_][a-z0-9_\-\+\.]*$/i;
593
594 return undef if $noerr;
595
596 die "invalid characters in tag\n";
597}
2d167ad0 598
b0edd8e6
DM
599sub pve_parse_startup_order {
600 my ($value) = @_;
601
602 return undef if !$value;
603
604 my $res = {};
605
606 foreach my $p (split(/,/, $value)) {
607 next if $p =~ m/^\s*$/;
608
609 if ($p =~ m/^(order=)?(\d+)$/) {
610 $res->{order} = $2;
611 } elsif ($p =~ m/^up=(\d+)$/) {
612 $res->{up} = $1;
613 } elsif ($p =~ m/^down=(\d+)$/) {
614 $res->{down} = $1;
615 } else {
616 return undef;
617 }
618 }
619
31b5a3a7 620 return $res;
b0edd8e6
DM
621}
622
623PVE::JSONSchema::register_standard_option('pve-startup-order', {
624 description => "Startup and shutdown behavior. Order is a non-negative number defining the general startup order. Shutdown in done with reverse ordering. Additionally you can set the 'up' or 'down' delay in seconds, which specifies a delay to wait before the next VM is started or stopped.",
625 optional => 1,
626 type => 'string', format => 'pve-startup-order',
627 typetext => '[[order=]\d+] [,up=\d+] [,down=\d+] ',
628});
629
6e234325
WB
630register_format('pve-tfa-secret', \&pve_verify_tfa_secret);
631sub pve_verify_tfa_secret {
632 my ($key, $noerr) = @_;
633
634 # The old format used 16 base32 chars or 40 hex digits. Since they have a common subset it's
635 # hard to distinguish them without the our previous length constraints, so add a 'v2' of the
636 # format to support arbitrary lengths properly:
637 if ($key =~ /^v2-0x[0-9a-fA-F]{16,128}$/ || # hex
638 $key =~ /^v2-[A-Z2-7=]{16,128}$/ || # base32
639 $key =~ /^(?:[A-Z2-7=]{16}|[A-Fa-f0-9]{40})$/) # and the old pattern copy&pasted
640 {
641 return $key;
642 }
643
644 return undef if $noerr;
645
646 die "unable to decode TFA secret\n";
647}
648
e143e9d8 649sub check_format {
2f9e609a 650 my ($format, $value, $path) = @_;
e143e9d8 651
2f9e609a 652 return parse_property_string($format, $value, $path) if ref($format) eq 'HASH';
e143e9d8
DM
653 return if $format eq 'regex';
654
23dc9401 655 if ($format =~ m/^(.*)-a?list$/) {
9bbc4e17 656
e143e9d8
DM
657 my $code = $format_list->{$1};
658
659 die "undefined format '$format'\n" if !$code;
660
661 # Note: we allow empty lists
662 foreach my $v (split_list($value)) {
663 &$code($v);
664 }
665
666 } elsif ($format =~ m/^(.*)-opt$/) {
667
668 my $code = $format_list->{$1};
669
670 die "undefined format '$format'\n" if !$code;
671
672 return if !$value; # allow empty string
673
674 &$code($value);
675
676 } else {
677
678 my $code = $format_list->{$format};
679
680 die "undefined format '$format'\n" if !$code;
681
2f9e609a 682 return parse_property_string($code, $value, $path) if ref($code) eq 'HASH';
e143e9d8
DM
683 &$code($value);
684 }
9bbc4e17 685}
e143e9d8 686
878fea8e
WB
687sub parse_size {
688 my ($value) = @_;
689
690 return undef if $value !~ m/^(\d+(\.\d+)?)([KMGT])?$/;
691 my ($size, $unit) = ($1, $3);
692 if ($unit) {
693 if ($unit eq 'K') {
694 $size = $size * 1024;
695 } elsif ($unit eq 'M') {
696 $size = $size * 1024 * 1024;
697 } elsif ($unit eq 'G') {
698 $size = $size * 1024 * 1024 * 1024;
699 } elsif ($unit eq 'T') {
700 $size = $size * 1024 * 1024 * 1024 * 1024;
701 }
702 }
703 return int($size);
704};
705
706sub format_size {
707 my ($size) = @_;
708
709 $size = int($size);
710
711 my $kb = int($size/1024);
712 return $size if $kb*1024 != $size;
713
714 my $mb = int($kb/1024);
715 return "${kb}K" if $mb*1024 != $kb;
716
717 my $gb = int($mb/1024);
718 return "${mb}M" if $gb*1024 != $mb;
719
720 my $tb = int($gb/1024);
721 return "${gb}G" if $tb*1024 != $gb;
722
723 return "${tb}T";
724};
725
1b71e564
WB
726sub parse_boolean {
727 my ($bool) = @_;
728 return 1 if $bool =~ m/^(1|on|yes|true)$/i;
729 return 0 if $bool =~ m/^(0|off|no|false)$/i;
730 return undef;
731}
732
095b88fd 733sub parse_property_string {
d1e490c1
WB
734 my ($format, $data, $path, $additional_properties) = @_;
735
736 # In property strings we default to not allowing additional properties
737 $additional_properties = 0 if !defined($additional_properties);
095b88fd 738
7c1617b0
WB
739 # Support named formats here, too:
740 if (!ref($format)) {
741 if (my $desc = $format_list->{$format}) {
742 $format = $desc;
743 } else {
744 die "unknown format: $format\n";
745 }
746 } elsif (ref($format) ne 'HASH') {
747 die "unexpected format value of type ".ref($format)."\n";
748 }
749
095b88fd
WB
750 my $default_key;
751
752 my $res = {};
753 foreach my $part (split(/,/, $data)) {
754 next if $part =~ /^\s*$/;
755
756 if ($part =~ /^([^=]+)=(.+)$/) {
757 my ($k, $v) = ($1, $2);
2d468b1a 758 die "duplicate key in comma-separated list property: $k\n" if defined($res->{$k});
095b88fd 759 my $schema = $format->{$k};
303a9b34 760 if (my $alias = $schema->{alias}) {
bf27456b
DM
761 if (my $key_alias = $schema->{keyAlias}) {
762 die "key alias '$key_alias' is already defined\n" if defined($res->{$key_alias});
763 $res->{$key_alias} = $k;
764 }
303a9b34
WB
765 $k = $alias;
766 $schema = $format->{$k};
767 }
bf27456b 768
2d468b1a 769 die "invalid key in comma-separated list property: $k\n" if !$schema;
095b88fd 770 if ($schema->{type} && $schema->{type} eq 'boolean') {
1b71e564 771 $v = parse_boolean($v) // $v;
095b88fd
WB
772 }
773 $res->{$k} = $v;
774 } elsif ($part !~ /=/) {
2d468b1a 775 die "duplicate key in comma-separated list property: $default_key\n" if $default_key;
095b88fd
WB
776 foreach my $key (keys %$format) {
777 if ($format->{$key}->{default_key}) {
778 $default_key = $key;
779 if (!$res->{$default_key}) {
780 $res->{$default_key} = $part;
781 last;
782 }
2d468b1a 783 die "duplicate key in comma-separated list property: $default_key\n";
095b88fd
WB
784 }
785 }
f0ba41a1 786 die "value without key, but schema does not define a default key\n" if !$default_key;
095b88fd 787 } else {
2d468b1a 788 die "missing key in comma-separated list property\n";
095b88fd
WB
789 }
790 }
791
792 my $errors = {};
d1e490c1 793 check_object($path, $format, $res, $additional_properties, $errors);
095b88fd 794 if (scalar(%$errors)) {
2d468b1a 795 raise "format error\n", errors => $errors;
095b88fd
WB
796 }
797
798 return $res;
799}
800
e143e9d8
DM
801sub add_error {
802 my ($errors, $path, $msg) = @_;
803
804 $path = '_root' if !$path;
9bbc4e17 805
e143e9d8
DM
806 if ($errors->{$path}) {
807 $errors->{$path} = join ('\n', $errors->{$path}, $msg);
808 } else {
809 $errors->{$path} = $msg;
810 }
811}
812
813sub is_number {
814 my $value = shift;
815
816 # see 'man perlretut'
9bbc4e17 817 return $value =~ /^[+-]?(\d+\.\d+|\d+\.|\.\d+|\d+)([eE][+-]?\d+)?$/;
e143e9d8
DM
818}
819
820sub is_integer {
821 my $value = shift;
822
823 return $value =~ m/^[+-]?\d+$/;
824}
825
826sub check_type {
827 my ($path, $type, $value, $errors) = @_;
828
829 return 1 if !$type;
830
831 if (!defined($value)) {
832 return 1 if $type eq 'null';
9bbc4e17 833 die "internal error"
e143e9d8
DM
834 }
835
836 if (my $tt = ref($type)) {
837 if ($tt eq 'ARRAY') {
838 foreach my $t (@$type) {
839 my $tmperr = {};
840 check_type($path, $t, $value, $tmperr);
9bbc4e17 841 return 1 if !scalar(%$tmperr);
e143e9d8
DM
842 }
843 my $ttext = join ('|', @$type);
9bbc4e17 844 add_error($errors, $path, "type check ('$ttext') failed");
e143e9d8
DM
845 return undef;
846 } elsif ($tt eq 'HASH') {
847 my $tmperr = {};
848 check_prop($value, $type, $path, $tmperr);
9bbc4e17
TL
849 return 1 if !scalar(%$tmperr);
850 add_error($errors, $path, "type check failed");
e143e9d8
DM
851 return undef;
852 } else {
853 die "internal error - got reference type '$tt'";
854 }
855
856 } else {
857
858 return 1 if $type eq 'any';
859
860 if ($type eq 'null') {
861 if (defined($value)) {
862 add_error($errors, $path, "type check ('$type') failed - value is not null");
863 return undef;
864 }
865 return 1;
866 }
867
868 my $vt = ref($value);
869
870 if ($type eq 'array') {
871 if (!$vt || $vt ne 'ARRAY') {
872 add_error($errors, $path, "type check ('$type') failed");
873 return undef;
874 }
875 return 1;
876 } elsif ($type eq 'object') {
877 if (!$vt || $vt ne 'HASH') {
878 add_error($errors, $path, "type check ('$type') failed");
879 return undef;
880 }
881 return 1;
882 } elsif ($type eq 'coderef') {
883 if (!$vt || $vt ne 'CODE') {
884 add_error($errors, $path, "type check ('$type') failed");
885 return undef;
886 }
887 return 1;
88a490ff
WB
888 } elsif ($type eq 'string' && $vt eq 'Regexp') {
889 # qr// regexes can be used as strings and make sense for format=regex
890 return 1;
e143e9d8
DM
891 } else {
892 if ($vt) {
893 add_error($errors, $path, "type check ('$type') failed - got $vt");
894 return undef;
895 } else {
896 if ($type eq 'string') {
897 return 1; # nothing to check ?
898 } elsif ($type eq 'boolean') {
899 #if ($value =~ m/^(1|true|yes|on)$/i) {
900 if ($value eq '1') {
901 return 1;
902 #} elsif ($value =~ m/^(0|false|no|off)$/i) {
903 } elsif ($value eq '0') {
79501b2a 904 return 1; # return success (not value)
e143e9d8
DM
905 } else {
906 add_error($errors, $path, "type check ('$type') failed - got '$value'");
907 return undef;
908 }
909 } elsif ($type eq 'integer') {
910 if (!is_integer($value)) {
911 add_error($errors, $path, "type check ('$type') failed - got '$value'");
912 return undef;
913 }
914 return 1;
915 } elsif ($type eq 'number') {
916 if (!is_number($value)) {
917 add_error($errors, $path, "type check ('$type') failed - got '$value'");
918 return undef;
919 }
920 return 1;
921 } else {
922 return 1; # no need to verify unknown types
923 }
924 }
925 }
9bbc4e17 926 }
e143e9d8
DM
927
928 return undef;
929}
930
931sub check_object {
932 my ($path, $schema, $value, $additional_properties, $errors) = @_;
933
934 # print "Check Object " . Dumper($value) . "\nSchema: " . Dumper($schema);
935
936 my $st = ref($schema);
937 if (!$st || $st ne 'HASH') {
938 add_error($errors, $path, "Invalid schema definition.");
939 return;
940 }
941
942 my $vt = ref($value);
943 if (!$vt || $vt ne 'HASH') {
944 add_error($errors, $path, "an object is required");
945 return;
946 }
947
948 foreach my $k (keys %$schema) {
bf27456b 949 check_prop($value->{$k}, $schema->{$k}, $path ? "$path.$k" : $k, $errors);
e143e9d8
DM
950 }
951
952 foreach my $k (keys %$value) {
953
954 my $newpath = $path ? "$path.$k" : $k;
955
956 if (my $subschema = $schema->{$k}) {
957 if (my $requires = $subschema->{requires}) {
958 if (ref($requires)) {
959 #print "TEST: " . Dumper($value) . "\n", Dumper($requires) ;
960 check_prop($value, $requires, $path, $errors);
961 } elsif (!defined($value->{$requires})) {
9bbc4e17 962 add_error($errors, $path ? "$path.$requires" : $requires,
8b6e737a 963 "missing property - '$newpath' requires this property");
e143e9d8
DM
964 }
965 }
966
967 next; # value is already checked above
968 }
969
970 if (defined ($additional_properties) && !$additional_properties) {
971 add_error($errors, $newpath, "property is not defined in schema " .
972 "and the schema does not allow additional properties");
973 next;
974 }
975 check_prop($value->{$k}, $additional_properties, $newpath, $errors)
976 if ref($additional_properties);
977 }
978}
979
86425a09
WB
980sub check_object_warn {
981 my ($path, $schema, $value, $additional_properties) = @_;
982 my $errors = {};
983 check_object($path, $schema, $value, $additional_properties, $errors);
984 if (scalar(%$errors)) {
985 foreach my $k (keys %$errors) {
986 warn "parse error: $k: $errors->{$k}\n";
987 }
988 return 0;
989 }
990 return 1;
991}
992
e143e9d8
DM
993sub check_prop {
994 my ($value, $schema, $path, $errors) = @_;
995
996 die "internal error - no schema" if !$schema;
997 die "internal error" if !$errors;
998
999 #print "check_prop $path\n" if $value;
1000
1001 my $st = ref($schema);
1002 if (!$st || $st ne 'HASH') {
1003 add_error($errors, $path, "Invalid schema definition.");
1004 return;
1005 }
1006
1007 # if it extends another schema, it must pass that schema as well
1008 if($schema->{extends}) {
1009 check_prop($value, $schema->{extends}, $path, $errors);
1010 }
1011
1012 if (!defined ($value)) {
1013 return if $schema->{type} && $schema->{type} eq 'null';
445e8267 1014 if (!$schema->{optional} && !$schema->{alias} && !$schema->{group}) {
e143e9d8
DM
1015 add_error($errors, $path, "property is missing and it is not optional");
1016 }
1017 return;
1018 }
1019
1020 return if !check_type($path, $schema->{type}, $value, $errors);
1021
1022 if ($schema->{disallow}) {
1023 my $tmperr = {};
1024 if (check_type($path, $schema->{disallow}, $value, $tmperr)) {
1025 add_error($errors, $path, "disallowed value was matched");
1026 return;
1027 }
1028 }
1029
1030 if (my $vt = ref($value)) {
1031
1032 if ($vt eq 'ARRAY') {
1033 if ($schema->{items}) {
1034 my $it = ref($schema->{items});
1035 if ($it && $it eq 'ARRAY') {
1036 #die "implement me $path: $vt " . Dumper($schema) ."\n". Dumper($value);
1037 die "not implemented";
1038 } else {
1039 my $ind = 0;
1040 foreach my $el (@$value) {
1041 check_prop($el, $schema->{items}, "${path}[$ind]", $errors);
1042 $ind++;
1043 }
1044 }
1045 }
9bbc4e17 1046 return;
e143e9d8
DM
1047 } elsif ($schema->{properties} || $schema->{additionalProperties}) {
1048 check_object($path, defined($schema->{properties}) ? $schema->{properties} : {},
1049 $value, $schema->{additionalProperties}, $errors);
1050 return;
1051 }
1052
1053 } else {
1054
1055 if (my $format = $schema->{format}) {
2f9e609a 1056 eval { check_format($format, $value, $path); };
e143e9d8
DM
1057 if ($@) {
1058 add_error($errors, $path, "invalid format - $@");
1059 return;
1060 }
1061 }
1062
1063 if (my $pattern = $schema->{pattern}) {
1064 if ($value !~ m/^$pattern$/) {
1065 add_error($errors, $path, "value does not match the regex pattern");
1066 return;
1067 }
1068 }
1069
1070 if (defined (my $max = $schema->{maxLength})) {
1071 if (length($value) > $max) {
1072 add_error($errors, $path, "value may only be $max characters long");
1073 return;
1074 }
1075 }
1076
1077 if (defined (my $min = $schema->{minLength})) {
1078 if (length($value) < $min) {
1079 add_error($errors, $path, "value must be at least $min characters long");
1080 return;
1081 }
1082 }
9bbc4e17 1083
e143e9d8
DM
1084 if (is_number($value)) {
1085 if (defined (my $max = $schema->{maximum})) {
9bbc4e17 1086 if ($value > $max) {
e143e9d8
DM
1087 add_error($errors, $path, "value must have a maximum value of $max");
1088 return;
1089 }
1090 }
1091
1092 if (defined (my $min = $schema->{minimum})) {
9bbc4e17 1093 if ($value < $min) {
e143e9d8
DM
1094 add_error($errors, $path, "value must have a minimum value of $min");
1095 return;
1096 }
1097 }
1098 }
1099
1100 if (my $ea = $schema->{enum}) {
1101
1102 my $found;
1103 foreach my $ev (@$ea) {
1104 if ($ev eq $value) {
1105 $found = 1;
1106 last;
1107 }
1108 }
1109 if (!$found) {
1110 add_error($errors, $path, "value '$value' does not have a value in the enumeration '" .
1111 join(", ", @$ea) . "'");
1112 }
1113 }
1114 }
1115}
1116
1117sub validate {
1118 my ($instance, $schema, $errmsg) = @_;
1119
1120 my $errors = {};
1121 $errmsg = "Parameter verification failed.\n" if !$errmsg;
1122
1123 # todo: cycle detection is only needed for debugging, I guess
1124 # we can disable that in the final release
1125 # todo: is there a better/faster way to detect cycles?
1126 my $cycles = 0;
1127 find_cycle($instance, sub { $cycles = 1 });
1128 if ($cycles) {
1129 add_error($errors, undef, "data structure contains recursive cycles");
1130 } elsif ($schema) {
1131 check_prop($instance, $schema, '', $errors);
1132 }
9bbc4e17 1133
e143e9d8
DM
1134 if (scalar(%$errors)) {
1135 raise $errmsg, code => HTTP_BAD_REQUEST, errors => $errors;
1136 }
1137
1138 return 1;
1139}
1140
1141my $schema_valid_types = ["string", "object", "coderef", "array", "boolean", "number", "integer", "null", "any"];
1142my $default_schema_noref = {
1143 description => "This is the JSON Schema for JSON Schemas.",
1144 type => [ "object" ],
1145 additionalProperties => 0,
1146 properties => {
1147 type => {
1148 type => ["string", "array"],
1149 description => "This is a type definition value. This can be a simple type, or a union type",
1150 optional => 1,
1151 default => "any",
1152 items => {
1153 type => "string",
1154 enum => $schema_valid_types,
1155 },
1156 enum => $schema_valid_types,
1157 },
1158 optional => {
1159 type => "boolean",
1160 description => "This indicates that the instance property in the instance object is not required.",
1161 optional => 1,
1162 default => 0
1163 },
1164 properties => {
1165 type => "object",
1166 description => "This is a definition for the properties of an object value",
1167 optional => 1,
1168 default => {},
1169 },
1170 items => {
1171 type => "object",
1172 description => "When the value is an array, this indicates the schema to use to validate each item in an array",
1173 optional => 1,
1174 default => {},
1175 },
1176 additionalProperties => {
1177 type => [ "boolean", "object"],
1178 description => "This provides a default property definition for all properties that are not explicitly defined in an object type definition.",
1179 optional => 1,
1180 default => {},
1181 },
1182 minimum => {
1183 type => "number",
1184 optional => 1,
1185 description => "This indicates the minimum value for the instance property when the type of the instance value is a number.",
1186 },
1187 maximum => {
1188 type => "number",
1189 optional => 1,
1190 description => "This indicates the maximum value for the instance property when the type of the instance value is a number.",
1191 },
1192 minLength => {
1193 type => "integer",
1194 description => "When the instance value is a string, this indicates minimum length of the string",
1195 optional => 1,
1196 minimum => 0,
1197 default => 0,
9bbc4e17 1198 },
e143e9d8
DM
1199 maxLength => {
1200 type => "integer",
1201 description => "When the instance value is a string, this indicates maximum length of the string.",
1202 optional => 1,
1203 },
1204 typetext => {
1205 type => "string",
1206 optional => 1,
1207 description => "A text representation of the type (used to generate documentation).",
1208 },
1209 pattern => {
1210 type => "string",
1211 format => "regex",
166e27c7 1212 description => "When the instance value is a string, this provides a regular expression that a instance string value should match in order to be valid.",
e143e9d8
DM
1213 optional => 1,
1214 default => ".*",
166e27c7 1215 },
e143e9d8
DM
1216 enum => {
1217 type => "array",
1218 optional => 1,
1219 description => "This provides an enumeration of possible values that are valid for the instance property.",
1220 },
1221 description => {
1222 type => "string",
1223 optional => 1,
1224 description => "This provides a description of the purpose the instance property. The value can be a string or it can be an object with properties corresponding to various different instance languages (with an optional default property indicating the default description).",
1225 },
32f8e0c7
DM
1226 verbose_description => {
1227 type => "string",
1228 optional => 1,
1229 description => "This provides a more verbose description.",
1230 },
d5d10f85
WB
1231 format_description => {
1232 type => "string",
1233 optional => 1,
1234 description => "This provides a shorter (usually just one word) description for a property used to generate descriptions for comma separated list property strings.",
1235 },
166e27c7
WB
1236 title => {
1237 type => "string",
e143e9d8 1238 optional => 1,
166e27c7
WB
1239 description => "This provides the title of the property",
1240 },
03c1e2a0
DM
1241 renderer => {
1242 type => "string",
1243 optional => 1,
1244 description => "This is used to provide rendering hints to format cli command output.",
1245 },
166e27c7
WB
1246 requires => {
1247 type => [ "string", "object" ],
e143e9d8 1248 optional => 1,
166e27c7
WB
1249 description => "indicates a required property or a schema that must be validated if this property is present",
1250 },
1251 format => {
2f9e609a 1252 type => [ "string", "object" ],
e143e9d8 1253 optional => 1,
166e27c7
WB
1254 description => "This indicates what format the data is among some predefined formats which may include:\n\ndate - a string following the ISO format \naddress \nschema - a schema definition object \nperson \npage \nhtml - a string representing HTML",
1255 },
095b88fd
WB
1256 default_key => {
1257 type => "boolean",
1258 optional => 1,
1259 description => "Whether this is the default key in a comma separated list property string.",
1260 },
303a9b34
WB
1261 alias => {
1262 type => 'string',
1263 optional => 1,
1264 description => "When a key represents the same property as another it can be an alias to it, causing the parsed datastructure to use the other key to store the current value under.",
1265 },
bf27456b 1266 keyAlias => {
445e8267
WB
1267 type => 'string',
1268 optional => 1,
bf27456b
DM
1269 description => "Allows to store the current 'key' as value of another property. Only valid if used together with 'alias'.",
1270 requires => 'alias',
445e8267 1271 },
e143e9d8
DM
1272 default => {
1273 type => "any",
1274 optional => 1,
1275 description => "This indicates the default for the instance property."
1276 },
166e27c7 1277 completion => {
7829989f
DM
1278 type => 'coderef',
1279 description => "Bash completion function. This function should return a list of possible values.",
1280 optional => 1,
166e27c7
WB
1281 },
1282 disallow => {
1283 type => "object",
e143e9d8 1284 optional => 1,
166e27c7 1285 description => "This attribute may take the same values as the \"type\" attribute, however if the instance matches the type or if this value is an array and the instance matches any type or schema in the array, then this instance is not valid.",
e143e9d8 1286 },
166e27c7
WB
1287 extends => {
1288 type => "object",
e143e9d8 1289 optional => 1,
166e27c7 1290 description => "This indicates the schema extends the given schema. All instances of this schema must be valid to by the extended schema also.",
e143e9d8 1291 default => {},
166e27c7
WB
1292 },
1293 # this is from hyper schema
1294 links => {
1295 type => "array",
1296 description => "This defines the link relations of the instance objects",
1297 optional => 1,
e143e9d8 1298 items => {
166e27c7
WB
1299 type => "object",
1300 properties => {
1301 href => {
1302 type => "string",
1303 description => "This defines the target URL for the relation and can be parameterized using {propertyName} notation. It should be resolved as a URI-reference relative to the URI that was used to retrieve the instance document",
1304 },
1305 rel => {
1306 type => "string",
1307 description => "This is the name of the link relation",
1308 optional => 1,
1309 default => "full",
1310 },
e143e9d8 1311 method => {
166e27c7
WB
1312 type => "string",
1313 description => "For submission links, this defines the method that should be used to access the target resource",
1314 optional => 1,
1315 default => "GET",
e143e9d8
DM
1316 },
1317 },
1318 },
1319 },
f8d4eff9
SI
1320 print_width => {
1321 type => "integer",
1322 description => "For CLI context, this defines the maximal width to print before truncating",
1323 optional => 1,
1324 },
9bbc4e17 1325 }
e143e9d8
DM
1326};
1327
1328my $default_schema = Storable::dclone($default_schema_noref);
1329
1330$default_schema->{properties}->{properties}->{additionalProperties} = $default_schema;
1331$default_schema->{properties}->{additionalProperties}->{properties} = $default_schema->{properties};
1332
1333$default_schema->{properties}->{items}->{properties} = $default_schema->{properties};
1334$default_schema->{properties}->{items}->{additionalProperties} = 0;
1335
1336$default_schema->{properties}->{disallow}->{properties} = $default_schema->{properties};
1337$default_schema->{properties}->{disallow}->{additionalProperties} = 0;
1338
1339$default_schema->{properties}->{requires}->{properties} = $default_schema->{properties};
1340$default_schema->{properties}->{requires}->{additionalProperties} = 0;
1341
1342$default_schema->{properties}->{extends}->{properties} = $default_schema->{properties};
1343$default_schema->{properties}->{extends}->{additionalProperties} = 0;
1344
1345my $method_schema = {
1346 type => "object",
1347 additionalProperties => 0,
1348 properties => {
1349 description => {
1350 description => "This a description of the method",
1351 optional => 1,
1352 },
1353 name => {
1354 type => 'string',
1355 description => "This indicates the name of the function to call.",
1356 optional => 1,
1357 requires => {
1358 additionalProperties => 1,
1359 properties => {
1360 name => {},
1361 description => {},
1362 code => {},
1363 method => {},
1364 parameters => {},
1365 path => {},
1366 parameters => {},
1367 returns => {},
9bbc4e17 1368 }
e143e9d8
DM
1369 },
1370 },
1371 method => {
1372 type => 'string',
1373 description => "The HTTP method name.",
1374 enum => [ 'GET', 'POST', 'PUT', 'DELETE' ],
1375 optional => 1,
1376 },
1377 protected => {
1378 type => 'boolean',
9bbc4e17 1379 description => "Method needs special privileges - only pvedaemon can execute it",
e143e9d8
DM
1380 optional => 1,
1381 },
4c72ade0
FG
1382 allowtoken => {
1383 type => 'boolean',
1384 description => "Method is available for clients authenticated using an API token.",
1385 optional => 1,
1386 default => 1,
1387 },
62a8f27b
DM
1388 download => {
1389 type => 'boolean',
1390 description => "Method downloads the file content (filename is the return value of the method).",
1391 optional => 1,
1392 },
e143e9d8
DM
1393 proxyto => {
1394 type => 'string',
1395 description => "A parameter name. If specified, all calls to this method are proxied to the host contained in that parameter.",
1396 optional => 1,
1397 },
031efdd0
DM
1398 proxyto_callback => {
1399 type => 'coderef',
fb3a1b29 1400 description => "A function which is called to resolve the proxyto attribute. The default implementation returns the value of the 'proxyto' parameter.",
031efdd0
DM
1401 optional => 1,
1402 },
e143e9d8
DM
1403 permissions => {
1404 type => 'object',
1405 description => "Required access permissions. By default only 'root' is allowed to access this method.",
1406 optional => 1,
1407 additionalProperties => 0,
1408 properties => {
b18d1722
DM
1409 description => {
1410 description => "Describe access permissions.",
1411 optional => 1,
1412 },
e143e9d8 1413 user => {
9bbc4e17
TL
1414 description => "A simply way to allow access for 'all' authenticated users. Value 'world' is used to allow access without credentials.",
1415 type => 'string',
b18d1722 1416 enum => ['all', 'world'],
e143e9d8
DM
1417 optional => 1,
1418 },
b18d1722
DM
1419 check => {
1420 description => "Array of permission checks (prefix notation).",
9bbc4e17
TL
1421 type => 'array',
1422 optional => 1
b18d1722 1423 },
e143e9d8
DM
1424 },
1425 },
1426 match_name => {
1427 description => "Used internally",
1428 optional => 1,
1429 },
1430 match_re => {
1431 description => "Used internally",
1432 optional => 1,
1433 },
1434 path => {
1435 type => 'string',
1436 description => "path for URL matching (uri template)",
1437 },
1438 fragmentDelimiter => {
1439 type => 'string',
fb3a1b29 1440 description => "A way to override the default fragment delimiter '/'. This only works on a whole sub-class. You can set this to the empty string to match the whole rest of the URI.",
e143e9d8
DM
1441 optional => 1,
1442 },
1443 parameters => {
1444 type => 'object',
1445 description => "JSON Schema for parameters.",
1446 optional => 1,
1447 },
1448 returns => {
1449 type => 'object',
1450 description => "JSON Schema for return value.",
1451 optional => 1,
1452 },
1453 code => {
1454 type => 'coderef',
fb3a1b29 1455 description => "method implementation (code reference)",
e143e9d8
DM
1456 optional => 1,
1457 },
1458 subclass => {
1459 type => 'string',
1460 description => "Delegate call to this class (perl class string).",
1461 optional => 1,
1462 requires => {
1463 additionalProperties => 0,
1464 properties => {
1465 subclass => {},
1466 path => {},
1467 match_name => {},
1468 match_re => {},
1469 fragmentDelimiter => { optional => 1 }
9bbc4e17 1470 }
e143e9d8 1471 },
9bbc4e17 1472 },
e143e9d8
DM
1473 },
1474
1475};
1476
1477sub validate_schema {
9bbc4e17 1478 my ($schema) = @_;
e143e9d8
DM
1479
1480 my $errmsg = "internal error - unable to verify schema\n";
1481 validate($schema, $default_schema, $errmsg);
1482}
1483
1484sub validate_method_info {
1485 my $info = shift;
1486
1487 my $errmsg = "internal error - unable to verify method info\n";
1488 validate($info, $method_schema, $errmsg);
9bbc4e17 1489
e143e9d8
DM
1490 validate_schema($info->{parameters}) if $info->{parameters};
1491 validate_schema($info->{returns}) if $info->{returns};
1492}
1493
1494# run a self test on load
9bbc4e17 1495# make sure we can verify the default schema
e143e9d8
DM
1496validate_schema($default_schema_noref);
1497validate_schema($method_schema);
1498
1499# and now some utility methods (used by pve api)
1500sub method_get_child_link {
1501 my ($info) = @_;
1502
1503 return undef if !$info;
1504
1505 my $schema = $info->{returns};
1506 return undef if !$schema || !$schema->{type} || $schema->{type} ne 'array';
1507
1508 my $links = $schema->{links};
1509 return undef if !$links;
1510
1511 my $found;
1512 foreach my $lnk (@$links) {
1513 if ($lnk->{href} && $lnk->{rel} && ($lnk->{rel} eq 'child')) {
1514 $found = $lnk;
1515 last;
1516 }
1517 }
1518
1519 return $found;
1520}
1521
9bbc4e17 1522# a way to parse command line parameters, using a
e143e9d8
DM
1523# schema to configure Getopt::Long
1524sub get_options {
4842b651 1525 my ($schema, $args, $arg_param, $fixed_param, $param_mapping_hash) = @_;
e143e9d8
DM
1526
1527 if (!$schema || !$schema->{properties}) {
1528 raise("too many arguments\n", code => HTTP_BAD_REQUEST)
1529 if scalar(@$args) != 0;
1530 return {};
1531 }
1532
0ce82909
DM
1533 my $list_param;
1534 if ($arg_param && !ref($arg_param)) {
1535 my $pd = $schema->{properties}->{$arg_param};
1536 die "expected list format $pd->{format}"
1537 if !($pd && $pd->{format} && $pd->{format} =~ m/-list/);
1538 $list_param = $arg_param;
1539 }
1540
c7171ff2 1541 my @interactive = ();
e143e9d8
DM
1542 my @getopt = ();
1543 foreach my $prop (keys %{$schema->{properties}}) {
1544 my $pd = $schema->{properties}->{$prop};
aab47b58 1545 next if $list_param && $prop eq $list_param;
0ce82909 1546 next if defined($fixed_param->{$prop});
e143e9d8 1547
c7171ff2
WB
1548 my $mapping = $param_mapping_hash->{$prop};
1549 if ($mapping && $mapping->{interactive}) {
1550 # interactive parameters such as passwords: make the argument
1551 # optional and call the mapping function afterwards.
1552 push @getopt, "$prop:s";
1553 push @interactive, [$prop, $mapping->{func}];
e143e9d8
DM
1554 } elsif ($pd->{type} eq 'boolean') {
1555 push @getopt, "$prop:s";
1556 } else {
23dc9401 1557 if ($pd->{format} && $pd->{format} =~ m/-a?list/) {
8ba7c72b
DM
1558 push @getopt, "$prop=s@";
1559 } else {
1560 push @getopt, "$prop=s";
1561 }
e143e9d8
DM
1562 }
1563 }
1564
1068aeb3
WB
1565 Getopt::Long::Configure('prefix_pattern=(--|-)');
1566
e143e9d8
DM
1567 my $opts = {};
1568 raise("unable to parse option\n", code => HTTP_BAD_REQUEST)
1569 if !Getopt::Long::GetOptionsFromArray($args, $opts, @getopt);
1d21344c 1570
5851be88 1571 if (@$args) {
0ce82909
DM
1572 if ($list_param) {
1573 $opts->{$list_param} = $args;
1574 $args = [];
1575 } elsif (ref($arg_param)) {
5851be88
WB
1576 foreach my $arg_name (@$arg_param) {
1577 if ($opts->{'extra-args'}) {
1578 raise("internal error: extra-args must be the last argument\n", code => HTTP_BAD_REQUEST);
1579 }
1580 if ($arg_name eq 'extra-args') {
1581 $opts->{'extra-args'} = $args;
1582 $args = [];
1583 next;
1584 }
1585 raise("not enough arguments\n", code => HTTP_BAD_REQUEST) if !@$args;
1586 $opts->{$arg_name} = shift @$args;
0ce82909 1587 }
5851be88 1588 raise("too many arguments\n", code => HTTP_BAD_REQUEST) if @$args;
0ce82909
DM
1589 } else {
1590 raise("too many arguments\n", code => HTTP_BAD_REQUEST)
1591 if scalar(@$args) != 0;
1592 }
ff2bf45f
DM
1593 } else {
1594 if (ref($arg_param)) {
1595 foreach my $arg_name (@$arg_param) {
1596 if ($arg_name eq 'extra-args') {
1597 $opts->{'extra-args'} = [];
1598 } else {
1599 raise("not enough arguments\n", code => HTTP_BAD_REQUEST);
1600 }
1601 }
1602 }
1d21344c
DM
1603 }
1604
c7171ff2
WB
1605 foreach my $entry (@interactive) {
1606 my ($opt, $func) = @$entry;
1607 my $pd = $schema->{properties}->{$opt};
1608 my $value = $opts->{$opt};
1609 if (defined($value) || !$pd->{optional}) {
1610 $opts->{$opt} = $func->($value);
1611 }
1612 }
1613
c9902568 1614 # decode after Getopt as we are not sure how well it handles unicode
24197a9f 1615 foreach my $p (keys %$opts) {
c9902568
TL
1616 if (!ref($opts->{$p})) {
1617 $opts->{$p} = decode('locale', $opts->{$p});
1618 } elsif (ref($opts->{$p}) eq 'ARRAY') {
1619 my $tmp = [];
1620 foreach my $v (@{$opts->{$p}}) {
1621 push @$tmp, decode('locale', $v);
1622 }
1623 $opts->{$p} = $tmp;
1624 } elsif (ref($opts->{$p}) eq 'SCALAR') {
1625 $opts->{$p} = decode('locale', $$opts->{$p});
1626 } else {
1627 raise("decoding options failed, unknown reference\n", code => HTTP_BAD_REQUEST);
1628 }
24197a9f 1629 }
815b2aba 1630
e143e9d8
DM
1631 foreach my $p (keys %$opts) {
1632 if (my $pd = $schema->{properties}->{$p}) {
1633 if ($pd->{type} eq 'boolean') {
1634 if ($opts->{$p} eq '') {
1635 $opts->{$p} = 1;
1b71e564
WB
1636 } elsif (defined(my $bool = parse_boolean($opts->{$p}))) {
1637 $opts->{$p} = $bool;
e143e9d8
DM
1638 } else {
1639 raise("unable to parse boolean option\n", code => HTTP_BAD_REQUEST);
1640 }
23dc9401 1641 } elsif ($pd->{format}) {
8ba7c72b 1642
23dc9401 1643 if ($pd->{format} =~ m/-list/) {
8ba7c72b 1644 # allow --vmid 100 --vmid 101 and --vmid 100,101
23dc9401 1645 # allow --dow mon --dow fri and --dow mon,fri
43479146 1646 $opts->{$p} = join(",", @{$opts->{$p}}) if ref($opts->{$p}) eq 'ARRAY';
23dc9401 1647 } elsif ($pd->{format} =~ m/-alist/) {
8ba7c72b
DM
1648 # we encode array as \0 separated strings
1649 # Note: CGI.pm also use this encoding
1650 if (scalar(@{$opts->{$p}}) != 1) {
1651 $opts->{$p} = join("\0", @{$opts->{$p}});
1652 } else {
1653 # st that split_list knows it is \0 terminated
1654 my $v = $opts->{$p}->[0];
1655 $opts->{$p} = "$v\0";
1656 }
1657 }
e143e9d8 1658 }
9bbc4e17 1659 }
e143e9d8
DM
1660 }
1661
0ce82909
DM
1662 foreach my $p (keys %$fixed_param) {
1663 $opts->{$p} = $fixed_param->{$p};
e143e9d8
DM
1664 }
1665
1666 return $opts;
1667}
1668
1669# A way to parse configuration data by giving a json schema
1670sub parse_config {
1671 my ($schema, $filename, $raw) = @_;
1672
1673 # do fast check (avoid validate_schema($schema))
9bbc4e17 1674 die "got strange schema" if !$schema->{type} ||
e143e9d8
DM
1675 !$schema->{properties} || $schema->{type} ne 'object';
1676
1677 my $cfg = {};
1678
3c4d612a 1679 while ($raw =~ /^\s*(.+?)\s*$/gm) {
e143e9d8 1680 my $line = $1;
e143e9d8 1681
3c4d612a
WB
1682 next if $line =~ /^#/;
1683
1684 if ($line =~ m/^(\S+?):\s*(.*)$/) {
e143e9d8
DM
1685 my $key = $1;
1686 my $value = $2;
9bbc4e17 1687 if ($schema->{properties}->{$key} &&
e143e9d8
DM
1688 $schema->{properties}->{$key}->{type} eq 'boolean') {
1689
1b71e564 1690 $value = parse_boolean($value) // $value;
e143e9d8
DM
1691 }
1692 $cfg->{$key} = $value;
1693 } else {
1694 warn "ignore config line: $line\n"
1695 }
1696 }
1697
1698 my $errors = {};
1699 check_prop($cfg, $schema, '', $errors);
1700
1701 foreach my $k (keys %$errors) {
1702 warn "parse error in '$filename' - '$k': $errors->{$k}\n";
1703 delete $cfg->{$k};
9bbc4e17 1704 }
e143e9d8
DM
1705
1706 return $cfg;
1707}
1708
1709# generate simple key/value file
1710sub dump_config {
1711 my ($schema, $filename, $cfg) = @_;
1712
1713 # do fast check (avoid validate_schema($schema))
9bbc4e17 1714 die "got strange schema" if !$schema->{type} ||
e143e9d8
DM
1715 !$schema->{properties} || $schema->{type} ne 'object';
1716
1717 validate($cfg, $schema, "validation error in '$filename'\n");
1718
1719 my $data = '';
1720
821d408d 1721 foreach my $k (sort keys %$cfg) {
e143e9d8
DM
1722 $data .= "$k: $cfg->{$k}\n";
1723 }
1724
1725 return $data;
1726}
1727
bf27456b
DM
1728# helpers used to generate our manual pages
1729
1730my $find_schema_default_key = sub {
1731 my ($format) = @_;
1732
1733 my $default_key;
1734 my $keyAliasProps = {};
1735
1736 foreach my $key (keys %$format) {
1737 my $phash = $format->{$key};
1738 if ($phash->{default_key}) {
1739 die "multiple default keys in schema ($default_key, $key)\n"
1740 if defined($default_key);
1741 die "default key '$key' is an alias - this is not allowed\n"
1742 if defined($phash->{alias});
1743 die "default key '$key' with keyAlias attribute is not allowed\n"
1744 if $phash->{keyAlias};
bf27456b
DM
1745 $default_key = $key;
1746 }
1747 my $key_alias = $phash->{keyAlias};
c88c582d
DM
1748 die "found keyAlias without 'alias definition for '$key'\n"
1749 if $key_alias && !$phash->{alias};
1750
bf27456b
DM
1751 if ($phash->{alias} && $key_alias) {
1752 die "inconsistent keyAlias '$key_alias' definition"
1753 if defined($keyAliasProps->{$key_alias}) &&
1754 $keyAliasProps->{$key_alias} ne $phash->{alias};
1755 $keyAliasProps->{$key_alias} = $phash->{alias};
1756 }
1757 }
1758
1759 return wantarray ? ($default_key, $keyAliasProps) : $default_key;
1760};
1761
1762sub generate_typetext {
abc1afd8 1763 my ($format, $list_enums) = @_;
bf27456b 1764
d8c2b947 1765 my ($default_key, $keyAliasProps) = &$find_schema_default_key($format);
bf27456b
DM
1766
1767 my $res = '';
1768 my $add_sep = 0;
1769
1770 my $add_option_string = sub {
1771 my ($text, $optional) = @_;
1772
1773 if ($add_sep) {
1774 $text = ",$text";
1775 $res .= ' ';
1776 }
1777 $text = "[$text]" if $optional;
1778 $res .= $text;
1779 $add_sep = 1;
1780 };
1781
1782 my $format_key_value = sub {
1783 my ($key, $phash) = @_;
1784
1785 die "internal error" if defined($phash->{alias});
1786
1787 my $keytext = $key;
1788
1789 my $typetext = '';
1790
1791 if (my $desc = $phash->{format_description}) {
1792 $typetext .= "<$desc>";
1793 } elsif (my $text = $phash->{typetext}) {
1794 $typetext .= $text;
1795 } elsif (my $enum = $phash->{enum}) {
abc1afd8
DM
1796 if ($list_enums || (scalar(@$enum) <= 3)) {
1797 $typetext .= '<' . join('|', @$enum) . '>';
1798 } else {
1799 $typetext .= '<enum>';
1800 }
bf27456b
DM
1801 } elsif ($phash->{type} eq 'boolean') {
1802 $typetext .= '<1|0>';
1803 } elsif ($phash->{type} eq 'integer') {
1804 $typetext .= '<integer>';
1805 } elsif ($phash->{type} eq 'number') {
1806 $typetext .= '<number>';
1807 } else {
1808 die "internal error: neither format_description nor typetext found for option '$key'";
1809 }
1810
1811 if (defined($default_key) && ($default_key eq $key)) {
1812 &$add_option_string("[$keytext=]$typetext", $phash->{optional});
1813 } else {
1814 &$add_option_string("$keytext=$typetext", $phash->{optional});
1815 }
1816 };
1817
d8c2b947 1818 my $done = {};
bf27456b 1819
d8c2b947
DM
1820 my $cond_add_key = sub {
1821 my ($key) = @_;
1822
1823 return if $done->{$key}; # avoid duplicates
1824
1825 $done->{$key} = 1;
bf27456b
DM
1826
1827 my $phash = $format->{$key};
1828
d8c2b947
DM
1829 return if !$phash; # should not happen
1830
1831 return if $phash->{alias};
bf27456b
DM
1832
1833 &$format_key_value($key, $phash);
1834
d8c2b947
DM
1835 };
1836
1837 &$cond_add_key($default_key) if defined($default_key);
1838
1839 # add required keys first
1840 foreach my $key (sort keys %$format) {
1841 my $phash = $format->{$key};
1842 &$cond_add_key($key) if $phash && !$phash->{optional};
1843 }
1844
1845 # add the rest
1846 foreach my $key (sort keys %$format) {
1847 &$cond_add_key($key);
1848 }
1849
1850 foreach my $keyAlias (sort keys %$keyAliasProps) {
1851 &$add_option_string("<$keyAlias>=<$keyAliasProps->{$keyAlias }>", 1);
bf27456b
DM
1852 }
1853
1854 return $res;
1855}
1856
1857sub print_property_string {
1858 my ($data, $format, $skip, $path) = @_;
1859
1860 if (ref($format) ne 'HASH') {
1861 my $schema = get_format($format);
1862 die "not a valid format: $format\n" if !$schema;
1863 $format = $schema;
1864 }
1865
1866 my $errors = {};
1867 check_object($path, $format, $data, undef, $errors);
1868 if (scalar(%$errors)) {
1869 raise "format error", errors => $errors;
1870 }
1871
1872 my ($default_key, $keyAliasProps) = &$find_schema_default_key($format);
1873
1874 my $res = '';
1875 my $add_sep = 0;
1876
1877 my $add_option_string = sub {
1878 my ($text) = @_;
1879
1880 $res .= ',' if $add_sep;
1881 $res .= $text;
1882 $add_sep = 1;
1883 };
1884
1885 my $format_value = sub {
1886 my ($key, $value, $format) = @_;
1887
1888 if (defined($format) && ($format eq 'disk-size')) {
1889 return format_size($value);
1890 } else {
1891 die "illegal value with commas for $key\n" if $value =~ /,/;
1892 return $value;
1893 }
1894 };
1895
2289890b 1896 my $done = { map { $_ => 1 } @$skip };
bf27456b
DM
1897
1898 my $cond_add_key = sub {
971353e8 1899 my ($key, $isdefault) = @_;
bf27456b
DM
1900
1901 return if $done->{$key}; # avoid duplicates
1902
1903 $done->{$key} = 1;
1904
1905 my $value = $data->{$key};
1906
1907 return if !defined($value);
1908
1909 my $phash = $format->{$key};
1910
1911 # try to combine values if we have key aliases
1912 if (my $combine = $keyAliasProps->{$key}) {
1913 if (defined(my $combine_value = $data->{$combine})) {
1914 my $combine_format = $format->{$combine}->{format};
1915 my $value_str = &$format_value($key, $value, $phash->{format});
1916 my $combine_str = &$format_value($combine, $combine_value, $combine_format);
1917 &$add_option_string("${value_str}=${combine_str}");
1918 $done->{$combine} = 1;
1919 return;
1920 }
1921 }
1922
1923 if ($phash && $phash->{alias}) {
1924 $phash = $format->{$phash->{alias}};
1925 }
1926
1927 die "invalid key '$key'\n" if !$phash;
1928 die "internal error" if defined($phash->{alias});
1929
1930 my $value_str = &$format_value($key, $value, $phash->{format});
971353e8
WB
1931 if ($isdefault) {
1932 &$add_option_string($value_str);
1933 } else {
1934 &$add_option_string("$key=${value_str}");
1935 }
bf27456b
DM
1936 };
1937
1938 # add default key first
971353e8 1939 &$cond_add_key($default_key, 1) if defined($default_key);
bf27456b 1940
d8c2b947
DM
1941 # add required keys first
1942 foreach my $key (sort keys %$data) {
1943 my $phash = $format->{$key};
1944 &$cond_add_key($key) if $phash && !$phash->{optional};
1945 }
1946
1947 # add the rest
bf27456b
DM
1948 foreach my $key (sort keys %$data) {
1949 &$cond_add_key($key);
1950 }
1951
1952 return $res;
1953}
1954
1955sub schema_get_type_text {
abc1afd8 1956 my ($phash, $style) = @_;
bf27456b 1957
32f8e0c7
DM
1958 my $type = $phash->{type} || 'string';
1959
bf27456b
DM
1960 if ($phash->{typetext}) {
1961 return $phash->{typetext};
1962 } elsif ($phash->{format_description}) {
1963 return "<$phash->{format_description}>";
1964 } elsif ($phash->{enum}) {
25d9bda9 1965 return "<" . join(' | ', sort @{$phash->{enum}}) . ">";
bf27456b
DM
1966 } elsif ($phash->{pattern}) {
1967 return $phash->{pattern};
32f8e0c7 1968 } elsif ($type eq 'integer' || $type eq 'number') {
05185ea2 1969 # NOTE: always access values as number (avoid converion to string)
bf27456b 1970 if (defined($phash->{minimum}) && defined($phash->{maximum})) {
25d9bda9 1971 return "<$type> (" . ($phash->{minimum} + 0) . " - " .
05185ea2 1972 ($phash->{maximum} + 0) . ")";
bf27456b 1973 } elsif (defined($phash->{minimum})) {
25d9bda9 1974 return "<$type> (" . ($phash->{minimum} + 0) . " - N)";
bf27456b 1975 } elsif (defined($phash->{maximum})) {
25d9bda9 1976 return "<$type> (-N - " . ($phash->{maximum} + 0) . ")";
bf27456b 1977 }
32f8e0c7 1978 } elsif ($type eq 'string') {
bf27456b
DM
1979 if (my $format = $phash->{format}) {
1980 $format = get_format($format) if ref($format) ne 'HASH';
1981 if (ref($format) eq 'HASH') {
abc1afd8
DM
1982 my $list_enums = 0;
1983 $list_enums = 1 if $style && $style eq 'config-sub';
1984 return generate_typetext($format, $list_enums);
bf27456b
DM
1985 }
1986 }
1987 }
1988
25d9bda9 1989 return "<$type>";
bf27456b
DM
1990}
1991
e143e9d8 19921;