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