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