]> git.proxmox.com Git - pve-common.git/blob - src/PVE/JSONSchema.pm
cli: print_text_table: allow to limit output width
[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 requires => {
1085 type => [ "string", "object" ],
1086 optional => 1,
1087 description => "indicates a required property or a schema that must be validated if this property is present",
1088 },
1089 format => {
1090 type => [ "string", "object" ],
1091 optional => 1,
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 },
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 },
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 },
1104 keyAlias => {
1105 type => 'string',
1106 optional => 1,
1107 description => "Allows to store the current 'key' as value of another property. Only valid if used together with 'alias'.",
1108 requires => 'alias',
1109 },
1110 default => {
1111 type => "any",
1112 optional => 1,
1113 description => "This indicates the default for the instance property."
1114 },
1115 completion => {
1116 type => 'coderef',
1117 description => "Bash completion function. This function should return a list of possible values.",
1118 optional => 1,
1119 },
1120 disallow => {
1121 type => "object",
1122 optional => 1,
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.",
1124 },
1125 extends => {
1126 type => "object",
1127 optional => 1,
1128 description => "This indicates the schema extends the given schema. All instances of this schema must be valid to by the extended schema also.",
1129 default => {},
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,
1136 items => {
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 },
1149 method => {
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",
1154 },
1155 },
1156 },
1157 },
1158 print_width => {
1159 type => "integer",
1160 description => "For CLI context, this defines the maximal width to print before truncating",
1161 optional => 1,
1162 },
1163 }
1164 };
1165
1166 my $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
1183 my $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 },
1220 download => {
1221 type => 'boolean',
1222 description => "Method downloads the file content (filename is the return value of the method).",
1223 optional => 1,
1224 },
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 },
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 },
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 => {
1241 description => {
1242 description => "Describe access permissions.",
1243 optional => 1,
1244 },
1245 user => {
1246 description => "A simply way to allow access for 'all' authenticated users. Value 'world' is used to allow access without credentials.",
1247 type => 'string',
1248 enum => ['all', 'world'],
1249 optional => 1,
1250 },
1251 check => {
1252 description => "Array of permission checks (prefix notation).",
1253 type => 'array',
1254 optional => 1
1255 },
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
1309 sub validate_schema {
1310 my ($schema) = @_;
1311
1312 my $errmsg = "internal error - unable to verify schema\n";
1313 validate($schema, $default_schema, $errmsg);
1314 }
1315
1316 sub 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
1328 validate_schema($default_schema_noref);
1329 validate_schema($method_schema);
1330
1331 # and now some utility methods (used by pve api)
1332 sub 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
1356 sub get_options {
1357 my ($schema, $args, $arg_param, $fixed_param, $param_mapping_hash) = @_;
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
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
1373 my @interactive = ();
1374 my @getopt = ();
1375 foreach my $prop (keys %{$schema->{properties}}) {
1376 my $pd = $schema->{properties}->{$prop};
1377 next if $list_param && $prop eq $list_param;
1378 next if defined($fixed_param->{$prop});
1379
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}];
1386 } elsif ($pd->{type} eq 'boolean') {
1387 push @getopt, "$prop:s";
1388 } else {
1389 if ($pd->{format} && $pd->{format} =~ m/-a?list/) {
1390 push @getopt, "$prop=s@";
1391 } else {
1392 push @getopt, "$prop=s";
1393 }
1394 }
1395 }
1396
1397 Getopt::Long::Configure('prefix_pattern=(--|-)');
1398
1399 my $opts = {};
1400 raise("unable to parse option\n", code => HTTP_BAD_REQUEST)
1401 if !Getopt::Long::GetOptionsFromArray($args, $opts, @getopt);
1402
1403 if (@$args) {
1404 if ($list_param) {
1405 $opts->{$list_param} = $args;
1406 $args = [];
1407 } elsif (ref($arg_param)) {
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;
1419 }
1420 raise("too many arguments\n", code => HTTP_BAD_REQUEST) if @$args;
1421 } else {
1422 raise("too many arguments\n", code => HTTP_BAD_REQUEST)
1423 if scalar(@$args) != 0;
1424 }
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 }
1435 }
1436
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
1446 # decode after Getopt as we are not sure how well it handles unicode
1447 foreach my $p (keys %$opts) {
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 }
1461 }
1462
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;
1468 } elsif (defined(my $bool = parse_boolean($opts->{$p}))) {
1469 $opts->{$p} = $bool;
1470 } else {
1471 raise("unable to parse boolean option\n", code => HTTP_BAD_REQUEST);
1472 }
1473 } elsif ($pd->{format}) {
1474
1475 if ($pd->{format} =~ m/-list/) {
1476 # allow --vmid 100 --vmid 101 and --vmid 100,101
1477 # allow --dow mon --dow fri and --dow mon,fri
1478 $opts->{$p} = join(",", @{$opts->{$p}}) if ref($opts->{$p}) eq 'ARRAY';
1479 } elsif ($pd->{format} =~ m/-alist/) {
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 }
1490 }
1491 }
1492 }
1493
1494 foreach my $p (keys %$fixed_param) {
1495 $opts->{$p} = $fixed_param->{$p};
1496 }
1497
1498 return $opts;
1499 }
1500
1501 # A way to parse configuration data by giving a json schema
1502 sub 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
1511 while ($raw =~ /^\s*(.+?)\s*$/gm) {
1512 my $line = $1;
1513
1514 next if $line =~ /^#/;
1515
1516 if ($line =~ m/^(\S+?):\s*(.*)$/) {
1517 my $key = $1;
1518 my $value = $2;
1519 if ($schema->{properties}->{$key} &&
1520 $schema->{properties}->{$key}->{type} eq 'boolean') {
1521
1522 $value = parse_boolean($value) // $value;
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
1542 sub 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
1560 # helpers used to generate our manual pages
1561
1562 my $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};
1577 $default_key = $key;
1578 }
1579 my $key_alias = $phash->{keyAlias};
1580 die "found keyAlias without 'alias definition for '$key'\n"
1581 if $key_alias && !$phash->{alias};
1582
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
1594 sub generate_typetext {
1595 my ($format, $list_enums) = @_;
1596
1597 my ($default_key, $keyAliasProps) = &$find_schema_default_key($format);
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}) {
1628 if ($list_enums || (scalar(@$enum) <= 3)) {
1629 $typetext .= '<' . join('|', @$enum) . '>';
1630 } else {
1631 $typetext .= '<enum>';
1632 }
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
1650 my $done = {};
1651
1652 my $cond_add_key = sub {
1653 my ($key) = @_;
1654
1655 return if $done->{$key}; # avoid duplicates
1656
1657 $done->{$key} = 1;
1658
1659 my $phash = $format->{$key};
1660
1661 return if !$phash; # should not happen
1662
1663 return if $phash->{alias};
1664
1665 &$format_key_value($key, $phash);
1666
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);
1684 }
1685
1686 return $res;
1687 }
1688
1689 sub 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
1728 my $done = { map { $_ => 1 } @$skip };
1729
1730 my $cond_add_key = sub {
1731 my ($key, $isdefault) = @_;
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});
1763 if ($isdefault) {
1764 &$add_option_string($value_str);
1765 } else {
1766 &$add_option_string("$key=${value_str}");
1767 }
1768 };
1769
1770 # add default key first
1771 &$cond_add_key($default_key, 1) if defined($default_key);
1772
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
1780 foreach my $key (sort keys %$data) {
1781 &$cond_add_key($key);
1782 }
1783
1784 return $res;
1785 }
1786
1787 sub schema_get_type_text {
1788 my ($phash, $style) = @_;
1789
1790 my $type = $phash->{type} || 'string';
1791
1792 if ($phash->{typetext}) {
1793 return $phash->{typetext};
1794 } elsif ($phash->{format_description}) {
1795 return "<$phash->{format_description}>";
1796 } elsif ($phash->{enum}) {
1797 return "<" . join(' | ', sort @{$phash->{enum}}) . ">";
1798 } elsif ($phash->{pattern}) {
1799 return $phash->{pattern};
1800 } elsif ($type eq 'integer' || $type eq 'number') {
1801 # NOTE: always access values as number (avoid converion to string)
1802 if (defined($phash->{minimum}) && defined($phash->{maximum})) {
1803 return "<$type> (" . ($phash->{minimum} + 0) . " - " .
1804 ($phash->{maximum} + 0) . ")";
1805 } elsif (defined($phash->{minimum})) {
1806 return "<$type> (" . ($phash->{minimum} + 0) . " - N)";
1807 } elsif (defined($phash->{maximum})) {
1808 return "<$type> (-N - " . ($phash->{maximum} + 0) . ")";
1809 }
1810 } elsif ($type eq 'string') {
1811 if (my $format = $phash->{format}) {
1812 $format = get_format($format) if ref($format) ne 'HASH';
1813 if (ref($format) eq 'HASH') {
1814 my $list_enums = 0;
1815 $list_enums = 1 if $style && $style eq 'config-sub';
1816 return generate_typetext($format, $list_enums);
1817 }
1818 }
1819 }
1820
1821 return "<$type>";
1822 }
1823
1824 1;