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