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