]> git.proxmox.com Git - pve-common.git/blame - src/PVE/JSONSchema.pm
bump version to 4.0-78
[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;
88a490ff
WB
648 } elsif ($type eq 'string' && $vt eq 'Regexp') {
649 # qr// regexes can be used as strings and make sense for format=regex
650 return 1;
e143e9d8
DM
651 } else {
652 if ($vt) {
653 add_error($errors, $path, "type check ('$type') failed - got $vt");
654 return undef;
655 } else {
656 if ($type eq 'string') {
657 return 1; # nothing to check ?
658 } elsif ($type eq 'boolean') {
659 #if ($value =~ m/^(1|true|yes|on)$/i) {
660 if ($value eq '1') {
661 return 1;
662 #} elsif ($value =~ m/^(0|false|no|off)$/i) {
663 } elsif ($value eq '0') {
664 return 0;
665 } else {
666 add_error($errors, $path, "type check ('$type') failed - got '$value'");
667 return undef;
668 }
669 } elsif ($type eq 'integer') {
670 if (!is_integer($value)) {
671 add_error($errors, $path, "type check ('$type') failed - got '$value'");
672 return undef;
673 }
674 return 1;
675 } elsif ($type eq 'number') {
676 if (!is_number($value)) {
677 add_error($errors, $path, "type check ('$type') failed - got '$value'");
678 return undef;
679 }
680 return 1;
681 } else {
682 return 1; # no need to verify unknown types
683 }
684 }
685 }
686 }
687
688 return undef;
689}
690
691sub check_object {
692 my ($path, $schema, $value, $additional_properties, $errors) = @_;
693
694 # print "Check Object " . Dumper($value) . "\nSchema: " . Dumper($schema);
695
696 my $st = ref($schema);
697 if (!$st || $st ne 'HASH') {
698 add_error($errors, $path, "Invalid schema definition.");
699 return;
700 }
701
702 my $vt = ref($value);
703 if (!$vt || $vt ne 'HASH') {
704 add_error($errors, $path, "an object is required");
705 return;
706 }
707
708 foreach my $k (keys %$schema) {
bf27456b 709 check_prop($value->{$k}, $schema->{$k}, $path ? "$path.$k" : $k, $errors);
e143e9d8
DM
710 }
711
712 foreach my $k (keys %$value) {
713
714 my $newpath = $path ? "$path.$k" : $k;
715
716 if (my $subschema = $schema->{$k}) {
717 if (my $requires = $subschema->{requires}) {
718 if (ref($requires)) {
719 #print "TEST: " . Dumper($value) . "\n", Dumper($requires) ;
720 check_prop($value, $requires, $path, $errors);
721 } elsif (!defined($value->{$requires})) {
722 add_error($errors, $path ? "$path.$requires" : $requires,
723 "missing property - '$newpath' requiers this property");
724 }
725 }
726
727 next; # value is already checked above
728 }
729
730 if (defined ($additional_properties) && !$additional_properties) {
731 add_error($errors, $newpath, "property is not defined in schema " .
732 "and the schema does not allow additional properties");
733 next;
734 }
735 check_prop($value->{$k}, $additional_properties, $newpath, $errors)
736 if ref($additional_properties);
737 }
738}
739
86425a09
WB
740sub check_object_warn {
741 my ($path, $schema, $value, $additional_properties) = @_;
742 my $errors = {};
743 check_object($path, $schema, $value, $additional_properties, $errors);
744 if (scalar(%$errors)) {
745 foreach my $k (keys %$errors) {
746 warn "parse error: $k: $errors->{$k}\n";
747 }
748 return 0;
749 }
750 return 1;
751}
752
e143e9d8
DM
753sub check_prop {
754 my ($value, $schema, $path, $errors) = @_;
755
756 die "internal error - no schema" if !$schema;
757 die "internal error" if !$errors;
758
759 #print "check_prop $path\n" if $value;
760
761 my $st = ref($schema);
762 if (!$st || $st ne 'HASH') {
763 add_error($errors, $path, "Invalid schema definition.");
764 return;
765 }
766
767 # if it extends another schema, it must pass that schema as well
768 if($schema->{extends}) {
769 check_prop($value, $schema->{extends}, $path, $errors);
770 }
771
772 if (!defined ($value)) {
773 return if $schema->{type} && $schema->{type} eq 'null';
445e8267 774 if (!$schema->{optional} && !$schema->{alias} && !$schema->{group}) {
e143e9d8
DM
775 add_error($errors, $path, "property is missing and it is not optional");
776 }
777 return;
778 }
779
780 return if !check_type($path, $schema->{type}, $value, $errors);
781
782 if ($schema->{disallow}) {
783 my $tmperr = {};
784 if (check_type($path, $schema->{disallow}, $value, $tmperr)) {
785 add_error($errors, $path, "disallowed value was matched");
786 return;
787 }
788 }
789
790 if (my $vt = ref($value)) {
791
792 if ($vt eq 'ARRAY') {
793 if ($schema->{items}) {
794 my $it = ref($schema->{items});
795 if ($it && $it eq 'ARRAY') {
796 #die "implement me $path: $vt " . Dumper($schema) ."\n". Dumper($value);
797 die "not implemented";
798 } else {
799 my $ind = 0;
800 foreach my $el (@$value) {
801 check_prop($el, $schema->{items}, "${path}[$ind]", $errors);
802 $ind++;
803 }
804 }
805 }
806 return;
807 } elsif ($schema->{properties} || $schema->{additionalProperties}) {
808 check_object($path, defined($schema->{properties}) ? $schema->{properties} : {},
809 $value, $schema->{additionalProperties}, $errors);
810 return;
811 }
812
813 } else {
814
815 if (my $format = $schema->{format}) {
2f9e609a 816 eval { check_format($format, $value, $path); };
e143e9d8
DM
817 if ($@) {
818 add_error($errors, $path, "invalid format - $@");
819 return;
820 }
821 }
822
823 if (my $pattern = $schema->{pattern}) {
824 if ($value !~ m/^$pattern$/) {
825 add_error($errors, $path, "value does not match the regex pattern");
826 return;
827 }
828 }
829
830 if (defined (my $max = $schema->{maxLength})) {
831 if (length($value) > $max) {
832 add_error($errors, $path, "value may only be $max characters long");
833 return;
834 }
835 }
836
837 if (defined (my $min = $schema->{minLength})) {
838 if (length($value) < $min) {
839 add_error($errors, $path, "value must be at least $min characters long");
840 return;
841 }
842 }
843
844 if (is_number($value)) {
845 if (defined (my $max = $schema->{maximum})) {
846 if ($value > $max) {
847 add_error($errors, $path, "value must have a maximum value of $max");
848 return;
849 }
850 }
851
852 if (defined (my $min = $schema->{minimum})) {
853 if ($value < $min) {
854 add_error($errors, $path, "value must have a minimum value of $min");
855 return;
856 }
857 }
858 }
859
860 if (my $ea = $schema->{enum}) {
861
862 my $found;
863 foreach my $ev (@$ea) {
864 if ($ev eq $value) {
865 $found = 1;
866 last;
867 }
868 }
869 if (!$found) {
870 add_error($errors, $path, "value '$value' does not have a value in the enumeration '" .
871 join(", ", @$ea) . "'");
872 }
873 }
874 }
875}
876
877sub validate {
878 my ($instance, $schema, $errmsg) = @_;
879
880 my $errors = {};
881 $errmsg = "Parameter verification failed.\n" if !$errmsg;
882
883 # todo: cycle detection is only needed for debugging, I guess
884 # we can disable that in the final release
885 # todo: is there a better/faster way to detect cycles?
886 my $cycles = 0;
887 find_cycle($instance, sub { $cycles = 1 });
888 if ($cycles) {
889 add_error($errors, undef, "data structure contains recursive cycles");
890 } elsif ($schema) {
891 check_prop($instance, $schema, '', $errors);
892 }
893
894 if (scalar(%$errors)) {
895 raise $errmsg, code => HTTP_BAD_REQUEST, errors => $errors;
896 }
897
898 return 1;
899}
900
901my $schema_valid_types = ["string", "object", "coderef", "array", "boolean", "number", "integer", "null", "any"];
902my $default_schema_noref = {
903 description => "This is the JSON Schema for JSON Schemas.",
904 type => [ "object" ],
905 additionalProperties => 0,
906 properties => {
907 type => {
908 type => ["string", "array"],
909 description => "This is a type definition value. This can be a simple type, or a union type",
910 optional => 1,
911 default => "any",
912 items => {
913 type => "string",
914 enum => $schema_valid_types,
915 },
916 enum => $schema_valid_types,
917 },
918 optional => {
919 type => "boolean",
920 description => "This indicates that the instance property in the instance object is not required.",
921 optional => 1,
922 default => 0
923 },
924 properties => {
925 type => "object",
926 description => "This is a definition for the properties of an object value",
927 optional => 1,
928 default => {},
929 },
930 items => {
931 type => "object",
932 description => "When the value is an array, this indicates the schema to use to validate each item in an array",
933 optional => 1,
934 default => {},
935 },
936 additionalProperties => {
937 type => [ "boolean", "object"],
938 description => "This provides a default property definition for all properties that are not explicitly defined in an object type definition.",
939 optional => 1,
940 default => {},
941 },
942 minimum => {
943 type => "number",
944 optional => 1,
945 description => "This indicates the minimum value for the instance property when the type of the instance value is a number.",
946 },
947 maximum => {
948 type => "number",
949 optional => 1,
950 description => "This indicates the maximum value for the instance property when the type of the instance value is a number.",
951 },
952 minLength => {
953 type => "integer",
954 description => "When the instance value is a string, this indicates minimum length of the string",
955 optional => 1,
956 minimum => 0,
957 default => 0,
958 },
959 maxLength => {
960 type => "integer",
961 description => "When the instance value is a string, this indicates maximum length of the string.",
962 optional => 1,
963 },
964 typetext => {
965 type => "string",
966 optional => 1,
967 description => "A text representation of the type (used to generate documentation).",
968 },
969 pattern => {
970 type => "string",
971 format => "regex",
166e27c7 972 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
973 optional => 1,
974 default => ".*",
166e27c7 975 },
e143e9d8
DM
976 enum => {
977 type => "array",
978 optional => 1,
979 description => "This provides an enumeration of possible values that are valid for the instance property.",
980 },
981 description => {
982 type => "string",
983 optional => 1,
984 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).",
985 },
32f8e0c7
DM
986 verbose_description => {
987 type => "string",
988 optional => 1,
989 description => "This provides a more verbose description.",
990 },
d5d10f85
WB
991 format_description => {
992 type => "string",
993 optional => 1,
994 description => "This provides a shorter (usually just one word) description for a property used to generate descriptions for comma separated list property strings.",
995 },
166e27c7
WB
996 title => {
997 type => "string",
e143e9d8 998 optional => 1,
166e27c7
WB
999 description => "This provides the title of the property",
1000 },
1001 requires => {
1002 type => [ "string", "object" ],
e143e9d8 1003 optional => 1,
166e27c7
WB
1004 description => "indicates a required property or a schema that must be validated if this property is present",
1005 },
1006 format => {
2f9e609a 1007 type => [ "string", "object" ],
e143e9d8 1008 optional => 1,
166e27c7
WB
1009 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",
1010 },
095b88fd
WB
1011 default_key => {
1012 type => "boolean",
1013 optional => 1,
1014 description => "Whether this is the default key in a comma separated list property string.",
1015 },
303a9b34
WB
1016 alias => {
1017 type => 'string',
1018 optional => 1,
1019 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.",
1020 },
bf27456b 1021 keyAlias => {
445e8267
WB
1022 type => 'string',
1023 optional => 1,
bf27456b
DM
1024 description => "Allows to store the current 'key' as value of another property. Only valid if used together with 'alias'.",
1025 requires => 'alias',
445e8267 1026 },
e143e9d8
DM
1027 default => {
1028 type => "any",
1029 optional => 1,
1030 description => "This indicates the default for the instance property."
1031 },
166e27c7 1032 completion => {
7829989f
DM
1033 type => 'coderef',
1034 description => "Bash completion function. This function should return a list of possible values.",
1035 optional => 1,
166e27c7
WB
1036 },
1037 disallow => {
1038 type => "object",
e143e9d8 1039 optional => 1,
166e27c7 1040 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 1041 },
166e27c7
WB
1042 extends => {
1043 type => "object",
e143e9d8 1044 optional => 1,
166e27c7 1045 description => "This indicates the schema extends the given schema. All instances of this schema must be valid to by the extended schema also.",
e143e9d8 1046 default => {},
166e27c7
WB
1047 },
1048 # this is from hyper schema
1049 links => {
1050 type => "array",
1051 description => "This defines the link relations of the instance objects",
1052 optional => 1,
e143e9d8 1053 items => {
166e27c7
WB
1054 type => "object",
1055 properties => {
1056 href => {
1057 type => "string",
1058 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",
1059 },
1060 rel => {
1061 type => "string",
1062 description => "This is the name of the link relation",
1063 optional => 1,
1064 default => "full",
1065 },
e143e9d8 1066 method => {
166e27c7
WB
1067 type => "string",
1068 description => "For submission links, this defines the method that should be used to access the target resource",
1069 optional => 1,
1070 default => "GET",
e143e9d8
DM
1071 },
1072 },
1073 },
1074 },
1075 }
1076};
1077
1078my $default_schema = Storable::dclone($default_schema_noref);
1079
1080$default_schema->{properties}->{properties}->{additionalProperties} = $default_schema;
1081$default_schema->{properties}->{additionalProperties}->{properties} = $default_schema->{properties};
1082
1083$default_schema->{properties}->{items}->{properties} = $default_schema->{properties};
1084$default_schema->{properties}->{items}->{additionalProperties} = 0;
1085
1086$default_schema->{properties}->{disallow}->{properties} = $default_schema->{properties};
1087$default_schema->{properties}->{disallow}->{additionalProperties} = 0;
1088
1089$default_schema->{properties}->{requires}->{properties} = $default_schema->{properties};
1090$default_schema->{properties}->{requires}->{additionalProperties} = 0;
1091
1092$default_schema->{properties}->{extends}->{properties} = $default_schema->{properties};
1093$default_schema->{properties}->{extends}->{additionalProperties} = 0;
1094
1095my $method_schema = {
1096 type => "object",
1097 additionalProperties => 0,
1098 properties => {
1099 description => {
1100 description => "This a description of the method",
1101 optional => 1,
1102 },
1103 name => {
1104 type => 'string',
1105 description => "This indicates the name of the function to call.",
1106 optional => 1,
1107 requires => {
1108 additionalProperties => 1,
1109 properties => {
1110 name => {},
1111 description => {},
1112 code => {},
1113 method => {},
1114 parameters => {},
1115 path => {},
1116 parameters => {},
1117 returns => {},
1118 }
1119 },
1120 },
1121 method => {
1122 type => 'string',
1123 description => "The HTTP method name.",
1124 enum => [ 'GET', 'POST', 'PUT', 'DELETE' ],
1125 optional => 1,
1126 },
1127 protected => {
1128 type => 'boolean',
1129 description => "Method needs special privileges - only pvedaemon can execute it",
1130 optional => 1,
1131 },
1132 proxyto => {
1133 type => 'string',
1134 description => "A parameter name. If specified, all calls to this method are proxied to the host contained in that parameter.",
1135 optional => 1,
1136 },
1137 permissions => {
1138 type => 'object',
1139 description => "Required access permissions. By default only 'root' is allowed to access this method.",
1140 optional => 1,
1141 additionalProperties => 0,
1142 properties => {
b18d1722
DM
1143 description => {
1144 description => "Describe access permissions.",
1145 optional => 1,
1146 },
e143e9d8 1147 user => {
b18d1722 1148 description => "A simply way to allow access for 'all' authenticated users. Value 'world' is used to allow access without credentials.",
e143e9d8 1149 type => 'string',
b18d1722 1150 enum => ['all', 'world'],
e143e9d8
DM
1151 optional => 1,
1152 },
b18d1722
DM
1153 check => {
1154 description => "Array of permission checks (prefix notation).",
1155 type => 'array',
1156 optional => 1
1157 },
e143e9d8
DM
1158 },
1159 },
1160 match_name => {
1161 description => "Used internally",
1162 optional => 1,
1163 },
1164 match_re => {
1165 description => "Used internally",
1166 optional => 1,
1167 },
1168 path => {
1169 type => 'string',
1170 description => "path for URL matching (uri template)",
1171 },
1172 fragmentDelimiter => {
1173 type => 'string',
1174 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.",
1175 optional => 1,
1176 },
1177 parameters => {
1178 type => 'object',
1179 description => "JSON Schema for parameters.",
1180 optional => 1,
1181 },
638edfd4
DM
1182 formatter => {
1183 type => 'object',
1184 description => "Used to store page formatter information (set by PVE::RESTHandler->register_page_formatter).",
1185 optional => 1,
1186 },
e143e9d8
DM
1187 returns => {
1188 type => 'object',
1189 description => "JSON Schema for return value.",
1190 optional => 1,
1191 },
1192 code => {
1193 type => 'coderef',
1194 description => "method implementaion (code reference)",
1195 optional => 1,
1196 },
1197 subclass => {
1198 type => 'string',
1199 description => "Delegate call to this class (perl class string).",
1200 optional => 1,
1201 requires => {
1202 additionalProperties => 0,
1203 properties => {
1204 subclass => {},
1205 path => {},
1206 match_name => {},
1207 match_re => {},
1208 fragmentDelimiter => { optional => 1 }
1209 }
1210 },
1211 },
1212 },
1213
1214};
1215
1216sub validate_schema {
1217 my ($schema) = @_;
1218
1219 my $errmsg = "internal error - unable to verify schema\n";
1220 validate($schema, $default_schema, $errmsg);
1221}
1222
1223sub validate_method_info {
1224 my $info = shift;
1225
1226 my $errmsg = "internal error - unable to verify method info\n";
1227 validate($info, $method_schema, $errmsg);
1228
1229 validate_schema($info->{parameters}) if $info->{parameters};
1230 validate_schema($info->{returns}) if $info->{returns};
1231}
1232
1233# run a self test on load
1234# make sure we can verify the default schema
1235validate_schema($default_schema_noref);
1236validate_schema($method_schema);
1237
1238# and now some utility methods (used by pve api)
1239sub method_get_child_link {
1240 my ($info) = @_;
1241
1242 return undef if !$info;
1243
1244 my $schema = $info->{returns};
1245 return undef if !$schema || !$schema->{type} || $schema->{type} ne 'array';
1246
1247 my $links = $schema->{links};
1248 return undef if !$links;
1249
1250 my $found;
1251 foreach my $lnk (@$links) {
1252 if ($lnk->{href} && $lnk->{rel} && ($lnk->{rel} eq 'child')) {
1253 $found = $lnk;
1254 last;
1255 }
1256 }
1257
1258 return $found;
1259}
1260
1261# a way to parse command line parameters, using a
1262# schema to configure Getopt::Long
1263sub get_options {
0ce82909 1264 my ($schema, $args, $arg_param, $fixed_param, $pwcallback) = @_;
e143e9d8
DM
1265
1266 if (!$schema || !$schema->{properties}) {
1267 raise("too many arguments\n", code => HTTP_BAD_REQUEST)
1268 if scalar(@$args) != 0;
1269 return {};
1270 }
1271
0ce82909
DM
1272 my $list_param;
1273 if ($arg_param && !ref($arg_param)) {
1274 my $pd = $schema->{properties}->{$arg_param};
1275 die "expected list format $pd->{format}"
1276 if !($pd && $pd->{format} && $pd->{format} =~ m/-list/);
1277 $list_param = $arg_param;
1278 }
1279
e143e9d8
DM
1280 my @getopt = ();
1281 foreach my $prop (keys %{$schema->{properties}}) {
1282 my $pd = $schema->{properties}->{$prop};
aab47b58 1283 next if $list_param && $prop eq $list_param;
0ce82909 1284 next if defined($fixed_param->{$prop});
e143e9d8
DM
1285
1286 if ($prop eq 'password' && $pwcallback) {
1287 # we do not accept plain password on input line, instead
1288 # we turn this into a boolean option and ask for password below
1289 # using $pwcallback() (for security reasons).
1290 push @getopt, "$prop";
1291 } elsif ($pd->{type} eq 'boolean') {
1292 push @getopt, "$prop:s";
1293 } else {
23dc9401 1294 if ($pd->{format} && $pd->{format} =~ m/-a?list/) {
8ba7c72b
DM
1295 push @getopt, "$prop=s@";
1296 } else {
1297 push @getopt, "$prop=s";
1298 }
e143e9d8
DM
1299 }
1300 }
1301
1068aeb3
WB
1302 Getopt::Long::Configure('prefix_pattern=(--|-)');
1303
e143e9d8
DM
1304 my $opts = {};
1305 raise("unable to parse option\n", code => HTTP_BAD_REQUEST)
1306 if !Getopt::Long::GetOptionsFromArray($args, $opts, @getopt);
1d21344c 1307
5851be88 1308 if (@$args) {
0ce82909
DM
1309 if ($list_param) {
1310 $opts->{$list_param} = $args;
1311 $args = [];
1312 } elsif (ref($arg_param)) {
5851be88
WB
1313 foreach my $arg_name (@$arg_param) {
1314 if ($opts->{'extra-args'}) {
1315 raise("internal error: extra-args must be the last argument\n", code => HTTP_BAD_REQUEST);
1316 }
1317 if ($arg_name eq 'extra-args') {
1318 $opts->{'extra-args'} = $args;
1319 $args = [];
1320 next;
1321 }
1322 raise("not enough arguments\n", code => HTTP_BAD_REQUEST) if !@$args;
1323 $opts->{$arg_name} = shift @$args;
0ce82909 1324 }
5851be88 1325 raise("too many arguments\n", code => HTTP_BAD_REQUEST) if @$args;
0ce82909
DM
1326 } else {
1327 raise("too many arguments\n", code => HTTP_BAD_REQUEST)
1328 if scalar(@$args) != 0;
1329 }
1d21344c
DM
1330 }
1331
e143e9d8
DM
1332 if (my $pd = $schema->{properties}->{password}) {
1333 if ($pd->{type} ne 'boolean' && $pwcallback) {
1334 if ($opts->{password} || !$pd->{optional}) {
1335 $opts->{password} = &$pwcallback();
1336 }
1337 }
1338 }
815b2aba
DM
1339
1340 $opts = PVE::Tools::decode_utf8_parameters($opts);
815b2aba 1341
e143e9d8
DM
1342 foreach my $p (keys %$opts) {
1343 if (my $pd = $schema->{properties}->{$p}) {
1344 if ($pd->{type} eq 'boolean') {
1345 if ($opts->{$p} eq '') {
1346 $opts->{$p} = 1;
1347 } elsif ($opts->{$p} =~ m/^(1|true|yes|on)$/i) {
1348 $opts->{$p} = 1;
1349 } elsif ($opts->{$p} =~ m/^(0|false|no|off)$/i) {
1350 $opts->{$p} = 0;
1351 } else {
1352 raise("unable to parse boolean option\n", code => HTTP_BAD_REQUEST);
1353 }
23dc9401 1354 } elsif ($pd->{format}) {
8ba7c72b 1355
23dc9401 1356 if ($pd->{format} =~ m/-list/) {
8ba7c72b 1357 # allow --vmid 100 --vmid 101 and --vmid 100,101
23dc9401 1358 # allow --dow mon --dow fri and --dow mon,fri
43479146 1359 $opts->{$p} = join(",", @{$opts->{$p}}) if ref($opts->{$p}) eq 'ARRAY';
23dc9401 1360 } elsif ($pd->{format} =~ m/-alist/) {
8ba7c72b
DM
1361 # we encode array as \0 separated strings
1362 # Note: CGI.pm also use this encoding
1363 if (scalar(@{$opts->{$p}}) != 1) {
1364 $opts->{$p} = join("\0", @{$opts->{$p}});
1365 } else {
1366 # st that split_list knows it is \0 terminated
1367 my $v = $opts->{$p}->[0];
1368 $opts->{$p} = "$v\0";
1369 }
1370 }
e143e9d8
DM
1371 }
1372 }
1373 }
1374
0ce82909
DM
1375 foreach my $p (keys %$fixed_param) {
1376 $opts->{$p} = $fixed_param->{$p};
e143e9d8
DM
1377 }
1378
1379 return $opts;
1380}
1381
1382# A way to parse configuration data by giving a json schema
1383sub parse_config {
1384 my ($schema, $filename, $raw) = @_;
1385
1386 # do fast check (avoid validate_schema($schema))
1387 die "got strange schema" if !$schema->{type} ||
1388 !$schema->{properties} || $schema->{type} ne 'object';
1389
1390 my $cfg = {};
1391
3c4d612a 1392 while ($raw =~ /^\s*(.+?)\s*$/gm) {
e143e9d8 1393 my $line = $1;
e143e9d8 1394
3c4d612a
WB
1395 next if $line =~ /^#/;
1396
1397 if ($line =~ m/^(\S+?):\s*(.*)$/) {
e143e9d8
DM
1398 my $key = $1;
1399 my $value = $2;
1400 if ($schema->{properties}->{$key} &&
1401 $schema->{properties}->{$key}->{type} eq 'boolean') {
1402
1403 $value = 1 if $value =~ m/^(1|on|yes|true)$/i;
1404 $value = 0 if $value =~ m/^(0|off|no|false)$/i;
1405 }
1406 $cfg->{$key} = $value;
1407 } else {
1408 warn "ignore config line: $line\n"
1409 }
1410 }
1411
1412 my $errors = {};
1413 check_prop($cfg, $schema, '', $errors);
1414
1415 foreach my $k (keys %$errors) {
1416 warn "parse error in '$filename' - '$k': $errors->{$k}\n";
1417 delete $cfg->{$k};
1418 }
1419
1420 return $cfg;
1421}
1422
1423# generate simple key/value file
1424sub dump_config {
1425 my ($schema, $filename, $cfg) = @_;
1426
1427 # do fast check (avoid validate_schema($schema))
1428 die "got strange schema" if !$schema->{type} ||
1429 !$schema->{properties} || $schema->{type} ne 'object';
1430
1431 validate($cfg, $schema, "validation error in '$filename'\n");
1432
1433 my $data = '';
1434
1435 foreach my $k (keys %$cfg) {
1436 $data .= "$k: $cfg->{$k}\n";
1437 }
1438
1439 return $data;
1440}
1441
bf27456b
DM
1442# helpers used to generate our manual pages
1443
1444my $find_schema_default_key = sub {
1445 my ($format) = @_;
1446
1447 my $default_key;
1448 my $keyAliasProps = {};
1449
1450 foreach my $key (keys %$format) {
1451 my $phash = $format->{$key};
1452 if ($phash->{default_key}) {
1453 die "multiple default keys in schema ($default_key, $key)\n"
1454 if defined($default_key);
1455 die "default key '$key' is an alias - this is not allowed\n"
1456 if defined($phash->{alias});
1457 die "default key '$key' with keyAlias attribute is not allowed\n"
1458 if $phash->{keyAlias};
bf27456b
DM
1459 $default_key = $key;
1460 }
1461 my $key_alias = $phash->{keyAlias};
c88c582d
DM
1462 die "found keyAlias without 'alias definition for '$key'\n"
1463 if $key_alias && !$phash->{alias};
1464
bf27456b
DM
1465 if ($phash->{alias} && $key_alias) {
1466 die "inconsistent keyAlias '$key_alias' definition"
1467 if defined($keyAliasProps->{$key_alias}) &&
1468 $keyAliasProps->{$key_alias} ne $phash->{alias};
1469 $keyAliasProps->{$key_alias} = $phash->{alias};
1470 }
1471 }
1472
1473 return wantarray ? ($default_key, $keyAliasProps) : $default_key;
1474};
1475
1476sub generate_typetext {
1477 my ($format) = @_;
1478
d8c2b947 1479 my ($default_key, $keyAliasProps) = &$find_schema_default_key($format);
bf27456b
DM
1480
1481 my $res = '';
1482 my $add_sep = 0;
1483
1484 my $add_option_string = sub {
1485 my ($text, $optional) = @_;
1486
1487 if ($add_sep) {
1488 $text = ",$text";
1489 $res .= ' ';
1490 }
1491 $text = "[$text]" if $optional;
1492 $res .= $text;
1493 $add_sep = 1;
1494 };
1495
1496 my $format_key_value = sub {
1497 my ($key, $phash) = @_;
1498
1499 die "internal error" if defined($phash->{alias});
1500
1501 my $keytext = $key;
1502
1503 my $typetext = '';
1504
1505 if (my $desc = $phash->{format_description}) {
1506 $typetext .= "<$desc>";
1507 } elsif (my $text = $phash->{typetext}) {
1508 $typetext .= $text;
1509 } elsif (my $enum = $phash->{enum}) {
1510 $typetext .= '<' . join('|', @$enum) . '>';
1511 } elsif ($phash->{type} eq 'boolean') {
1512 $typetext .= '<1|0>';
1513 } elsif ($phash->{type} eq 'integer') {
1514 $typetext .= '<integer>';
1515 } elsif ($phash->{type} eq 'number') {
1516 $typetext .= '<number>';
1517 } else {
1518 die "internal error: neither format_description nor typetext found for option '$key'";
1519 }
1520
1521 if (defined($default_key) && ($default_key eq $key)) {
1522 &$add_option_string("[$keytext=]$typetext", $phash->{optional});
1523 } else {
1524 &$add_option_string("$keytext=$typetext", $phash->{optional});
1525 }
1526 };
1527
d8c2b947 1528 my $done = {};
bf27456b 1529
d8c2b947
DM
1530 my $cond_add_key = sub {
1531 my ($key) = @_;
1532
1533 return if $done->{$key}; # avoid duplicates
1534
1535 $done->{$key} = 1;
bf27456b
DM
1536
1537 my $phash = $format->{$key};
1538
d8c2b947
DM
1539 return if !$phash; # should not happen
1540
1541 return if $phash->{alias};
bf27456b
DM
1542
1543 &$format_key_value($key, $phash);
1544
d8c2b947
DM
1545 };
1546
1547 &$cond_add_key($default_key) if defined($default_key);
1548
1549 # add required keys first
1550 foreach my $key (sort keys %$format) {
1551 my $phash = $format->{$key};
1552 &$cond_add_key($key) if $phash && !$phash->{optional};
1553 }
1554
1555 # add the rest
1556 foreach my $key (sort keys %$format) {
1557 &$cond_add_key($key);
1558 }
1559
1560 foreach my $keyAlias (sort keys %$keyAliasProps) {
1561 &$add_option_string("<$keyAlias>=<$keyAliasProps->{$keyAlias }>", 1);
bf27456b
DM
1562 }
1563
1564 return $res;
1565}
1566
1567sub print_property_string {
1568 my ($data, $format, $skip, $path) = @_;
1569
1570 if (ref($format) ne 'HASH') {
1571 my $schema = get_format($format);
1572 die "not a valid format: $format\n" if !$schema;
1573 $format = $schema;
1574 }
1575
1576 my $errors = {};
1577 check_object($path, $format, $data, undef, $errors);
1578 if (scalar(%$errors)) {
1579 raise "format error", errors => $errors;
1580 }
1581
1582 my ($default_key, $keyAliasProps) = &$find_schema_default_key($format);
1583
1584 my $res = '';
1585 my $add_sep = 0;
1586
1587 my $add_option_string = sub {
1588 my ($text) = @_;
1589
1590 $res .= ',' if $add_sep;
1591 $res .= $text;
1592 $add_sep = 1;
1593 };
1594
1595 my $format_value = sub {
1596 my ($key, $value, $format) = @_;
1597
1598 if (defined($format) && ($format eq 'disk-size')) {
1599 return format_size($value);
1600 } else {
1601 die "illegal value with commas for $key\n" if $value =~ /,/;
1602 return $value;
1603 }
1604 };
1605
2289890b 1606 my $done = { map { $_ => 1 } @$skip };
bf27456b
DM
1607
1608 my $cond_add_key = sub {
971353e8 1609 my ($key, $isdefault) = @_;
bf27456b
DM
1610
1611 return if $done->{$key}; # avoid duplicates
1612
1613 $done->{$key} = 1;
1614
1615 my $value = $data->{$key};
1616
1617 return if !defined($value);
1618
1619 my $phash = $format->{$key};
1620
1621 # try to combine values if we have key aliases
1622 if (my $combine = $keyAliasProps->{$key}) {
1623 if (defined(my $combine_value = $data->{$combine})) {
1624 my $combine_format = $format->{$combine}->{format};
1625 my $value_str = &$format_value($key, $value, $phash->{format});
1626 my $combine_str = &$format_value($combine, $combine_value, $combine_format);
1627 &$add_option_string("${value_str}=${combine_str}");
1628 $done->{$combine} = 1;
1629 return;
1630 }
1631 }
1632
1633 if ($phash && $phash->{alias}) {
1634 $phash = $format->{$phash->{alias}};
1635 }
1636
1637 die "invalid key '$key'\n" if !$phash;
1638 die "internal error" if defined($phash->{alias});
1639
1640 my $value_str = &$format_value($key, $value, $phash->{format});
971353e8
WB
1641 if ($isdefault) {
1642 &$add_option_string($value_str);
1643 } else {
1644 &$add_option_string("$key=${value_str}");
1645 }
bf27456b
DM
1646 };
1647
1648 # add default key first
971353e8 1649 &$cond_add_key($default_key, 1) if defined($default_key);
bf27456b 1650
d8c2b947
DM
1651 # add required keys first
1652 foreach my $key (sort keys %$data) {
1653 my $phash = $format->{$key};
1654 &$cond_add_key($key) if $phash && !$phash->{optional};
1655 }
1656
1657 # add the rest
bf27456b
DM
1658 foreach my $key (sort keys %$data) {
1659 &$cond_add_key($key);
1660 }
1661
1662 return $res;
1663}
1664
1665sub schema_get_type_text {
1666 my ($phash) = @_;
1667
32f8e0c7
DM
1668 my $type = $phash->{type} || 'string';
1669
bf27456b
DM
1670 if ($phash->{typetext}) {
1671 return $phash->{typetext};
1672 } elsif ($phash->{format_description}) {
1673 return "<$phash->{format_description}>";
1674 } elsif ($phash->{enum}) {
1675 return "(" . join(' | ', sort @{$phash->{enum}}) . ")";
1676 } elsif ($phash->{pattern}) {
1677 return $phash->{pattern};
32f8e0c7 1678 } elsif ($type eq 'integer' || $type eq 'number') {
05185ea2 1679 # NOTE: always access values as number (avoid converion to string)
bf27456b 1680 if (defined($phash->{minimum}) && defined($phash->{maximum})) {
b54ad320 1681 return "$type (" . ($phash->{minimum} + 0) . " - " .
05185ea2 1682 ($phash->{maximum} + 0) . ")";
bf27456b 1683 } elsif (defined($phash->{minimum})) {
b54ad320 1684 return "$type (" . ($phash->{minimum} + 0) . " - N)";
bf27456b 1685 } elsif (defined($phash->{maximum})) {
05185ea2 1686 return "$type (-N - " . ($phash->{maximum} + 0) . ")";
bf27456b 1687 }
32f8e0c7 1688 } elsif ($type eq 'string') {
bf27456b
DM
1689 if (my $format = $phash->{format}) {
1690 $format = get_format($format) if ref($format) ne 'HASH';
1691 if (ref($format) eq 'HASH') {
1692 return generate_typetext($format);
1693 }
1694 }
1695 }
1696
bf27456b
DM
1697 return $type;
1698}
1699
e143e9d8 17001;