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