]> git.proxmox.com Git - pve-common.git/blame - src/PVE/JSONSchema.pm
JSONSchema: increase maxLength of config-digest to 64
[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
1090sub check_object {
1091 my ($path, $schema, $value, $additional_properties, $errors) = @_;
1092
1093 # print "Check Object " . Dumper($value) . "\nSchema: " . Dumper($schema);
1094
1095 my $st = ref($schema);
1096 if (!$st || $st ne 'HASH') {
1097 add_error($errors, $path, "Invalid schema definition.");
1098 return;
1099 }
1100
1101 my $vt = ref($value);
1102 if (!$vt || $vt ne 'HASH') {
1103 add_error($errors, $path, "an object is required");
1104 return;
1105 }
1106
1107 foreach my $k (keys %$schema) {
bf27456b 1108 check_prop($value->{$k}, $schema->{$k}, $path ? "$path.$k" : $k, $errors);
e143e9d8
DM
1109 }
1110
1111 foreach my $k (keys %$value) {
1112
1113 my $newpath = $path ? "$path.$k" : $k;
1114
1115 if (my $subschema = $schema->{$k}) {
1116 if (my $requires = $subschema->{requires}) {
1117 if (ref($requires)) {
1118 #print "TEST: " . Dumper($value) . "\n", Dumper($requires) ;
1119 check_prop($value, $requires, $path, $errors);
1120 } elsif (!defined($value->{$requires})) {
9bbc4e17 1121 add_error($errors, $path ? "$path.$requires" : $requires,
8b6e737a 1122 "missing property - '$newpath' requires this property");
e143e9d8
DM
1123 }
1124 }
1125
1126 next; # value is already checked above
1127 }
1128
1129 if (defined ($additional_properties) && !$additional_properties) {
1130 add_error($errors, $newpath, "property is not defined in schema " .
1131 "and the schema does not allow additional properties");
1132 next;
1133 }
1134 check_prop($value->{$k}, $additional_properties, $newpath, $errors)
1135 if ref($additional_properties);
1136 }
1137}
1138
86425a09
WB
1139sub check_object_warn {
1140 my ($path, $schema, $value, $additional_properties) = @_;
1141 my $errors = {};
1142 check_object($path, $schema, $value, $additional_properties, $errors);
1143 if (scalar(%$errors)) {
1144 foreach my $k (keys %$errors) {
1145 warn "parse error: $k: $errors->{$k}\n";
1146 }
1147 return 0;
1148 }
1149 return 1;
1150}
1151
e143e9d8
DM
1152sub check_prop {
1153 my ($value, $schema, $path, $errors) = @_;
1154
1155 die "internal error - no schema" if !$schema;
1156 die "internal error" if !$errors;
1157
1158 #print "check_prop $path\n" if $value;
1159
1160 my $st = ref($schema);
1161 if (!$st || $st ne 'HASH') {
1162 add_error($errors, $path, "Invalid schema definition.");
1163 return;
1164 }
1165
1166 # if it extends another schema, it must pass that schema as well
1167 if($schema->{extends}) {
1168 check_prop($value, $schema->{extends}, $path, $errors);
1169 }
1170
1171 if (!defined ($value)) {
1172 return if $schema->{type} && $schema->{type} eq 'null';
445e8267 1173 if (!$schema->{optional} && !$schema->{alias} && !$schema->{group}) {
e143e9d8
DM
1174 add_error($errors, $path, "property is missing and it is not optional");
1175 }
1176 return;
1177 }
1178
1179 return if !check_type($path, $schema->{type}, $value, $errors);
1180
1181 if ($schema->{disallow}) {
1182 my $tmperr = {};
1183 if (check_type($path, $schema->{disallow}, $value, $tmperr)) {
1184 add_error($errors, $path, "disallowed value was matched");
1185 return;
1186 }
1187 }
1188
1189 if (my $vt = ref($value)) {
1190
1191 if ($vt eq 'ARRAY') {
1192 if ($schema->{items}) {
1193 my $it = ref($schema->{items});
1194 if ($it && $it eq 'ARRAY') {
1195 #die "implement me $path: $vt " . Dumper($schema) ."\n". Dumper($value);
1196 die "not implemented";
1197 } else {
1198 my $ind = 0;
1199 foreach my $el (@$value) {
1200 check_prop($el, $schema->{items}, "${path}[$ind]", $errors);
1201 $ind++;
1202 }
1203 }
1204 }
9bbc4e17 1205 return;
e143e9d8
DM
1206 } elsif ($schema->{properties} || $schema->{additionalProperties}) {
1207 check_object($path, defined($schema->{properties}) ? $schema->{properties} : {},
1208 $value, $schema->{additionalProperties}, $errors);
1209 return;
1210 }
1211
1212 } else {
1213
1214 if (my $format = $schema->{format}) {
2f9e609a 1215 eval { check_format($format, $value, $path); };
e143e9d8
DM
1216 if ($@) {
1217 add_error($errors, $path, "invalid format - $@");
1218 return;
1219 }
1220 }
1221
1222 if (my $pattern = $schema->{pattern}) {
1223 if ($value !~ m/^$pattern$/) {
1224 add_error($errors, $path, "value does not match the regex pattern");
1225 return;
1226 }
1227 }
1228
1229 if (defined (my $max = $schema->{maxLength})) {
1230 if (length($value) > $max) {
1231 add_error($errors, $path, "value may only be $max characters long");
1232 return;
1233 }
1234 }
1235
1236 if (defined (my $min = $schema->{minLength})) {
1237 if (length($value) < $min) {
1238 add_error($errors, $path, "value must be at least $min characters long");
1239 return;
1240 }
1241 }
9bbc4e17 1242
e143e9d8
DM
1243 if (is_number($value)) {
1244 if (defined (my $max = $schema->{maximum})) {
9bbc4e17 1245 if ($value > $max) {
e143e9d8
DM
1246 add_error($errors, $path, "value must have a maximum value of $max");
1247 return;
1248 }
1249 }
1250
1251 if (defined (my $min = $schema->{minimum})) {
9bbc4e17 1252 if ($value < $min) {
e143e9d8
DM
1253 add_error($errors, $path, "value must have a minimum value of $min");
1254 return;
1255 }
1256 }
1257 }
1258
1259 if (my $ea = $schema->{enum}) {
1260
1261 my $found;
1262 foreach my $ev (@$ea) {
1263 if ($ev eq $value) {
1264 $found = 1;
1265 last;
1266 }
1267 }
1268 if (!$found) {
1269 add_error($errors, $path, "value '$value' does not have a value in the enumeration '" .
1270 join(", ", @$ea) . "'");
1271 }
1272 }
1273 }
1274}
1275
1276sub validate {
1277 my ($instance, $schema, $errmsg) = @_;
1278
1279 my $errors = {};
1280 $errmsg = "Parameter verification failed.\n" if !$errmsg;
1281
1282 # todo: cycle detection is only needed for debugging, I guess
1283 # we can disable that in the final release
1284 # todo: is there a better/faster way to detect cycles?
1285 my $cycles = 0;
6ab98c4e
SR
1286 # 'download' responses can contain a filehandle, don't cycle-check that as
1287 # it produces a warning
1288 my $is_download = ref($instance) eq 'HASH' && exists($instance->{download});
1289 find_cycle($instance, sub { $cycles = 1 }) if !$is_download;
e143e9d8
DM
1290 if ($cycles) {
1291 add_error($errors, undef, "data structure contains recursive cycles");
1292 } elsif ($schema) {
1293 check_prop($instance, $schema, '', $errors);
1294 }
9bbc4e17 1295
e143e9d8
DM
1296 if (scalar(%$errors)) {
1297 raise $errmsg, code => HTTP_BAD_REQUEST, errors => $errors;
1298 }
1299
1300 return 1;
1301}
1302
1303my $schema_valid_types = ["string", "object", "coderef", "array", "boolean", "number", "integer", "null", "any"];
1304my $default_schema_noref = {
1305 description => "This is the JSON Schema for JSON Schemas.",
1306 type => [ "object" ],
1307 additionalProperties => 0,
1308 properties => {
1309 type => {
1310 type => ["string", "array"],
1311 description => "This is a type definition value. This can be a simple type, or a union type",
1312 optional => 1,
1313 default => "any",
1314 items => {
1315 type => "string",
1316 enum => $schema_valid_types,
1317 },
1318 enum => $schema_valid_types,
1319 },
1320 optional => {
1321 type => "boolean",
1322 description => "This indicates that the instance property in the instance object is not required.",
1323 optional => 1,
1324 default => 0
1325 },
1326 properties => {
1327 type => "object",
1328 description => "This is a definition for the properties of an object value",
1329 optional => 1,
1330 default => {},
1331 },
1332 items => {
1333 type => "object",
1334 description => "When the value is an array, this indicates the schema to use to validate each item in an array",
1335 optional => 1,
1336 default => {},
1337 },
1338 additionalProperties => {
1339 type => [ "boolean", "object"],
1340 description => "This provides a default property definition for all properties that are not explicitly defined in an object type definition.",
1341 optional => 1,
1342 default => {},
1343 },
1344 minimum => {
1345 type => "number",
1346 optional => 1,
1347 description => "This indicates the minimum value for the instance property when the type of the instance value is a number.",
1348 },
1349 maximum => {
1350 type => "number",
1351 optional => 1,
1352 description => "This indicates the maximum value for the instance property when the type of the instance value is a number.",
1353 },
1354 minLength => {
1355 type => "integer",
1356 description => "When the instance value is a string, this indicates minimum length of the string",
1357 optional => 1,
1358 minimum => 0,
1359 default => 0,
9bbc4e17 1360 },
e143e9d8
DM
1361 maxLength => {
1362 type => "integer",
1363 description => "When the instance value is a string, this indicates maximum length of the string.",
1364 optional => 1,
1365 },
1366 typetext => {
1367 type => "string",
1368 optional => 1,
1369 description => "A text representation of the type (used to generate documentation).",
1370 },
1371 pattern => {
1372 type => "string",
1373 format => "regex",
166e27c7 1374 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
1375 optional => 1,
1376 default => ".*",
166e27c7 1377 },
e143e9d8
DM
1378 enum => {
1379 type => "array",
1380 optional => 1,
1381 description => "This provides an enumeration of possible values that are valid for the instance property.",
1382 },
1383 description => {
1384 type => "string",
1385 optional => 1,
1386 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).",
1387 },
32f8e0c7
DM
1388 verbose_description => {
1389 type => "string",
1390 optional => 1,
1391 description => "This provides a more verbose description.",
1392 },
d5d10f85
WB
1393 format_description => {
1394 type => "string",
1395 optional => 1,
1396 description => "This provides a shorter (usually just one word) description for a property used to generate descriptions for comma separated list property strings.",
1397 },
166e27c7
WB
1398 title => {
1399 type => "string",
e143e9d8 1400 optional => 1,
166e27c7
WB
1401 description => "This provides the title of the property",
1402 },
03c1e2a0
DM
1403 renderer => {
1404 type => "string",
1405 optional => 1,
1406 description => "This is used to provide rendering hints to format cli command output.",
1407 },
166e27c7
WB
1408 requires => {
1409 type => [ "string", "object" ],
e143e9d8 1410 optional => 1,
166e27c7
WB
1411 description => "indicates a required property or a schema that must be validated if this property is present",
1412 },
1413 format => {
2f9e609a 1414 type => [ "string", "object" ],
e143e9d8 1415 optional => 1,
166e27c7
WB
1416 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",
1417 },
095b88fd
WB
1418 default_key => {
1419 type => "boolean",
1420 optional => 1,
1421 description => "Whether this is the default key in a comma separated list property string.",
1422 },
303a9b34
WB
1423 alias => {
1424 type => 'string',
1425 optional => 1,
1426 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.",
1427 },
bf27456b 1428 keyAlias => {
445e8267
WB
1429 type => 'string',
1430 optional => 1,
bf27456b
DM
1431 description => "Allows to store the current 'key' as value of another property. Only valid if used together with 'alias'.",
1432 requires => 'alias',
445e8267 1433 },
e143e9d8
DM
1434 default => {
1435 type => "any",
1436 optional => 1,
1437 description => "This indicates the default for the instance property."
1438 },
166e27c7 1439 completion => {
7829989f
DM
1440 type => 'coderef',
1441 description => "Bash completion function. This function should return a list of possible values.",
1442 optional => 1,
166e27c7
WB
1443 },
1444 disallow => {
1445 type => "object",
e143e9d8 1446 optional => 1,
166e27c7 1447 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 1448 },
166e27c7
WB
1449 extends => {
1450 type => "object",
e143e9d8 1451 optional => 1,
166e27c7 1452 description => "This indicates the schema extends the given schema. All instances of this schema must be valid to by the extended schema also.",
e143e9d8 1453 default => {},
166e27c7
WB
1454 },
1455 # this is from hyper schema
1456 links => {
1457 type => "array",
1458 description => "This defines the link relations of the instance objects",
1459 optional => 1,
e143e9d8 1460 items => {
166e27c7
WB
1461 type => "object",
1462 properties => {
1463 href => {
1464 type => "string",
1465 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",
1466 },
1467 rel => {
1468 type => "string",
1469 description => "This is the name of the link relation",
1470 optional => 1,
1471 default => "full",
1472 },
e143e9d8 1473 method => {
166e27c7
WB
1474 type => "string",
1475 description => "For submission links, this defines the method that should be used to access the target resource",
1476 optional => 1,
1477 default => "GET",
e143e9d8
DM
1478 },
1479 },
1480 },
1481 },
f8d4eff9
SI
1482 print_width => {
1483 type => "integer",
1484 description => "For CLI context, this defines the maximal width to print before truncating",
1485 optional => 1,
1486 },
9bbc4e17 1487 }
e143e9d8
DM
1488};
1489
1490my $default_schema = Storable::dclone($default_schema_noref);
1491
1492$default_schema->{properties}->{properties}->{additionalProperties} = $default_schema;
1493$default_schema->{properties}->{additionalProperties}->{properties} = $default_schema->{properties};
1494
1495$default_schema->{properties}->{items}->{properties} = $default_schema->{properties};
1496$default_schema->{properties}->{items}->{additionalProperties} = 0;
1497
1498$default_schema->{properties}->{disallow}->{properties} = $default_schema->{properties};
1499$default_schema->{properties}->{disallow}->{additionalProperties} = 0;
1500
1501$default_schema->{properties}->{requires}->{properties} = $default_schema->{properties};
1502$default_schema->{properties}->{requires}->{additionalProperties} = 0;
1503
1504$default_schema->{properties}->{extends}->{properties} = $default_schema->{properties};
1505$default_schema->{properties}->{extends}->{additionalProperties} = 0;
1506
1507my $method_schema = {
1508 type => "object",
1509 additionalProperties => 0,
1510 properties => {
1511 description => {
1512 description => "This a description of the method",
1513 optional => 1,
1514 },
1515 name => {
1516 type => 'string',
1517 description => "This indicates the name of the function to call.",
1518 optional => 1,
1519 requires => {
1520 additionalProperties => 1,
1521 properties => {
1522 name => {},
1523 description => {},
1524 code => {},
1525 method => {},
1526 parameters => {},
1527 path => {},
1528 parameters => {},
1529 returns => {},
9bbc4e17 1530 }
e143e9d8
DM
1531 },
1532 },
1533 method => {
1534 type => 'string',
1535 description => "The HTTP method name.",
1536 enum => [ 'GET', 'POST', 'PUT', 'DELETE' ],
1537 optional => 1,
1538 },
1539 protected => {
1540 type => 'boolean',
9bbc4e17 1541 description => "Method needs special privileges - only pvedaemon can execute it",
e143e9d8
DM
1542 optional => 1,
1543 },
4c72ade0
FG
1544 allowtoken => {
1545 type => 'boolean',
1546 description => "Method is available for clients authenticated using an API token.",
1547 optional => 1,
1548 default => 1,
1549 },
62a8f27b
DM
1550 download => {
1551 type => 'boolean',
1552 description => "Method downloads the file content (filename is the return value of the method).",
1553 optional => 1,
1554 },
e143e9d8
DM
1555 proxyto => {
1556 type => 'string',
1557 description => "A parameter name. If specified, all calls to this method are proxied to the host contained in that parameter.",
1558 optional => 1,
1559 },
031efdd0
DM
1560 proxyto_callback => {
1561 type => 'coderef',
fb3a1b29 1562 description => "A function which is called to resolve the proxyto attribute. The default implementation returns the value of the 'proxyto' parameter.",
031efdd0
DM
1563 optional => 1,
1564 },
e143e9d8
DM
1565 permissions => {
1566 type => 'object',
1567 description => "Required access permissions. By default only 'root' is allowed to access this method.",
1568 optional => 1,
1569 additionalProperties => 0,
1570 properties => {
b18d1722
DM
1571 description => {
1572 description => "Describe access permissions.",
1573 optional => 1,
1574 },
e143e9d8 1575 user => {
9bbc4e17
TL
1576 description => "A simply way to allow access for 'all' authenticated users. Value 'world' is used to allow access without credentials.",
1577 type => 'string',
b18d1722 1578 enum => ['all', 'world'],
e143e9d8
DM
1579 optional => 1,
1580 },
b18d1722
DM
1581 check => {
1582 description => "Array of permission checks (prefix notation).",
9bbc4e17
TL
1583 type => 'array',
1584 optional => 1
b18d1722 1585 },
e143e9d8
DM
1586 },
1587 },
1588 match_name => {
1589 description => "Used internally",
1590 optional => 1,
1591 },
1592 match_re => {
1593 description => "Used internally",
1594 optional => 1,
1595 },
1596 path => {
1597 type => 'string',
1598 description => "path for URL matching (uri template)",
1599 },
1600 fragmentDelimiter => {
1601 type => 'string',
fb3a1b29 1602 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
1603 optional => 1,
1604 },
1605 parameters => {
1606 type => 'object',
1607 description => "JSON Schema for parameters.",
1608 optional => 1,
1609 },
1610 returns => {
1611 type => 'object',
1612 description => "JSON Schema for return value.",
1613 optional => 1,
1614 },
1615 code => {
1616 type => 'coderef',
fb3a1b29 1617 description => "method implementation (code reference)",
e143e9d8
DM
1618 optional => 1,
1619 },
1620 subclass => {
1621 type => 'string',
1622 description => "Delegate call to this class (perl class string).",
1623 optional => 1,
1624 requires => {
1625 additionalProperties => 0,
1626 properties => {
1627 subclass => {},
1628 path => {},
1629 match_name => {},
1630 match_re => {},
1631 fragmentDelimiter => { optional => 1 }
9bbc4e17 1632 }
e143e9d8 1633 },
9bbc4e17 1634 },
e143e9d8
DM
1635 },
1636
1637};
1638
1639sub validate_schema {
9bbc4e17 1640 my ($schema) = @_;
e143e9d8
DM
1641
1642 my $errmsg = "internal error - unable to verify schema\n";
1643 validate($schema, $default_schema, $errmsg);
1644}
1645
1646sub validate_method_info {
1647 my $info = shift;
1648
1649 my $errmsg = "internal error - unable to verify method info\n";
1650 validate($info, $method_schema, $errmsg);
9bbc4e17 1651
e143e9d8
DM
1652 validate_schema($info->{parameters}) if $info->{parameters};
1653 validate_schema($info->{returns}) if $info->{returns};
1654}
1655
1656# run a self test on load
9bbc4e17 1657# make sure we can verify the default schema
e143e9d8
DM
1658validate_schema($default_schema_noref);
1659validate_schema($method_schema);
1660
1661# and now some utility methods (used by pve api)
1662sub method_get_child_link {
1663 my ($info) = @_;
1664
1665 return undef if !$info;
1666
1667 my $schema = $info->{returns};
1668 return undef if !$schema || !$schema->{type} || $schema->{type} ne 'array';
1669
1670 my $links = $schema->{links};
1671 return undef if !$links;
1672
1673 my $found;
1674 foreach my $lnk (@$links) {
1675 if ($lnk->{href} && $lnk->{rel} && ($lnk->{rel} eq 'child')) {
1676 $found = $lnk;
1677 last;
1678 }
1679 }
1680
1681 return $found;
1682}
1683
9bbc4e17 1684# a way to parse command line parameters, using a
e143e9d8
DM
1685# schema to configure Getopt::Long
1686sub get_options {
4842b651 1687 my ($schema, $args, $arg_param, $fixed_param, $param_mapping_hash) = @_;
e143e9d8
DM
1688
1689 if (!$schema || !$schema->{properties}) {
1690 raise("too many arguments\n", code => HTTP_BAD_REQUEST)
1691 if scalar(@$args) != 0;
1692 return {};
1693 }
1694
0ce82909
DM
1695 my $list_param;
1696 if ($arg_param && !ref($arg_param)) {
1697 my $pd = $schema->{properties}->{$arg_param};
1698 die "expected list format $pd->{format}"
1699 if !($pd && $pd->{format} && $pd->{format} =~ m/-list/);
1700 $list_param = $arg_param;
1701 }
1702
c7171ff2 1703 my @interactive = ();
e143e9d8
DM
1704 my @getopt = ();
1705 foreach my $prop (keys %{$schema->{properties}}) {
1706 my $pd = $schema->{properties}->{$prop};
aab47b58 1707 next if $list_param && $prop eq $list_param;
0ce82909 1708 next if defined($fixed_param->{$prop});
e143e9d8 1709
c7171ff2
WB
1710 my $mapping = $param_mapping_hash->{$prop};
1711 if ($mapping && $mapping->{interactive}) {
1712 # interactive parameters such as passwords: make the argument
1713 # optional and call the mapping function afterwards.
1714 push @getopt, "$prop:s";
1715 push @interactive, [$prop, $mapping->{func}];
e143e9d8
DM
1716 } elsif ($pd->{type} eq 'boolean') {
1717 push @getopt, "$prop:s";
1718 } else {
a26f3858 1719 if ($pd->{format} && $pd->{format} =~ m/-list/) {
8ba7c72b 1720 push @getopt, "$prop=s@";
07f136d6
DC
1721 } elsif ($pd->{type} eq 'array') {
1722 push @getopt, "$prop=s@";
8ba7c72b
DM
1723 } else {
1724 push @getopt, "$prop=s";
1725 }
e143e9d8
DM
1726 }
1727 }
1728
1068aeb3
WB
1729 Getopt::Long::Configure('prefix_pattern=(--|-)');
1730
e143e9d8
DM
1731 my $opts = {};
1732 raise("unable to parse option\n", code => HTTP_BAD_REQUEST)
1733 if !Getopt::Long::GetOptionsFromArray($args, $opts, @getopt);
1d21344c 1734
5851be88 1735 if (@$args) {
0ce82909
DM
1736 if ($list_param) {
1737 $opts->{$list_param} = $args;
1738 $args = [];
1739 } elsif (ref($arg_param)) {
804bc621
TL
1740 for (my $i = 0; $i < scalar(@$arg_param); $i++) {
1741 my $arg_name = $arg_param->[$i];
5851be88
WB
1742 if ($opts->{'extra-args'}) {
1743 raise("internal error: extra-args must be the last argument\n", code => HTTP_BAD_REQUEST);
1744 }
1745 if ($arg_name eq 'extra-args') {
1746 $opts->{'extra-args'} = $args;
1747 $args = [];
1748 next;
1749 }
804bc621
TL
1750 if (!@$args) {
1751 # check if all left-over arg_param are optional, else we
1752 # must die as the mapping is then ambigious
26764d7c
WB
1753 for (; $i < scalar(@$arg_param); $i++) {
1754 my $prop = $arg_param->[$i];
804bc621
TL
1755 raise("not enough arguments\n", code => HTTP_BAD_REQUEST)
1756 if !$schema->{properties}->{$prop}->{optional};
1757 }
26764d7c
WB
1758 if ($arg_param->[-1] eq 'extra-args') {
1759 $opts->{'extra-args'} = [];
1760 }
1761 last;
804bc621 1762 }
5851be88 1763 $opts->{$arg_name} = shift @$args;
0ce82909 1764 }
5851be88 1765 raise("too many arguments\n", code => HTTP_BAD_REQUEST) if @$args;
0ce82909
DM
1766 } else {
1767 raise("too many arguments\n", code => HTTP_BAD_REQUEST)
1768 if scalar(@$args) != 0;
1769 }
ff2bf45f
DM
1770 } else {
1771 if (ref($arg_param)) {
1772 foreach my $arg_name (@$arg_param) {
1773 if ($arg_name eq 'extra-args') {
1774 $opts->{'extra-args'} = [];
3fe29ce6 1775 } elsif (!$schema->{properties}->{$arg_name}->{optional}) {
ff2bf45f
DM
1776 raise("not enough arguments\n", code => HTTP_BAD_REQUEST);
1777 }
1778 }
1779 }
1d21344c
DM
1780 }
1781
c7171ff2
WB
1782 foreach my $entry (@interactive) {
1783 my ($opt, $func) = @$entry;
1784 my $pd = $schema->{properties}->{$opt};
1785 my $value = $opts->{$opt};
1786 if (defined($value) || !$pd->{optional}) {
1787 $opts->{$opt} = $func->($value);
1788 }
1789 }
1790
c9902568 1791 # decode after Getopt as we are not sure how well it handles unicode
24197a9f 1792 foreach my $p (keys %$opts) {
c9902568
TL
1793 if (!ref($opts->{$p})) {
1794 $opts->{$p} = decode('locale', $opts->{$p});
1795 } elsif (ref($opts->{$p}) eq 'ARRAY') {
1796 my $tmp = [];
1797 foreach my $v (@{$opts->{$p}}) {
1798 push @$tmp, decode('locale', $v);
1799 }
1800 $opts->{$p} = $tmp;
1801 } elsif (ref($opts->{$p}) eq 'SCALAR') {
1802 $opts->{$p} = decode('locale', $$opts->{$p});
1803 } else {
1804 raise("decoding options failed, unknown reference\n", code => HTTP_BAD_REQUEST);
1805 }
24197a9f 1806 }
815b2aba 1807
e143e9d8
DM
1808 foreach my $p (keys %$opts) {
1809 if (my $pd = $schema->{properties}->{$p}) {
1810 if ($pd->{type} eq 'boolean') {
1811 if ($opts->{$p} eq '') {
1812 $opts->{$p} = 1;
1b71e564
WB
1813 } elsif (defined(my $bool = parse_boolean($opts->{$p}))) {
1814 $opts->{$p} = $bool;
e143e9d8
DM
1815 } else {
1816 raise("unable to parse boolean option\n", code => HTTP_BAD_REQUEST);
1817 }
23dc9401 1818 } elsif ($pd->{format}) {
8ba7c72b 1819
23dc9401 1820 if ($pd->{format} =~ m/-list/) {
8ba7c72b 1821 # allow --vmid 100 --vmid 101 and --vmid 100,101
23dc9401 1822 # allow --dow mon --dow fri and --dow mon,fri
43479146 1823 $opts->{$p} = join(",", @{$opts->{$p}}) if ref($opts->{$p}) eq 'ARRAY';
8ba7c72b 1824 }
e143e9d8 1825 }
9bbc4e17 1826 }
e143e9d8
DM
1827 }
1828
0ce82909
DM
1829 foreach my $p (keys %$fixed_param) {
1830 $opts->{$p} = $fixed_param->{$p};
e143e9d8
DM
1831 }
1832
1833 return $opts;
1834}
1835
1836# A way to parse configuration data by giving a json schema
2f85ab8f
WB
1837sub parse_config : prototype($$$;$) {
1838 my ($schema, $filename, $raw, $comment_key) = @_;
e143e9d8
DM
1839
1840 # do fast check (avoid validate_schema($schema))
9bbc4e17 1841 die "got strange schema" if !$schema->{type} ||
e143e9d8
DM
1842 !$schema->{properties} || $schema->{type} ne 'object';
1843
1844 my $cfg = {};
1845
2f85ab8f
WB
1846 my $comment_data;
1847 my $handle_comment = sub { $_[0] =~ /^#/ };
1848 if (defined($comment_key)) {
1849 $comment_data = '';
1850 my $comment_re = qr/^\Q$comment_key\E:\s*(.*\S)\s*$/;
1851 $handle_comment = sub {
1852 if ($_[0] =~ /^\#(.*)\s*$/ || $_[0] =~ $comment_re) {
1853 $comment_data .= PVE::Tools::decode_text($1) . "\n";
1854 return 1;
1855 }
1856 return undef;
1857 };
1858 }
1859
3c4d612a 1860 while ($raw =~ /^\s*(.+?)\s*$/gm) {
e143e9d8 1861 my $line = $1;
e143e9d8 1862
2f85ab8f 1863 next if $handle_comment->($line);
3c4d612a
WB
1864
1865 if ($line =~ m/^(\S+?):\s*(.*)$/) {
e143e9d8
DM
1866 my $key = $1;
1867 my $value = $2;
9bbc4e17 1868 if ($schema->{properties}->{$key} &&
e143e9d8
DM
1869 $schema->{properties}->{$key}->{type} eq 'boolean') {
1870
1b71e564 1871 $value = parse_boolean($value) // $value;
e143e9d8 1872 }
07f136d6
DC
1873 if (
1874 $schema->{properties}->{$key}
1875 && $schema->{properties}->{$key}->{type} eq 'array'
1876 ) {
1877
1878 $cfg->{$key} //= [];
1879 push $cfg->{$key}->@*, $value;
1880 next;
1881 }
e143e9d8
DM
1882 $cfg->{$key} = $value;
1883 } else {
1884 warn "ignore config line: $line\n"
1885 }
1886 }
1887
2f85ab8f
WB
1888 if (defined($comment_data)) {
1889 $cfg->{$comment_key} = $comment_data;
1890 }
1891
e143e9d8
DM
1892 my $errors = {};
1893 check_prop($cfg, $schema, '', $errors);
1894
1895 foreach my $k (keys %$errors) {
1896 warn "parse error in '$filename' - '$k': $errors->{$k}\n";
1897 delete $cfg->{$k};
9bbc4e17 1898 }
e143e9d8
DM
1899
1900 return $cfg;
1901}
1902
1903# generate simple key/value file
1904sub dump_config {
1905 my ($schema, $filename, $cfg) = @_;
1906
1907 # do fast check (avoid validate_schema($schema))
9bbc4e17 1908 die "got strange schema" if !$schema->{type} ||
e143e9d8
DM
1909 !$schema->{properties} || $schema->{type} ne 'object';
1910
1911 validate($cfg, $schema, "validation error in '$filename'\n");
1912
1913 my $data = '';
1914
821d408d 1915 foreach my $k (sort keys %$cfg) {
e143e9d8
DM
1916 $data .= "$k: $cfg->{$k}\n";
1917 }
1918
1919 return $data;
1920}
1921
bf27456b
DM
1922# helpers used to generate our manual pages
1923
1924my $find_schema_default_key = sub {
1925 my ($format) = @_;
1926
1927 my $default_key;
1928 my $keyAliasProps = {};
1929
1930 foreach my $key (keys %$format) {
1931 my $phash = $format->{$key};
1932 if ($phash->{default_key}) {
1933 die "multiple default keys in schema ($default_key, $key)\n"
1934 if defined($default_key);
1935 die "default key '$key' is an alias - this is not allowed\n"
1936 if defined($phash->{alias});
1937 die "default key '$key' with keyAlias attribute is not allowed\n"
1938 if $phash->{keyAlias};
bf27456b
DM
1939 $default_key = $key;
1940 }
1941 my $key_alias = $phash->{keyAlias};
c88c582d
DM
1942 die "found keyAlias without 'alias definition for '$key'\n"
1943 if $key_alias && !$phash->{alias};
1944
bf27456b
DM
1945 if ($phash->{alias} && $key_alias) {
1946 die "inconsistent keyAlias '$key_alias' definition"
1947 if defined($keyAliasProps->{$key_alias}) &&
1948 $keyAliasProps->{$key_alias} ne $phash->{alias};
1949 $keyAliasProps->{$key_alias} = $phash->{alias};
1950 }
1951 }
1952
1953 return wantarray ? ($default_key, $keyAliasProps) : $default_key;
1954};
1955
1956sub generate_typetext {
abc1afd8 1957 my ($format, $list_enums) = @_;
bf27456b 1958
d8c2b947 1959 my ($default_key, $keyAliasProps) = &$find_schema_default_key($format);
bf27456b
DM
1960
1961 my $res = '';
1962 my $add_sep = 0;
1963
1964 my $add_option_string = sub {
1965 my ($text, $optional) = @_;
1966
1967 if ($add_sep) {
1968 $text = ",$text";
1969 $res .= ' ';
1970 }
1971 $text = "[$text]" if $optional;
1972 $res .= $text;
1973 $add_sep = 1;
1974 };
1975
1976 my $format_key_value = sub {
1977 my ($key, $phash) = @_;
1978
1979 die "internal error" if defined($phash->{alias});
1980
1981 my $keytext = $key;
1982
1983 my $typetext = '';
1984
1985 if (my $desc = $phash->{format_description}) {
1986 $typetext .= "<$desc>";
1987 } elsif (my $text = $phash->{typetext}) {
1988 $typetext .= $text;
1989 } elsif (my $enum = $phash->{enum}) {
abc1afd8
DM
1990 if ($list_enums || (scalar(@$enum) <= 3)) {
1991 $typetext .= '<' . join('|', @$enum) . '>';
1992 } else {
1993 $typetext .= '<enum>';
1994 }
bf27456b
DM
1995 } elsif ($phash->{type} eq 'boolean') {
1996 $typetext .= '<1|0>';
1997 } elsif ($phash->{type} eq 'integer') {
1998 $typetext .= '<integer>';
1999 } elsif ($phash->{type} eq 'number') {
2000 $typetext .= '<number>';
2001 } else {
2002 die "internal error: neither format_description nor typetext found for option '$key'";
2003 }
2004
2005 if (defined($default_key) && ($default_key eq $key)) {
2006 &$add_option_string("[$keytext=]$typetext", $phash->{optional});
2007 } else {
2008 &$add_option_string("$keytext=$typetext", $phash->{optional});
2009 }
2010 };
2011
d8c2b947 2012 my $done = {};
bf27456b 2013
d8c2b947
DM
2014 my $cond_add_key = sub {
2015 my ($key) = @_;
2016
2017 return if $done->{$key}; # avoid duplicates
2018
2019 $done->{$key} = 1;
bf27456b
DM
2020
2021 my $phash = $format->{$key};
2022
d8c2b947
DM
2023 return if !$phash; # should not happen
2024
2025 return if $phash->{alias};
bf27456b
DM
2026
2027 &$format_key_value($key, $phash);
2028
d8c2b947
DM
2029 };
2030
2031 &$cond_add_key($default_key) if defined($default_key);
2032
2033 # add required keys first
2034 foreach my $key (sort keys %$format) {
2035 my $phash = $format->{$key};
2036 &$cond_add_key($key) if $phash && !$phash->{optional};
2037 }
2038
2039 # add the rest
2040 foreach my $key (sort keys %$format) {
2041 &$cond_add_key($key);
2042 }
2043
2044 foreach my $keyAlias (sort keys %$keyAliasProps) {
2045 &$add_option_string("<$keyAlias>=<$keyAliasProps->{$keyAlias }>", 1);
bf27456b
DM
2046 }
2047
2048 return $res;
2049}
2050
2051sub print_property_string {
2052 my ($data, $format, $skip, $path) = @_;
2053
d500c038 2054 my $validator;
bf27456b
DM
2055 if (ref($format) ne 'HASH') {
2056 my $schema = get_format($format);
2057 die "not a valid format: $format\n" if !$schema;
d500c038
SR
2058 # named formats can have validators attached
2059 $validator = $format_validators->{$format};
bf27456b
DM
2060 $format = $schema;
2061 }
2062
2063 my $errors = {};
2064 check_object($path, $format, $data, undef, $errors);
2065 if (scalar(%$errors)) {
2066 raise "format error", errors => $errors;
2067 }
2068
d500c038
SR
2069 $data = $validator->($data) if $validator;
2070
bf27456b
DM
2071 my ($default_key, $keyAliasProps) = &$find_schema_default_key($format);
2072
2073 my $res = '';
2074 my $add_sep = 0;
2075
2076 my $add_option_string = sub {
2077 my ($text) = @_;
2078
2079 $res .= ',' if $add_sep;
2080 $res .= $text;
2081 $add_sep = 1;
2082 };
2083
2084 my $format_value = sub {
2085 my ($key, $value, $format) = @_;
2086
2087 if (defined($format) && ($format eq 'disk-size')) {
2088 return format_size($value);
2089 } else {
2090 die "illegal value with commas for $key\n" if $value =~ /,/;
2091 return $value;
2092 }
2093 };
2094
2289890b 2095 my $done = { map { $_ => 1 } @$skip };
bf27456b
DM
2096
2097 my $cond_add_key = sub {
971353e8 2098 my ($key, $isdefault) = @_;
bf27456b
DM
2099
2100 return if $done->{$key}; # avoid duplicates
2101
2102 $done->{$key} = 1;
2103
2104 my $value = $data->{$key};
2105
2106 return if !defined($value);
2107
2108 my $phash = $format->{$key};
2109
2110 # try to combine values if we have key aliases
2111 if (my $combine = $keyAliasProps->{$key}) {
2112 if (defined(my $combine_value = $data->{$combine})) {
2113 my $combine_format = $format->{$combine}->{format};
2114 my $value_str = &$format_value($key, $value, $phash->{format});
2115 my $combine_str = &$format_value($combine, $combine_value, $combine_format);
2116 &$add_option_string("${value_str}=${combine_str}");
2117 $done->{$combine} = 1;
2118 return;
2119 }
2120 }
2121
2122 if ($phash && $phash->{alias}) {
2123 $phash = $format->{$phash->{alias}};
2124 }
2125
2126 die "invalid key '$key'\n" if !$phash;
2127 die "internal error" if defined($phash->{alias});
2128
2129 my $value_str = &$format_value($key, $value, $phash->{format});
971353e8
WB
2130 if ($isdefault) {
2131 &$add_option_string($value_str);
2132 } else {
2133 &$add_option_string("$key=${value_str}");
2134 }
bf27456b
DM
2135 };
2136
2137 # add default key first
971353e8 2138 &$cond_add_key($default_key, 1) if defined($default_key);
bf27456b 2139
d8c2b947
DM
2140 # add required keys first
2141 foreach my $key (sort keys %$data) {
2142 my $phash = $format->{$key};
2143 &$cond_add_key($key) if $phash && !$phash->{optional};
2144 }
2145
2146 # add the rest
bf27456b
DM
2147 foreach my $key (sort keys %$data) {
2148 &$cond_add_key($key);
2149 }
2150
2151 return $res;
2152}
2153
2154sub schema_get_type_text {
abc1afd8 2155 my ($phash, $style) = @_;
bf27456b 2156
32f8e0c7
DM
2157 my $type = $phash->{type} || 'string';
2158
bf27456b
DM
2159 if ($phash->{typetext}) {
2160 return $phash->{typetext};
2161 } elsif ($phash->{format_description}) {
2162 return "<$phash->{format_description}>";
2163 } elsif ($phash->{enum}) {
25d9bda9 2164 return "<" . join(' | ', sort @{$phash->{enum}}) . ">";
bf27456b
DM
2165 } elsif ($phash->{pattern}) {
2166 return $phash->{pattern};
32f8e0c7 2167 } elsif ($type eq 'integer' || $type eq 'number') {
05185ea2 2168 # NOTE: always access values as number (avoid converion to string)
bf27456b 2169 if (defined($phash->{minimum}) && defined($phash->{maximum})) {
25d9bda9 2170 return "<$type> (" . ($phash->{minimum} + 0) . " - " .
05185ea2 2171 ($phash->{maximum} + 0) . ")";
bf27456b 2172 } elsif (defined($phash->{minimum})) {
25d9bda9 2173 return "<$type> (" . ($phash->{minimum} + 0) . " - N)";
bf27456b 2174 } elsif (defined($phash->{maximum})) {
25d9bda9 2175 return "<$type> (-N - " . ($phash->{maximum} + 0) . ")";
bf27456b 2176 }
32f8e0c7 2177 } elsif ($type eq 'string') {
bf27456b
DM
2178 if (my $format = $phash->{format}) {
2179 $format = get_format($format) if ref($format) ne 'HASH';
2180 if (ref($format) eq 'HASH') {
abc1afd8
DM
2181 my $list_enums = 0;
2182 $list_enums = 1 if $style && $style eq 'config-sub';
2183 return generate_typetext($format, $list_enums);
bf27456b
DM
2184 }
2185 }
2186 }
2187
25d9bda9 2188 return "<$type>";
bf27456b
DM
2189}
2190
e143e9d8 21911;