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