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