1 package PVE
::JSONSchema
;
5 use Storable
; # for dclone
9 use Devel
::Cycle
-quiet
; # todo: remove?
10 use PVE
::Tools
qw(split_list $IPV6RE $IPV4RE);
11 use PVE
::Exception
qw(raise);
12 use HTTP
::Status
qw(:constants);
13 use Net
::IP
qw(:PROC);
21 register_standard_option
24 our $CONFIGID_RE = qr/[a-z][a-z0-9_-]+/i;
26 # Note: This class implements something similar to JSON schema, but it is not 100% complete.
27 # see: http://tools.ietf.org/html/draft-zyp-json-schema-02
28 # see: http://json-schema.org/
30 # the code is similar to the javascript parser from http://code.google.com/p/jsonschema/
32 my $standard_options = {};
33 sub register_standard_option
{
34 my ($name, $schema) = @_;
36 die "standard option '$name' already registered\n"
37 if $standard_options->{$name};
39 $standard_options->{$name} = $schema;
42 sub get_standard_option
{
43 my ($name, $base) = @_;
45 my $std = $standard_options->{$name};
46 die "no such standard option '$name'\n" if !$std;
48 my $res = $base || {};
50 foreach my $opt (keys %$std) {
51 next if defined($res->{$opt});
52 $res->{$opt} = $std->{$opt};
58 register_standard_option
('pve-vmid', {
59 description
=> "The (unique) ID of the VM.",
60 type
=> 'integer', format
=> 'pve-vmid',
64 register_standard_option
('pve-node', {
65 description
=> "The cluster node name.",
66 type
=> 'string', format
=> 'pve-node',
69 register_standard_option
('pve-node-list', {
70 description
=> "List of cluster node names.",
71 type
=> 'string', format
=> 'pve-node-list',
74 register_standard_option
('pve-iface', {
75 description
=> "Network interface name.",
76 type
=> 'string', format
=> 'pve-iface',
77 minLength
=> 2, maxLength
=> 20,
80 register_standard_option
('pve-storage-id', {
81 description
=> "The storage identifier.",
82 type
=> 'string', format
=> 'pve-storage-id',
85 register_standard_option
('pve-config-digest', {
86 description
=> 'Prevent changes if current configuration file has different SHA1 digest. This can be used to prevent concurrent modifications.',
89 maxLength
=> 40, # sha1 hex digest length is 40
92 register_standard_option
('skiplock', {
93 description
=> "Ignore locks - only root is allowed to use this option.",
98 register_standard_option
('extra-args', {
99 description
=> "Extra arguments as array",
101 items
=> { type
=> 'string' },
105 register_standard_option
('fingerprint-sha256', {
106 description
=> "Certificate SHA 256 fingerprint.",
108 pattern
=> '([A-Fa-f0-9]{2}:){31}[A-Fa-f0-9]{2}',
111 register_standard_option
('pve-output-format', {
113 description
=> 'Output format.',
114 enum
=> [ 'text', 'json', 'json-pretty', 'yaml' ],
119 register_standard_option
('pve-snapshot-name', {
120 description
=> "The name of the snapshot.",
121 type
=> 'string', format
=> 'pve-configid',
125 my $format_list = {};
126 my $format_validators = {};
128 sub register_format
{
129 my ($name, $format, $validator) = @_;
131 die "JSON schema format '$name' already registered\n"
132 if $format_list->{$name};
135 die "A \$validator function can only be specified for hash-based formats\n"
136 if ref($format) ne 'HASH';
137 $format_validators->{$name} = $validator;
140 $format_list->{$name} = $format;
145 return $format_list->{$name};
148 my $renderer_hash = {};
150 sub register_renderer
{
151 my ($name, $code) = @_;
153 die "renderer '$name' already registered\n"
154 if $renderer_hash->{$name};
156 $renderer_hash->{$name} = $code;
161 return $renderer_hash->{$name};
164 # register some common type for pve
166 register_format
('string', sub {}); # allow format => 'string-list'
168 register_format
('urlencoded', \
&pve_verify_urlencoded
);
169 sub pve_verify_urlencoded
{
170 my ($text, $noerr) = @_;
171 if ($text !~ /^[-%a-zA-Z0-9_.!~*'()]*$/) {
172 return undef if $noerr;
173 die "invalid urlencoded string: $text\n";
178 register_format
('pve-configid', \
&pve_verify_configid
);
179 sub pve_verify_configid
{
180 my ($id, $noerr) = @_;
182 if ($id !~ m/^$CONFIGID_RE$/) {
183 return undef if $noerr;
184 die "invalid configuration ID '$id'\n";
189 PVE
::JSONSchema
::register_format
('pve-storage-id', \
&parse_storage_id
);
190 sub parse_storage_id
{
191 my ($storeid, $noerr) = @_;
193 return parse_id
($storeid, 'storage', $noerr);
196 PVE
::JSONSchema
::register_format
('acme-plugin-id', \
&parse_acme_plugin_id
);
197 sub parse_acme_plugin_id
{
198 my ($pluginid, $noerr) = @_;
200 return parse_id
($pluginid, 'ACME plugin', $noerr);
204 my ($id, $type, $noerr) = @_;
206 if ($id !~ m/^[a-z][a-z0-9\-\_\.]*[a-z0-9]$/i) {
207 return undef if $noerr;
208 die "$type ID '$id' contains illegal characters\n";
213 register_format
('pve-vmid', \
&pve_verify_vmid
);
214 sub pve_verify_vmid
{
215 my ($vmid, $noerr) = @_;
217 if ($vmid !~ m/^[1-9][0-9]{2,8}$/) {
218 return undef if $noerr;
219 die "value does not look like a valid VM ID\n";
224 register_format
('pve-node', \
&pve_verify_node_name
);
225 sub pve_verify_node_name
{
226 my ($node, $noerr) = @_;
228 if ($node !~ m/^([a-zA-Z0-9]([a-zA-Z0-9\-]*[a-zA-Z0-9])?)$/) {
229 return undef if $noerr;
230 die "value does not look like a valid node name\n";
236 my ($idmap, $idformat) = @_;
238 return undef if !$idmap;
242 foreach my $entry (PVE
::Tools
::split_list
($idmap)) {
244 $map->{identity
} = 1;
245 } elsif ($entry =~ m/^([^:]+):([^:]+)$/) {
246 my ($source, $target) = ($1, $2);
248 check_format
($idformat, $source, '');
249 check_format
($idformat, $target, '');
251 die "entry '$entry' contains invalid ID - $@\n" if $@;
253 die "duplicate mapping for source '$source'\n"
254 if exists $map->{entries
}->{$source};
256 $map->{entries
}->{$source} = $target;
259 check_format
($idformat, $entry);
261 die "entry '$entry' contains invalid ID - $@\n" if $@;
263 die "default target ID can only be provided once\n"
264 if exists $map->{default};
266 $map->{default} = $entry;
270 die "identity mapping cannot be combined with other mappings\n"
271 if $map->{identity
} && ($map->{default} || exists $map->{entries
});
276 register_format
('storagepair', \
&verify_storagepair
);
277 sub verify_storagepair
{
278 my ($storagepair, $noerr) = @_;
280 # note: this only checks a single list entry
281 # when using a storagepair-list map, you need to pass the full
282 # parameter to parse_idmap
283 eval { parse_idmap
($storagepair, 'pve-storage-id') };
285 return undef if $noerr;
292 register_format
('mac-addr', \
&pve_verify_mac_addr
);
293 sub pve_verify_mac_addr
{
294 my ($mac_addr, $noerr) = @_;
296 # don't allow I/G bit to be set, most of the time it breaks things, see:
297 # https://pve.proxmox.com/pipermail/pve-devel/2019-March/035998.html
298 if ($mac_addr !~ m/^[a-f0-9][02468ace](?::[a-f0-9]{2}){5}$/i) {
299 return undef if $noerr;
300 die "value does not look like a valid unicast MAC address\n";
305 register_standard_option
('mac-addr', {
307 description
=> 'Unicast MAC address.',
308 verbose_description
=> 'A common MAC address with the I/G (Individual/Group) bit not set.',
309 format_description
=> "XX:XX:XX:XX:XX:XX",
311 format
=> 'mac-addr',
314 register_format
('ipv4', \
&pve_verify_ipv4
);
315 sub pve_verify_ipv4
{
316 my ($ipv4, $noerr) = @_;
318 if ($ipv4 !~ m/^(?:$IPV4RE)$/) {
319 return undef if $noerr;
320 die "value does not look like a valid IPv4 address\n";
325 register_format
('ipv6', \
&pve_verify_ipv6
);
326 sub pve_verify_ipv6
{
327 my ($ipv6, $noerr) = @_;
329 if ($ipv6 !~ m/^(?:$IPV6RE)$/) {
330 return undef if $noerr;
331 die "value does not look like a valid IPv6 address\n";
336 register_format
('ip', \
&pve_verify_ip
);
338 my ($ip, $noerr) = @_;
340 if ($ip !~ m/^(?:(?:$IPV4RE)|(?:$IPV6RE))$/) {
341 return undef if $noerr;
342 die "value does not look like a valid IP address\n";
347 PVE
::JSONSchema
::register_format
('ldap-simple-attr', \
&verify_ldap_simple_attr
);
348 sub verify_ldap_simple_attr
{
349 my ($attr, $noerr) = @_;
351 if ($attr =~ m/^[a-zA-Z0-9]+$/) {
355 die "value '$attr' does not look like a simple ldap attribute name\n" if !$noerr;
360 my $ipv4_mask_hash = {
378 '255.255.128.0' => 17,
379 '255.255.192.0' => 18,
380 '255.255.224.0' => 19,
381 '255.255.240.0' => 20,
382 '255.255.248.0' => 21,
383 '255.255.252.0' => 22,
384 '255.255.254.0' => 23,
385 '255.255.255.0' => 24,
386 '255.255.255.128' => 25,
387 '255.255.255.192' => 26,
388 '255.255.255.224' => 27,
389 '255.255.255.240' => 28,
390 '255.255.255.248' => 29,
391 '255.255.255.252' => 30,
392 '255.255.255.254' => 31,
393 '255.255.255.255' => 32,
396 sub get_netmask_bits
{
398 return $ipv4_mask_hash->{$mask};
401 register_format
('ipv4mask', \
&pve_verify_ipv4mask
);
402 sub pve_verify_ipv4mask
{
403 my ($mask, $noerr) = @_;
405 if (!defined($ipv4_mask_hash->{$mask})) {
406 return undef if $noerr;
407 die "value does not look like a valid IP netmask\n";
412 register_format
('CIDRv6', \
&pve_verify_cidrv6
);
413 sub pve_verify_cidrv6
{
414 my ($cidr, $noerr) = @_;
416 if ($cidr =~ m!^(?:$IPV6RE)(?:/(\d+))$! && ($1 > 7) && ($1 <= 128)) {
420 return undef if $noerr;
421 die "value does not look like a valid IPv6 CIDR network\n";
424 register_format
('CIDRv4', \
&pve_verify_cidrv4
);
425 sub pve_verify_cidrv4
{
426 my ($cidr, $noerr) = @_;
428 if ($cidr =~ m!^(?:$IPV4RE)(?:/(\d+))$! && ($1 > 7) && ($1 <= 32)) {
432 return undef if $noerr;
433 die "value does not look like a valid IPv4 CIDR network\n";
436 register_format
('CIDR', \
&pve_verify_cidr
);
437 sub pve_verify_cidr
{
438 my ($cidr, $noerr) = @_;
440 if (!(pve_verify_cidrv4
($cidr, 1) ||
441 pve_verify_cidrv6
($cidr, 1)))
443 return undef if $noerr;
444 die "value does not look like a valid CIDR network\n";
450 register_format
('pve-ipv4-config', \
&pve_verify_ipv4_config
);
451 sub pve_verify_ipv4_config
{
452 my ($config, $noerr) = @_;
454 return $config if $config =~ /^(?:dhcp|manual)$/ ||
455 pve_verify_cidrv4
($config, 1);
456 return undef if $noerr;
457 die "value does not look like a valid ipv4 network configuration\n";
460 register_format
('pve-ipv6-config', \
&pve_verify_ipv6_config
);
461 sub pve_verify_ipv6_config
{
462 my ($config, $noerr) = @_;
464 return $config if $config =~ /^(?:auto|dhcp|manual)$/ ||
465 pve_verify_cidrv6
($config, 1);
466 return undef if $noerr;
467 die "value does not look like a valid ipv6 network configuration\n";
470 register_format
('email', \
&pve_verify_email
);
471 sub pve_verify_email
{
472 my ($email, $noerr) = @_;
474 if ($email !~ /^$PVE::Tools::EMAIL_RE$/) {
475 return undef if $noerr;
476 die "value does not look like a valid email address\n";
481 register_format
('email-or-username', \
&pve_verify_email_or_username
);
482 sub pve_verify_email_or_username
{
483 my ($email, $noerr) = @_;
485 if ($email !~ /^$PVE::Tools::EMAIL_RE$/ &&
486 $email !~ /^$PVE::Tools::EMAIL_USER_RE$/) {
487 return undef if $noerr;
488 die "value does not look like a valid email address or user name\n";
493 register_format
('dns-name', \
&pve_verify_dns_name
);
494 sub pve_verify_dns_name
{
495 my ($name, $noerr) = @_;
497 my $namere = "([a-zA-Z0-9]([a-zA-Z0-9\-]*[a-zA-Z0-9])?)";
499 if ($name !~ /^(${namere}\.)*${namere}$/) {
500 return undef if $noerr;
501 die "value does not look like a valid DNS name\n";
506 register_format
('timezone', \
&pve_verify_timezone
);
507 sub pve_verify_timezone
{
508 my ($timezone, $noerr) = @_;
510 return $timezone if $timezone eq 'UTC';
512 open(my $fh, "<", "/usr/share/zoneinfo/zone.tab");
513 while (my $line = <$fh>) {
514 next if $line =~ /^\s*#/;
516 my $zone = (split /\t/, $line)[2];
517 return $timezone if $timezone eq $zone; # found
521 return undef if $noerr;
522 die "invalid time zone '$timezone'\n";
525 # network interface name
526 register_format
('pve-iface', \
&pve_verify_iface
);
527 sub pve_verify_iface
{
528 my ($id, $noerr) = @_;
530 if ($id !~ m/^[a-z][a-z0-9_]{1,20}([:\.]\d+)?$/i) {
531 return undef if $noerr;
532 die "invalid network interface name '$id'\n";
537 # general addresses by name or IP
538 register_format
('address', \
&pve_verify_address
);
539 sub pve_verify_address
{
540 my ($addr, $noerr) = @_;
542 if (!(pve_verify_ip
($addr, 1) ||
543 pve_verify_dns_name
($addr, 1)))
545 return undef if $noerr;
546 die "value does not look like a valid address: $addr\n";
551 register_format
('disk-size', \
&pve_verify_disk_size
);
552 sub pve_verify_disk_size
{
553 my ($size, $noerr) = @_;
554 if (!defined(parse_size
($size))) {
555 return undef if $noerr;
556 die "value does not look like a valid disk size: $size\n";
561 register_standard_option
('spice-proxy', {
562 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).",
563 type
=> 'string', format
=> 'address',
566 register_standard_option
('remote-viewer-config', {
567 description
=> "Returned values can be directly passed to the 'remote-viewer' application.",
568 additionalProperties
=> 1,
570 type
=> { type
=> 'string' },
571 password
=> { type
=> 'string' },
572 proxy
=> { type
=> 'string' },
573 host
=> { type
=> 'string' },
574 'tls-port' => { type
=> 'integer' },
578 register_format
('pve-startup-order', \
&pve_verify_startup_order
);
579 sub pve_verify_startup_order
{
580 my ($value, $noerr) = @_;
582 return $value if pve_parse_startup_order
($value);
584 return undef if $noerr;
586 die "unable to parse startup options\n";
591 type
=> 'number', minimum
=> '0',
592 format_description
=> 'LIMIT',
595 my $bwlimit_format = {
598 description
=> 'default bandwidth limit in KiB/s',
602 description
=> 'bandwidth limit in KiB/s for restoring guests from backups',
606 description
=> 'bandwidth limit in KiB/s for migrating guests (including moving local disks)',
610 description
=> 'bandwidth limit in KiB/s for cloning disks',
614 description
=> 'bandwidth limit in KiB/s for moving disks',
617 register_format
('bwlimit', $bwlimit_format);
618 register_standard_option
('bwlimit', {
619 description
=> "Set bandwidth/io limits various operations.",
622 format
=> $bwlimit_format,
625 # used for pve-tag-list in e.g., guest configs
626 register_format
('pve-tag', \
&pve_verify_tag
);
628 my ($value, $noerr) = @_;
630 return $value if $value =~ m/^[a-z0-9_][a-z0-9_\-\+\.]*$/i;
632 return undef if $noerr;
634 die "invalid characters in tag\n";
637 sub pve_parse_startup_order
{
640 return undef if !$value;
644 foreach my $p (split(/,/, $value)) {
645 next if $p =~ m/^\s*$/;
647 if ($p =~ m/^(order=)?(\d+)$/) {
649 } elsif ($p =~ m/^up=(\d+)$/) {
651 } elsif ($p =~ m/^down=(\d+)$/) {
661 PVE
::JSONSchema
::register_standard_option
('pve-startup-order', {
662 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.",
664 type
=> 'string', format
=> 'pve-startup-order',
665 typetext
=> '[[order=]\d+] [,up=\d+] [,down=\d+] ',
668 register_format
('pve-tfa-secret', \
&pve_verify_tfa_secret
);
669 sub pve_verify_tfa_secret
{
670 my ($key, $noerr) = @_;
672 # The old format used 16 base32 chars or 40 hex digits. Since they have a common subset it's
673 # hard to distinguish them without the our previous length constraints, so add a 'v2' of the
674 # format to support arbitrary lengths properly:
675 if ($key =~ /^v2-0x[0-9a-fA-F]{16,128}$/ || # hex
676 $key =~ /^v2-[A-Z2-7=]{16,128}$/ || # base32
677 $key =~ /^(?:[A-Z2-7=]{16}|[A-Fa-f0-9]{40})$/) # and the old pattern copy&pasted
682 return undef if $noerr;
684 die "unable to decode TFA secret\n";
688 my ($format, $value, $path) = @_;
690 if (ref($format) eq 'HASH') {
691 # hash ref cannot have validator/list/opt handling attached
692 return parse_property_string
($format, $value, $path);
695 if (ref($format) eq 'CODE') {
696 # we are the (sole, old-style) validator
697 return $format->($value);
700 return if $format eq 'regex';
703 $format =~ m/^(.*?)(?:-a?(list|opt))?$/;
704 my ($format_name, $format_type) = ($1, $2 // 'none');
705 my $registered = get_format
($format_name);
706 die "undefined format '$format'\n" if !$registered;
708 die "'-$format_type' format must have code ref, not hash\n"
709 if $format_type ne 'none' && ref($registered) ne 'CODE';
711 if ($format_type eq 'list') {
712 # Note: we allow empty lists
713 foreach my $v (split_list
($value)) {
714 $parsed = $registered->($v);
716 } elsif ($format_type eq 'opt') {
717 $parsed = $registered->($value) if $value;
719 if (ref($registered) eq 'HASH') {
720 # Note: this is the only case where a validator function could be
721 # attached, hence it's safe to handle that in parse_property_string.
722 # We do however have to call it with $format_name instead of
723 # $registered, so it knows about the name (and thus any validators).
724 $parsed = parse_property_string
($format, $value, $path);
726 $parsed = $registered->($value);
736 return undef if $value !~ m/^(\d+(\.\d+)?)([KMGT])?$/;
737 my ($size, $unit) = ($1, $3);
740 $size = $size * 1024;
741 } elsif ($unit eq 'M') {
742 $size = $size * 1024 * 1024;
743 } elsif ($unit eq 'G') {
744 $size = $size * 1024 * 1024 * 1024;
745 } elsif ($unit eq 'T') {
746 $size = $size * 1024 * 1024 * 1024 * 1024;
757 my $kb = int($size/1024);
758 return $size if $kb*1024 != $size;
760 my $mb = int($kb/1024);
761 return "${kb}K" if $mb*1024 != $kb;
763 my $gb = int($mb/1024);
764 return "${mb}M" if $gb*1024 != $mb;
766 my $tb = int($gb/1024);
767 return "${gb}G" if $tb*1024 != $gb;
774 return 1 if $bool =~ m/^(1|on|yes|true)$/i;
775 return 0 if $bool =~ m/^(0|off|no|false)$/i;
779 sub parse_property_string
{
780 my ($format, $data, $path, $additional_properties) = @_;
782 # In property strings we default to not allowing additional properties
783 $additional_properties = 0 if !defined($additional_properties);
785 # Support named formats here, too:
788 if (my $reg = get_format
($format)) {
789 die "parse_property_string only accepts hash based named formats\n"
790 if ref($reg) ne 'HASH';
792 # named formats can have validators attached
793 $validator = $format_validators->{$format};
797 die "unknown format: $format\n";
799 } elsif (ref($format) ne 'HASH') {
800 die "unexpected format value of type ".ref($format)."\n";
806 foreach my $part (split(/,/, $data)) {
807 next if $part =~ /^\s*$/;
809 if ($part =~ /^([^=]+)=(.+)$/) {
810 my ($k, $v) = ($1, $2);
811 die "duplicate key in comma-separated list property: $k\n" if defined($res->{$k});
812 my $schema = $format->{$k};
813 if (my $alias = $schema->{alias
}) {
814 if (my $key_alias = $schema->{keyAlias
}) {
815 die "key alias '$key_alias' is already defined\n" if defined($res->{$key_alias});
816 $res->{$key_alias} = $k;
819 $schema = $format->{$k};
822 die "invalid key in comma-separated list property: $k\n" if !$schema;
823 if ($schema->{type
} && $schema->{type
} eq 'boolean') {
824 $v = parse_boolean
($v) // $v;
827 } elsif ($part !~ /=/) {
828 die "duplicate key in comma-separated list property: $default_key\n" if $default_key;
829 foreach my $key (keys %$format) {
830 if ($format->{$key}->{default_key
}) {
832 if (!$res->{$default_key}) {
833 $res->{$default_key} = $part;
836 die "duplicate key in comma-separated list property: $default_key\n";
839 die "value without key, but schema does not define a default key\n" if !$default_key;
841 die "missing key in comma-separated list property\n";
846 check_object
($path, $format, $res, $additional_properties, $errors);
847 if (scalar(%$errors)) {
848 raise
"format error\n", errors
=> $errors;
851 return $validator->($res) if $validator;
856 my ($errors, $path, $msg) = @_;
858 $path = '_root' if !$path;
860 if ($errors->{$path}) {
861 $errors->{$path} = join ('\n', $errors->{$path}, $msg);
863 $errors->{$path} = $msg;
870 # see 'man perlretut'
871 return $value =~ /^[+-]?(\d+\.\d+|\d+\.|\.\d+|\d+)([eE][+-]?\d+)?$/;
877 return $value =~ m/^[+-]?\d+$/;
881 my ($path, $type, $value, $errors) = @_;
885 if (!defined($value)) {
886 return 1 if $type eq 'null';
890 if (my $tt = ref($type)) {
891 if ($tt eq 'ARRAY') {
892 foreach my $t (@$type) {
894 check_type
($path, $t, $value, $tmperr);
895 return 1 if !scalar(%$tmperr);
897 my $ttext = join ('|', @$type);
898 add_error
($errors, $path, "type check ('$ttext') failed");
900 } elsif ($tt eq 'HASH') {
902 check_prop
($value, $type, $path, $tmperr);
903 return 1 if !scalar(%$tmperr);
904 add_error
($errors, $path, "type check failed");
907 die "internal error - got reference type '$tt'";
912 return 1 if $type eq 'any';
914 if ($type eq 'null') {
915 if (defined($value)) {
916 add_error
($errors, $path, "type check ('$type') failed - value is not null");
922 my $vt = ref($value);
924 if ($type eq 'array') {
925 if (!$vt || $vt ne 'ARRAY') {
926 add_error
($errors, $path, "type check ('$type') failed");
930 } elsif ($type eq 'object') {
931 if (!$vt || $vt ne 'HASH') {
932 add_error
($errors, $path, "type check ('$type') failed");
936 } elsif ($type eq 'coderef') {
937 if (!$vt || $vt ne 'CODE') {
938 add_error
($errors, $path, "type check ('$type') failed");
942 } elsif ($type eq 'string' && $vt eq 'Regexp') {
943 # qr// regexes can be used as strings and make sense for format=regex
947 add_error
($errors, $path, "type check ('$type') failed - got $vt");
950 if ($type eq 'string') {
951 return 1; # nothing to check ?
952 } elsif ($type eq 'boolean') {
953 #if ($value =~ m/^(1|true|yes|on)$/i) {
956 #} elsif ($value =~ m/^(0|false|no|off)$/i) {
957 } elsif ($value eq '0') {
958 return 1; # return success (not value)
960 add_error
($errors, $path, "type check ('$type') failed - got '$value'");
963 } elsif ($type eq 'integer') {
964 if (!is_integer
($value)) {
965 add_error
($errors, $path, "type check ('$type') failed - got '$value'");
969 } elsif ($type eq 'number') {
970 if (!is_number
($value)) {
971 add_error
($errors, $path, "type check ('$type') failed - got '$value'");
976 return 1; # no need to verify unknown types
986 my ($path, $schema, $value, $additional_properties, $errors) = @_;
988 # print "Check Object " . Dumper($value) . "\nSchema: " . Dumper($schema);
990 my $st = ref($schema);
991 if (!$st || $st ne 'HASH') {
992 add_error
($errors, $path, "Invalid schema definition.");
996 my $vt = ref($value);
997 if (!$vt || $vt ne 'HASH') {
998 add_error
($errors, $path, "an object is required");
1002 foreach my $k (keys %$schema) {
1003 check_prop
($value->{$k}, $schema->{$k}, $path ?
"$path.$k" : $k, $errors);
1006 foreach my $k (keys %$value) {
1008 my $newpath = $path ?
"$path.$k" : $k;
1010 if (my $subschema = $schema->{$k}) {
1011 if (my $requires = $subschema->{requires
}) {
1012 if (ref($requires)) {
1013 #print "TEST: " . Dumper($value) . "\n", Dumper($requires) ;
1014 check_prop
($value, $requires, $path, $errors);
1015 } elsif (!defined($value->{$requires})) {
1016 add_error
($errors, $path ?
"$path.$requires" : $requires,
1017 "missing property - '$newpath' requires this property");
1021 next; # value is already checked above
1024 if (defined ($additional_properties) && !$additional_properties) {
1025 add_error
($errors, $newpath, "property is not defined in schema " .
1026 "and the schema does not allow additional properties");
1029 check_prop
($value->{$k}, $additional_properties, $newpath, $errors)
1030 if ref($additional_properties);
1034 sub check_object_warn
{
1035 my ($path, $schema, $value, $additional_properties) = @_;
1037 check_object
($path, $schema, $value, $additional_properties, $errors);
1038 if (scalar(%$errors)) {
1039 foreach my $k (keys %$errors) {
1040 warn "parse error: $k: $errors->{$k}\n";
1048 my ($value, $schema, $path, $errors) = @_;
1050 die "internal error - no schema" if !$schema;
1051 die "internal error" if !$errors;
1053 #print "check_prop $path\n" if $value;
1055 my $st = ref($schema);
1056 if (!$st || $st ne 'HASH') {
1057 add_error
($errors, $path, "Invalid schema definition.");
1061 # if it extends another schema, it must pass that schema as well
1062 if($schema->{extends
}) {
1063 check_prop
($value, $schema->{extends
}, $path, $errors);
1066 if (!defined ($value)) {
1067 return if $schema->{type
} && $schema->{type
} eq 'null';
1068 if (!$schema->{optional
} && !$schema->{alias
} && !$schema->{group
}) {
1069 add_error
($errors, $path, "property is missing and it is not optional");
1074 return if !check_type
($path, $schema->{type
}, $value, $errors);
1076 if ($schema->{disallow
}) {
1078 if (check_type
($path, $schema->{disallow
}, $value, $tmperr)) {
1079 add_error
($errors, $path, "disallowed value was matched");
1084 if (my $vt = ref($value)) {
1086 if ($vt eq 'ARRAY') {
1087 if ($schema->{items
}) {
1088 my $it = ref($schema->{items
});
1089 if ($it && $it eq 'ARRAY') {
1090 #die "implement me $path: $vt " . Dumper($schema) ."\n". Dumper($value);
1091 die "not implemented";
1094 foreach my $el (@$value) {
1095 check_prop
($el, $schema->{items
}, "${path}[$ind]", $errors);
1101 } elsif ($schema->{properties
} || $schema->{additionalProperties
}) {
1102 check_object
($path, defined($schema->{properties
}) ?
$schema->{properties
} : {},
1103 $value, $schema->{additionalProperties
}, $errors);
1109 if (my $format = $schema->{format
}) {
1110 eval { check_format
($format, $value, $path); };
1112 add_error
($errors, $path, "invalid format - $@");
1117 if (my $pattern = $schema->{pattern
}) {
1118 if ($value !~ m/^$pattern$/) {
1119 add_error
($errors, $path, "value does not match the regex pattern");
1124 if (defined (my $max = $schema->{maxLength
})) {
1125 if (length($value) > $max) {
1126 add_error
($errors, $path, "value may only be $max characters long");
1131 if (defined (my $min = $schema->{minLength
})) {
1132 if (length($value) < $min) {
1133 add_error
($errors, $path, "value must be at least $min characters long");
1138 if (is_number
($value)) {
1139 if (defined (my $max = $schema->{maximum
})) {
1140 if ($value > $max) {
1141 add_error
($errors, $path, "value must have a maximum value of $max");
1146 if (defined (my $min = $schema->{minimum
})) {
1147 if ($value < $min) {
1148 add_error
($errors, $path, "value must have a minimum value of $min");
1154 if (my $ea = $schema->{enum
}) {
1157 foreach my $ev (@$ea) {
1158 if ($ev eq $value) {
1164 add_error
($errors, $path, "value '$value' does not have a value in the enumeration '" .
1165 join(", ", @$ea) . "'");
1172 my ($instance, $schema, $errmsg) = @_;
1175 $errmsg = "Parameter verification failed.\n" if !$errmsg;
1177 # todo: cycle detection is only needed for debugging, I guess
1178 # we can disable that in the final release
1179 # todo: is there a better/faster way to detect cycles?
1181 find_cycle
($instance, sub { $cycles = 1 });
1183 add_error
($errors, undef, "data structure contains recursive cycles");
1185 check_prop
($instance, $schema, '', $errors);
1188 if (scalar(%$errors)) {
1189 raise
$errmsg, code
=> HTTP_BAD_REQUEST
, errors
=> $errors;
1195 my $schema_valid_types = ["string", "object", "coderef", "array", "boolean", "number", "integer", "null", "any"];
1196 my $default_schema_noref = {
1197 description
=> "This is the JSON Schema for JSON Schemas.",
1198 type
=> [ "object" ],
1199 additionalProperties
=> 0,
1202 type
=> ["string", "array"],
1203 description
=> "This is a type definition value. This can be a simple type, or a union type",
1208 enum
=> $schema_valid_types,
1210 enum
=> $schema_valid_types,
1214 description
=> "This indicates that the instance property in the instance object is not required.",
1220 description
=> "This is a definition for the properties of an object value",
1226 description
=> "When the value is an array, this indicates the schema to use to validate each item in an array",
1230 additionalProperties
=> {
1231 type
=> [ "boolean", "object"],
1232 description
=> "This provides a default property definition for all properties that are not explicitly defined in an object type definition.",
1239 description
=> "This indicates the minimum value for the instance property when the type of the instance value is a number.",
1244 description
=> "This indicates the maximum value for the instance property when the type of the instance value is a number.",
1248 description
=> "When the instance value is a string, this indicates minimum length of the string",
1255 description
=> "When the instance value is a string, this indicates maximum length of the string.",
1261 description
=> "A text representation of the type (used to generate documentation).",
1266 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.",
1273 description
=> "This provides an enumeration of possible values that are valid for the instance property.",
1278 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).",
1280 verbose_description
=> {
1283 description
=> "This provides a more verbose description.",
1285 format_description
=> {
1288 description
=> "This provides a shorter (usually just one word) description for a property used to generate descriptions for comma separated list property strings.",
1293 description
=> "This provides the title of the property",
1298 description
=> "This is used to provide rendering hints to format cli command output.",
1301 type
=> [ "string", "object" ],
1303 description
=> "indicates a required property or a schema that must be validated if this property is present",
1306 type
=> [ "string", "object" ],
1308 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",
1313 description
=> "Whether this is the default key in a comma separated list property string.",
1318 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.",
1323 description
=> "Allows to store the current 'key' as value of another property. Only valid if used together with 'alias'.",
1324 requires
=> 'alias',
1329 description
=> "This indicates the default for the instance property."
1333 description
=> "Bash completion function. This function should return a list of possible values.",
1339 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.",
1344 description
=> "This indicates the schema extends the given schema. All instances of this schema must be valid to by the extended schema also.",
1347 # this is from hyper schema
1350 description
=> "This defines the link relations of the instance objects",
1357 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",
1361 description
=> "This is the name of the link relation",
1367 description
=> "For submission links, this defines the method that should be used to access the target resource",
1376 description
=> "For CLI context, this defines the maximal width to print before truncating",
1382 my $default_schema = Storable
::dclone
($default_schema_noref);
1384 $default_schema->{properties
}->{properties
}->{additionalProperties
} = $default_schema;
1385 $default_schema->{properties
}->{additionalProperties
}->{properties
} = $default_schema->{properties
};
1387 $default_schema->{properties
}->{items
}->{properties
} = $default_schema->{properties
};
1388 $default_schema->{properties
}->{items
}->{additionalProperties
} = 0;
1390 $default_schema->{properties
}->{disallow
}->{properties
} = $default_schema->{properties
};
1391 $default_schema->{properties
}->{disallow
}->{additionalProperties
} = 0;
1393 $default_schema->{properties
}->{requires
}->{properties
} = $default_schema->{properties
};
1394 $default_schema->{properties
}->{requires
}->{additionalProperties
} = 0;
1396 $default_schema->{properties
}->{extends
}->{properties
} = $default_schema->{properties
};
1397 $default_schema->{properties
}->{extends
}->{additionalProperties
} = 0;
1399 my $method_schema = {
1401 additionalProperties
=> 0,
1404 description
=> "This a description of the method",
1409 description
=> "This indicates the name of the function to call.",
1412 additionalProperties
=> 1,
1427 description
=> "The HTTP method name.",
1428 enum
=> [ 'GET', 'POST', 'PUT', 'DELETE' ],
1433 description
=> "Method needs special privileges - only pvedaemon can execute it",
1438 description
=> "Method is available for clients authenticated using an API token.",
1444 description
=> "Method downloads the file content (filename is the return value of the method).",
1449 description
=> "A parameter name. If specified, all calls to this method are proxied to the host contained in that parameter.",
1452 proxyto_callback
=> {
1454 description
=> "A function which is called to resolve the proxyto attribute. The default implementation returns the value of the 'proxyto' parameter.",
1459 description
=> "Required access permissions. By default only 'root' is allowed to access this method.",
1461 additionalProperties
=> 0,
1464 description
=> "Describe access permissions.",
1468 description
=> "A simply way to allow access for 'all' authenticated users. Value 'world' is used to allow access without credentials.",
1470 enum
=> ['all', 'world'],
1474 description
=> "Array of permission checks (prefix notation).",
1481 description
=> "Used internally",
1485 description
=> "Used internally",
1490 description
=> "path for URL matching (uri template)",
1492 fragmentDelimiter
=> {
1494 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.",
1499 description
=> "JSON Schema for parameters.",
1504 description
=> "JSON Schema for return value.",
1509 description
=> "method implementation (code reference)",
1514 description
=> "Delegate call to this class (perl class string).",
1517 additionalProperties
=> 0,
1523 fragmentDelimiter
=> { optional
=> 1 }
1531 sub validate_schema
{
1534 my $errmsg = "internal error - unable to verify schema\n";
1535 validate
($schema, $default_schema, $errmsg);
1538 sub validate_method_info
{
1541 my $errmsg = "internal error - unable to verify method info\n";
1542 validate
($info, $method_schema, $errmsg);
1544 validate_schema
($info->{parameters
}) if $info->{parameters
};
1545 validate_schema
($info->{returns
}) if $info->{returns
};
1548 # run a self test on load
1549 # make sure we can verify the default schema
1550 validate_schema
($default_schema_noref);
1551 validate_schema
($method_schema);
1553 # and now some utility methods (used by pve api)
1554 sub method_get_child_link
{
1557 return undef if !$info;
1559 my $schema = $info->{returns
};
1560 return undef if !$schema || !$schema->{type
} || $schema->{type
} ne 'array';
1562 my $links = $schema->{links
};
1563 return undef if !$links;
1566 foreach my $lnk (@$links) {
1567 if ($lnk->{href
} && $lnk->{rel
} && ($lnk->{rel
} eq 'child')) {
1576 # a way to parse command line parameters, using a
1577 # schema to configure Getopt::Long
1579 my ($schema, $args, $arg_param, $fixed_param, $param_mapping_hash) = @_;
1581 if (!$schema || !$schema->{properties
}) {
1582 raise
("too many arguments\n", code
=> HTTP_BAD_REQUEST
)
1583 if scalar(@$args) != 0;
1588 if ($arg_param && !ref($arg_param)) {
1589 my $pd = $schema->{properties
}->{$arg_param};
1590 die "expected list format $pd->{format}"
1591 if !($pd && $pd->{format
} && $pd->{format
} =~ m/-list/);
1592 $list_param = $arg_param;
1595 my @interactive = ();
1597 foreach my $prop (keys %{$schema->{properties
}}) {
1598 my $pd = $schema->{properties
}->{$prop};
1599 next if $list_param && $prop eq $list_param;
1600 next if defined($fixed_param->{$prop});
1602 my $mapping = $param_mapping_hash->{$prop};
1603 if ($mapping && $mapping->{interactive
}) {
1604 # interactive parameters such as passwords: make the argument
1605 # optional and call the mapping function afterwards.
1606 push @getopt, "$prop:s";
1607 push @interactive, [$prop, $mapping->{func
}];
1608 } elsif ($pd->{type
} eq 'boolean') {
1609 push @getopt, "$prop:s";
1611 if ($pd->{format
} && $pd->{format
} =~ m/-a?list/) {
1612 push @getopt, "$prop=s@";
1614 push @getopt, "$prop=s";
1619 Getopt
::Long
::Configure
('prefix_pattern=(--|-)');
1622 raise
("unable to parse option\n", code
=> HTTP_BAD_REQUEST
)
1623 if !Getopt
::Long
::GetOptionsFromArray
($args, $opts, @getopt);
1627 $opts->{$list_param} = $args;
1629 } elsif (ref($arg_param)) {
1630 for (my $i = 0; $i < scalar(@$arg_param); $i++) {
1631 my $arg_name = $arg_param->[$i];
1632 if ($opts->{'extra-args'}) {
1633 raise
("internal error: extra-args must be the last argument\n", code
=> HTTP_BAD_REQUEST
);
1635 if ($arg_name eq 'extra-args') {
1636 $opts->{'extra-args'} = $args;
1641 # check if all left-over arg_param are optional, else we
1642 # must die as the mapping is then ambigious
1643 for (my $j = $i; $j < scalar(@$arg_param); $j++) {
1644 my $prop = $arg_param->[$j];
1645 raise
("not enough arguments\n", code
=> HTTP_BAD_REQUEST
)
1646 if !$schema->{properties
}->{$prop}->{optional
};
1649 $opts->{$arg_name} = shift @$args;
1651 raise
("too many arguments\n", code
=> HTTP_BAD_REQUEST
) if @$args;
1653 raise
("too many arguments\n", code
=> HTTP_BAD_REQUEST
)
1654 if scalar(@$args) != 0;
1657 if (ref($arg_param)) {
1658 foreach my $arg_name (@$arg_param) {
1659 if ($arg_name eq 'extra-args') {
1660 $opts->{'extra-args'} = [];
1661 } elsif (!$schema->{properties
}->{$arg_name}->{optional
}) {
1662 raise
("not enough arguments\n", code
=> HTTP_BAD_REQUEST
);
1668 foreach my $entry (@interactive) {
1669 my ($opt, $func) = @$entry;
1670 my $pd = $schema->{properties
}->{$opt};
1671 my $value = $opts->{$opt};
1672 if (defined($value) || !$pd->{optional
}) {
1673 $opts->{$opt} = $func->($value);
1677 # decode after Getopt as we are not sure how well it handles unicode
1678 foreach my $p (keys %$opts) {
1679 if (!ref($opts->{$p})) {
1680 $opts->{$p} = decode
('locale', $opts->{$p});
1681 } elsif (ref($opts->{$p}) eq 'ARRAY') {
1683 foreach my $v (@{$opts->{$p}}) {
1684 push @$tmp, decode
('locale', $v);
1687 } elsif (ref($opts->{$p}) eq 'SCALAR') {
1688 $opts->{$p} = decode
('locale', $$opts->{$p});
1690 raise
("decoding options failed, unknown reference\n", code
=> HTTP_BAD_REQUEST
);
1694 foreach my $p (keys %$opts) {
1695 if (my $pd = $schema->{properties
}->{$p}) {
1696 if ($pd->{type
} eq 'boolean') {
1697 if ($opts->{$p} eq '') {
1699 } elsif (defined(my $bool = parse_boolean
($opts->{$p}))) {
1700 $opts->{$p} = $bool;
1702 raise
("unable to parse boolean option\n", code
=> HTTP_BAD_REQUEST
);
1704 } elsif ($pd->{format
}) {
1706 if ($pd->{format
} =~ m/-list/) {
1707 # allow --vmid 100 --vmid 101 and --vmid 100,101
1708 # allow --dow mon --dow fri and --dow mon,fri
1709 $opts->{$p} = join(",", @{$opts->{$p}}) if ref($opts->{$p}) eq 'ARRAY';
1710 } elsif ($pd->{format
} =~ m/-alist/) {
1711 # we encode array as \0 separated strings
1712 # Note: CGI.pm also use this encoding
1713 if (scalar(@{$opts->{$p}}) != 1) {
1714 $opts->{$p} = join("\0", @{$opts->{$p}});
1716 # st that split_list knows it is \0 terminated
1717 my $v = $opts->{$p}->[0];
1718 $opts->{$p} = "$v\0";
1725 foreach my $p (keys %$fixed_param) {
1726 $opts->{$p} = $fixed_param->{$p};
1732 # A way to parse configuration data by giving a json schema
1734 my ($schema, $filename, $raw) = @_;
1736 # do fast check (avoid validate_schema($schema))
1737 die "got strange schema" if !$schema->{type
} ||
1738 !$schema->{properties
} || $schema->{type
} ne 'object';
1742 while ($raw =~ /^\s*(.+?)\s*$/gm) {
1745 next if $line =~ /^#/;
1747 if ($line =~ m/^(\S+?):\s*(.*)$/) {
1750 if ($schema->{properties
}->{$key} &&
1751 $schema->{properties
}->{$key}->{type
} eq 'boolean') {
1753 $value = parse_boolean
($value) // $value;
1755 $cfg->{$key} = $value;
1757 warn "ignore config line: $line\n"
1762 check_prop
($cfg, $schema, '', $errors);
1764 foreach my $k (keys %$errors) {
1765 warn "parse error in '$filename' - '$k': $errors->{$k}\n";
1772 # generate simple key/value file
1774 my ($schema, $filename, $cfg) = @_;
1776 # do fast check (avoid validate_schema($schema))
1777 die "got strange schema" if !$schema->{type
} ||
1778 !$schema->{properties
} || $schema->{type
} ne 'object';
1780 validate
($cfg, $schema, "validation error in '$filename'\n");
1784 foreach my $k (sort keys %$cfg) {
1785 $data .= "$k: $cfg->{$k}\n";
1791 # helpers used to generate our manual pages
1793 my $find_schema_default_key = sub {
1797 my $keyAliasProps = {};
1799 foreach my $key (keys %$format) {
1800 my $phash = $format->{$key};
1801 if ($phash->{default_key
}) {
1802 die "multiple default keys in schema ($default_key, $key)\n"
1803 if defined($default_key);
1804 die "default key '$key' is an alias - this is not allowed\n"
1805 if defined($phash->{alias
});
1806 die "default key '$key' with keyAlias attribute is not allowed\n"
1807 if $phash->{keyAlias
};
1808 $default_key = $key;
1810 my $key_alias = $phash->{keyAlias
};
1811 die "found keyAlias without 'alias definition for '$key'\n"
1812 if $key_alias && !$phash->{alias
};
1814 if ($phash->{alias
} && $key_alias) {
1815 die "inconsistent keyAlias '$key_alias' definition"
1816 if defined($keyAliasProps->{$key_alias}) &&
1817 $keyAliasProps->{$key_alias} ne $phash->{alias
};
1818 $keyAliasProps->{$key_alias} = $phash->{alias
};
1822 return wantarray ?
($default_key, $keyAliasProps) : $default_key;
1825 sub generate_typetext
{
1826 my ($format, $list_enums) = @_;
1828 my ($default_key, $keyAliasProps) = &$find_schema_default_key($format);
1833 my $add_option_string = sub {
1834 my ($text, $optional) = @_;
1840 $text = "[$text]" if $optional;
1845 my $format_key_value = sub {
1846 my ($key, $phash) = @_;
1848 die "internal error" if defined($phash->{alias
});
1854 if (my $desc = $phash->{format_description
}) {
1855 $typetext .= "<$desc>";
1856 } elsif (my $text = $phash->{typetext
}) {
1858 } elsif (my $enum = $phash->{enum
}) {
1859 if ($list_enums || (scalar(@$enum) <= 3)) {
1860 $typetext .= '<' . join('|', @$enum) . '>';
1862 $typetext .= '<enum>';
1864 } elsif ($phash->{type
} eq 'boolean') {
1865 $typetext .= '<1|0>';
1866 } elsif ($phash->{type
} eq 'integer') {
1867 $typetext .= '<integer>';
1868 } elsif ($phash->{type
} eq 'number') {
1869 $typetext .= '<number>';
1871 die "internal error: neither format_description nor typetext found for option '$key'";
1874 if (defined($default_key) && ($default_key eq $key)) {
1875 &$add_option_string("[$keytext=]$typetext", $phash->{optional
});
1877 &$add_option_string("$keytext=$typetext", $phash->{optional
});
1883 my $cond_add_key = sub {
1886 return if $done->{$key}; # avoid duplicates
1890 my $phash = $format->{$key};
1892 return if !$phash; # should not happen
1894 return if $phash->{alias
};
1896 &$format_key_value($key, $phash);
1900 &$cond_add_key($default_key) if defined($default_key);
1902 # add required keys first
1903 foreach my $key (sort keys %$format) {
1904 my $phash = $format->{$key};
1905 &$cond_add_key($key) if $phash && !$phash->{optional
};
1909 foreach my $key (sort keys %$format) {
1910 &$cond_add_key($key);
1913 foreach my $keyAlias (sort keys %$keyAliasProps) {
1914 &$add_option_string("<$keyAlias>=<$keyAliasProps->{$keyAlias }>", 1);
1920 sub print_property_string
{
1921 my ($data, $format, $skip, $path) = @_;
1924 if (ref($format) ne 'HASH') {
1925 my $schema = get_format
($format);
1926 die "not a valid format: $format\n" if !$schema;
1927 # named formats can have validators attached
1928 $validator = $format_validators->{$format};
1933 check_object
($path, $format, $data, undef, $errors);
1934 if (scalar(%$errors)) {
1935 raise
"format error", errors
=> $errors;
1938 $data = $validator->($data) if $validator;
1940 my ($default_key, $keyAliasProps) = &$find_schema_default_key($format);
1945 my $add_option_string = sub {
1948 $res .= ',' if $add_sep;
1953 my $format_value = sub {
1954 my ($key, $value, $format) = @_;
1956 if (defined($format) && ($format eq 'disk-size')) {
1957 return format_size
($value);
1959 die "illegal value with commas for $key\n" if $value =~ /,/;
1964 my $done = { map { $_ => 1 } @$skip };
1966 my $cond_add_key = sub {
1967 my ($key, $isdefault) = @_;
1969 return if $done->{$key}; # avoid duplicates
1973 my $value = $data->{$key};
1975 return if !defined($value);
1977 my $phash = $format->{$key};
1979 # try to combine values if we have key aliases
1980 if (my $combine = $keyAliasProps->{$key}) {
1981 if (defined(my $combine_value = $data->{$combine})) {
1982 my $combine_format = $format->{$combine}->{format
};
1983 my $value_str = &$format_value($key, $value, $phash->{format
});
1984 my $combine_str = &$format_value($combine, $combine_value, $combine_format);
1985 &$add_option_string("${value_str}=${combine_str}");
1986 $done->{$combine} = 1;
1991 if ($phash && $phash->{alias
}) {
1992 $phash = $format->{$phash->{alias
}};
1995 die "invalid key '$key'\n" if !$phash;
1996 die "internal error" if defined($phash->{alias
});
1998 my $value_str = &$format_value($key, $value, $phash->{format
});
2000 &$add_option_string($value_str);
2002 &$add_option_string("$key=${value_str}");
2006 # add default key first
2007 &$cond_add_key($default_key, 1) if defined($default_key);
2009 # add required keys first
2010 foreach my $key (sort keys %$data) {
2011 my $phash = $format->{$key};
2012 &$cond_add_key($key) if $phash && !$phash->{optional
};
2016 foreach my $key (sort keys %$data) {
2017 &$cond_add_key($key);
2023 sub schema_get_type_text
{
2024 my ($phash, $style) = @_;
2026 my $type = $phash->{type
} || 'string';
2028 if ($phash->{typetext
}) {
2029 return $phash->{typetext
};
2030 } elsif ($phash->{format_description
}) {
2031 return "<$phash->{format_description}>";
2032 } elsif ($phash->{enum
}) {
2033 return "<" . join(' | ', sort @{$phash->{enum
}}) . ">";
2034 } elsif ($phash->{pattern
}) {
2035 return $phash->{pattern
};
2036 } elsif ($type eq 'integer' || $type eq 'number') {
2037 # NOTE: always access values as number (avoid converion to string)
2038 if (defined($phash->{minimum
}) && defined($phash->{maximum
})) {
2039 return "<$type> (" . ($phash->{minimum
} + 0) . " - " .
2040 ($phash->{maximum
} + 0) . ")";
2041 } elsif (defined($phash->{minimum
})) {
2042 return "<$type> (" . ($phash->{minimum
} + 0) . " - N)";
2043 } elsif (defined($phash->{maximum
})) {
2044 return "<$type> (-N - " . ($phash->{maximum
} + 0) . ")";
2046 } elsif ($type eq 'string') {
2047 if (my $format = $phash->{format
}) {
2048 $format = get_format
($format) if ref($format) ne 'HASH';
2049 if (ref($format) eq 'HASH') {
2051 $list_enums = 1 if $style && $style eq 'config-sub';
2052 return generate_typetext
($format, $list_enums);