]> git.proxmox.com Git - pve-common.git/blob - src/PVE/JSONSchema.pm
PVE::CLIFormatter::extract_properties_to_print - new helper
[pve-common.git] / src / PVE / JSONSchema.pm
1 package PVE::JSONSchema;
2
3 use strict;
4 use warnings;
5 use Storable; # for dclone
6 use Getopt::Long;
7 use Encode::Locale;
8 use Encode;
9 use Devel::Cycle -quiet; # todo: remove?
10 use PVE::Tools qw(split_list $IPV6RE $IPV4RE);
11 use PVE::Exception qw(raise);
12 use HTTP::Status qw(:constants);
13 use Net::IP qw(:PROC);
14 use Data::Dumper;
15
16 use base 'Exporter';
17
18 our @EXPORT_OK = qw(
19 register_standard_option
20 get_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
29 my $standard_options = {};
30 sub 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
39 sub get_standard_option {
40 my ($name, $base) = @_;
41
42 my $std = $standard_options->{$name};
43 die "no such standard option '$name'\n" if !$std;
44
45 my $res = $base || {};
46
47 foreach my $opt (keys %$std) {
48 next if defined($res->{$opt});
49 $res->{$opt} = $std->{$opt};
50 }
51
52 return $res;
53 };
54
55 register_standard_option('pve-vmid', {
56 description => "The (unique) ID of the VM.",
57 type => 'integer', format => 'pve-vmid',
58 minimum => 1
59 });
60
61 register_standard_option('pve-node', {
62 description => "The cluster node name.",
63 type => 'string', format => 'pve-node',
64 });
65
66 register_standard_option('pve-node-list', {
67 description => "List of cluster node names.",
68 type => 'string', format => 'pve-node-list',
69 });
70
71 register_standard_option('pve-iface', {
72 description => "Network interface name.",
73 type => 'string', format => 'pve-iface',
74 minLength => 2, maxLength => 20,
75 });
76
77 register_standard_option('pve-storage-id', {
78 description => "The storage identifier.",
79 type => 'string', format => 'pve-storage-id',
80 });
81
82 register_standard_option('pve-config-digest', {
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
89 register_standard_option('skiplock', {
90 description => "Ignore locks - only root is allowed to use this option.",
91 type => 'boolean',
92 optional => 1,
93 });
94
95 register_standard_option('extra-args', {
96 description => "Extra arguments as array",
97 type => 'array',
98 items => { type => 'string' },
99 optional => 1
100 });
101
102 register_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
108 my $format_list = {};
109
110 sub 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
119 sub get_format {
120 my ($format) = @_;
121 return $format_list->{$format};
122 }
123
124 my $renderer_hash = {};
125
126 sub 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
135 sub get_renderer {
136 my ($name) = @_;
137 return $renderer_hash->{$name};
138 }
139
140 # register some common type for pve
141
142 register_format('string', sub {}); # allow format => 'string-list'
143
144 register_format('urlencoded', \&pve_verify_urlencoded);
145 sub 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
154 register_format('pve-configid', \&pve_verify_configid);
155 sub pve_verify_configid {
156 my ($id, $noerr) = @_;
157
158 if ($id !~ m/^[a-z][a-z0-9_]+$/i) {
159 return undef if $noerr;
160 die "invalid configuration ID '$id'\n";
161 }
162 return $id;
163 }
164
165 PVE::JSONSchema::register_format('pve-storage-id', \&parse_storage_id);
166 sub 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
177 register_format('pve-vmid', \&pve_verify_vmid);
178 sub pve_verify_vmid {
179 my ($vmid, $noerr) = @_;
180
181 if ($vmid !~ m/^[1-9][0-9]{2,8}$/) {
182 return undef if $noerr;
183 die "value does not look like a valid VM ID\n";
184 }
185 return $vmid;
186 }
187
188 register_format('pve-node', \&pve_verify_node_name);
189 sub pve_verify_node_name {
190 my ($node, $noerr) = @_;
191
192 if ($node !~ m/^([a-zA-Z0-9]([a-zA-Z0-9\-]*[a-zA-Z0-9])?)$/) {
193 return undef if $noerr;
194 die "value does not look like a valid node name\n";
195 }
196 return $node;
197 }
198
199 register_format('ipv4', \&pve_verify_ipv4);
200 sub pve_verify_ipv4 {
201 my ($ipv4, $noerr) = @_;
202
203 if ($ipv4 !~ m/^(?:$IPV4RE)$/) {
204 return undef if $noerr;
205 die "value does not look like a valid IPv4 address\n";
206 }
207 return $ipv4;
208 }
209
210 register_format('ipv6', \&pve_verify_ipv6);
211 sub pve_verify_ipv6 {
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
221 register_format('ip', \&pve_verify_ip);
222 sub 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
232 my $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,
262 '255.255.255.252' => 30,
263 '255.255.255.254' => 31,
264 '255.255.255.255' => 32,
265 };
266
267 register_format('ipv4mask', \&pve_verify_ipv4mask);
268 sub pve_verify_ipv4mask {
269 my ($mask, $noerr) = @_;
270
271 if (!defined($ipv4_mask_hash->{$mask})) {
272 return undef if $noerr;
273 die "value does not look like a valid IP netmask\n";
274 }
275 return $mask;
276 }
277
278 register_format('CIDRv6', \&pve_verify_cidrv6);
279 sub pve_verify_cidrv6 {
280 my ($cidr, $noerr) = @_;
281
282 if ($cidr =~ m!^(?:$IPV6RE)(?:/(\d+))$! && ($1 > 7) && ($1 <= 128)) {
283 return $cidr;
284 }
285
286 return undef if $noerr;
287 die "value does not look like a valid IPv6 CIDR network\n";
288 }
289
290 register_format('CIDRv4', \&pve_verify_cidrv4);
291 sub pve_verify_cidrv4 {
292 my ($cidr, $noerr) = @_;
293
294 if ($cidr =~ m!^(?:$IPV4RE)(?:/(\d+))$! && ($1 > 7) && ($1 <= 32)) {
295 return $cidr;
296 }
297
298 return undef if $noerr;
299 die "value does not look like a valid IPv4 CIDR network\n";
300 }
301
302 register_format('CIDR', \&pve_verify_cidr);
303 sub 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
316 register_format('pve-ipv4-config', \&pve_verify_ipv4_config);
317 sub 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
326 register_format('pve-ipv6-config', \&pve_verify_ipv6_config);
327 sub 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";
334 }
335
336 register_format('email', \&pve_verify_email);
337 sub pve_verify_email {
338 my ($email, $noerr) = @_;
339
340 # we use same regex as in Utils.js
341 if ($email !~ /^(\w+)([\-+.][\w]+)*@(\w[\-\w]*\.){1,5}([A-Za-z]){2,63}$/) {
342 return undef if $noerr;
343 die "value does not look like a valid email address\n";
344 }
345 return $email;
346 }
347
348 register_format('dns-name', \&pve_verify_dns_name);
349 sub pve_verify_dns_name {
350 my ($name, $noerr) = @_;
351
352 my $namere = "([a-zA-Z0-9]([a-zA-Z0-9\-]*[a-zA-Z0-9])?)";
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
361 # network interface name
362 register_format('pve-iface', \&pve_verify_iface);
363 sub 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
373 # general addresses by name or IP
374 register_format('address', \&pve_verify_address);
375 sub 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
387 register_format('disk-size', \&pve_verify_disk_size);
388 sub 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
397 register_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).",
399 type => 'string', format => 'address',
400 });
401
402 register_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
414 register_format('pve-startup-order', \&pve_verify_startup_order);
415 sub 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
425 my %bwlimit_opt = (
426 optional => 1,
427 type => 'number', minimum => '0',
428 format_description => 'LIMIT',
429 );
430
431 my $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 };
453 register_format('bwlimit', $bwlimit_format);
454 register_standard_option('bwlimit', {
455 description => "Set bandwidth/io limits various operations.",
456 optional => 1,
457 type => 'string',
458 format => $bwlimit_format,
459 });
460
461 sub 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
485 PVE::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
492 sub check_format {
493 my ($format, $value, $path) = @_;
494
495 return parse_property_string($format, $value, $path) if ref($format) eq 'HASH';
496 return if $format eq 'regex';
497
498 if ($format =~ m/^(.*)-a?list$/) {
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
525 return parse_property_string($code, $value, $path) if ref($code) eq 'HASH';
526 &$code($value);
527 }
528 }
529
530 sub 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
549 sub 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
569 sub 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
576 sub parse_property_string {
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);
581
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
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);
601 die "duplicate key in comma-separated list property: $k\n" if defined($res->{$k});
602 my $schema = $format->{$k};
603 if (my $alias = $schema->{alias}) {
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 }
608 $k = $alias;
609 $schema = $format->{$k};
610 }
611
612 die "invalid key in comma-separated list property: $k\n" if !$schema;
613 if ($schema->{type} && $schema->{type} eq 'boolean') {
614 $v = parse_boolean($v) // $v;
615 }
616 $res->{$k} = $v;
617 } elsif ($part !~ /=/) {
618 die "duplicate key in comma-separated list property: $default_key\n" if $default_key;
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 }
626 die "duplicate key in comma-separated list property: $default_key\n";
627 }
628 }
629 die "value without key, but schema does not define a default key\n" if !$default_key;
630 } else {
631 die "missing key in comma-separated list property\n";
632 }
633 }
634
635 my $errors = {};
636 check_object($path, $format, $res, $additional_properties, $errors);
637 if (scalar(%$errors)) {
638 raise "format error\n", errors => $errors;
639 }
640
641 return $res;
642 }
643
644 sub 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
656 sub is_number {
657 my $value = shift;
658
659 # see 'man perlretut'
660 return $value =~ /^[+-]?(\d+\.\d+|\d+\.|\.\d+|\d+)([eE][+-]?\d+)?$/;
661 }
662
663 sub is_integer {
664 my $value = shift;
665
666 return $value =~ m/^[+-]?\d+$/;
667 }
668
669 sub 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;
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;
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') {
747 return 1; # return success (not value)
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
774 sub 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) {
792 check_prop($value->{$k}, $schema->{$k}, $path ? "$path.$k" : $k, $errors);
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,
806 "missing property - '$newpath' requires this property");
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
823 sub 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
836 sub 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';
857 if (!$schema->{optional} && !$schema->{alias} && !$schema->{group}) {
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}) {
899 eval { check_format($format, $value, $path); };
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
960 sub 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
984 my $schema_valid_types = ["string", "object", "coderef", "array", "boolean", "number", "integer", "null", "any"];
985 my $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",
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.",
1056 optional => 1,
1057 default => ".*",
1058 },
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 },
1069 verbose_description => {
1070 type => "string",
1071 optional => 1,
1072 description => "This provides a more verbose description.",
1073 },
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 },
1079 title => {
1080 type => "string",
1081 optional => 1,
1082 description => "This provides the title of the property",
1083 },
1084 renderer => {
1085 type => "string",
1086 optional => 1,
1087 description => "This is used to provide rendering hints to format cli command output.",
1088 },
1089 requires => {
1090 type => [ "string", "object" ],
1091 optional => 1,
1092 description => "indicates a required property or a schema that must be validated if this property is present",
1093 },
1094 format => {
1095 type => [ "string", "object" ],
1096 optional => 1,
1097 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",
1098 },
1099 default_key => {
1100 type => "boolean",
1101 optional => 1,
1102 description => "Whether this is the default key in a comma separated list property string.",
1103 },
1104 alias => {
1105 type => 'string',
1106 optional => 1,
1107 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.",
1108 },
1109 keyAlias => {
1110 type => 'string',
1111 optional => 1,
1112 description => "Allows to store the current 'key' as value of another property. Only valid if used together with 'alias'.",
1113 requires => 'alias',
1114 },
1115 default => {
1116 type => "any",
1117 optional => 1,
1118 description => "This indicates the default for the instance property."
1119 },
1120 completion => {
1121 type => 'coderef',
1122 description => "Bash completion function. This function should return a list of possible values.",
1123 optional => 1,
1124 },
1125 disallow => {
1126 type => "object",
1127 optional => 1,
1128 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.",
1129 },
1130 extends => {
1131 type => "object",
1132 optional => 1,
1133 description => "This indicates the schema extends the given schema. All instances of this schema must be valid to by the extended schema also.",
1134 default => {},
1135 },
1136 # this is from hyper schema
1137 links => {
1138 type => "array",
1139 description => "This defines the link relations of the instance objects",
1140 optional => 1,
1141 items => {
1142 type => "object",
1143 properties => {
1144 href => {
1145 type => "string",
1146 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",
1147 },
1148 rel => {
1149 type => "string",
1150 description => "This is the name of the link relation",
1151 optional => 1,
1152 default => "full",
1153 },
1154 method => {
1155 type => "string",
1156 description => "For submission links, this defines the method that should be used to access the target resource",
1157 optional => 1,
1158 default => "GET",
1159 },
1160 },
1161 },
1162 },
1163 print_width => {
1164 type => "integer",
1165 description => "For CLI context, this defines the maximal width to print before truncating",
1166 optional => 1,
1167 },
1168 }
1169 };
1170
1171 my $default_schema = Storable::dclone($default_schema_noref);
1172
1173 $default_schema->{properties}->{properties}->{additionalProperties} = $default_schema;
1174 $default_schema->{properties}->{additionalProperties}->{properties} = $default_schema->{properties};
1175
1176 $default_schema->{properties}->{items}->{properties} = $default_schema->{properties};
1177 $default_schema->{properties}->{items}->{additionalProperties} = 0;
1178
1179 $default_schema->{properties}->{disallow}->{properties} = $default_schema->{properties};
1180 $default_schema->{properties}->{disallow}->{additionalProperties} = 0;
1181
1182 $default_schema->{properties}->{requires}->{properties} = $default_schema->{properties};
1183 $default_schema->{properties}->{requires}->{additionalProperties} = 0;
1184
1185 $default_schema->{properties}->{extends}->{properties} = $default_schema->{properties};
1186 $default_schema->{properties}->{extends}->{additionalProperties} = 0;
1187
1188 my $method_schema = {
1189 type => "object",
1190 additionalProperties => 0,
1191 properties => {
1192 description => {
1193 description => "This a description of the method",
1194 optional => 1,
1195 },
1196 name => {
1197 type => 'string',
1198 description => "This indicates the name of the function to call.",
1199 optional => 1,
1200 requires => {
1201 additionalProperties => 1,
1202 properties => {
1203 name => {},
1204 description => {},
1205 code => {},
1206 method => {},
1207 parameters => {},
1208 path => {},
1209 parameters => {},
1210 returns => {},
1211 }
1212 },
1213 },
1214 method => {
1215 type => 'string',
1216 description => "The HTTP method name.",
1217 enum => [ 'GET', 'POST', 'PUT', 'DELETE' ],
1218 optional => 1,
1219 },
1220 protected => {
1221 type => 'boolean',
1222 description => "Method needs special privileges - only pvedaemon can execute it",
1223 optional => 1,
1224 },
1225 download => {
1226 type => 'boolean',
1227 description => "Method downloads the file content (filename is the return value of the method).",
1228 optional => 1,
1229 },
1230 proxyto => {
1231 type => 'string',
1232 description => "A parameter name. If specified, all calls to this method are proxied to the host contained in that parameter.",
1233 optional => 1,
1234 },
1235 proxyto_callback => {
1236 type => 'coderef',
1237 description => "A function which is called to resolve the proxyto attribute. The default implementaion returns the value of the 'proxyto' parameter.",
1238 optional => 1,
1239 },
1240 permissions => {
1241 type => 'object',
1242 description => "Required access permissions. By default only 'root' is allowed to access this method.",
1243 optional => 1,
1244 additionalProperties => 0,
1245 properties => {
1246 description => {
1247 description => "Describe access permissions.",
1248 optional => 1,
1249 },
1250 user => {
1251 description => "A simply way to allow access for 'all' authenticated users. Value 'world' is used to allow access without credentials.",
1252 type => 'string',
1253 enum => ['all', 'world'],
1254 optional => 1,
1255 },
1256 check => {
1257 description => "Array of permission checks (prefix notation).",
1258 type => 'array',
1259 optional => 1
1260 },
1261 },
1262 },
1263 match_name => {
1264 description => "Used internally",
1265 optional => 1,
1266 },
1267 match_re => {
1268 description => "Used internally",
1269 optional => 1,
1270 },
1271 path => {
1272 type => 'string',
1273 description => "path for URL matching (uri template)",
1274 },
1275 fragmentDelimiter => {
1276 type => 'string',
1277 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.",
1278 optional => 1,
1279 },
1280 parameters => {
1281 type => 'object',
1282 description => "JSON Schema for parameters.",
1283 optional => 1,
1284 },
1285 returns => {
1286 type => 'object',
1287 description => "JSON Schema for return value.",
1288 optional => 1,
1289 },
1290 code => {
1291 type => 'coderef',
1292 description => "method implementaion (code reference)",
1293 optional => 1,
1294 },
1295 subclass => {
1296 type => 'string',
1297 description => "Delegate call to this class (perl class string).",
1298 optional => 1,
1299 requires => {
1300 additionalProperties => 0,
1301 properties => {
1302 subclass => {},
1303 path => {},
1304 match_name => {},
1305 match_re => {},
1306 fragmentDelimiter => { optional => 1 }
1307 }
1308 },
1309 },
1310 },
1311
1312 };
1313
1314 sub validate_schema {
1315 my ($schema) = @_;
1316
1317 my $errmsg = "internal error - unable to verify schema\n";
1318 validate($schema, $default_schema, $errmsg);
1319 }
1320
1321 sub validate_method_info {
1322 my $info = shift;
1323
1324 my $errmsg = "internal error - unable to verify method info\n";
1325 validate($info, $method_schema, $errmsg);
1326
1327 validate_schema($info->{parameters}) if $info->{parameters};
1328 validate_schema($info->{returns}) if $info->{returns};
1329 }
1330
1331 # run a self test on load
1332 # make sure we can verify the default schema
1333 validate_schema($default_schema_noref);
1334 validate_schema($method_schema);
1335
1336 # and now some utility methods (used by pve api)
1337 sub method_get_child_link {
1338 my ($info) = @_;
1339
1340 return undef if !$info;
1341
1342 my $schema = $info->{returns};
1343 return undef if !$schema || !$schema->{type} || $schema->{type} ne 'array';
1344
1345 my $links = $schema->{links};
1346 return undef if !$links;
1347
1348 my $found;
1349 foreach my $lnk (@$links) {
1350 if ($lnk->{href} && $lnk->{rel} && ($lnk->{rel} eq 'child')) {
1351 $found = $lnk;
1352 last;
1353 }
1354 }
1355
1356 return $found;
1357 }
1358
1359 # a way to parse command line parameters, using a
1360 # schema to configure Getopt::Long
1361 sub get_options {
1362 my ($schema, $args, $arg_param, $fixed_param, $param_mapping_hash) = @_;
1363
1364 if (!$schema || !$schema->{properties}) {
1365 raise("too many arguments\n", code => HTTP_BAD_REQUEST)
1366 if scalar(@$args) != 0;
1367 return {};
1368 }
1369
1370 my $list_param;
1371 if ($arg_param && !ref($arg_param)) {
1372 my $pd = $schema->{properties}->{$arg_param};
1373 die "expected list format $pd->{format}"
1374 if !($pd && $pd->{format} && $pd->{format} =~ m/-list/);
1375 $list_param = $arg_param;
1376 }
1377
1378 my @interactive = ();
1379 my @getopt = ();
1380 foreach my $prop (keys %{$schema->{properties}}) {
1381 my $pd = $schema->{properties}->{$prop};
1382 next if $list_param && $prop eq $list_param;
1383 next if defined($fixed_param->{$prop});
1384
1385 my $mapping = $param_mapping_hash->{$prop};
1386 if ($mapping && $mapping->{interactive}) {
1387 # interactive parameters such as passwords: make the argument
1388 # optional and call the mapping function afterwards.
1389 push @getopt, "$prop:s";
1390 push @interactive, [$prop, $mapping->{func}];
1391 } elsif ($pd->{type} eq 'boolean') {
1392 push @getopt, "$prop:s";
1393 } else {
1394 if ($pd->{format} && $pd->{format} =~ m/-a?list/) {
1395 push @getopt, "$prop=s@";
1396 } else {
1397 push @getopt, "$prop=s";
1398 }
1399 }
1400 }
1401
1402 Getopt::Long::Configure('prefix_pattern=(--|-)');
1403
1404 my $opts = {};
1405 raise("unable to parse option\n", code => HTTP_BAD_REQUEST)
1406 if !Getopt::Long::GetOptionsFromArray($args, $opts, @getopt);
1407
1408 if (@$args) {
1409 if ($list_param) {
1410 $opts->{$list_param} = $args;
1411 $args = [];
1412 } elsif (ref($arg_param)) {
1413 foreach my $arg_name (@$arg_param) {
1414 if ($opts->{'extra-args'}) {
1415 raise("internal error: extra-args must be the last argument\n", code => HTTP_BAD_REQUEST);
1416 }
1417 if ($arg_name eq 'extra-args') {
1418 $opts->{'extra-args'} = $args;
1419 $args = [];
1420 next;
1421 }
1422 raise("not enough arguments\n", code => HTTP_BAD_REQUEST) if !@$args;
1423 $opts->{$arg_name} = shift @$args;
1424 }
1425 raise("too many arguments\n", code => HTTP_BAD_REQUEST) if @$args;
1426 } else {
1427 raise("too many arguments\n", code => HTTP_BAD_REQUEST)
1428 if scalar(@$args) != 0;
1429 }
1430 } else {
1431 if (ref($arg_param)) {
1432 foreach my $arg_name (@$arg_param) {
1433 if ($arg_name eq 'extra-args') {
1434 $opts->{'extra-args'} = [];
1435 } else {
1436 raise("not enough arguments\n", code => HTTP_BAD_REQUEST);
1437 }
1438 }
1439 }
1440 }
1441
1442 foreach my $entry (@interactive) {
1443 my ($opt, $func) = @$entry;
1444 my $pd = $schema->{properties}->{$opt};
1445 my $value = $opts->{$opt};
1446 if (defined($value) || !$pd->{optional}) {
1447 $opts->{$opt} = $func->($value);
1448 }
1449 }
1450
1451 # decode after Getopt as we are not sure how well it handles unicode
1452 foreach my $p (keys %$opts) {
1453 if (!ref($opts->{$p})) {
1454 $opts->{$p} = decode('locale', $opts->{$p});
1455 } elsif (ref($opts->{$p}) eq 'ARRAY') {
1456 my $tmp = [];
1457 foreach my $v (@{$opts->{$p}}) {
1458 push @$tmp, decode('locale', $v);
1459 }
1460 $opts->{$p} = $tmp;
1461 } elsif (ref($opts->{$p}) eq 'SCALAR') {
1462 $opts->{$p} = decode('locale', $$opts->{$p});
1463 } else {
1464 raise("decoding options failed, unknown reference\n", code => HTTP_BAD_REQUEST);
1465 }
1466 }
1467
1468 foreach my $p (keys %$opts) {
1469 if (my $pd = $schema->{properties}->{$p}) {
1470 if ($pd->{type} eq 'boolean') {
1471 if ($opts->{$p} eq '') {
1472 $opts->{$p} = 1;
1473 } elsif (defined(my $bool = parse_boolean($opts->{$p}))) {
1474 $opts->{$p} = $bool;
1475 } else {
1476 raise("unable to parse boolean option\n", code => HTTP_BAD_REQUEST);
1477 }
1478 } elsif ($pd->{format}) {
1479
1480 if ($pd->{format} =~ m/-list/) {
1481 # allow --vmid 100 --vmid 101 and --vmid 100,101
1482 # allow --dow mon --dow fri and --dow mon,fri
1483 $opts->{$p} = join(",", @{$opts->{$p}}) if ref($opts->{$p}) eq 'ARRAY';
1484 } elsif ($pd->{format} =~ m/-alist/) {
1485 # we encode array as \0 separated strings
1486 # Note: CGI.pm also use this encoding
1487 if (scalar(@{$opts->{$p}}) != 1) {
1488 $opts->{$p} = join("\0", @{$opts->{$p}});
1489 } else {
1490 # st that split_list knows it is \0 terminated
1491 my $v = $opts->{$p}->[0];
1492 $opts->{$p} = "$v\0";
1493 }
1494 }
1495 }
1496 }
1497 }
1498
1499 foreach my $p (keys %$fixed_param) {
1500 $opts->{$p} = $fixed_param->{$p};
1501 }
1502
1503 return $opts;
1504 }
1505
1506 # A way to parse configuration data by giving a json schema
1507 sub parse_config {
1508 my ($schema, $filename, $raw) = @_;
1509
1510 # do fast check (avoid validate_schema($schema))
1511 die "got strange schema" if !$schema->{type} ||
1512 !$schema->{properties} || $schema->{type} ne 'object';
1513
1514 my $cfg = {};
1515
1516 while ($raw =~ /^\s*(.+?)\s*$/gm) {
1517 my $line = $1;
1518
1519 next if $line =~ /^#/;
1520
1521 if ($line =~ m/^(\S+?):\s*(.*)$/) {
1522 my $key = $1;
1523 my $value = $2;
1524 if ($schema->{properties}->{$key} &&
1525 $schema->{properties}->{$key}->{type} eq 'boolean') {
1526
1527 $value = parse_boolean($value) // $value;
1528 }
1529 $cfg->{$key} = $value;
1530 } else {
1531 warn "ignore config line: $line\n"
1532 }
1533 }
1534
1535 my $errors = {};
1536 check_prop($cfg, $schema, '', $errors);
1537
1538 foreach my $k (keys %$errors) {
1539 warn "parse error in '$filename' - '$k': $errors->{$k}\n";
1540 delete $cfg->{$k};
1541 }
1542
1543 return $cfg;
1544 }
1545
1546 # generate simple key/value file
1547 sub dump_config {
1548 my ($schema, $filename, $cfg) = @_;
1549
1550 # do fast check (avoid validate_schema($schema))
1551 die "got strange schema" if !$schema->{type} ||
1552 !$schema->{properties} || $schema->{type} ne 'object';
1553
1554 validate($cfg, $schema, "validation error in '$filename'\n");
1555
1556 my $data = '';
1557
1558 foreach my $k (keys %$cfg) {
1559 $data .= "$k: $cfg->{$k}\n";
1560 }
1561
1562 return $data;
1563 }
1564
1565 # helpers used to generate our manual pages
1566
1567 my $find_schema_default_key = sub {
1568 my ($format) = @_;
1569
1570 my $default_key;
1571 my $keyAliasProps = {};
1572
1573 foreach my $key (keys %$format) {
1574 my $phash = $format->{$key};
1575 if ($phash->{default_key}) {
1576 die "multiple default keys in schema ($default_key, $key)\n"
1577 if defined($default_key);
1578 die "default key '$key' is an alias - this is not allowed\n"
1579 if defined($phash->{alias});
1580 die "default key '$key' with keyAlias attribute is not allowed\n"
1581 if $phash->{keyAlias};
1582 $default_key = $key;
1583 }
1584 my $key_alias = $phash->{keyAlias};
1585 die "found keyAlias without 'alias definition for '$key'\n"
1586 if $key_alias && !$phash->{alias};
1587
1588 if ($phash->{alias} && $key_alias) {
1589 die "inconsistent keyAlias '$key_alias' definition"
1590 if defined($keyAliasProps->{$key_alias}) &&
1591 $keyAliasProps->{$key_alias} ne $phash->{alias};
1592 $keyAliasProps->{$key_alias} = $phash->{alias};
1593 }
1594 }
1595
1596 return wantarray ? ($default_key, $keyAliasProps) : $default_key;
1597 };
1598
1599 sub generate_typetext {
1600 my ($format, $list_enums) = @_;
1601
1602 my ($default_key, $keyAliasProps) = &$find_schema_default_key($format);
1603
1604 my $res = '';
1605 my $add_sep = 0;
1606
1607 my $add_option_string = sub {
1608 my ($text, $optional) = @_;
1609
1610 if ($add_sep) {
1611 $text = ",$text";
1612 $res .= ' ';
1613 }
1614 $text = "[$text]" if $optional;
1615 $res .= $text;
1616 $add_sep = 1;
1617 };
1618
1619 my $format_key_value = sub {
1620 my ($key, $phash) = @_;
1621
1622 die "internal error" if defined($phash->{alias});
1623
1624 my $keytext = $key;
1625
1626 my $typetext = '';
1627
1628 if (my $desc = $phash->{format_description}) {
1629 $typetext .= "<$desc>";
1630 } elsif (my $text = $phash->{typetext}) {
1631 $typetext .= $text;
1632 } elsif (my $enum = $phash->{enum}) {
1633 if ($list_enums || (scalar(@$enum) <= 3)) {
1634 $typetext .= '<' . join('|', @$enum) . '>';
1635 } else {
1636 $typetext .= '<enum>';
1637 }
1638 } elsif ($phash->{type} eq 'boolean') {
1639 $typetext .= '<1|0>';
1640 } elsif ($phash->{type} eq 'integer') {
1641 $typetext .= '<integer>';
1642 } elsif ($phash->{type} eq 'number') {
1643 $typetext .= '<number>';
1644 } else {
1645 die "internal error: neither format_description nor typetext found for option '$key'";
1646 }
1647
1648 if (defined($default_key) && ($default_key eq $key)) {
1649 &$add_option_string("[$keytext=]$typetext", $phash->{optional});
1650 } else {
1651 &$add_option_string("$keytext=$typetext", $phash->{optional});
1652 }
1653 };
1654
1655 my $done = {};
1656
1657 my $cond_add_key = sub {
1658 my ($key) = @_;
1659
1660 return if $done->{$key}; # avoid duplicates
1661
1662 $done->{$key} = 1;
1663
1664 my $phash = $format->{$key};
1665
1666 return if !$phash; # should not happen
1667
1668 return if $phash->{alias};
1669
1670 &$format_key_value($key, $phash);
1671
1672 };
1673
1674 &$cond_add_key($default_key) if defined($default_key);
1675
1676 # add required keys first
1677 foreach my $key (sort keys %$format) {
1678 my $phash = $format->{$key};
1679 &$cond_add_key($key) if $phash && !$phash->{optional};
1680 }
1681
1682 # add the rest
1683 foreach my $key (sort keys %$format) {
1684 &$cond_add_key($key);
1685 }
1686
1687 foreach my $keyAlias (sort keys %$keyAliasProps) {
1688 &$add_option_string("<$keyAlias>=<$keyAliasProps->{$keyAlias }>", 1);
1689 }
1690
1691 return $res;
1692 }
1693
1694 sub print_property_string {
1695 my ($data, $format, $skip, $path) = @_;
1696
1697 if (ref($format) ne 'HASH') {
1698 my $schema = get_format($format);
1699 die "not a valid format: $format\n" if !$schema;
1700 $format = $schema;
1701 }
1702
1703 my $errors = {};
1704 check_object($path, $format, $data, undef, $errors);
1705 if (scalar(%$errors)) {
1706 raise "format error", errors => $errors;
1707 }
1708
1709 my ($default_key, $keyAliasProps) = &$find_schema_default_key($format);
1710
1711 my $res = '';
1712 my $add_sep = 0;
1713
1714 my $add_option_string = sub {
1715 my ($text) = @_;
1716
1717 $res .= ',' if $add_sep;
1718 $res .= $text;
1719 $add_sep = 1;
1720 };
1721
1722 my $format_value = sub {
1723 my ($key, $value, $format) = @_;
1724
1725 if (defined($format) && ($format eq 'disk-size')) {
1726 return format_size($value);
1727 } else {
1728 die "illegal value with commas for $key\n" if $value =~ /,/;
1729 return $value;
1730 }
1731 };
1732
1733 my $done = { map { $_ => 1 } @$skip };
1734
1735 my $cond_add_key = sub {
1736 my ($key, $isdefault) = @_;
1737
1738 return if $done->{$key}; # avoid duplicates
1739
1740 $done->{$key} = 1;
1741
1742 my $value = $data->{$key};
1743
1744 return if !defined($value);
1745
1746 my $phash = $format->{$key};
1747
1748 # try to combine values if we have key aliases
1749 if (my $combine = $keyAliasProps->{$key}) {
1750 if (defined(my $combine_value = $data->{$combine})) {
1751 my $combine_format = $format->{$combine}->{format};
1752 my $value_str = &$format_value($key, $value, $phash->{format});
1753 my $combine_str = &$format_value($combine, $combine_value, $combine_format);
1754 &$add_option_string("${value_str}=${combine_str}");
1755 $done->{$combine} = 1;
1756 return;
1757 }
1758 }
1759
1760 if ($phash && $phash->{alias}) {
1761 $phash = $format->{$phash->{alias}};
1762 }
1763
1764 die "invalid key '$key'\n" if !$phash;
1765 die "internal error" if defined($phash->{alias});
1766
1767 my $value_str = &$format_value($key, $value, $phash->{format});
1768 if ($isdefault) {
1769 &$add_option_string($value_str);
1770 } else {
1771 &$add_option_string("$key=${value_str}");
1772 }
1773 };
1774
1775 # add default key first
1776 &$cond_add_key($default_key, 1) if defined($default_key);
1777
1778 # add required keys first
1779 foreach my $key (sort keys %$data) {
1780 my $phash = $format->{$key};
1781 &$cond_add_key($key) if $phash && !$phash->{optional};
1782 }
1783
1784 # add the rest
1785 foreach my $key (sort keys %$data) {
1786 &$cond_add_key($key);
1787 }
1788
1789 return $res;
1790 }
1791
1792 sub schema_get_type_text {
1793 my ($phash, $style) = @_;
1794
1795 my $type = $phash->{type} || 'string';
1796
1797 if ($phash->{typetext}) {
1798 return $phash->{typetext};
1799 } elsif ($phash->{format_description}) {
1800 return "<$phash->{format_description}>";
1801 } elsif ($phash->{enum}) {
1802 return "<" . join(' | ', sort @{$phash->{enum}}) . ">";
1803 } elsif ($phash->{pattern}) {
1804 return $phash->{pattern};
1805 } elsif ($type eq 'integer' || $type eq 'number') {
1806 # NOTE: always access values as number (avoid converion to string)
1807 if (defined($phash->{minimum}) && defined($phash->{maximum})) {
1808 return "<$type> (" . ($phash->{minimum} + 0) . " - " .
1809 ($phash->{maximum} + 0) . ")";
1810 } elsif (defined($phash->{minimum})) {
1811 return "<$type> (" . ($phash->{minimum} + 0) . " - N)";
1812 } elsif (defined($phash->{maximum})) {
1813 return "<$type> (-N - " . ($phash->{maximum} + 0) . ")";
1814 }
1815 } elsif ($type eq 'string') {
1816 if (my $format = $phash->{format}) {
1817 $format = get_format($format) if ref($format) ne 'HASH';
1818 if (ref($format) eq 'HASH') {
1819 my $list_enums = 0;
1820 $list_enums = 1 if $style && $style eq 'config-sub';
1821 return generate_typetext($format, $list_enums);
1822 }
1823 }
1824 }
1825
1826 return "<$type>";
1827 }
1828
1829 1;