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