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