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