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