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