]> git.proxmox.com Git - pve-common.git/blame - src/PVE/JSONSchema.pm
tools: optional prefix for random_ether_addr
[pve-common.git] / src / PVE / JSONSchema.pm
CommitLineData
e143e9d8
DM
1package PVE::JSONSchema;
2
e143e9d8 3use strict;
c36f332e 4use warnings;
e143e9d8
DM
5use Storable; # for dclone
6use Getopt::Long;
7use Devel::Cycle -quiet; # todo: remove?
e272bcb7 8use PVE::Tools qw(split_list $IPV6RE $IPV4RE);
e143e9d8
DM
9use PVE::Exception qw(raise);
10use HTTP::Status qw(:constants);
23b56245 11use Net::IP qw(:PROC);
bf27456b 12use Data::Dumper;
e143e9d8
DM
13
14use base 'Exporter';
15
16our @EXPORT_OK = qw(
17register_standard_option
18get_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
27my $standard_options = {};
28sub 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
37sub get_standard_option {
38 my ($name, $base) = @_;
39
40 my $std = $standard_options->{$name};
3432ae0c 41 die "no such standard option '$name'\n" if !$std;
e143e9d8
DM
42
43 my $res = $base || {};
44
45 foreach my $opt (keys %$std) {
c38ac70f 46 next if defined($res->{$opt});
e143e9d8
DM
47 $res->{$opt} = $std->{$opt};
48 }
49
50 return $res;
51};
52
53register_standard_option('pve-vmid', {
54 description => "The (unique) ID of the VM.",
55 type => 'integer', format => 'pve-vmid',
56 minimum => 1
57});
58
59register_standard_option('pve-node', {
60 description => "The cluster node name.",
61 type => 'string', format => 'pve-node',
62});
63
64register_standard_option('pve-node-list', {
65 description => "List of cluster node names.",
66 type => 'string', format => 'pve-node-list',
67});
68
69register_standard_option('pve-iface', {
70 description => "Network interface name.",
71 type => 'string', format => 'pve-iface',
72 minLength => 2, maxLength => 20,
73});
74
28a2669d 75register_standard_option('pve-storage-id', {
05e787c5
DM
76 description => "The storage identifier.",
77 type => 'string', format => 'pve-storage-id',
78});
79
28a2669d 80register_standard_option('pve-config-digest', {
dc5eae7d
DM
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
28a2669d 87register_standard_option('extra-args', {
5851be88
WB
88 description => "Extra arguments as array",
89 type => 'array',
90 items => { type => 'string' },
91 optional => 1
92});
93
e143e9d8
DM
94my $format_list = {};
95
96sub 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
2421fba1
WB
105sub get_format {
106 my ($format) = @_;
107 return $format_list->{$format};
108}
109
e143e9d8 110# register some common type for pve
8ba7c72b
DM
111
112register_format('string', sub {}); # allow format => 'string-list'
113
c77b4c96
WB
114register_format('urlencoded', \&pve_verify_urlencoded);
115sub 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
e143e9d8
DM
124register_format('pve-configid', \&pve_verify_configid);
125sub pve_verify_configid {
126 my ($id, $noerr) = @_;
127
128 if ($id !~ m/^[a-z][a-z0-9_]+$/i) {
129 return undef if $noerr;
39ed3462 130 die "invalid configuration ID '$id'\n";
e143e9d8
DM
131 }
132 return $id;
133}
134
05e787c5
DM
135PVE::JSONSchema::register_format('pve-storage-id', \&parse_storage_id);
136sub 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
e143e9d8
DM
147register_format('pve-vmid', \&pve_verify_vmid);
148sub pve_verify_vmid {
149 my ($vmid, $noerr) = @_;
150
50ae94c9 151 if ($vmid !~ m/^[1-9][0-9]{2,8}$/) {
e143e9d8
DM
152 return undef if $noerr;
153 die "value does not look like a valid VM ID\n";
154 }
155 return $vmid;
156}
157
158register_format('pve-node', \&pve_verify_node_name);
159sub pve_verify_node_name {
160 my ($node, $noerr) = @_;
161
e6db55c0 162 if ($node !~ m/^([a-zA-Z0-9]([a-zA-Z0-9\-]*[a-zA-Z0-9])?)$/) {
e143e9d8
DM
163 return undef if $noerr;
164 die "value does not look like a valid node name\n";
165 }
166 return $node;
167}
168
169register_format('ipv4', \&pve_verify_ipv4);
170sub pve_verify_ipv4 {
171 my ($ipv4, $noerr) = @_;
172
ed5880ac
DM
173 if ($ipv4 !~ m/^(?:$IPV4RE)$/) {
174 return undef if $noerr;
175 die "value does not look like a valid IPv4 address\n";
e143e9d8
DM
176 }
177 return $ipv4;
178}
a13c6f08 179
ed5880ac 180register_format('ipv6', \&pve_verify_ipv6);
93276209 181sub pve_verify_ipv6 {
ed5880ac
DM
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
191register_format('ip', \&pve_verify_ip);
192sub 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
a13c6f08
DM
202my $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,
e43faad9
WB
232 '255.255.255.252' => 30,
233 '255.255.255.254' => 31,
234 '255.255.255.255' => 32,
a13c6f08
DM
235};
236
e143e9d8
DM
237register_format('ipv4mask', \&pve_verify_ipv4mask);
238sub pve_verify_ipv4mask {
239 my ($mask, $noerr) = @_;
240
a13c6f08 241 if (!defined($ipv4_mask_hash->{$mask})) {
e143e9d8
DM
242 return undef if $noerr;
243 die "value does not look like a valid IP netmask\n";
244 }
245 return $mask;
246}
247
703c1f88
WB
248register_format('CIDRv6', \&pve_verify_cidrv6);
249sub pve_verify_cidrv6 {
e272bcb7
DM
250 my ($cidr, $noerr) = @_;
251
703c1f88 252 if ($cidr =~ m!^(?:$IPV6RE)(?:/(\d+))$! && ($1 > 7) && ($1 <= 120)) {
e272bcb7 253 return $cidr;
703c1f88
WB
254 }
255
256 return undef if $noerr;
257 die "value does not look like a valid IPv6 CIDR network\n";
258}
259
260register_format('CIDRv4', \&pve_verify_cidrv4);
261sub pve_verify_cidrv4 {
262 my ($cidr, $noerr) = @_;
263
0526cc2d 264 if ($cidr =~ m!^(?:$IPV4RE)(?:/(\d+))$! && ($1 > 7) && ($1 <= 32)) {
e272bcb7
DM
265 return $cidr;
266 }
267
268 return undef if $noerr;
703c1f88
WB
269 die "value does not look like a valid IPv4 CIDR network\n";
270}
271
272register_format('CIDR', \&pve_verify_cidr);
273sub 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
286register_format('pve-ipv4-config', \&pve_verify_ipv4_config);
287sub 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
296register_format('pve-ipv6-config', \&pve_verify_ipv6_config);
297sub 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";
e272bcb7
DM
304}
305
e143e9d8
DM
306register_format('email', \&pve_verify_email);
307sub pve_verify_email {
308 my ($email, $noerr) = @_;
309
87cb0e60
EK
310 # we use same regex as in Utils.js
311 if ($email !~ /^(\w+)([\-+.][\w]+)*@(\w[\-\w]*\.){1,5}([A-Za-z]){2,63}$/) {
e143e9d8
DM
312 return undef if $noerr;
313 die "value does not look like a valid email address\n";
314 }
315 return $email;
316}
317
34ebb226
DM
318register_format('dns-name', \&pve_verify_dns_name);
319sub pve_verify_dns_name {
320 my ($name, $noerr) = @_;
321
ce33e978 322 my $namere = "([a-zA-Z0-9]([a-zA-Z0-9\-]*[a-zA-Z0-9])?)";
34ebb226
DM
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
e143e9d8
DM
331# network interface name
332register_format('pve-iface', \&pve_verify_iface);
333sub 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
d07b7084
WB
343# general addresses by name or IP
344register_format('address', \&pve_verify_address);
345sub 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
b944a22a
WB
357register_format('disk-size', \&pve_verify_disk_size);
358sub 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
f0a10afc
DM
367register_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).",
d07b7084 369 type => 'string', format => 'address',
f0a10afc
DM
370});
371
372register_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
c70c3bbc 384register_format('pve-startup-order', \&pve_verify_startup_order);
b0edd8e6
DM
385sub 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
395sub 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
419PVE::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
e143e9d8 426sub check_format {
2f9e609a 427 my ($format, $value, $path) = @_;
e143e9d8 428
2f9e609a 429 return parse_property_string($format, $value, $path) if ref($format) eq 'HASH';
e143e9d8
DM
430 return if $format eq 'regex';
431
23dc9401 432 if ($format =~ m/^(.*)-a?list$/) {
e143e9d8
DM
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
2f9e609a 459 return parse_property_string($code, $value, $path) if ref($code) eq 'HASH';
e143e9d8
DM
460 &$code($value);
461 }
462}
463
878fea8e
WB
464sub 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
483sub 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
095b88fd 503sub parse_property_string {
d1e490c1
WB
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);
095b88fd
WB
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);
2d468b1a 517 die "duplicate key in comma-separated list property: $k\n" if defined($res->{$k});
095b88fd 518 my $schema = $format->{$k};
303a9b34 519 if (my $alias = $schema->{alias}) {
bf27456b
DM
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 }
303a9b34
WB
524 $k = $alias;
525 $schema = $format->{$k};
526 }
bf27456b 527
2d468b1a 528 die "invalid key in comma-separated list property: $k\n" if !$schema;
095b88fd
WB
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 !~ /=/) {
2d468b1a 535 die "duplicate key in comma-separated list property: $default_key\n" if $default_key;
095b88fd
WB
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 }
2d468b1a 543 die "duplicate key in comma-separated list property: $default_key\n";
095b88fd
WB
544 }
545 }
f0ba41a1 546 die "value without key, but schema does not define a default key\n" if !$default_key;
095b88fd 547 } else {
2d468b1a 548 die "missing key in comma-separated list property\n";
095b88fd
WB
549 }
550 }
551
552 my $errors = {};
d1e490c1 553 check_object($path, $format, $res, $additional_properties, $errors);
095b88fd 554 if (scalar(%$errors)) {
2d468b1a 555 raise "format error\n", errors => $errors;
095b88fd
WB
556 }
557
558 return $res;
559}
560
e143e9d8
DM
561sub 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
573sub is_number {
574 my $value = shift;
575
576 # see 'man perlretut'
577 return $value =~ /^[+-]?(\d+\.\d+|\d+\.|\.\d+|\d+)([eE][+-]?\d+)?$/;
578}
579
580sub is_integer {
581 my $value = shift;
582
583 return $value =~ m/^[+-]?\d+$/;
584}
585
586sub 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
688sub 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) {
bf27456b 706 check_prop($value->{$k}, $schema->{$k}, $path ? "$path.$k" : $k, $errors);
e143e9d8
DM
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
86425a09
WB
737sub 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
e143e9d8
DM
750sub 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';
445e8267 771 if (!$schema->{optional} && !$schema->{alias} && !$schema->{group}) {
e143e9d8
DM
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}) {
2f9e609a 813 eval { check_format($format, $value, $path); };
e143e9d8
DM
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
874sub 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
898my $schema_valid_types = ["string", "object", "coderef", "array", "boolean", "number", "integer", "null", "any"];
899my $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",
166e27c7 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.",
e143e9d8
DM
970 optional => 1,
971 default => ".*",
166e27c7 972 },
e143e9d8
DM
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 },
32f8e0c7
DM
983 verbose_description => {
984 type => "string",
985 optional => 1,
986 description => "This provides a more verbose description.",
987 },
d5d10f85
WB
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 },
166e27c7
WB
993 title => {
994 type => "string",
e143e9d8 995 optional => 1,
166e27c7
WB
996 description => "This provides the title of the property",
997 },
998 requires => {
999 type => [ "string", "object" ],
e143e9d8 1000 optional => 1,
166e27c7
WB
1001 description => "indicates a required property or a schema that must be validated if this property is present",
1002 },
1003 format => {
2f9e609a 1004 type => [ "string", "object" ],
e143e9d8 1005 optional => 1,
166e27c7
WB
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 },
095b88fd
WB
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 },
303a9b34
WB
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 },
bf27456b 1018 keyAlias => {
445e8267
WB
1019 type => 'string',
1020 optional => 1,
bf27456b
DM
1021 description => "Allows to store the current 'key' as value of another property. Only valid if used together with 'alias'.",
1022 requires => 'alias',
445e8267 1023 },
e143e9d8
DM
1024 default => {
1025 type => "any",
1026 optional => 1,
1027 description => "This indicates the default for the instance property."
1028 },
166e27c7 1029 completion => {
7829989f
DM
1030 type => 'coderef',
1031 description => "Bash completion function. This function should return a list of possible values.",
1032 optional => 1,
166e27c7
WB
1033 },
1034 disallow => {
1035 type => "object",
e143e9d8 1036 optional => 1,
166e27c7 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.",
e143e9d8 1038 },
166e27c7
WB
1039 extends => {
1040 type => "object",
e143e9d8 1041 optional => 1,
166e27c7 1042 description => "This indicates the schema extends the given schema. All instances of this schema must be valid to by the extended schema also.",
e143e9d8 1043 default => {},
166e27c7
WB
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,
e143e9d8 1050 items => {
166e27c7
WB
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 },
e143e9d8 1063 method => {
166e27c7
WB
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",
e143e9d8
DM
1068 },
1069 },
1070 },
1071 },
1072 }
1073};
1074
1075my $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
1092my $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 => {
b18d1722
DM
1140 description => {
1141 description => "Describe access permissions.",
1142 optional => 1,
1143 },
e143e9d8 1144 user => {
b18d1722 1145 description => "A simply way to allow access for 'all' authenticated users. Value 'world' is used to allow access without credentials.",
e143e9d8 1146 type => 'string',
b18d1722 1147 enum => ['all', 'world'],
e143e9d8
DM
1148 optional => 1,
1149 },
b18d1722
DM
1150 check => {
1151 description => "Array of permission checks (prefix notation).",
1152 type => 'array',
1153 optional => 1
1154 },
e143e9d8
DM
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 },
638edfd4
DM
1179 formatter => {
1180 type => 'object',
1181 description => "Used to store page formatter information (set by PVE::RESTHandler->register_page_formatter).",
1182 optional => 1,
1183 },
e143e9d8
DM
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
1213sub validate_schema {
1214 my ($schema) = @_;
1215
1216 my $errmsg = "internal error - unable to verify schema\n";
1217 validate($schema, $default_schema, $errmsg);
1218}
1219
1220sub 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
1232validate_schema($default_schema_noref);
1233validate_schema($method_schema);
1234
1235# and now some utility methods (used by pve api)
1236sub 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
1260sub get_options {
0ce82909 1261 my ($schema, $args, $arg_param, $fixed_param, $pwcallback) = @_;
e143e9d8
DM
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
0ce82909
DM
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
e143e9d8
DM
1277 my @getopt = ();
1278 foreach my $prop (keys %{$schema->{properties}}) {
1279 my $pd = $schema->{properties}->{$prop};
aab47b58 1280 next if $list_param && $prop eq $list_param;
0ce82909 1281 next if defined($fixed_param->{$prop});
e143e9d8
DM
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 {
23dc9401 1291 if ($pd->{format} && $pd->{format} =~ m/-a?list/) {
8ba7c72b
DM
1292 push @getopt, "$prop=s@";
1293 } else {
1294 push @getopt, "$prop=s";
1295 }
e143e9d8
DM
1296 }
1297 }
1298
1068aeb3
WB
1299 Getopt::Long::Configure('prefix_pattern=(--|-)');
1300
e143e9d8
DM
1301 my $opts = {};
1302 raise("unable to parse option\n", code => HTTP_BAD_REQUEST)
1303 if !Getopt::Long::GetOptionsFromArray($args, $opts, @getopt);
1d21344c 1304
5851be88 1305 if (@$args) {
0ce82909
DM
1306 if ($list_param) {
1307 $opts->{$list_param} = $args;
1308 $args = [];
1309 } elsif (ref($arg_param)) {
5851be88
WB
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;
0ce82909 1321 }
5851be88 1322 raise("too many arguments\n", code => HTTP_BAD_REQUEST) if @$args;
0ce82909
DM
1323 } else {
1324 raise("too many arguments\n", code => HTTP_BAD_REQUEST)
1325 if scalar(@$args) != 0;
1326 }
1d21344c
DM
1327 }
1328
e143e9d8
DM
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 }
815b2aba
DM
1336
1337 $opts = PVE::Tools::decode_utf8_parameters($opts);
815b2aba 1338
e143e9d8
DM
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 }
23dc9401 1351 } elsif ($pd->{format}) {
8ba7c72b 1352
23dc9401 1353 if ($pd->{format} =~ m/-list/) {
8ba7c72b 1354 # allow --vmid 100 --vmid 101 and --vmid 100,101
23dc9401 1355 # allow --dow mon --dow fri and --dow mon,fri
43479146 1356 $opts->{$p} = join(",", @{$opts->{$p}}) if ref($opts->{$p}) eq 'ARRAY';
23dc9401 1357 } elsif ($pd->{format} =~ m/-alist/) {
8ba7c72b
DM
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 }
e143e9d8
DM
1368 }
1369 }
1370 }
1371
0ce82909
DM
1372 foreach my $p (keys %$fixed_param) {
1373 $opts->{$p} = $fixed_param->{$p};
e143e9d8
DM
1374 }
1375
1376 return $opts;
1377}
1378
1379# A way to parse configuration data by giving a json schema
1380sub 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
3c4d612a 1389 while ($raw =~ /^\s*(.+?)\s*$/gm) {
e143e9d8 1390 my $line = $1;
e143e9d8 1391
3c4d612a
WB
1392 next if $line =~ /^#/;
1393
1394 if ($line =~ m/^(\S+?):\s*(.*)$/) {
e143e9d8
DM
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
1421sub 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
bf27456b
DM
1439# helpers used to generate our manual pages
1440
1441my $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};
bf27456b
DM
1456 $default_key = $key;
1457 }
1458 my $key_alias = $phash->{keyAlias};
c88c582d
DM
1459 die "found keyAlias without 'alias definition for '$key'\n"
1460 if $key_alias && !$phash->{alias};
1461
bf27456b
DM
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
1473sub generate_typetext {
1474 my ($format) = @_;
1475
d8c2b947 1476 my ($default_key, $keyAliasProps) = &$find_schema_default_key($format);
bf27456b
DM
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
d8c2b947 1525 my $done = {};
bf27456b 1526
d8c2b947
DM
1527 my $cond_add_key = sub {
1528 my ($key) = @_;
1529
1530 return if $done->{$key}; # avoid duplicates
1531
1532 $done->{$key} = 1;
bf27456b
DM
1533
1534 my $phash = $format->{$key};
1535
d8c2b947
DM
1536 return if !$phash; # should not happen
1537
1538 return if $phash->{alias};
bf27456b
DM
1539
1540 &$format_key_value($key, $phash);
1541
d8c2b947
DM
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);
bf27456b
DM
1559 }
1560
1561 return $res;
1562}
1563
1564sub 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
2289890b 1603 my $done = { map { $_ => 1 } @$skip };
bf27456b
DM
1604
1605 my $cond_add_key = sub {
971353e8 1606 my ($key, $isdefault) = @_;
bf27456b
DM
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});
971353e8
WB
1638 if ($isdefault) {
1639 &$add_option_string($value_str);
1640 } else {
1641 &$add_option_string("$key=${value_str}");
1642 }
bf27456b
DM
1643 };
1644
1645 # add default key first
971353e8 1646 &$cond_add_key($default_key, 1) if defined($default_key);
bf27456b 1647
d8c2b947
DM
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
bf27456b
DM
1655 foreach my $key (sort keys %$data) {
1656 &$cond_add_key($key);
1657 }
1658
1659 return $res;
1660}
1661
1662sub schema_get_type_text {
1663 my ($phash) = @_;
1664
32f8e0c7
DM
1665 my $type = $phash->{type} || 'string';
1666
bf27456b
DM
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};
32f8e0c7 1675 } elsif ($type eq 'integer' || $type eq 'number') {
bf27456b 1676 if (defined($phash->{minimum}) && defined($phash->{maximum})) {
32f8e0c7 1677 return "$type ($phash->{minimum} - $phash->{maximum})";
bf27456b 1678 } elsif (defined($phash->{minimum})) {
32f8e0c7 1679 return "$type ($phash->{minimum} - N)";
bf27456b 1680 } elsif (defined($phash->{maximum})) {
32f8e0c7 1681 return "$type (-N - $phash->{maximum})";
bf27456b 1682 }
32f8e0c7 1683 } elsif ($type eq 'string') {
bf27456b
DM
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
bf27456b
DM
1692 return $type;
1693}
1694
e143e9d8 16951;