]> git.proxmox.com Git - pve-common.git/blame - src/PVE/JSONSchema.pm
network tests: test #5141
[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);
bc24aad3 13use JSON;
23b56245 14use Net::IP qw(:PROC);
bf27456b 15use Data::Dumper;
e143e9d8
DM
16
17use base 'Exporter';
18
19our @EXPORT_OK = qw(
d3ef19c1 20register_standard_option
e143e9d8 21get_standard_option
79ac628b 22parse_property_string
d3ef19c1 23print_property_string
e143e9d8
DM
24);
25
a8f93334
DJ
26our $CONFIGID_RE = qr/[a-z][a-z0-9_-]+/i;
27
9bbc4e17 28# Note: This class implements something similar to JSON schema, but it is not 100% complete.
e143e9d8
DM
29# see: http://tools.ietf.org/html/draft-zyp-json-schema-02
30# see: http://json-schema.org/
31
32# the code is similar to the javascript parser from http://code.google.com/p/jsonschema/
33
34my $standard_options = {};
35sub register_standard_option {
36 my ($name, $schema) = @_;
37
9bbc4e17 38 die "standard option '$name' already registered\n"
e143e9d8
DM
39 if $standard_options->{$name};
40
41 $standard_options->{$name} = $schema;
42}
43
44sub get_standard_option {
45 my ($name, $base) = @_;
46
47 my $std = $standard_options->{$name};
3432ae0c 48 die "no such standard option '$name'\n" if !$std;
e143e9d8
DM
49
50 my $res = $base || {};
51
52 foreach my $opt (keys %$std) {
c38ac70f 53 next if defined($res->{$opt});
e143e9d8
DM
54 $res->{$opt} = $std->{$opt};
55 }
56
57 return $res;
58};
59
60register_standard_option('pve-vmid', {
61 description => "The (unique) ID of the VM.",
bca61737
FE
62 type => 'integer',
63 format => 'pve-vmid',
64 minimum => 100,
65 maximum => 999_999_999,
e143e9d8
DM
66});
67
68register_standard_option('pve-node', {
69 description => "The cluster node name.",
70 type => 'string', format => 'pve-node',
71});
72
73register_standard_option('pve-node-list', {
74 description => "List of cluster node names.",
75 type => 'string', format => 'pve-node-list',
76});
77
78register_standard_option('pve-iface', {
79 description => "Network interface name.",
80 type => 'string', format => 'pve-iface',
81 minLength => 2, maxLength => 20,
82});
83
28a2669d 84register_standard_option('pve-storage-id', {
05e787c5
DM
85 description => "The storage identifier.",
86 type => 'string', format => 'pve-storage-id',
9bbc4e17 87});
05e787c5 88
6e55ce7d
FG
89register_standard_option('pve-bridge-id', {
90 description => "Bridge to attach guest network devices to.",
91 type => 'string', format => 'pve-bridge-id',
92 format_description => 'bridge',
93});
94
28a2669d 95register_standard_option('pve-config-digest', {
13ae568f
LW
96 description => 'Prevent changes if current configuration file has a different digest. '
97 . 'This can be used to prevent concurrent modifications.',
dc5eae7d
DM
98 type => 'string',
99 optional => 1,
13ae568f
LW
100 # sha1 hex digests are 40 characters long
101 # sha256 hex digests are 64 characters long (sha256 is used in our Rust code)
102 maxLength => 64,
dc5eae7d
DM
103});
104
26bcdf92
WB
105register_standard_option('skiplock', {
106 description => "Ignore locks - only root is allowed to use this option.",
107 type => 'boolean',
108 optional => 1,
109});
110
28a2669d 111register_standard_option('extra-args', {
5851be88
WB
112 description => "Extra arguments as array",
113 type => 'array',
114 items => { type => 'string' },
115 optional => 1
116});
117
b21cf575
TL
118register_standard_option('fingerprint-sha256', {
119 description => "Certificate SHA 256 fingerprint.",
120 type => 'string',
121 pattern => '([A-Fa-f0-9]{2}:){31}[A-Fa-f0-9]{2}',
122});
123
ac15655f
DM
124register_standard_option('pve-output-format', {
125 type => 'string',
126 description => 'Output format.',
ac6c61bf 127 enum => [ 'text', 'json', 'json-pretty', 'yaml' ],
ac15655f
DM
128 optional => 1,
129 default => 'text',
130});
131
98d5b8cb
TL
132register_standard_option('pve-snapshot-name', {
133 description => "The name of the snapshot.",
134 type => 'string', format => 'pve-configid',
135 maxLength => 40,
136});
137
e143e9d8 138my $format_list = {};
70fdc050 139my $format_validators = {};
e143e9d8
DM
140
141sub register_format {
70fdc050 142 my ($name, $format, $validator) = @_;
e143e9d8 143
70fdc050
SR
144 die "JSON schema format '$name' already registered\n"
145 if $format_list->{$name};
e143e9d8 146
70fdc050
SR
147 if ($validator) {
148 die "A \$validator function can only be specified for hash-based formats\n"
149 if ref($format) ne 'HASH';
150 $format_validators->{$name} = $validator;
151 }
152
153 $format_list->{$name} = $format;
e143e9d8
DM
154}
155
2421fba1 156sub get_format {
70fdc050
SR
157 my ($name) = @_;
158 return $format_list->{$name};
2421fba1
WB
159}
160
b5212042
DM
161my $renderer_hash = {};
162
163sub register_renderer {
164 my ($name, $code) = @_;
165
166 die "renderer '$name' already registered\n"
167 if $renderer_hash->{$name};
168
169 $renderer_hash->{$name} = $code;
170}
171
172sub get_renderer {
173 my ($name) = @_;
174 return $renderer_hash->{$name};
175}
176
e143e9d8 177# register some common type for pve
8ba7c72b
DM
178
179register_format('string', sub {}); # allow format => 'string-list'
180
c77b4c96
WB
181register_format('urlencoded', \&pve_verify_urlencoded);
182sub pve_verify_urlencoded {
183 my ($text, $noerr) = @_;
184 if ($text !~ /^[-%a-zA-Z0-9_.!~*'()]*$/) {
185 return undef if $noerr;
186 die "invalid urlencoded string: $text\n";
187 }
188 return $text;
189}
190
e143e9d8
DM
191register_format('pve-configid', \&pve_verify_configid);
192sub pve_verify_configid {
193 my ($id, $noerr) = @_;
9bbc4e17 194
a8f93334 195 if ($id !~ m/^$CONFIGID_RE$/) {
e143e9d8 196 return undef if $noerr;
9bbc4e17 197 die "invalid configuration ID '$id'\n";
e143e9d8
DM
198 }
199 return $id;
200}
201
05e787c5
DM
202PVE::JSONSchema::register_format('pve-storage-id', \&parse_storage_id);
203sub parse_storage_id {
204 my ($storeid, $noerr) = @_;
205
731950fd
WL
206 return parse_id($storeid, 'storage', $noerr);
207}
208
6e55ce7d
FG
209PVE::JSONSchema::register_format('pve-bridge-id', \&parse_bridge_id);
210sub parse_bridge_id {
211 my ($id, $noerr) = @_;
212
213 if ($id !~ m/^[-_.\w\d]+$/) {
214 return undef if $noerr;
215 die "invalid bridge ID '$id'\n";
216 }
217 return $id;
218}
219
a8117ff3
WL
220PVE::JSONSchema::register_format('acme-plugin-id', \&parse_acme_plugin_id);
221sub parse_acme_plugin_id {
222 my ($pluginid, $noerr) = @_;
223
224 return parse_id($pluginid, 'ACME plugin', $noerr);
225}
226
731950fd
WL
227sub parse_id {
228 my ($id, $type, $noerr) = @_;
229
230 if ($id !~ m/^[a-z][a-z0-9\-\_\.]*[a-z0-9]$/i) {
05e787c5 231 return undef if $noerr;
731950fd 232 die "$type ID '$id' contains illegal characters\n";
05e787c5 233 }
731950fd 234 return $id;
05e787c5
DM
235}
236
e143e9d8
DM
237register_format('pve-vmid', \&pve_verify_vmid);
238sub pve_verify_vmid {
239 my ($vmid, $noerr) = @_;
240
50ae94c9 241 if ($vmid !~ m/^[1-9][0-9]{2,8}$/) {
e143e9d8
DM
242 return undef if $noerr;
243 die "value does not look like a valid VM ID\n";
244 }
245 return $vmid;
246}
247
248register_format('pve-node', \&pve_verify_node_name);
249sub pve_verify_node_name {
250 my ($node, $noerr) = @_;
251
e6db55c0 252 if ($node !~ m/^([a-zA-Z0-9]([a-zA-Z0-9\-]*[a-zA-Z0-9])?)$/) {
e143e9d8
DM
253 return undef if $noerr;
254 die "value does not look like a valid node name\n";
255 }
256 return $node;
257}
258
f627fab7
FG
259# maps source to target ID using an ID map
260sub map_id {
261 my ($map, $source) = @_;
262
263 return $source if !defined($map);
264
265 return $map->{entries}->{$source}
266 if $map->{entries} && defined($map->{entries}->{$source});
267
268 return $map->{default} if $map->{default};
269
270 # identity (fallback)
271 return $source;
272}
273
18f93ddf
FG
274sub parse_idmap {
275 my ($idmap, $idformat) = @_;
276
277 return undef if !$idmap;
278
279 my $map = {};
280
281 foreach my $entry (PVE::Tools::split_list($idmap)) {
282 if ($entry eq '1') {
283 $map->{identity} = 1;
284 } elsif ($entry =~ m/^([^:]+):([^:]+)$/) {
285 my ($source, $target) = ($1, $2);
286 eval {
e12df964
TL
287 check_format($idformat, $source, '');
288 check_format($idformat, $target, '');
18f93ddf 289 };
e12df964 290 die "entry '$entry' contains invalid ID - $@\n" if $@;
18f93ddf
FG
291
292 die "duplicate mapping for source '$source'\n"
e12df964 293 if exists $map->{entries}->{$source};
18f93ddf
FG
294
295 $map->{entries}->{$source} = $target;
296 } else {
297 eval {
e12df964 298 check_format($idformat, $entry);
18f93ddf 299 };
e12df964 300 die "entry '$entry' contains invalid ID - $@\n" if $@;
18f93ddf
FG
301
302 die "default target ID can only be provided once\n"
e12df964 303 if exists $map->{default};
18f93ddf
FG
304
305 $map->{default} = $entry;
306 }
307 }
308
309 die "identity mapping cannot be combined with other mappings\n"
e12df964 310 if $map->{identity} && ($map->{default} || exists $map->{entries});
18f93ddf
FG
311
312 return $map;
313}
314
92075098
FG
315my $verify_idpair = sub {
316 my ($input, $noerr, $format) = @_;
18f93ddf 317
92075098 318 eval { parse_idmap($input, $format) };
18f93ddf
FG
319 if ($@) {
320 return undef if $noerr;
321 die "$@\n";
322 }
323
92075098
FG
324 return $input;
325};
326
666b0d86
FG
327PVE::JSONSchema::register_standard_option('pve-targetstorage', {
328 description => "Mapping from source to target storages. Providing only a single storage ID maps all source storages to that storage. Providing the special value '1' will map each source storage to itself.",
329 type => 'string',
330 format => 'storage-pair-list',
331 optional => 1,
332});
333
92075098 334# note: this only checks a single list entry
da9f41f5 335# when using a storage-pair-list map, you need to pass the full parameter to
92075098 336# parse_idmap
da9f41f5 337register_format('storage-pair', \&verify_storagepair);
92075098
FG
338sub verify_storagepair {
339 my ($storagepair, $noerr) = @_;
340 return $verify_idpair->($storagepair, $noerr, 'pve-storage-id');
341}
18f93ddf 342
6e55ce7d
FG
343# note: this only checks a single list entry
344# when using a bridge-pair-list map, you need to pass the full parameter to
345# parse_idmap
346register_format('bridge-pair', \&verify_bridgepair);
347sub verify_bridgepair {
348 my ($bridgepair, $noerr) = @_;
349 return $verify_idpair->($bridgepair, $noerr, 'pve-bridge-id');
350}
351
14324ea8
CE
352register_format('mac-addr', \&pve_verify_mac_addr);
353sub pve_verify_mac_addr {
354 my ($mac_addr, $noerr) = @_;
355
4fdf30c4
TL
356 # don't allow I/G bit to be set, most of the time it breaks things, see:
357 # https://pve.proxmox.com/pipermail/pve-devel/2019-March/035998.html
a750d596 358 if ($mac_addr !~ m/^[a-f0-9][02468ace](?::[a-f0-9]{2}){5}$/i) {
14324ea8 359 return undef if $noerr;
a750d596 360 die "value does not look like a valid unicast MAC address\n";
14324ea8
CE
361 }
362 return $mac_addr;
a750d596 363
14324ea8 364}
a750d596
SI
365register_standard_option('mac-addr', {
366 type => 'string',
367 description => 'Unicast MAC address.',
4fdf30c4 368 verbose_description => 'A common MAC address with the I/G (Individual/Group) bit not set.',
a750d596
SI
369 format_description => "XX:XX:XX:XX:XX:XX",
370 optional => 1,
371 format => 'mac-addr',
372});
14324ea8 373
e143e9d8
DM
374register_format('ipv4', \&pve_verify_ipv4);
375sub pve_verify_ipv4 {
376 my ($ipv4, $noerr) = @_;
377
ed5880ac
DM
378 if ($ipv4 !~ m/^(?:$IPV4RE)$/) {
379 return undef if $noerr;
380 die "value does not look like a valid IPv4 address\n";
e143e9d8
DM
381 }
382 return $ipv4;
383}
a13c6f08 384
ed5880ac 385register_format('ipv6', \&pve_verify_ipv6);
93276209 386sub pve_verify_ipv6 {
ed5880ac
DM
387 my ($ipv6, $noerr) = @_;
388
389 if ($ipv6 !~ m/^(?:$IPV6RE)$/) {
390 return undef if $noerr;
391 die "value does not look like a valid IPv6 address\n";
392 }
393 return $ipv6;
394}
395
396register_format('ip', \&pve_verify_ip);
397sub pve_verify_ip {
398 my ($ip, $noerr) = @_;
399
400 if ($ip !~ m/^(?:(?:$IPV4RE)|(?:$IPV6RE))$/) {
401 return undef if $noerr;
402 die "value does not look like a valid IP address\n";
403 }
404 return $ip;
405}
406
283ac2ba
DC
407PVE::JSONSchema::register_format('ldap-simple-attr', \&verify_ldap_simple_attr);
408sub verify_ldap_simple_attr {
409 my ($attr, $noerr) = @_;
410
411 if ($attr =~ m/^[a-zA-Z0-9]+$/) {
412 return $attr;
413 }
414
415 die "value '$attr' does not look like a simple ldap attribute name\n" if !$noerr;
416
417 return undef;
418}
419
a13c6f08 420my $ipv4_mask_hash = {
aad3582e 421 '0.0.0.0' => 0,
a13c6f08
DM
422 '128.0.0.0' => 1,
423 '192.0.0.0' => 2,
424 '224.0.0.0' => 3,
425 '240.0.0.0' => 4,
426 '248.0.0.0' => 5,
427 '252.0.0.0' => 6,
428 '254.0.0.0' => 7,
429 '255.0.0.0' => 8,
430 '255.128.0.0' => 9,
431 '255.192.0.0' => 10,
432 '255.224.0.0' => 11,
433 '255.240.0.0' => 12,
434 '255.248.0.0' => 13,
435 '255.252.0.0' => 14,
436 '255.254.0.0' => 15,
437 '255.255.0.0' => 16,
438 '255.255.128.0' => 17,
439 '255.255.192.0' => 18,
440 '255.255.224.0' => 19,
441 '255.255.240.0' => 20,
442 '255.255.248.0' => 21,
443 '255.255.252.0' => 22,
444 '255.255.254.0' => 23,
445 '255.255.255.0' => 24,
446 '255.255.255.128' => 25,
447 '255.255.255.192' => 26,
448 '255.255.255.224' => 27,
449 '255.255.255.240' => 28,
450 '255.255.255.248' => 29,
e43faad9
WB
451 '255.255.255.252' => 30,
452 '255.255.255.254' => 31,
453 '255.255.255.255' => 32,
a13c6f08
DM
454};
455
aad3582e
DC
456sub get_netmask_bits {
457 my ($mask) = @_;
458 return $ipv4_mask_hash->{$mask};
459}
460
e143e9d8
DM
461register_format('ipv4mask', \&pve_verify_ipv4mask);
462sub pve_verify_ipv4mask {
463 my ($mask, $noerr) = @_;
464
a13c6f08 465 if (!defined($ipv4_mask_hash->{$mask})) {
e143e9d8
DM
466 return undef if $noerr;
467 die "value does not look like a valid IP netmask\n";
468 }
469 return $mask;
470}
471
703c1f88
WB
472register_format('CIDRv6', \&pve_verify_cidrv6);
473sub pve_verify_cidrv6 {
e272bcb7
DM
474 my ($cidr, $noerr) = @_;
475
70ea2250 476 if ($cidr =~ m!^(?:$IPV6RE)(?:/(\d+))$! && ($1 > 7) && ($1 <= 128)) {
e272bcb7 477 return $cidr;
703c1f88
WB
478 }
479
480 return undef if $noerr;
481 die "value does not look like a valid IPv6 CIDR network\n";
482}
483
484register_format('CIDRv4', \&pve_verify_cidrv4);
485sub pve_verify_cidrv4 {
486 my ($cidr, $noerr) = @_;
487
0526cc2d 488 if ($cidr =~ m!^(?:$IPV4RE)(?:/(\d+))$! && ($1 > 7) && ($1 <= 32)) {
e272bcb7
DM
489 return $cidr;
490 }
491
492 return undef if $noerr;
703c1f88
WB
493 die "value does not look like a valid IPv4 CIDR network\n";
494}
495
496register_format('CIDR', \&pve_verify_cidr);
497sub pve_verify_cidr {
498 my ($cidr, $noerr) = @_;
499
500 if (!(pve_verify_cidrv4($cidr, 1) ||
501 pve_verify_cidrv6($cidr, 1)))
502 {
503 return undef if $noerr;
504 die "value does not look like a valid CIDR network\n";
505 }
506
507 return $cidr;
508}
509
510register_format('pve-ipv4-config', \&pve_verify_ipv4_config);
511sub pve_verify_ipv4_config {
512 my ($config, $noerr) = @_;
513
514 return $config if $config =~ /^(?:dhcp|manual)$/ ||
515 pve_verify_cidrv4($config, 1);
516 return undef if $noerr;
517 die "value does not look like a valid ipv4 network configuration\n";
518}
519
520register_format('pve-ipv6-config', \&pve_verify_ipv6_config);
521sub pve_verify_ipv6_config {
522 my ($config, $noerr) = @_;
523
524 return $config if $config =~ /^(?:auto|dhcp|manual)$/ ||
525 pve_verify_cidrv6($config, 1);
526 return undef if $noerr;
527 die "value does not look like a valid ipv6 network configuration\n";
e272bcb7
DM
528}
529
e143e9d8
DM
530register_format('email', \&pve_verify_email);
531sub pve_verify_email {
532 my ($email, $noerr) = @_;
533
4c4bd104 534 if ($email !~ /^$PVE::Tools::EMAIL_RE$/) {
e143e9d8
DM
535 return undef if $noerr;
536 die "value does not look like a valid email address\n";
537 }
538 return $email;
539}
540
ff8d3b1d
FE
541register_format('email-or-username', \&pve_verify_email_or_username);
542sub pve_verify_email_or_username {
543 my ($email, $noerr) = @_;
544
545 if ($email !~ /^$PVE::Tools::EMAIL_RE$/ &&
546 $email !~ /^$PVE::Tools::EMAIL_USER_RE$/) {
547 return undef if $noerr;
548 die "value does not look like a valid email address or user name\n";
549 }
550 return $email;
551}
552
34ebb226
DM
553register_format('dns-name', \&pve_verify_dns_name);
554sub pve_verify_dns_name {
555 my ($name, $noerr) = @_;
556
ce33e978 557 my $namere = "([a-zA-Z0-9]([a-zA-Z0-9\-]*[a-zA-Z0-9])?)";
34ebb226
DM
558
559 if ($name !~ /^(${namere}\.)*${namere}$/) {
560 return undef if $noerr;
561 die "value does not look like a valid DNS name\n";
562 }
563 return $name;
564}
565
e76308e6
OB
566register_format('timezone', \&pve_verify_timezone);
567sub pve_verify_timezone {
568 my ($timezone, $noerr) = @_;
569
e76308e6 570 return $timezone if $timezone eq 'UTC';
36b9c073
TL
571
572 open(my $fh, "<", "/usr/share/zoneinfo/zone.tab");
573 while (my $line = <$fh>) {
574 next if $line =~ /^\s*#/;
e76308e6 575 chomp $line;
36b9c073
TL
576 my $zone = (split /\t/, $line)[2];
577 return $timezone if $timezone eq $zone; # found
e76308e6
OB
578 }
579 close $fh;
580
581 return undef if $noerr;
582 die "invalid time zone '$timezone'\n";
e76308e6
OB
583}
584
e143e9d8
DM
585# network interface name
586register_format('pve-iface', \&pve_verify_iface);
587sub pve_verify_iface {
588 my ($id, $noerr) = @_;
9bbc4e17 589
e143e9d8
DM
590 if ($id !~ m/^[a-z][a-z0-9_]{1,20}([:\.]\d+)?$/i) {
591 return undef if $noerr;
9bbc4e17 592 die "invalid network interface name '$id'\n";
e143e9d8
DM
593 }
594 return $id;
595}
596
d07b7084
WB
597# general addresses by name or IP
598register_format('address', \&pve_verify_address);
599sub pve_verify_address {
600 my ($addr, $noerr) = @_;
601
602 if (!(pve_verify_ip($addr, 1) ||
603 pve_verify_dns_name($addr, 1)))
604 {
605 return undef if $noerr;
606 die "value does not look like a valid address: $addr\n";
607 }
608 return $addr;
609}
610
b944a22a
WB
611register_format('disk-size', \&pve_verify_disk_size);
612sub pve_verify_disk_size {
613 my ($size, $noerr) = @_;
614 if (!defined(parse_size($size))) {
615 return undef if $noerr;
616 die "value does not look like a valid disk size: $size\n";
617 }
618 return $size;
619}
620
f0a10afc 621register_standard_option('spice-proxy', {
fb3a1b29 622 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 623 type => 'string', format => 'address',
9bbc4e17 624});
f0a10afc
DM
625
626register_standard_option('remote-viewer-config', {
627 description => "Returned values can be directly passed to the 'remote-viewer' application.",
628 additionalProperties => 1,
629 properties => {
630 type => { type => 'string' },
631 password => { type => 'string' },
632 proxy => { type => 'string' },
633 host => { type => 'string' },
634 'tls-port' => { type => 'integer' },
635 },
636});
637
c70c3bbc 638register_format('pve-startup-order', \&pve_verify_startup_order);
b0edd8e6
DM
639sub pve_verify_startup_order {
640 my ($value, $noerr) = @_;
641
642 return $value if pve_parse_startup_order($value);
643
644 return undef if $noerr;
645
646 die "unable to parse startup options\n";
647}
648
2d167ad0
WB
649my %bwlimit_opt = (
650 optional => 1,
651 type => 'number', minimum => '0',
652 format_description => 'LIMIT',
653);
654
655my $bwlimit_format = {
656 default => {
657 %bwlimit_opt,
34e75688 658 description => 'default bandwidth limit in KiB/s',
2d167ad0
WB
659 },
660 restore => {
661 %bwlimit_opt,
34e75688 662 description => 'bandwidth limit in KiB/s for restoring guests from backups',
2d167ad0
WB
663 },
664 migration => {
665 %bwlimit_opt,
34e75688 666 description => 'bandwidth limit in KiB/s for migrating guests (including moving local disks)',
2d167ad0
WB
667 },
668 clone => {
669 %bwlimit_opt,
34e75688 670 description => 'bandwidth limit in KiB/s for cloning disks',
2d167ad0
WB
671 },
672 move => {
673 %bwlimit_opt,
34e75688 674 description => 'bandwidth limit in KiB/s for moving disks',
2d167ad0
WB
675 },
676};
677register_format('bwlimit', $bwlimit_format);
678register_standard_option('bwlimit', {
7bbfaff1 679 description => "Set I/O bandwidth limit for various operations (in KiB/s).",
2d167ad0
WB
680 optional => 1,
681 type => 'string',
682 format => $bwlimit_format,
683});
484b6b39 684
b75893dd
FG
685my $remote_format = {
686 host => {
687 type => 'string',
688 format_description => 'Remote Proxmox hostname or IP',
689 },
690 port => {
691 type => 'integer',
692 optional => 1,
693 },
694 apitoken => {
695 type => 'string',
696 format_description => 'A full Proxmox API token including the secret value.',
697 },
698 fingerprint => get_standard_option(
699 'fingerprint-sha256',
700 {
701 optional => 1,
702 format_description => 'Remote host\'s certificate fingerprint, if not trusted by system store.',
703 }
704 ),
705};
706register_format('proxmox-remote', $remote_format);
707register_standard_option('proxmox-remote', {
708 description => "Specification of a remote endpoint.",
709 type => 'string', format => 'proxmox-remote',
710});
711
056bbe2d
DC
712our $PVE_TAG_RE = qr/[a-z0-9_][a-z0-9_\-\+\.]*/i;
713
484b6b39
DC
714# used for pve-tag-list in e.g., guest configs
715register_format('pve-tag', \&pve_verify_tag);
716sub pve_verify_tag {
717 my ($value, $noerr) = @_;
718
056bbe2d 719 return $value if $value =~ m/^${PVE_TAG_RE}$/i;
484b6b39
DC
720
721 return undef if $noerr;
722
723 die "invalid characters in tag\n";
724}
2d167ad0 725
b0edd8e6
DM
726sub pve_parse_startup_order {
727 my ($value) = @_;
728
729 return undef if !$value;
730
731 my $res = {};
732
733 foreach my $p (split(/,/, $value)) {
734 next if $p =~ m/^\s*$/;
735
736 if ($p =~ m/^(order=)?(\d+)$/) {
737 $res->{order} = $2;
738 } elsif ($p =~ m/^up=(\d+)$/) {
739 $res->{up} = $1;
740 } elsif ($p =~ m/^down=(\d+)$/) {
741 $res->{down} = $1;
742 } else {
743 return undef;
744 }
745 }
746
31b5a3a7 747 return $res;
b0edd8e6
DM
748}
749
750PVE::JSONSchema::register_standard_option('pve-startup-order', {
751 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.",
752 optional => 1,
753 type => 'string', format => 'pve-startup-order',
754 typetext => '[[order=]\d+] [,up=\d+] [,down=\d+] ',
755});
756
6e234325
WB
757register_format('pve-tfa-secret', \&pve_verify_tfa_secret);
758sub pve_verify_tfa_secret {
759 my ($key, $noerr) = @_;
760
761 # The old format used 16 base32 chars or 40 hex digits. Since they have a common subset it's
762 # hard to distinguish them without the our previous length constraints, so add a 'v2' of the
763 # format to support arbitrary lengths properly:
764 if ($key =~ /^v2-0x[0-9a-fA-F]{16,128}$/ || # hex
765 $key =~ /^v2-[A-Z2-7=]{16,128}$/ || # base32
766 $key =~ /^(?:[A-Z2-7=]{16}|[A-Fa-f0-9]{40})$/) # and the old pattern copy&pasted
767 {
768 return $key;
769 }
770
771 return undef if $noerr;
772
773 die "unable to decode TFA secret\n";
774}
775
f43ace29
DC
776
777PVE::JSONSchema::register_format('pve-task-status-type', \&verify_task_status_type);
778sub verify_task_status_type {
779 my ($value, $noerr) = @_;
780
781 return $value if $value =~ m/^(ok|error|warning|unknown)$/i;
782
783 return undef if $noerr;
784
785 die "invalid status '$value'\n";
786}
787
e143e9d8 788sub check_format {
2f9e609a 789 my ($format, $value, $path) = @_;
e143e9d8 790
70fdc050
SR
791 if (ref($format) eq 'HASH') {
792 # hash ref cannot have validator/list/opt handling attached
793 return parse_property_string($format, $value, $path);
794 }
e143e9d8 795
70fdc050
SR
796 if (ref($format) eq 'CODE') {
797 # we are the (sole, old-style) validator
798 return $format->($value);
799 }
9bbc4e17 800
70fdc050
SR
801 return if $format eq 'regex';
802
803 my $parsed;
a26f3858 804 $format =~ m/^(.*?)(?:-(list|opt))?$/;
70fdc050
SR
805 my ($format_name, $format_type) = ($1, $2 // 'none');
806 my $registered = get_format($format_name);
807 die "undefined format '$format'\n" if !$registered;
e143e9d8 808
70fdc050
SR
809 die "'-$format_type' format must have code ref, not hash\n"
810 if $format_type ne 'none' && ref($registered) ne 'CODE';
e143e9d8 811
70fdc050 812 if ($format_type eq 'list') {
31d4beb4 813 $parsed = [];
e143e9d8
DM
814 # Note: we allow empty lists
815 foreach my $v (split_list($value)) {
31d4beb4 816 push @{$parsed}, $registered->($v);
e143e9d8 817 }
70fdc050
SR
818 } elsif ($format_type eq 'opt') {
819 $parsed = $registered->($value) if $value;
91477ace 820 } else {
70fdc050
SR
821 if (ref($registered) eq 'HASH') {
822 # Note: this is the only case where a validator function could be
823 # attached, hence it's safe to handle that in parse_property_string.
824 # We do however have to call it with $format_name instead of
825 # $registered, so it knows about the name (and thus any validators).
826 $parsed = parse_property_string($format, $value, $path);
827 } else {
828 $parsed = $registered->($value);
829 }
e143e9d8 830 }
70fdc050
SR
831
832 return $parsed;
9bbc4e17 833}
e143e9d8 834
878fea8e
WB
835sub parse_size {
836 my ($value) = @_;
837
838 return undef if $value !~ m/^(\d+(\.\d+)?)([KMGT])?$/;
839 my ($size, $unit) = ($1, $3);
840 if ($unit) {
841 if ($unit eq 'K') {
842 $size = $size * 1024;
843 } elsif ($unit eq 'M') {
844 $size = $size * 1024 * 1024;
845 } elsif ($unit eq 'G') {
846 $size = $size * 1024 * 1024 * 1024;
847 } elsif ($unit eq 'T') {
848 $size = $size * 1024 * 1024 * 1024 * 1024;
849 }
850 }
851 return int($size);
852};
853
854sub format_size {
855 my ($size) = @_;
856
857 $size = int($size);
858
859 my $kb = int($size/1024);
860 return $size if $kb*1024 != $size;
861
862 my $mb = int($kb/1024);
863 return "${kb}K" if $mb*1024 != $kb;
864
865 my $gb = int($mb/1024);
866 return "${mb}M" if $gb*1024 != $mb;
867
868 my $tb = int($gb/1024);
869 return "${gb}G" if $tb*1024 != $gb;
870
871 return "${tb}T";
872};
873
1b71e564
WB
874sub parse_boolean {
875 my ($bool) = @_;
876 return 1 if $bool =~ m/^(1|on|yes|true)$/i;
877 return 0 if $bool =~ m/^(0|off|no|false)$/i;
878 return undef;
879}
880
095b88fd 881sub parse_property_string {
d1e490c1
WB
882 my ($format, $data, $path, $additional_properties) = @_;
883
884 # In property strings we default to not allowing additional properties
885 $additional_properties = 0 if !defined($additional_properties);
095b88fd 886
7c1617b0 887 # Support named formats here, too:
70fdc050 888 my $validator;
7c1617b0 889 if (!ref($format)) {
70fdc050
SR
890 if (my $reg = get_format($format)) {
891 die "parse_property_string only accepts hash based named formats\n"
892 if ref($reg) ne 'HASH';
893
894 # named formats can have validators attached
895 $validator = $format_validators->{$format};
896
897 $format = $reg;
7c1617b0
WB
898 } else {
899 die "unknown format: $format\n";
900 }
901 } elsif (ref($format) ne 'HASH') {
902 die "unexpected format value of type ".ref($format)."\n";
903 }
904
095b88fd
WB
905 my $default_key;
906
907 my $res = {};
908 foreach my $part (split(/,/, $data)) {
909 next if $part =~ /^\s*$/;
910
911 if ($part =~ /^([^=]+)=(.+)$/) {
912 my ($k, $v) = ($1, $2);
2d468b1a 913 die "duplicate key in comma-separated list property: $k\n" if defined($res->{$k});
095b88fd 914 my $schema = $format->{$k};
303a9b34 915 if (my $alias = $schema->{alias}) {
bf27456b
DM
916 if (my $key_alias = $schema->{keyAlias}) {
917 die "key alias '$key_alias' is already defined\n" if defined($res->{$key_alias});
918 $res->{$key_alias} = $k;
919 }
303a9b34
WB
920 $k = $alias;
921 $schema = $format->{$k};
922 }
bf27456b 923
2d468b1a 924 die "invalid key in comma-separated list property: $k\n" if !$schema;
095b88fd 925 if ($schema->{type} && $schema->{type} eq 'boolean') {
1b71e564 926 $v = parse_boolean($v) // $v;
095b88fd
WB
927 }
928 $res->{$k} = $v;
929 } elsif ($part !~ /=/) {
2d468b1a 930 die "duplicate key in comma-separated list property: $default_key\n" if $default_key;
095b88fd
WB
931 foreach my $key (keys %$format) {
932 if ($format->{$key}->{default_key}) {
933 $default_key = $key;
934 if (!$res->{$default_key}) {
935 $res->{$default_key} = $part;
936 last;
937 }
2d468b1a 938 die "duplicate key in comma-separated list property: $default_key\n";
095b88fd
WB
939 }
940 }
f0ba41a1 941 die "value without key, but schema does not define a default key\n" if !$default_key;
095b88fd 942 } else {
2d468b1a 943 die "missing key in comma-separated list property\n";
095b88fd
WB
944 }
945 }
946
947 my $errors = {};
d1e490c1 948 check_object($path, $format, $res, $additional_properties, $errors);
095b88fd 949 if (scalar(%$errors)) {
2d468b1a 950 raise "format error\n", errors => $errors;
095b88fd
WB
951 }
952
70fdc050 953 return $validator->($res) if $validator;
095b88fd
WB
954 return $res;
955}
956
e143e9d8
DM
957sub add_error {
958 my ($errors, $path, $msg) = @_;
959
960 $path = '_root' if !$path;
9bbc4e17 961
e143e9d8
DM
962 if ($errors->{$path}) {
963 $errors->{$path} = join ('\n', $errors->{$path}, $msg);
964 } else {
965 $errors->{$path} = $msg;
966 }
967}
968
969sub is_number {
970 my $value = shift;
971
972 # see 'man perlretut'
9bbc4e17 973 return $value =~ /^[+-]?(\d+\.\d+|\d+\.|\.\d+|\d+)([eE][+-]?\d+)?$/;
e143e9d8
DM
974}
975
976sub is_integer {
977 my $value = shift;
978
979 return $value =~ m/^[+-]?\d+$/;
980}
981
982sub check_type {
983 my ($path, $type, $value, $errors) = @_;
984
985 return 1 if !$type;
986
987 if (!defined($value)) {
988 return 1 if $type eq 'null';
9bbc4e17 989 die "internal error"
e143e9d8
DM
990 }
991
992 if (my $tt = ref($type)) {
993 if ($tt eq 'ARRAY') {
994 foreach my $t (@$type) {
995 my $tmperr = {};
996 check_type($path, $t, $value, $tmperr);
9bbc4e17 997 return 1 if !scalar(%$tmperr);
e143e9d8
DM
998 }
999 my $ttext = join ('|', @$type);
9bbc4e17 1000 add_error($errors, $path, "type check ('$ttext') failed");
e143e9d8
DM
1001 return undef;
1002 } elsif ($tt eq 'HASH') {
1003 my $tmperr = {};
1004 check_prop($value, $type, $path, $tmperr);
9bbc4e17
TL
1005 return 1 if !scalar(%$tmperr);
1006 add_error($errors, $path, "type check failed");
e143e9d8
DM
1007 return undef;
1008 } else {
1009 die "internal error - got reference type '$tt'";
1010 }
1011
1012 } else {
1013
1014 return 1 if $type eq 'any';
1015
1016 if ($type eq 'null') {
1017 if (defined($value)) {
1018 add_error($errors, $path, "type check ('$type') failed - value is not null");
1019 return undef;
1020 }
1021 return 1;
1022 }
1023
1024 my $vt = ref($value);
1025
1026 if ($type eq 'array') {
1027 if (!$vt || $vt ne 'ARRAY') {
1028 add_error($errors, $path, "type check ('$type') failed");
1029 return undef;
1030 }
1031 return 1;
1032 } elsif ($type eq 'object') {
1033 if (!$vt || $vt ne 'HASH') {
1034 add_error($errors, $path, "type check ('$type') failed");
1035 return undef;
1036 }
1037 return 1;
1038 } elsif ($type eq 'coderef') {
1039 if (!$vt || $vt ne 'CODE') {
1040 add_error($errors, $path, "type check ('$type') failed");
1041 return undef;
1042 }
1043 return 1;
88a490ff
WB
1044 } elsif ($type eq 'string' && $vt eq 'Regexp') {
1045 # qr// regexes can be used as strings and make sense for format=regex
1046 return 1;
e143e9d8
DM
1047 } else {
1048 if ($vt) {
bc24aad3
DC
1049 if ($type eq 'boolean' && JSON::is_bool($value)) {
1050 return 1;
1051 }
e143e9d8
DM
1052 add_error($errors, $path, "type check ('$type') failed - got $vt");
1053 return undef;
1054 } else {
1055 if ($type eq 'string') {
1056 return 1; # nothing to check ?
1057 } elsif ($type eq 'boolean') {
1058 #if ($value =~ m/^(1|true|yes|on)$/i) {
1059 if ($value eq '1') {
1060 return 1;
1061 #} elsif ($value =~ m/^(0|false|no|off)$/i) {
1062 } elsif ($value eq '0') {
79501b2a 1063 return 1; # return success (not value)
e143e9d8
DM
1064 } else {
1065 add_error($errors, $path, "type check ('$type') failed - got '$value'");
1066 return undef;
1067 }
1068 } elsif ($type eq 'integer') {
1069 if (!is_integer($value)) {
1070 add_error($errors, $path, "type check ('$type') failed - got '$value'");
1071 return undef;
1072 }
1073 return 1;
1074 } elsif ($type eq 'number') {
1075 if (!is_number($value)) {
1076 add_error($errors, $path, "type check ('$type') failed - got '$value'");
1077 return undef;
1078 }
1079 return 1;
1080 } else {
1081 return 1; # no need to verify unknown types
1082 }
1083 }
1084 }
9bbc4e17 1085 }
e143e9d8
DM
1086
1087 return undef;
1088}
1089
94258346
DC
1090my sub get_instance_type {
1091 my ($schema, $key, $value) = @_;
1092
1093 if (my $type_property = $schema->{$key}->{'type-property'}) {
1094 return $value->{$type_property};
1095 }
1096
1097 return undef;
1098}
1099
e143e9d8
DM
1100sub check_object {
1101 my ($path, $schema, $value, $additional_properties, $errors) = @_;
1102
1103 # print "Check Object " . Dumper($value) . "\nSchema: " . Dumper($schema);
1104
1105 my $st = ref($schema);
1106 if (!$st || $st ne 'HASH') {
1107 add_error($errors, $path, "Invalid schema definition.");
1108 return;
1109 }
1110
1111 my $vt = ref($value);
1112 if (!$vt || $vt ne 'HASH') {
1113 add_error($errors, $path, "an object is required");
1114 return;
1115 }
1116
1117 foreach my $k (keys %$schema) {
94258346
DC
1118 my $instance_type = get_instance_type($schema, $k, $value);
1119 check_prop($value->{$k}, $schema->{$k}, $path ? "$path.$k" : $k, $errors, $instance_type);
e143e9d8
DM
1120 }
1121
1122 foreach my $k (keys %$value) {
1123
1124 my $newpath = $path ? "$path.$k" : $k;
1125
1126 if (my $subschema = $schema->{$k}) {
1127 if (my $requires = $subschema->{requires}) {
1128 if (ref($requires)) {
1129 #print "TEST: " . Dumper($value) . "\n", Dumper($requires) ;
1130 check_prop($value, $requires, $path, $errors);
1131 } elsif (!defined($value->{$requires})) {
9bbc4e17 1132 add_error($errors, $path ? "$path.$requires" : $requires,
8b6e737a 1133 "missing property - '$newpath' requires this property");
e143e9d8
DM
1134 }
1135 }
1136
94258346
DC
1137 # if it's a oneOf, check if there is a matching type
1138 my $matched_type = 1;
1139 if ($subschema->{oneOf}) {
1140 my $instance_type = get_instance_type($schema, $k, $value);
1141 $matched_type = 0;
1142 for my $alternative ($subschema->{oneOf}->@*) {
1143 if (my $instance_types = $alternative->{'instance-types'}) {
1144 if (!grep { $instance_type eq $_ } $instance_types->@*) {
1145 next;
1146 }
1147 }
1148 $matched_type = 1;
1149 last;
1150 }
1151 }
1152
1153 next if $matched_type; # value is already checked above
e143e9d8
DM
1154 }
1155
1156 if (defined ($additional_properties) && !$additional_properties) {
1157 add_error($errors, $newpath, "property is not defined in schema " .
1158 "and the schema does not allow additional properties");
1159 next;
1160 }
1161 check_prop($value->{$k}, $additional_properties, $newpath, $errors)
1162 if ref($additional_properties);
1163 }
1164}
1165
86425a09
WB
1166sub check_object_warn {
1167 my ($path, $schema, $value, $additional_properties) = @_;
1168 my $errors = {};
1169 check_object($path, $schema, $value, $additional_properties, $errors);
1170 if (scalar(%$errors)) {
1171 foreach my $k (keys %$errors) {
1172 warn "parse error: $k: $errors->{$k}\n";
1173 }
1174 return 0;
1175 }
1176 return 1;
1177}
1178
e143e9d8 1179sub check_prop {
94258346 1180 my ($value, $schema, $path, $errors, $instance_type) = @_;
e143e9d8
DM
1181
1182 die "internal error - no schema" if !$schema;
1183 die "internal error" if !$errors;
1184
1185 #print "check_prop $path\n" if $value;
1186
1187 my $st = ref($schema);
1188 if (!$st || $st ne 'HASH') {
1189 add_error($errors, $path, "Invalid schema definition.");
1190 return;
1191 }
1192
94258346
DC
1193 # must pass any of the given schemas
1194 my $optional_for_type = 0;
1195 if ($schema->{oneOf}) {
1196 # in case we have an instance_type given, just check for that variant
1197 if ($schema->{'type-property'}) {
1198 $optional_for_type = 1;
1199 for (my $i = 0; $i < scalar($schema->{oneOf}->@*); $i++) {
1200 last if !$instance_type; # treat as optional if we don't have a type
1201 my $inner_schema = $schema->{oneOf}->[$i];
1202
1203 if (!defined($inner_schema->{'instance-types'})) {
1204 add_error($errors, $path, "missing 'instance-types' in oneOf alternative");
1205 return;
1206 }
1207
1208 next if !grep { $_ eq $instance_type } $inner_schema->{'instance-types'}->@*;
1209 $optional_for_type = $inner_schema->{optional} // 0;
1210 check_prop($value, $inner_schema, $path, $errors);
1211 }
1212 } else {
1213 my $is_valid = 0;
1214 my $collected_errors = {};
1215 for (my $i = 0; $i < scalar($schema->{oneOf}->@*); $i++) {
1216 my $inner_schema = $schema->{oneOf}->[$i];
1217 my $inner_errors = {};
1218 check_prop($value, $inner_schema, "$path.oneOf[$i]", $inner_errors);
1219 if (!$inner_errors->%*) {
1220 $is_valid = 1;
1221 last;
1222 }
1223
1224 for my $inner_path (keys $inner_errors->%*) {
1225 add_error($collected_errors, $inner_path, $inner_errors->{$path});
1226 }
1227 }
1228
1229 if (!$is_valid) {
1230 for my $inner_path (keys $collected_errors->%*) {
1231 add_error($errors, $inner_path, $collected_errors->{$path});
1232 }
1233 }
1234 }
1235 } elsif ($instance_type) {
1236 if (!defined($schema->{'instance-types'})) {
1237 add_error($errors, $path, "missing 'instance-types'");
1238 return;
1239 }
1240 if (grep { $_ eq $instance_type} $schema->{'instance_types'}->@*) {
1241 $optional_for_type = 1;
1242 }
1243 }
1244
e143e9d8
DM
1245 # if it extends another schema, it must pass that schema as well
1246 if($schema->{extends}) {
1247 check_prop($value, $schema->{extends}, $path, $errors);
1248 }
1249
1250 if (!defined ($value)) {
1251 return if $schema->{type} && $schema->{type} eq 'null';
94258346 1252 if (!$schema->{optional} && !$schema->{alias} && !$schema->{group} && !$optional_for_type) {
e143e9d8
DM
1253 add_error($errors, $path, "property is missing and it is not optional");
1254 }
1255 return;
1256 }
1257
1258 return if !check_type($path, $schema->{type}, $value, $errors);
1259
1260 if ($schema->{disallow}) {
1261 my $tmperr = {};
1262 if (check_type($path, $schema->{disallow}, $value, $tmperr)) {
1263 add_error($errors, $path, "disallowed value was matched");
1264 return;
1265 }
1266 }
1267
1268 if (my $vt = ref($value)) {
1269
1270 if ($vt eq 'ARRAY') {
1271 if ($schema->{items}) {
1272 my $it = ref($schema->{items});
1273 if ($it && $it eq 'ARRAY') {
1274 #die "implement me $path: $vt " . Dumper($schema) ."\n". Dumper($value);
1275 die "not implemented";
1276 } else {
1277 my $ind = 0;
1278 foreach my $el (@$value) {
1279 check_prop($el, $schema->{items}, "${path}[$ind]", $errors);
1280 $ind++;
1281 }
1282 }
1283 }
9bbc4e17 1284 return;
e143e9d8
DM
1285 } elsif ($schema->{properties} || $schema->{additionalProperties}) {
1286 check_object($path, defined($schema->{properties}) ? $schema->{properties} : {},
1287 $value, $schema->{additionalProperties}, $errors);
1288 return;
1289 }
1290
1291 } else {
1292
1293 if (my $format = $schema->{format}) {
2f9e609a 1294 eval { check_format($format, $value, $path); };
e143e9d8
DM
1295 if ($@) {
1296 add_error($errors, $path, "invalid format - $@");
1297 return;
1298 }
1299 }
1300
1301 if (my $pattern = $schema->{pattern}) {
1302 if ($value !~ m/^$pattern$/) {
1303 add_error($errors, $path, "value does not match the regex pattern");
1304 return;
1305 }
1306 }
1307
1308 if (defined (my $max = $schema->{maxLength})) {
1309 if (length($value) > $max) {
1310 add_error($errors, $path, "value may only be $max characters long");
1311 return;
1312 }
1313 }
1314
1315 if (defined (my $min = $schema->{minLength})) {
1316 if (length($value) < $min) {
1317 add_error($errors, $path, "value must be at least $min characters long");
1318 return;
1319 }
1320 }
9bbc4e17 1321
e143e9d8
DM
1322 if (is_number($value)) {
1323 if (defined (my $max = $schema->{maximum})) {
9bbc4e17 1324 if ($value > $max) {
e143e9d8
DM
1325 add_error($errors, $path, "value must have a maximum value of $max");
1326 return;
1327 }
1328 }
1329
1330 if (defined (my $min = $schema->{minimum})) {
9bbc4e17 1331 if ($value < $min) {
e143e9d8
DM
1332 add_error($errors, $path, "value must have a minimum value of $min");
1333 return;
1334 }
1335 }
1336 }
1337
1338 if (my $ea = $schema->{enum}) {
1339
1340 my $found;
1341 foreach my $ev (@$ea) {
1342 if ($ev eq $value) {
1343 $found = 1;
1344 last;
1345 }
1346 }
1347 if (!$found) {
1348 add_error($errors, $path, "value '$value' does not have a value in the enumeration '" .
1349 join(", ", @$ea) . "'");
1350 }
1351 }
1352 }
1353}
1354
1355sub validate {
1356 my ($instance, $schema, $errmsg) = @_;
1357
1358 my $errors = {};
1359 $errmsg = "Parameter verification failed.\n" if !$errmsg;
1360
1361 # todo: cycle detection is only needed for debugging, I guess
1362 # we can disable that in the final release
1363 # todo: is there a better/faster way to detect cycles?
1364 my $cycles = 0;
6ab98c4e
SR
1365 # 'download' responses can contain a filehandle, don't cycle-check that as
1366 # it produces a warning
1367 my $is_download = ref($instance) eq 'HASH' && exists($instance->{download});
1368 find_cycle($instance, sub { $cycles = 1 }) if !$is_download;
e143e9d8
DM
1369 if ($cycles) {
1370 add_error($errors, undef, "data structure contains recursive cycles");
1371 } elsif ($schema) {
1372 check_prop($instance, $schema, '', $errors);
1373 }
9bbc4e17 1374
e143e9d8
DM
1375 if (scalar(%$errors)) {
1376 raise $errmsg, code => HTTP_BAD_REQUEST, errors => $errors;
1377 }
1378
1379 return 1;
1380}
1381
1382my $schema_valid_types = ["string", "object", "coderef", "array", "boolean", "number", "integer", "null", "any"];
1383my $default_schema_noref = {
1384 description => "This is the JSON Schema for JSON Schemas.",
1385 type => [ "object" ],
1386 additionalProperties => 0,
1387 properties => {
1388 type => {
1389 type => ["string", "array"],
1390 description => "This is a type definition value. This can be a simple type, or a union type",
1391 optional => 1,
1392 default => "any",
1393 items => {
1394 type => "string",
1395 enum => $schema_valid_types,
1396 },
1397 enum => $schema_valid_types,
1398 },
94258346
DC
1399 oneOf => {
1400 type => 'array',
1401 description => "This represents the alternative options for this Schema instance.",
1402 optional => 1,
1403 items => {
1404 type => 'object',
1405 description => "A valid option of the properties",
1406 },
1407 },
1408 'instance-types' => {
1409 type => 'array',
1410 description => "Indicate to which type the parameter (or variant if inside a oneOf) belongs.",
1411 optional => 1,
1412 items => {
1413 type => 'string',
1414 },
1415 },
1416 'type-property' => {
1417 type => 'string',
1418 description => "The property to check for instance types.",
1419 optional => 1,
1420 },
e143e9d8
DM
1421 optional => {
1422 type => "boolean",
1423 description => "This indicates that the instance property in the instance object is not required.",
1424 optional => 1,
1425 default => 0
1426 },
1427 properties => {
1428 type => "object",
1429 description => "This is a definition for the properties of an object value",
1430 optional => 1,
1431 default => {},
1432 },
1433 items => {
1434 type => "object",
1435 description => "When the value is an array, this indicates the schema to use to validate each item in an array",
1436 optional => 1,
1437 default => {},
1438 },
1439 additionalProperties => {
1440 type => [ "boolean", "object"],
1441 description => "This provides a default property definition for all properties that are not explicitly defined in an object type definition.",
1442 optional => 1,
1443 default => {},
1444 },
1445 minimum => {
1446 type => "number",
1447 optional => 1,
1448 description => "This indicates the minimum value for the instance property when the type of the instance value is a number.",
1449 },
1450 maximum => {
1451 type => "number",
1452 optional => 1,
1453 description => "This indicates the maximum value for the instance property when the type of the instance value is a number.",
1454 },
1455 minLength => {
1456 type => "integer",
1457 description => "When the instance value is a string, this indicates minimum length of the string",
1458 optional => 1,
1459 minimum => 0,
1460 default => 0,
9bbc4e17 1461 },
e143e9d8
DM
1462 maxLength => {
1463 type => "integer",
1464 description => "When the instance value is a string, this indicates maximum length of the string.",
1465 optional => 1,
1466 },
1467 typetext => {
1468 type => "string",
1469 optional => 1,
1470 description => "A text representation of the type (used to generate documentation).",
1471 },
1472 pattern => {
1473 type => "string",
1474 format => "regex",
166e27c7 1475 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
1476 optional => 1,
1477 default => ".*",
166e27c7 1478 },
e143e9d8
DM
1479 enum => {
1480 type => "array",
1481 optional => 1,
1482 description => "This provides an enumeration of possible values that are valid for the instance property.",
1483 },
1484 description => {
1485 type => "string",
1486 optional => 1,
1487 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).",
1488 },
32f8e0c7
DM
1489 verbose_description => {
1490 type => "string",
1491 optional => 1,
1492 description => "This provides a more verbose description.",
1493 },
d5d10f85
WB
1494 format_description => {
1495 type => "string",
1496 optional => 1,
1497 description => "This provides a shorter (usually just one word) description for a property used to generate descriptions for comma separated list property strings.",
1498 },
166e27c7
WB
1499 title => {
1500 type => "string",
e143e9d8 1501 optional => 1,
166e27c7
WB
1502 description => "This provides the title of the property",
1503 },
03c1e2a0
DM
1504 renderer => {
1505 type => "string",
1506 optional => 1,
1507 description => "This is used to provide rendering hints to format cli command output.",
1508 },
166e27c7
WB
1509 requires => {
1510 type => [ "string", "object" ],
e143e9d8 1511 optional => 1,
166e27c7
WB
1512 description => "indicates a required property or a schema that must be validated if this property is present",
1513 },
1514 format => {
2f9e609a 1515 type => [ "string", "object" ],
e143e9d8 1516 optional => 1,
166e27c7
WB
1517 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",
1518 },
095b88fd
WB
1519 default_key => {
1520 type => "boolean",
1521 optional => 1,
1522 description => "Whether this is the default key in a comma separated list property string.",
1523 },
303a9b34
WB
1524 alias => {
1525 type => 'string',
1526 optional => 1,
1527 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.",
1528 },
bf27456b 1529 keyAlias => {
445e8267
WB
1530 type => 'string',
1531 optional => 1,
bf27456b
DM
1532 description => "Allows to store the current 'key' as value of another property. Only valid if used together with 'alias'.",
1533 requires => 'alias',
445e8267 1534 },
e143e9d8
DM
1535 default => {
1536 type => "any",
1537 optional => 1,
1538 description => "This indicates the default for the instance property."
1539 },
166e27c7 1540 completion => {
7829989f
DM
1541 type => 'coderef',
1542 description => "Bash completion function. This function should return a list of possible values.",
1543 optional => 1,
166e27c7
WB
1544 },
1545 disallow => {
1546 type => "object",
e143e9d8 1547 optional => 1,
166e27c7 1548 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 1549 },
166e27c7
WB
1550 extends => {
1551 type => "object",
e143e9d8 1552 optional => 1,
166e27c7 1553 description => "This indicates the schema extends the given schema. All instances of this schema must be valid to by the extended schema also.",
e143e9d8 1554 default => {},
166e27c7
WB
1555 },
1556 # this is from hyper schema
1557 links => {
1558 type => "array",
1559 description => "This defines the link relations of the instance objects",
1560 optional => 1,
e143e9d8 1561 items => {
166e27c7
WB
1562 type => "object",
1563 properties => {
1564 href => {
1565 type => "string",
1566 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",
1567 },
1568 rel => {
1569 type => "string",
1570 description => "This is the name of the link relation",
1571 optional => 1,
1572 default => "full",
1573 },
e143e9d8 1574 method => {
166e27c7
WB
1575 type => "string",
1576 description => "For submission links, this defines the method that should be used to access the target resource",
1577 optional => 1,
1578 default => "GET",
e143e9d8
DM
1579 },
1580 },
1581 },
1582 },
f8d4eff9
SI
1583 print_width => {
1584 type => "integer",
1585 description => "For CLI context, this defines the maximal width to print before truncating",
1586 optional => 1,
1587 },
9bbc4e17 1588 }
e143e9d8
DM
1589};
1590
1591my $default_schema = Storable::dclone($default_schema_noref);
1592
1593$default_schema->{properties}->{properties}->{additionalProperties} = $default_schema;
1594$default_schema->{properties}->{additionalProperties}->{properties} = $default_schema->{properties};
94258346 1595$default_schema->{properties}->{oneOf}->{items}->{properties} = $default_schema->{properties};
e143e9d8
DM
1596
1597$default_schema->{properties}->{items}->{properties} = $default_schema->{properties};
1598$default_schema->{properties}->{items}->{additionalProperties} = 0;
1599
1600$default_schema->{properties}->{disallow}->{properties} = $default_schema->{properties};
1601$default_schema->{properties}->{disallow}->{additionalProperties} = 0;
1602
1603$default_schema->{properties}->{requires}->{properties} = $default_schema->{properties};
1604$default_schema->{properties}->{requires}->{additionalProperties} = 0;
1605
1606$default_schema->{properties}->{extends}->{properties} = $default_schema->{properties};
1607$default_schema->{properties}->{extends}->{additionalProperties} = 0;
1608
1609my $method_schema = {
1610 type => "object",
1611 additionalProperties => 0,
1612 properties => {
1613 description => {
1614 description => "This a description of the method",
1615 optional => 1,
1616 },
1617 name => {
1618 type => 'string',
1619 description => "This indicates the name of the function to call.",
1620 optional => 1,
1621 requires => {
1622 additionalProperties => 1,
1623 properties => {
1624 name => {},
1625 description => {},
1626 code => {},
1627 method => {},
1628 parameters => {},
1629 path => {},
1630 parameters => {},
1631 returns => {},
9bbc4e17 1632 }
e143e9d8
DM
1633 },
1634 },
1635 method => {
1636 type => 'string',
1637 description => "The HTTP method name.",
1638 enum => [ 'GET', 'POST', 'PUT', 'DELETE' ],
1639 optional => 1,
1640 },
1641 protected => {
1642 type => 'boolean',
9bbc4e17 1643 description => "Method needs special privileges - only pvedaemon can execute it",
e143e9d8
DM
1644 optional => 1,
1645 },
4c72ade0
FG
1646 allowtoken => {
1647 type => 'boolean',
1648 description => "Method is available for clients authenticated using an API token.",
1649 optional => 1,
1650 default => 1,
1651 },
62a8f27b
DM
1652 download => {
1653 type => 'boolean',
1654 description => "Method downloads the file content (filename is the return value of the method).",
1655 optional => 1,
1656 },
e143e9d8
DM
1657 proxyto => {
1658 type => 'string',
1659 description => "A parameter name. If specified, all calls to this method are proxied to the host contained in that parameter.",
1660 optional => 1,
1661 },
031efdd0
DM
1662 proxyto_callback => {
1663 type => 'coderef',
fb3a1b29 1664 description => "A function which is called to resolve the proxyto attribute. The default implementation returns the value of the 'proxyto' parameter.",
031efdd0
DM
1665 optional => 1,
1666 },
e143e9d8
DM
1667 permissions => {
1668 type => 'object',
1669 description => "Required access permissions. By default only 'root' is allowed to access this method.",
1670 optional => 1,
1671 additionalProperties => 0,
1672 properties => {
b18d1722
DM
1673 description => {
1674 description => "Describe access permissions.",
1675 optional => 1,
1676 },
e143e9d8 1677 user => {
9bbc4e17
TL
1678 description => "A simply way to allow access for 'all' authenticated users. Value 'world' is used to allow access without credentials.",
1679 type => 'string',
b18d1722 1680 enum => ['all', 'world'],
e143e9d8
DM
1681 optional => 1,
1682 },
b18d1722
DM
1683 check => {
1684 description => "Array of permission checks (prefix notation).",
9bbc4e17
TL
1685 type => 'array',
1686 optional => 1
b18d1722 1687 },
e143e9d8
DM
1688 },
1689 },
1690 match_name => {
1691 description => "Used internally",
1692 optional => 1,
1693 },
1694 match_re => {
1695 description => "Used internally",
1696 optional => 1,
1697 },
1698 path => {
1699 type => 'string',
1700 description => "path for URL matching (uri template)",
1701 },
1702 fragmentDelimiter => {
1703 type => 'string',
fb3a1b29 1704 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
1705 optional => 1,
1706 },
1707 parameters => {
1708 type => 'object',
1709 description => "JSON Schema for parameters.",
1710 optional => 1,
1711 },
1712 returns => {
1713 type => 'object',
1714 description => "JSON Schema for return value.",
1715 optional => 1,
1716 },
1717 code => {
1718 type => 'coderef',
fb3a1b29 1719 description => "method implementation (code reference)",
e143e9d8
DM
1720 optional => 1,
1721 },
1722 subclass => {
1723 type => 'string',
1724 description => "Delegate call to this class (perl class string).",
1725 optional => 1,
1726 requires => {
1727 additionalProperties => 0,
1728 properties => {
1729 subclass => {},
1730 path => {},
1731 match_name => {},
1732 match_re => {},
1733 fragmentDelimiter => { optional => 1 }
9bbc4e17 1734 }
e143e9d8 1735 },
9bbc4e17 1736 },
e143e9d8
DM
1737 },
1738
1739};
1740
1741sub validate_schema {
9bbc4e17 1742 my ($schema) = @_;
e143e9d8
DM
1743
1744 my $errmsg = "internal error - unable to verify schema\n";
1745 validate($schema, $default_schema, $errmsg);
1746}
1747
1748sub validate_method_info {
1749 my $info = shift;
1750
1751 my $errmsg = "internal error - unable to verify method info\n";
1752 validate($info, $method_schema, $errmsg);
9bbc4e17 1753
e143e9d8
DM
1754 validate_schema($info->{parameters}) if $info->{parameters};
1755 validate_schema($info->{returns}) if $info->{returns};
1756}
1757
1758# run a self test on load
9bbc4e17 1759# make sure we can verify the default schema
e143e9d8
DM
1760validate_schema($default_schema_noref);
1761validate_schema($method_schema);
1762
1763# and now some utility methods (used by pve api)
1764sub method_get_child_link {
1765 my ($info) = @_;
1766
1767 return undef if !$info;
1768
1769 my $schema = $info->{returns};
1770 return undef if !$schema || !$schema->{type} || $schema->{type} ne 'array';
1771
1772 my $links = $schema->{links};
1773 return undef if !$links;
1774
1775 my $found;
1776 foreach my $lnk (@$links) {
1777 if ($lnk->{href} && $lnk->{rel} && ($lnk->{rel} eq 'child')) {
1778 $found = $lnk;
1779 last;
1780 }
1781 }
1782
1783 return $found;
1784}
1785
9bbc4e17 1786# a way to parse command line parameters, using a
e143e9d8
DM
1787# schema to configure Getopt::Long
1788sub get_options {
4842b651 1789 my ($schema, $args, $arg_param, $fixed_param, $param_mapping_hash) = @_;
e143e9d8
DM
1790
1791 if (!$schema || !$schema->{properties}) {
1792 raise("too many arguments\n", code => HTTP_BAD_REQUEST)
1793 if scalar(@$args) != 0;
1794 return {};
1795 }
1796
0ce82909
DM
1797 my $list_param;
1798 if ($arg_param && !ref($arg_param)) {
1799 my $pd = $schema->{properties}->{$arg_param};
1800 die "expected list format $pd->{format}"
1801 if !($pd && $pd->{format} && $pd->{format} =~ m/-list/);
1802 $list_param = $arg_param;
1803 }
1804
c7171ff2 1805 my @interactive = ();
e143e9d8
DM
1806 my @getopt = ();
1807 foreach my $prop (keys %{$schema->{properties}}) {
1808 my $pd = $schema->{properties}->{$prop};
aab47b58 1809 next if $list_param && $prop eq $list_param;
0ce82909 1810 next if defined($fixed_param->{$prop});
e143e9d8 1811
c7171ff2
WB
1812 my $mapping = $param_mapping_hash->{$prop};
1813 if ($mapping && $mapping->{interactive}) {
1814 # interactive parameters such as passwords: make the argument
1815 # optional and call the mapping function afterwards.
1816 push @getopt, "$prop:s";
1817 push @interactive, [$prop, $mapping->{func}];
94258346 1818 } elsif ($pd->{type} && $pd->{type} eq 'boolean') {
e143e9d8
DM
1819 push @getopt, "$prop:s";
1820 } else {
a26f3858 1821 if ($pd->{format} && $pd->{format} =~ m/-list/) {
8ba7c72b 1822 push @getopt, "$prop=s@";
94258346 1823 } elsif ($pd->{type} && $pd->{type} eq 'array') {
07f136d6 1824 push @getopt, "$prop=s@";
8ba7c72b
DM
1825 } else {
1826 push @getopt, "$prop=s";
1827 }
e143e9d8
DM
1828 }
1829 }
1830
1068aeb3
WB
1831 Getopt::Long::Configure('prefix_pattern=(--|-)');
1832
e143e9d8
DM
1833 my $opts = {};
1834 raise("unable to parse option\n", code => HTTP_BAD_REQUEST)
1835 if !Getopt::Long::GetOptionsFromArray($args, $opts, @getopt);
1d21344c 1836
5851be88 1837 if (@$args) {
0ce82909
DM
1838 if ($list_param) {
1839 $opts->{$list_param} = $args;
1840 $args = [];
1841 } elsif (ref($arg_param)) {
804bc621
TL
1842 for (my $i = 0; $i < scalar(@$arg_param); $i++) {
1843 my $arg_name = $arg_param->[$i];
5851be88
WB
1844 if ($opts->{'extra-args'}) {
1845 raise("internal error: extra-args must be the last argument\n", code => HTTP_BAD_REQUEST);
1846 }
1847 if ($arg_name eq 'extra-args') {
1848 $opts->{'extra-args'} = $args;
1849 $args = [];
1850 next;
1851 }
804bc621
TL
1852 if (!@$args) {
1853 # check if all left-over arg_param are optional, else we
1854 # must die as the mapping is then ambigious
26764d7c
WB
1855 for (; $i < scalar(@$arg_param); $i++) {
1856 my $prop = $arg_param->[$i];
804bc621
TL
1857 raise("not enough arguments\n", code => HTTP_BAD_REQUEST)
1858 if !$schema->{properties}->{$prop}->{optional};
1859 }
26764d7c
WB
1860 if ($arg_param->[-1] eq 'extra-args') {
1861 $opts->{'extra-args'} = [];
1862 }
1863 last;
804bc621 1864 }
5851be88 1865 $opts->{$arg_name} = shift @$args;
0ce82909 1866 }
5851be88 1867 raise("too many arguments\n", code => HTTP_BAD_REQUEST) if @$args;
0ce82909
DM
1868 } else {
1869 raise("too many arguments\n", code => HTTP_BAD_REQUEST)
1870 if scalar(@$args) != 0;
1871 }
ff2bf45f
DM
1872 } else {
1873 if (ref($arg_param)) {
1874 foreach my $arg_name (@$arg_param) {
1875 if ($arg_name eq 'extra-args') {
1876 $opts->{'extra-args'} = [];
3fe29ce6 1877 } elsif (!$schema->{properties}->{$arg_name}->{optional}) {
ff2bf45f
DM
1878 raise("not enough arguments\n", code => HTTP_BAD_REQUEST);
1879 }
1880 }
1881 }
1d21344c
DM
1882 }
1883
c7171ff2
WB
1884 foreach my $entry (@interactive) {
1885 my ($opt, $func) = @$entry;
1886 my $pd = $schema->{properties}->{$opt};
1887 my $value = $opts->{$opt};
1888 if (defined($value) || !$pd->{optional}) {
1889 $opts->{$opt} = $func->($value);
1890 }
1891 }
1892
c9902568 1893 # decode after Getopt as we are not sure how well it handles unicode
24197a9f 1894 foreach my $p (keys %$opts) {
c9902568
TL
1895 if (!ref($opts->{$p})) {
1896 $opts->{$p} = decode('locale', $opts->{$p});
1897 } elsif (ref($opts->{$p}) eq 'ARRAY') {
1898 my $tmp = [];
1899 foreach my $v (@{$opts->{$p}}) {
1900 push @$tmp, decode('locale', $v);
1901 }
1902 $opts->{$p} = $tmp;
1903 } elsif (ref($opts->{$p}) eq 'SCALAR') {
1904 $opts->{$p} = decode('locale', $$opts->{$p});
1905 } else {
1906 raise("decoding options failed, unknown reference\n", code => HTTP_BAD_REQUEST);
1907 }
24197a9f 1908 }
815b2aba 1909
e143e9d8
DM
1910 foreach my $p (keys %$opts) {
1911 if (my $pd = $schema->{properties}->{$p}) {
94258346 1912 if ($pd->{type} && $pd->{type} eq 'boolean') {
e143e9d8
DM
1913 if ($opts->{$p} eq '') {
1914 $opts->{$p} = 1;
1b71e564
WB
1915 } elsif (defined(my $bool = parse_boolean($opts->{$p}))) {
1916 $opts->{$p} = $bool;
e143e9d8
DM
1917 } else {
1918 raise("unable to parse boolean option\n", code => HTTP_BAD_REQUEST);
1919 }
23dc9401 1920 } elsif ($pd->{format}) {
8ba7c72b 1921
23dc9401 1922 if ($pd->{format} =~ m/-list/) {
8ba7c72b 1923 # allow --vmid 100 --vmid 101 and --vmid 100,101
23dc9401 1924 # allow --dow mon --dow fri and --dow mon,fri
43479146 1925 $opts->{$p} = join(",", @{$opts->{$p}}) if ref($opts->{$p}) eq 'ARRAY';
8ba7c72b 1926 }
e143e9d8 1927 }
9bbc4e17 1928 }
e143e9d8
DM
1929 }
1930
0ce82909
DM
1931 foreach my $p (keys %$fixed_param) {
1932 $opts->{$p} = $fixed_param->{$p};
e143e9d8
DM
1933 }
1934
1935 return $opts;
1936}
1937
1938# A way to parse configuration data by giving a json schema
2f85ab8f
WB
1939sub parse_config : prototype($$$;$) {
1940 my ($schema, $filename, $raw, $comment_key) = @_;
e143e9d8
DM
1941
1942 # do fast check (avoid validate_schema($schema))
9bbc4e17 1943 die "got strange schema" if !$schema->{type} ||
e143e9d8
DM
1944 !$schema->{properties} || $schema->{type} ne 'object';
1945
1946 my $cfg = {};
1947
2f85ab8f
WB
1948 my $comment_data;
1949 my $handle_comment = sub { $_[0] =~ /^#/ };
1950 if (defined($comment_key)) {
1951 $comment_data = '';
1952 my $comment_re = qr/^\Q$comment_key\E:\s*(.*\S)\s*$/;
1953 $handle_comment = sub {
1954 if ($_[0] =~ /^\#(.*)\s*$/ || $_[0] =~ $comment_re) {
1955 $comment_data .= PVE::Tools::decode_text($1) . "\n";
1956 return 1;
1957 }
1958 return undef;
1959 };
1960 }
1961
3c4d612a 1962 while ($raw =~ /^\s*(.+?)\s*$/gm) {
e143e9d8 1963 my $line = $1;
e143e9d8 1964
2f85ab8f 1965 next if $handle_comment->($line);
3c4d612a
WB
1966
1967 if ($line =~ m/^(\S+?):\s*(.*)$/) {
e143e9d8
DM
1968 my $key = $1;
1969 my $value = $2;
9bbc4e17 1970 if ($schema->{properties}->{$key} &&
e143e9d8
DM
1971 $schema->{properties}->{$key}->{type} eq 'boolean') {
1972
1b71e564 1973 $value = parse_boolean($value) // $value;
e143e9d8 1974 }
07f136d6
DC
1975 if (
1976 $schema->{properties}->{$key}
1977 && $schema->{properties}->{$key}->{type} eq 'array'
1978 ) {
1979
1980 $cfg->{$key} //= [];
1981 push $cfg->{$key}->@*, $value;
1982 next;
1983 }
e143e9d8
DM
1984 $cfg->{$key} = $value;
1985 } else {
1986 warn "ignore config line: $line\n"
1987 }
1988 }
1989
2f85ab8f
WB
1990 if (defined($comment_data)) {
1991 $cfg->{$comment_key} = $comment_data;
1992 }
1993
e143e9d8
DM
1994 my $errors = {};
1995 check_prop($cfg, $schema, '', $errors);
1996
1997 foreach my $k (keys %$errors) {
1998 warn "parse error in '$filename' - '$k': $errors->{$k}\n";
1999 delete $cfg->{$k};
9bbc4e17 2000 }
e143e9d8
DM
2001
2002 return $cfg;
2003}
2004
2005# generate simple key/value file
2006sub dump_config {
2007 my ($schema, $filename, $cfg) = @_;
2008
2009 # do fast check (avoid validate_schema($schema))
9bbc4e17 2010 die "got strange schema" if !$schema->{type} ||
e143e9d8
DM
2011 !$schema->{properties} || $schema->{type} ne 'object';
2012
2013 validate($cfg, $schema, "validation error in '$filename'\n");
2014
2015 my $data = '';
2016
821d408d 2017 foreach my $k (sort keys %$cfg) {
e143e9d8
DM
2018 $data .= "$k: $cfg->{$k}\n";
2019 }
2020
2021 return $data;
2022}
2023
bf27456b
DM
2024# helpers used to generate our manual pages
2025
2026my $find_schema_default_key = sub {
2027 my ($format) = @_;
2028
2029 my $default_key;
2030 my $keyAliasProps = {};
2031
2032 foreach my $key (keys %$format) {
2033 my $phash = $format->{$key};
2034 if ($phash->{default_key}) {
2035 die "multiple default keys in schema ($default_key, $key)\n"
2036 if defined($default_key);
2037 die "default key '$key' is an alias - this is not allowed\n"
2038 if defined($phash->{alias});
2039 die "default key '$key' with keyAlias attribute is not allowed\n"
2040 if $phash->{keyAlias};
bf27456b
DM
2041 $default_key = $key;
2042 }
2043 my $key_alias = $phash->{keyAlias};
c88c582d
DM
2044 die "found keyAlias without 'alias definition for '$key'\n"
2045 if $key_alias && !$phash->{alias};
2046
bf27456b
DM
2047 if ($phash->{alias} && $key_alias) {
2048 die "inconsistent keyAlias '$key_alias' definition"
2049 if defined($keyAliasProps->{$key_alias}) &&
2050 $keyAliasProps->{$key_alias} ne $phash->{alias};
2051 $keyAliasProps->{$key_alias} = $phash->{alias};
2052 }
2053 }
2054
2055 return wantarray ? ($default_key, $keyAliasProps) : $default_key;
2056};
2057
2058sub generate_typetext {
abc1afd8 2059 my ($format, $list_enums) = @_;
bf27456b 2060
d8c2b947 2061 my ($default_key, $keyAliasProps) = &$find_schema_default_key($format);
bf27456b
DM
2062
2063 my $res = '';
2064 my $add_sep = 0;
2065
2066 my $add_option_string = sub {
2067 my ($text, $optional) = @_;
2068
2069 if ($add_sep) {
2070 $text = ",$text";
2071 $res .= ' ';
2072 }
2073 $text = "[$text]" if $optional;
2074 $res .= $text;
2075 $add_sep = 1;
2076 };
2077
2078 my $format_key_value = sub {
2079 my ($key, $phash) = @_;
2080
2081 die "internal error" if defined($phash->{alias});
2082
2083 my $keytext = $key;
2084
2085 my $typetext = '';
2086
2087 if (my $desc = $phash->{format_description}) {
2088 $typetext .= "<$desc>";
2089 } elsif (my $text = $phash->{typetext}) {
2090 $typetext .= $text;
2091 } elsif (my $enum = $phash->{enum}) {
abc1afd8
DM
2092 if ($list_enums || (scalar(@$enum) <= 3)) {
2093 $typetext .= '<' . join('|', @$enum) . '>';
2094 } else {
2095 $typetext .= '<enum>';
2096 }
bf27456b
DM
2097 } elsif ($phash->{type} eq 'boolean') {
2098 $typetext .= '<1|0>';
2099 } elsif ($phash->{type} eq 'integer') {
2100 $typetext .= '<integer>';
2101 } elsif ($phash->{type} eq 'number') {
2102 $typetext .= '<number>';
2103 } else {
2104 die "internal error: neither format_description nor typetext found for option '$key'";
2105 }
2106
2107 if (defined($default_key) && ($default_key eq $key)) {
2108 &$add_option_string("[$keytext=]$typetext", $phash->{optional});
2109 } else {
2110 &$add_option_string("$keytext=$typetext", $phash->{optional});
2111 }
2112 };
2113
d8c2b947 2114 my $done = {};
bf27456b 2115
d8c2b947
DM
2116 my $cond_add_key = sub {
2117 my ($key) = @_;
2118
2119 return if $done->{$key}; # avoid duplicates
2120
2121 $done->{$key} = 1;
bf27456b
DM
2122
2123 my $phash = $format->{$key};
2124
d8c2b947
DM
2125 return if !$phash; # should not happen
2126
2127 return if $phash->{alias};
bf27456b
DM
2128
2129 &$format_key_value($key, $phash);
2130
d8c2b947
DM
2131 };
2132
2133 &$cond_add_key($default_key) if defined($default_key);
2134
2135 # add required keys first
2136 foreach my $key (sort keys %$format) {
2137 my $phash = $format->{$key};
2138 &$cond_add_key($key) if $phash && !$phash->{optional};
2139 }
2140
2141 # add the rest
2142 foreach my $key (sort keys %$format) {
2143 &$cond_add_key($key);
2144 }
2145
2146 foreach my $keyAlias (sort keys %$keyAliasProps) {
2147 &$add_option_string("<$keyAlias>=<$keyAliasProps->{$keyAlias }>", 1);
bf27456b
DM
2148 }
2149
2150 return $res;
2151}
2152
2153sub print_property_string {
2154 my ($data, $format, $skip, $path) = @_;
2155
d500c038 2156 my $validator;
bf27456b
DM
2157 if (ref($format) ne 'HASH') {
2158 my $schema = get_format($format);
2159 die "not a valid format: $format\n" if !$schema;
d500c038
SR
2160 # named formats can have validators attached
2161 $validator = $format_validators->{$format};
bf27456b
DM
2162 $format = $schema;
2163 }
2164
2165 my $errors = {};
2166 check_object($path, $format, $data, undef, $errors);
2167 if (scalar(%$errors)) {
2168 raise "format error", errors => $errors;
2169 }
2170
d500c038
SR
2171 $data = $validator->($data) if $validator;
2172
bf27456b
DM
2173 my ($default_key, $keyAliasProps) = &$find_schema_default_key($format);
2174
2175 my $res = '';
2176 my $add_sep = 0;
2177
2178 my $add_option_string = sub {
2179 my ($text) = @_;
2180
2181 $res .= ',' if $add_sep;
2182 $res .= $text;
2183 $add_sep = 1;
2184 };
2185
2186 my $format_value = sub {
2187 my ($key, $value, $format) = @_;
2188
2189 if (defined($format) && ($format eq 'disk-size')) {
2190 return format_size($value);
2191 } else {
2192 die "illegal value with commas for $key\n" if $value =~ /,/;
2193 return $value;
2194 }
2195 };
2196
2289890b 2197 my $done = { map { $_ => 1 } @$skip };
bf27456b
DM
2198
2199 my $cond_add_key = sub {
971353e8 2200 my ($key, $isdefault) = @_;
bf27456b
DM
2201
2202 return if $done->{$key}; # avoid duplicates
2203
2204 $done->{$key} = 1;
2205
2206 my $value = $data->{$key};
2207
2208 return if !defined($value);
2209
2210 my $phash = $format->{$key};
2211
2212 # try to combine values if we have key aliases
2213 if (my $combine = $keyAliasProps->{$key}) {
2214 if (defined(my $combine_value = $data->{$combine})) {
2215 my $combine_format = $format->{$combine}->{format};
2216 my $value_str = &$format_value($key, $value, $phash->{format});
2217 my $combine_str = &$format_value($combine, $combine_value, $combine_format);
2218 &$add_option_string("${value_str}=${combine_str}");
2219 $done->{$combine} = 1;
2220 return;
2221 }
2222 }
2223
2224 if ($phash && $phash->{alias}) {
2225 $phash = $format->{$phash->{alias}};
2226 }
2227
2228 die "invalid key '$key'\n" if !$phash;
2229 die "internal error" if defined($phash->{alias});
2230
2231 my $value_str = &$format_value($key, $value, $phash->{format});
971353e8
WB
2232 if ($isdefault) {
2233 &$add_option_string($value_str);
2234 } else {
2235 &$add_option_string("$key=${value_str}");
2236 }
bf27456b
DM
2237 };
2238
2239 # add default key first
971353e8 2240 &$cond_add_key($default_key, 1) if defined($default_key);
bf27456b 2241
d8c2b947
DM
2242 # add required keys first
2243 foreach my $key (sort keys %$data) {
2244 my $phash = $format->{$key};
2245 &$cond_add_key($key) if $phash && !$phash->{optional};
2246 }
2247
2248 # add the rest
bf27456b
DM
2249 foreach my $key (sort keys %$data) {
2250 &$cond_add_key($key);
2251 }
2252
2253 return $res;
2254}
2255
2256sub schema_get_type_text {
abc1afd8 2257 my ($phash, $style) = @_;
bf27456b 2258
32f8e0c7
DM
2259 my $type = $phash->{type} || 'string';
2260
bf27456b
DM
2261 if ($phash->{typetext}) {
2262 return $phash->{typetext};
2263 } elsif ($phash->{format_description}) {
2264 return "<$phash->{format_description}>";
2265 } elsif ($phash->{enum}) {
25d9bda9 2266 return "<" . join(' | ', sort @{$phash->{enum}}) . ">";
bf27456b
DM
2267 } elsif ($phash->{pattern}) {
2268 return $phash->{pattern};
32f8e0c7 2269 } elsif ($type eq 'integer' || $type eq 'number') {
05185ea2 2270 # NOTE: always access values as number (avoid converion to string)
bf27456b 2271 if (defined($phash->{minimum}) && defined($phash->{maximum})) {
25d9bda9 2272 return "<$type> (" . ($phash->{minimum} + 0) . " - " .
05185ea2 2273 ($phash->{maximum} + 0) . ")";
bf27456b 2274 } elsif (defined($phash->{minimum})) {
25d9bda9 2275 return "<$type> (" . ($phash->{minimum} + 0) . " - N)";
bf27456b 2276 } elsif (defined($phash->{maximum})) {
25d9bda9 2277 return "<$type> (-N - " . ($phash->{maximum} + 0) . ")";
bf27456b 2278 }
32f8e0c7 2279 } elsif ($type eq 'string') {
bf27456b
DM
2280 if (my $format = $phash->{format}) {
2281 $format = get_format($format) if ref($format) ne 'HASH';
2282 if (ref($format) eq 'HASH') {
abc1afd8
DM
2283 my $list_enums = 0;
2284 $list_enums = 1 if $style && $style eq 'config-sub';
2285 return generate_typetext($format, $list_enums);
bf27456b
DM
2286 }
2287 }
2288 }
2289
25d9bda9 2290 return "<$type>";
bf27456b
DM
2291}
2292
e143e9d8 22931;