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