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 my $verify_idpair = sub {
277 my ($input, $noerr, $format) = @_;
279 eval { parse_idmap
($input, $format) };
281 return undef if $noerr;
288 # note: this only checks a single list entry
289 # when using a storagepair-list map, you need to pass the full parameter to
291 register_format
('storagepair', \
&verify_storagepair
);
292 sub verify_storagepair
{
293 my ($storagepair, $noerr) = @_;
294 return $verify_idpair->($storagepair, $noerr, 'pve-storage-id');
298 register_format
('mac-addr', \
&pve_verify_mac_addr
);
299 sub pve_verify_mac_addr
{
300 my ($mac_addr, $noerr) = @_;
302 # don't allow I/G bit to be set, most of the time it breaks things, see:
303 # https://pve.proxmox.com/pipermail/pve-devel/2019-March/035998.html
304 if ($mac_addr !~ m/^[a-f0-9][02468ace](?::[a-f0-9]{2}){5}$/i) {
305 return undef if $noerr;
306 die "value does not look like a valid unicast MAC address\n";
311 register_standard_option
('mac-addr', {
313 description
=> 'Unicast MAC address.',
314 verbose_description
=> 'A common MAC address with the I/G (Individual/Group) bit not set.',
315 format_description
=> "XX:XX:XX:XX:XX:XX",
317 format
=> 'mac-addr',
320 register_format
('ipv4', \
&pve_verify_ipv4
);
321 sub pve_verify_ipv4
{
322 my ($ipv4, $noerr) = @_;
324 if ($ipv4 !~ m/^(?:$IPV4RE)$/) {
325 return undef if $noerr;
326 die "value does not look like a valid IPv4 address\n";
331 register_format
('ipv6', \
&pve_verify_ipv6
);
332 sub pve_verify_ipv6
{
333 my ($ipv6, $noerr) = @_;
335 if ($ipv6 !~ m/^(?:$IPV6RE)$/) {
336 return undef if $noerr;
337 die "value does not look like a valid IPv6 address\n";
342 register_format
('ip', \
&pve_verify_ip
);
344 my ($ip, $noerr) = @_;
346 if ($ip !~ m/^(?:(?:$IPV4RE)|(?:$IPV6RE))$/) {
347 return undef if $noerr;
348 die "value does not look like a valid IP address\n";
353 PVE
::JSONSchema
::register_format
('ldap-simple-attr', \
&verify_ldap_simple_attr
);
354 sub verify_ldap_simple_attr
{
355 my ($attr, $noerr) = @_;
357 if ($attr =~ m/^[a-zA-Z0-9]+$/) {
361 die "value '$attr' does not look like a simple ldap attribute name\n" if !$noerr;
366 my $ipv4_mask_hash = {
384 '255.255.128.0' => 17,
385 '255.255.192.0' => 18,
386 '255.255.224.0' => 19,
387 '255.255.240.0' => 20,
388 '255.255.248.0' => 21,
389 '255.255.252.0' => 22,
390 '255.255.254.0' => 23,
391 '255.255.255.0' => 24,
392 '255.255.255.128' => 25,
393 '255.255.255.192' => 26,
394 '255.255.255.224' => 27,
395 '255.255.255.240' => 28,
396 '255.255.255.248' => 29,
397 '255.255.255.252' => 30,
398 '255.255.255.254' => 31,
399 '255.255.255.255' => 32,
402 sub get_netmask_bits
{
404 return $ipv4_mask_hash->{$mask};
407 register_format
('ipv4mask', \
&pve_verify_ipv4mask
);
408 sub pve_verify_ipv4mask
{
409 my ($mask, $noerr) = @_;
411 if (!defined($ipv4_mask_hash->{$mask})) {
412 return undef if $noerr;
413 die "value does not look like a valid IP netmask\n";
418 register_format
('CIDRv6', \
&pve_verify_cidrv6
);
419 sub pve_verify_cidrv6
{
420 my ($cidr, $noerr) = @_;
422 if ($cidr =~ m!^(?:$IPV6RE)(?:/(\d+))$! && ($1 > 7) && ($1 <= 128)) {
426 return undef if $noerr;
427 die "value does not look like a valid IPv6 CIDR network\n";
430 register_format
('CIDRv4', \
&pve_verify_cidrv4
);
431 sub pve_verify_cidrv4
{
432 my ($cidr, $noerr) = @_;
434 if ($cidr =~ m!^(?:$IPV4RE)(?:/(\d+))$! && ($1 > 7) && ($1 <= 32)) {
438 return undef if $noerr;
439 die "value does not look like a valid IPv4 CIDR network\n";
442 register_format
('CIDR', \
&pve_verify_cidr
);
443 sub pve_verify_cidr
{
444 my ($cidr, $noerr) = @_;
446 if (!(pve_verify_cidrv4
($cidr, 1) ||
447 pve_verify_cidrv6
($cidr, 1)))
449 return undef if $noerr;
450 die "value does not look like a valid CIDR network\n";
456 register_format
('pve-ipv4-config', \
&pve_verify_ipv4_config
);
457 sub pve_verify_ipv4_config
{
458 my ($config, $noerr) = @_;
460 return $config if $config =~ /^(?:dhcp|manual)$/ ||
461 pve_verify_cidrv4
($config, 1);
462 return undef if $noerr;
463 die "value does not look like a valid ipv4 network configuration\n";
466 register_format
('pve-ipv6-config', \
&pve_verify_ipv6_config
);
467 sub pve_verify_ipv6_config
{
468 my ($config, $noerr) = @_;
470 return $config if $config =~ /^(?:auto|dhcp|manual)$/ ||
471 pve_verify_cidrv6
($config, 1);
472 return undef if $noerr;
473 die "value does not look like a valid ipv6 network configuration\n";
476 register_format
('email', \
&pve_verify_email
);
477 sub pve_verify_email
{
478 my ($email, $noerr) = @_;
480 if ($email !~ /^$PVE::Tools::EMAIL_RE$/) {
481 return undef if $noerr;
482 die "value does not look like a valid email address\n";
487 register_format
('email-or-username', \
&pve_verify_email_or_username
);
488 sub pve_verify_email_or_username
{
489 my ($email, $noerr) = @_;
491 if ($email !~ /^$PVE::Tools::EMAIL_RE$/ &&
492 $email !~ /^$PVE::Tools::EMAIL_USER_RE$/) {
493 return undef if $noerr;
494 die "value does not look like a valid email address or user name\n";
499 register_format
('dns-name', \
&pve_verify_dns_name
);
500 sub pve_verify_dns_name
{
501 my ($name, $noerr) = @_;
503 my $namere = "([a-zA-Z0-9]([a-zA-Z0-9\-]*[a-zA-Z0-9])?)";
505 if ($name !~ /^(${namere}\.)*${namere}$/) {
506 return undef if $noerr;
507 die "value does not look like a valid DNS name\n";
512 register_format
('timezone', \
&pve_verify_timezone
);
513 sub pve_verify_timezone
{
514 my ($timezone, $noerr) = @_;
516 return $timezone if $timezone eq 'UTC';
518 open(my $fh, "<", "/usr/share/zoneinfo/zone.tab");
519 while (my $line = <$fh>) {
520 next if $line =~ /^\s*#/;
522 my $zone = (split /\t/, $line)[2];
523 return $timezone if $timezone eq $zone; # found
527 return undef if $noerr;
528 die "invalid time zone '$timezone'\n";
531 # network interface name
532 register_format
('pve-iface', \
&pve_verify_iface
);
533 sub pve_verify_iface
{
534 my ($id, $noerr) = @_;
536 if ($id !~ m/^[a-z][a-z0-9_]{1,20}([:\.]\d+)?$/i) {
537 return undef if $noerr;
538 die "invalid network interface name '$id'\n";
543 # general addresses by name or IP
544 register_format
('address', \
&pve_verify_address
);
545 sub pve_verify_address
{
546 my ($addr, $noerr) = @_;
548 if (!(pve_verify_ip
($addr, 1) ||
549 pve_verify_dns_name
($addr, 1)))
551 return undef if $noerr;
552 die "value does not look like a valid address: $addr\n";
557 register_format
('disk-size', \
&pve_verify_disk_size
);
558 sub pve_verify_disk_size
{
559 my ($size, $noerr) = @_;
560 if (!defined(parse_size
($size))) {
561 return undef if $noerr;
562 die "value does not look like a valid disk size: $size\n";
567 register_standard_option
('spice-proxy', {
568 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).",
569 type
=> 'string', format
=> 'address',
572 register_standard_option
('remote-viewer-config', {
573 description
=> "Returned values can be directly passed to the 'remote-viewer' application.",
574 additionalProperties
=> 1,
576 type
=> { type
=> 'string' },
577 password
=> { type
=> 'string' },
578 proxy
=> { type
=> 'string' },
579 host
=> { type
=> 'string' },
580 'tls-port' => { type
=> 'integer' },
584 register_format
('pve-startup-order', \
&pve_verify_startup_order
);
585 sub pve_verify_startup_order
{
586 my ($value, $noerr) = @_;
588 return $value if pve_parse_startup_order
($value);
590 return undef if $noerr;
592 die "unable to parse startup options\n";
597 type
=> 'number', minimum
=> '0',
598 format_description
=> 'LIMIT',
601 my $bwlimit_format = {
604 description
=> 'default bandwidth limit in KiB/s',
608 description
=> 'bandwidth limit in KiB/s for restoring guests from backups',
612 description
=> 'bandwidth limit in KiB/s for migrating guests (including moving local disks)',
616 description
=> 'bandwidth limit in KiB/s for cloning disks',
620 description
=> 'bandwidth limit in KiB/s for moving disks',
623 register_format
('bwlimit', $bwlimit_format);
624 register_standard_option
('bwlimit', {
625 description
=> "Set bandwidth/io limits various operations.",
628 format
=> $bwlimit_format,
631 # used for pve-tag-list in e.g., guest configs
632 register_format
('pve-tag', \
&pve_verify_tag
);
634 my ($value, $noerr) = @_;
636 return $value if $value =~ m/^[a-z0-9_][a-z0-9_\-\+\.]*$/i;
638 return undef if $noerr;
640 die "invalid characters in tag\n";
643 sub pve_parse_startup_order
{
646 return undef if !$value;
650 foreach my $p (split(/,/, $value)) {
651 next if $p =~ m/^\s*$/;
653 if ($p =~ m/^(order=)?(\d+)$/) {
655 } elsif ($p =~ m/^up=(\d+)$/) {
657 } elsif ($p =~ m/^down=(\d+)$/) {
667 PVE
::JSONSchema
::register_standard_option
('pve-startup-order', {
668 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.",
670 type
=> 'string', format
=> 'pve-startup-order',
671 typetext
=> '[[order=]\d+] [,up=\d+] [,down=\d+] ',
674 register_format
('pve-tfa-secret', \
&pve_verify_tfa_secret
);
675 sub pve_verify_tfa_secret
{
676 my ($key, $noerr) = @_;
678 # The old format used 16 base32 chars or 40 hex digits. Since they have a common subset it's
679 # hard to distinguish them without the our previous length constraints, so add a 'v2' of the
680 # format to support arbitrary lengths properly:
681 if ($key =~ /^v2-0x[0-9a-fA-F]{16,128}$/ || # hex
682 $key =~ /^v2-[A-Z2-7=]{16,128}$/ || # base32
683 $key =~ /^(?:[A-Z2-7=]{16}|[A-Fa-f0-9]{40})$/) # and the old pattern copy&pasted
688 return undef if $noerr;
690 die "unable to decode TFA secret\n";
694 my ($format, $value, $path) = @_;
696 if (ref($format) eq 'HASH') {
697 # hash ref cannot have validator/list/opt handling attached
698 return parse_property_string
($format, $value, $path);
701 if (ref($format) eq 'CODE') {
702 # we are the (sole, old-style) validator
703 return $format->($value);
706 return if $format eq 'regex';
709 $format =~ m/^(.*?)(?:-a?(list|opt))?$/;
710 my ($format_name, $format_type) = ($1, $2 // 'none');
711 my $registered = get_format
($format_name);
712 die "undefined format '$format'\n" if !$registered;
714 die "'-$format_type' format must have code ref, not hash\n"
715 if $format_type ne 'none' && ref($registered) ne 'CODE';
717 if ($format_type eq 'list') {
718 # Note: we allow empty lists
719 foreach my $v (split_list
($value)) {
720 $parsed = $registered->($v);
722 } elsif ($format_type eq 'opt') {
723 $parsed = $registered->($value) if $value;
725 if (ref($registered) eq 'HASH') {
726 # Note: this is the only case where a validator function could be
727 # attached, hence it's safe to handle that in parse_property_string.
728 # We do however have to call it with $format_name instead of
729 # $registered, so it knows about the name (and thus any validators).
730 $parsed = parse_property_string
($format, $value, $path);
732 $parsed = $registered->($value);
742 return undef if $value !~ m/^(\d+(\.\d+)?)([KMGT])?$/;
743 my ($size, $unit) = ($1, $3);
746 $size = $size * 1024;
747 } elsif ($unit eq 'M') {
748 $size = $size * 1024 * 1024;
749 } elsif ($unit eq 'G') {
750 $size = $size * 1024 * 1024 * 1024;
751 } elsif ($unit eq 'T') {
752 $size = $size * 1024 * 1024 * 1024 * 1024;
763 my $kb = int($size/1024);
764 return $size if $kb*1024 != $size;
766 my $mb = int($kb/1024);
767 return "${kb}K" if $mb*1024 != $kb;
769 my $gb = int($mb/1024);
770 return "${mb}M" if $gb*1024 != $mb;
772 my $tb = int($gb/1024);
773 return "${gb}G" if $tb*1024 != $gb;
780 return 1 if $bool =~ m/^(1|on|yes|true)$/i;
781 return 0 if $bool =~ m/^(0|off|no|false)$/i;
785 sub parse_property_string
{
786 my ($format, $data, $path, $additional_properties) = @_;
788 # In property strings we default to not allowing additional properties
789 $additional_properties = 0 if !defined($additional_properties);
791 # Support named formats here, too:
794 if (my $reg = get_format
($format)) {
795 die "parse_property_string only accepts hash based named formats\n"
796 if ref($reg) ne 'HASH';
798 # named formats can have validators attached
799 $validator = $format_validators->{$format};
803 die "unknown format: $format\n";
805 } elsif (ref($format) ne 'HASH') {
806 die "unexpected format value of type ".ref($format)."\n";
812 foreach my $part (split(/,/, $data)) {
813 next if $part =~ /^\s*$/;
815 if ($part =~ /^([^=]+)=(.+)$/) {
816 my ($k, $v) = ($1, $2);
817 die "duplicate key in comma-separated list property: $k\n" if defined($res->{$k});
818 my $schema = $format->{$k};
819 if (my $alias = $schema->{alias
}) {
820 if (my $key_alias = $schema->{keyAlias
}) {
821 die "key alias '$key_alias' is already defined\n" if defined($res->{$key_alias});
822 $res->{$key_alias} = $k;
825 $schema = $format->{$k};
828 die "invalid key in comma-separated list property: $k\n" if !$schema;
829 if ($schema->{type
} && $schema->{type
} eq 'boolean') {
830 $v = parse_boolean
($v) // $v;
833 } elsif ($part !~ /=/) {
834 die "duplicate key in comma-separated list property: $default_key\n" if $default_key;
835 foreach my $key (keys %$format) {
836 if ($format->{$key}->{default_key
}) {
838 if (!$res->{$default_key}) {
839 $res->{$default_key} = $part;
842 die "duplicate key in comma-separated list property: $default_key\n";
845 die "value without key, but schema does not define a default key\n" if !$default_key;
847 die "missing key in comma-separated list property\n";
852 check_object
($path, $format, $res, $additional_properties, $errors);
853 if (scalar(%$errors)) {
854 raise
"format error\n", errors
=> $errors;
857 return $validator->($res) if $validator;
862 my ($errors, $path, $msg) = @_;
864 $path = '_root' if !$path;
866 if ($errors->{$path}) {
867 $errors->{$path} = join ('\n', $errors->{$path}, $msg);
869 $errors->{$path} = $msg;
876 # see 'man perlretut'
877 return $value =~ /^[+-]?(\d+\.\d+|\d+\.|\.\d+|\d+)([eE][+-]?\d+)?$/;
883 return $value =~ m/^[+-]?\d+$/;
887 my ($path, $type, $value, $errors) = @_;
891 if (!defined($value)) {
892 return 1 if $type eq 'null';
896 if (my $tt = ref($type)) {
897 if ($tt eq 'ARRAY') {
898 foreach my $t (@$type) {
900 check_type
($path, $t, $value, $tmperr);
901 return 1 if !scalar(%$tmperr);
903 my $ttext = join ('|', @$type);
904 add_error
($errors, $path, "type check ('$ttext') failed");
906 } elsif ($tt eq 'HASH') {
908 check_prop
($value, $type, $path, $tmperr);
909 return 1 if !scalar(%$tmperr);
910 add_error
($errors, $path, "type check failed");
913 die "internal error - got reference type '$tt'";
918 return 1 if $type eq 'any';
920 if ($type eq 'null') {
921 if (defined($value)) {
922 add_error
($errors, $path, "type check ('$type') failed - value is not null");
928 my $vt = ref($value);
930 if ($type eq 'array') {
931 if (!$vt || $vt ne 'ARRAY') {
932 add_error
($errors, $path, "type check ('$type') failed");
936 } elsif ($type eq 'object') {
937 if (!$vt || $vt ne 'HASH') {
938 add_error
($errors, $path, "type check ('$type') failed");
942 } elsif ($type eq 'coderef') {
943 if (!$vt || $vt ne 'CODE') {
944 add_error
($errors, $path, "type check ('$type') failed");
948 } elsif ($type eq 'string' && $vt eq 'Regexp') {
949 # qr// regexes can be used as strings and make sense for format=regex
953 add_error
($errors, $path, "type check ('$type') failed - got $vt");
956 if ($type eq 'string') {
957 return 1; # nothing to check ?
958 } elsif ($type eq 'boolean') {
959 #if ($value =~ m/^(1|true|yes|on)$/i) {
962 #} elsif ($value =~ m/^(0|false|no|off)$/i) {
963 } elsif ($value eq '0') {
964 return 1; # return success (not value)
966 add_error
($errors, $path, "type check ('$type') failed - got '$value'");
969 } elsif ($type eq 'integer') {
970 if (!is_integer
($value)) {
971 add_error
($errors, $path, "type check ('$type') failed - got '$value'");
975 } elsif ($type eq 'number') {
976 if (!is_number
($value)) {
977 add_error
($errors, $path, "type check ('$type') failed - got '$value'");
982 return 1; # no need to verify unknown types
992 my ($path, $schema, $value, $additional_properties, $errors) = @_;
994 # print "Check Object " . Dumper($value) . "\nSchema: " . Dumper($schema);
996 my $st = ref($schema);
997 if (!$st || $st ne 'HASH') {
998 add_error
($errors, $path, "Invalid schema definition.");
1002 my $vt = ref($value);
1003 if (!$vt || $vt ne 'HASH') {
1004 add_error
($errors, $path, "an object is required");
1008 foreach my $k (keys %$schema) {
1009 check_prop
($value->{$k}, $schema->{$k}, $path ?
"$path.$k" : $k, $errors);
1012 foreach my $k (keys %$value) {
1014 my $newpath = $path ?
"$path.$k" : $k;
1016 if (my $subschema = $schema->{$k}) {
1017 if (my $requires = $subschema->{requires
}) {
1018 if (ref($requires)) {
1019 #print "TEST: " . Dumper($value) . "\n", Dumper($requires) ;
1020 check_prop
($value, $requires, $path, $errors);
1021 } elsif (!defined($value->{$requires})) {
1022 add_error
($errors, $path ?
"$path.$requires" : $requires,
1023 "missing property - '$newpath' requires this property");
1027 next; # value is already checked above
1030 if (defined ($additional_properties) && !$additional_properties) {
1031 add_error
($errors, $newpath, "property is not defined in schema " .
1032 "and the schema does not allow additional properties");
1035 check_prop
($value->{$k}, $additional_properties, $newpath, $errors)
1036 if ref($additional_properties);
1040 sub check_object_warn
{
1041 my ($path, $schema, $value, $additional_properties) = @_;
1043 check_object
($path, $schema, $value, $additional_properties, $errors);
1044 if (scalar(%$errors)) {
1045 foreach my $k (keys %$errors) {
1046 warn "parse error: $k: $errors->{$k}\n";
1054 my ($value, $schema, $path, $errors) = @_;
1056 die "internal error - no schema" if !$schema;
1057 die "internal error" if !$errors;
1059 #print "check_prop $path\n" if $value;
1061 my $st = ref($schema);
1062 if (!$st || $st ne 'HASH') {
1063 add_error
($errors, $path, "Invalid schema definition.");
1067 # if it extends another schema, it must pass that schema as well
1068 if($schema->{extends
}) {
1069 check_prop
($value, $schema->{extends
}, $path, $errors);
1072 if (!defined ($value)) {
1073 return if $schema->{type
} && $schema->{type
} eq 'null';
1074 if (!$schema->{optional
} && !$schema->{alias
} && !$schema->{group
}) {
1075 add_error
($errors, $path, "property is missing and it is not optional");
1080 return if !check_type
($path, $schema->{type
}, $value, $errors);
1082 if ($schema->{disallow
}) {
1084 if (check_type
($path, $schema->{disallow
}, $value, $tmperr)) {
1085 add_error
($errors, $path, "disallowed value was matched");
1090 if (my $vt = ref($value)) {
1092 if ($vt eq 'ARRAY') {
1093 if ($schema->{items
}) {
1094 my $it = ref($schema->{items
});
1095 if ($it && $it eq 'ARRAY') {
1096 #die "implement me $path: $vt " . Dumper($schema) ."\n". Dumper($value);
1097 die "not implemented";
1100 foreach my $el (@$value) {
1101 check_prop
($el, $schema->{items
}, "${path}[$ind]", $errors);
1107 } elsif ($schema->{properties
} || $schema->{additionalProperties
}) {
1108 check_object
($path, defined($schema->{properties
}) ?
$schema->{properties
} : {},
1109 $value, $schema->{additionalProperties
}, $errors);
1115 if (my $format = $schema->{format
}) {
1116 eval { check_format
($format, $value, $path); };
1118 add_error
($errors, $path, "invalid format - $@");
1123 if (my $pattern = $schema->{pattern
}) {
1124 if ($value !~ m/^$pattern$/) {
1125 add_error
($errors, $path, "value does not match the regex pattern");
1130 if (defined (my $max = $schema->{maxLength
})) {
1131 if (length($value) > $max) {
1132 add_error
($errors, $path, "value may only be $max characters long");
1137 if (defined (my $min = $schema->{minLength
})) {
1138 if (length($value) < $min) {
1139 add_error
($errors, $path, "value must be at least $min characters long");
1144 if (is_number
($value)) {
1145 if (defined (my $max = $schema->{maximum
})) {
1146 if ($value > $max) {
1147 add_error
($errors, $path, "value must have a maximum value of $max");
1152 if (defined (my $min = $schema->{minimum
})) {
1153 if ($value < $min) {
1154 add_error
($errors, $path, "value must have a minimum value of $min");
1160 if (my $ea = $schema->{enum
}) {
1163 foreach my $ev (@$ea) {
1164 if ($ev eq $value) {
1170 add_error
($errors, $path, "value '$value' does not have a value in the enumeration '" .
1171 join(", ", @$ea) . "'");
1178 my ($instance, $schema, $errmsg) = @_;
1181 $errmsg = "Parameter verification failed.\n" if !$errmsg;
1183 # todo: cycle detection is only needed for debugging, I guess
1184 # we can disable that in the final release
1185 # todo: is there a better/faster way to detect cycles?
1187 find_cycle
($instance, sub { $cycles = 1 });
1189 add_error
($errors, undef, "data structure contains recursive cycles");
1191 check_prop
($instance, $schema, '', $errors);
1194 if (scalar(%$errors)) {
1195 raise
$errmsg, code
=> HTTP_BAD_REQUEST
, errors
=> $errors;
1201 my $schema_valid_types = ["string", "object", "coderef", "array", "boolean", "number", "integer", "null", "any"];
1202 my $default_schema_noref = {
1203 description
=> "This is the JSON Schema for JSON Schemas.",
1204 type
=> [ "object" ],
1205 additionalProperties
=> 0,
1208 type
=> ["string", "array"],
1209 description
=> "This is a type definition value. This can be a simple type, or a union type",
1214 enum
=> $schema_valid_types,
1216 enum
=> $schema_valid_types,
1220 description
=> "This indicates that the instance property in the instance object is not required.",
1226 description
=> "This is a definition for the properties of an object value",
1232 description
=> "When the value is an array, this indicates the schema to use to validate each item in an array",
1236 additionalProperties
=> {
1237 type
=> [ "boolean", "object"],
1238 description
=> "This provides a default property definition for all properties that are not explicitly defined in an object type definition.",
1245 description
=> "This indicates the minimum value for the instance property when the type of the instance value is a number.",
1250 description
=> "This indicates the maximum value for the instance property when the type of the instance value is a number.",
1254 description
=> "When the instance value is a string, this indicates minimum length of the string",
1261 description
=> "When the instance value is a string, this indicates maximum length of the string.",
1267 description
=> "A text representation of the type (used to generate documentation).",
1272 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.",
1279 description
=> "This provides an enumeration of possible values that are valid for the instance property.",
1284 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).",
1286 verbose_description
=> {
1289 description
=> "This provides a more verbose description.",
1291 format_description
=> {
1294 description
=> "This provides a shorter (usually just one word) description for a property used to generate descriptions for comma separated list property strings.",
1299 description
=> "This provides the title of the property",
1304 description
=> "This is used to provide rendering hints to format cli command output.",
1307 type
=> [ "string", "object" ],
1309 description
=> "indicates a required property or a schema that must be validated if this property is present",
1312 type
=> [ "string", "object" ],
1314 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",
1319 description
=> "Whether this is the default key in a comma separated list property string.",
1324 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.",
1329 description
=> "Allows to store the current 'key' as value of another property. Only valid if used together with 'alias'.",
1330 requires
=> 'alias',
1335 description
=> "This indicates the default for the instance property."
1339 description
=> "Bash completion function. This function should return a list of possible values.",
1345 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.",
1350 description
=> "This indicates the schema extends the given schema. All instances of this schema must be valid to by the extended schema also.",
1353 # this is from hyper schema
1356 description
=> "This defines the link relations of the instance objects",
1363 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",
1367 description
=> "This is the name of the link relation",
1373 description
=> "For submission links, this defines the method that should be used to access the target resource",
1382 description
=> "For CLI context, this defines the maximal width to print before truncating",
1388 my $default_schema = Storable
::dclone
($default_schema_noref);
1390 $default_schema->{properties
}->{properties
}->{additionalProperties
} = $default_schema;
1391 $default_schema->{properties
}->{additionalProperties
}->{properties
} = $default_schema->{properties
};
1393 $default_schema->{properties
}->{items
}->{properties
} = $default_schema->{properties
};
1394 $default_schema->{properties
}->{items
}->{additionalProperties
} = 0;
1396 $default_schema->{properties
}->{disallow
}->{properties
} = $default_schema->{properties
};
1397 $default_schema->{properties
}->{disallow
}->{additionalProperties
} = 0;
1399 $default_schema->{properties
}->{requires
}->{properties
} = $default_schema->{properties
};
1400 $default_schema->{properties
}->{requires
}->{additionalProperties
} = 0;
1402 $default_schema->{properties
}->{extends
}->{properties
} = $default_schema->{properties
};
1403 $default_schema->{properties
}->{extends
}->{additionalProperties
} = 0;
1405 my $method_schema = {
1407 additionalProperties
=> 0,
1410 description
=> "This a description of the method",
1415 description
=> "This indicates the name of the function to call.",
1418 additionalProperties
=> 1,
1433 description
=> "The HTTP method name.",
1434 enum
=> [ 'GET', 'POST', 'PUT', 'DELETE' ],
1439 description
=> "Method needs special privileges - only pvedaemon can execute it",
1444 description
=> "Method is available for clients authenticated using an API token.",
1450 description
=> "Method downloads the file content (filename is the return value of the method).",
1455 description
=> "A parameter name. If specified, all calls to this method are proxied to the host contained in that parameter.",
1458 proxyto_callback
=> {
1460 description
=> "A function which is called to resolve the proxyto attribute. The default implementation returns the value of the 'proxyto' parameter.",
1465 description
=> "Required access permissions. By default only 'root' is allowed to access this method.",
1467 additionalProperties
=> 0,
1470 description
=> "Describe access permissions.",
1474 description
=> "A simply way to allow access for 'all' authenticated users. Value 'world' is used to allow access without credentials.",
1476 enum
=> ['all', 'world'],
1480 description
=> "Array of permission checks (prefix notation).",
1487 description
=> "Used internally",
1491 description
=> "Used internally",
1496 description
=> "path for URL matching (uri template)",
1498 fragmentDelimiter
=> {
1500 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.",
1505 description
=> "JSON Schema for parameters.",
1510 description
=> "JSON Schema for return value.",
1515 description
=> "method implementation (code reference)",
1520 description
=> "Delegate call to this class (perl class string).",
1523 additionalProperties
=> 0,
1529 fragmentDelimiter
=> { optional
=> 1 }
1537 sub validate_schema
{
1540 my $errmsg = "internal error - unable to verify schema\n";
1541 validate
($schema, $default_schema, $errmsg);
1544 sub validate_method_info
{
1547 my $errmsg = "internal error - unable to verify method info\n";
1548 validate
($info, $method_schema, $errmsg);
1550 validate_schema
($info->{parameters
}) if $info->{parameters
};
1551 validate_schema
($info->{returns
}) if $info->{returns
};
1554 # run a self test on load
1555 # make sure we can verify the default schema
1556 validate_schema
($default_schema_noref);
1557 validate_schema
($method_schema);
1559 # and now some utility methods (used by pve api)
1560 sub method_get_child_link
{
1563 return undef if !$info;
1565 my $schema = $info->{returns
};
1566 return undef if !$schema || !$schema->{type
} || $schema->{type
} ne 'array';
1568 my $links = $schema->{links
};
1569 return undef if !$links;
1572 foreach my $lnk (@$links) {
1573 if ($lnk->{href
} && $lnk->{rel
} && ($lnk->{rel
} eq 'child')) {
1582 # a way to parse command line parameters, using a
1583 # schema to configure Getopt::Long
1585 my ($schema, $args, $arg_param, $fixed_param, $param_mapping_hash) = @_;
1587 if (!$schema || !$schema->{properties
}) {
1588 raise
("too many arguments\n", code
=> HTTP_BAD_REQUEST
)
1589 if scalar(@$args) != 0;
1594 if ($arg_param && !ref($arg_param)) {
1595 my $pd = $schema->{properties
}->{$arg_param};
1596 die "expected list format $pd->{format}"
1597 if !($pd && $pd->{format
} && $pd->{format
} =~ m/-list/);
1598 $list_param = $arg_param;
1601 my @interactive = ();
1603 foreach my $prop (keys %{$schema->{properties
}}) {
1604 my $pd = $schema->{properties
}->{$prop};
1605 next if $list_param && $prop eq $list_param;
1606 next if defined($fixed_param->{$prop});
1608 my $mapping = $param_mapping_hash->{$prop};
1609 if ($mapping && $mapping->{interactive
}) {
1610 # interactive parameters such as passwords: make the argument
1611 # optional and call the mapping function afterwards.
1612 push @getopt, "$prop:s";
1613 push @interactive, [$prop, $mapping->{func
}];
1614 } elsif ($pd->{type
} eq 'boolean') {
1615 push @getopt, "$prop:s";
1617 if ($pd->{format
} && $pd->{format
} =~ m/-a?list/) {
1618 push @getopt, "$prop=s@";
1620 push @getopt, "$prop=s";
1625 Getopt
::Long
::Configure
('prefix_pattern=(--|-)');
1628 raise
("unable to parse option\n", code
=> HTTP_BAD_REQUEST
)
1629 if !Getopt
::Long
::GetOptionsFromArray
($args, $opts, @getopt);
1633 $opts->{$list_param} = $args;
1635 } elsif (ref($arg_param)) {
1636 for (my $i = 0; $i < scalar(@$arg_param); $i++) {
1637 my $arg_name = $arg_param->[$i];
1638 if ($opts->{'extra-args'}) {
1639 raise
("internal error: extra-args must be the last argument\n", code
=> HTTP_BAD_REQUEST
);
1641 if ($arg_name eq 'extra-args') {
1642 $opts->{'extra-args'} = $args;
1647 # check if all left-over arg_param are optional, else we
1648 # must die as the mapping is then ambigious
1649 for (; $i < scalar(@$arg_param); $i++) {
1650 my $prop = $arg_param->[$i];
1651 raise
("not enough arguments\n", code
=> HTTP_BAD_REQUEST
)
1652 if !$schema->{properties
}->{$prop}->{optional
};
1654 if ($arg_param->[-1] eq 'extra-args') {
1655 $opts->{'extra-args'} = [];
1659 $opts->{$arg_name} = shift @$args;
1661 raise
("too many arguments\n", code
=> HTTP_BAD_REQUEST
) if @$args;
1663 raise
("too many arguments\n", code
=> HTTP_BAD_REQUEST
)
1664 if scalar(@$args) != 0;
1667 if (ref($arg_param)) {
1668 foreach my $arg_name (@$arg_param) {
1669 if ($arg_name eq 'extra-args') {
1670 $opts->{'extra-args'} = [];
1671 } elsif (!$schema->{properties
}->{$arg_name}->{optional
}) {
1672 raise
("not enough arguments\n", code
=> HTTP_BAD_REQUEST
);
1678 foreach my $entry (@interactive) {
1679 my ($opt, $func) = @$entry;
1680 my $pd = $schema->{properties
}->{$opt};
1681 my $value = $opts->{$opt};
1682 if (defined($value) || !$pd->{optional
}) {
1683 $opts->{$opt} = $func->($value);
1687 # decode after Getopt as we are not sure how well it handles unicode
1688 foreach my $p (keys %$opts) {
1689 if (!ref($opts->{$p})) {
1690 $opts->{$p} = decode
('locale', $opts->{$p});
1691 } elsif (ref($opts->{$p}) eq 'ARRAY') {
1693 foreach my $v (@{$opts->{$p}}) {
1694 push @$tmp, decode
('locale', $v);
1697 } elsif (ref($opts->{$p}) eq 'SCALAR') {
1698 $opts->{$p} = decode
('locale', $$opts->{$p});
1700 raise
("decoding options failed, unknown reference\n", code
=> HTTP_BAD_REQUEST
);
1704 foreach my $p (keys %$opts) {
1705 if (my $pd = $schema->{properties
}->{$p}) {
1706 if ($pd->{type
} eq 'boolean') {
1707 if ($opts->{$p} eq '') {
1709 } elsif (defined(my $bool = parse_boolean
($opts->{$p}))) {
1710 $opts->{$p} = $bool;
1712 raise
("unable to parse boolean option\n", code
=> HTTP_BAD_REQUEST
);
1714 } elsif ($pd->{format
}) {
1716 if ($pd->{format
} =~ m/-list/) {
1717 # allow --vmid 100 --vmid 101 and --vmid 100,101
1718 # allow --dow mon --dow fri and --dow mon,fri
1719 $opts->{$p} = join(",", @{$opts->{$p}}) if ref($opts->{$p}) eq 'ARRAY';
1720 } elsif ($pd->{format
} =~ m/-alist/) {
1721 # we encode array as \0 separated strings
1722 # Note: CGI.pm also use this encoding
1723 if (scalar(@{$opts->{$p}}) != 1) {
1724 $opts->{$p} = join("\0", @{$opts->{$p}});
1726 # st that split_list knows it is \0 terminated
1727 my $v = $opts->{$p}->[0];
1728 $opts->{$p} = "$v\0";
1735 foreach my $p (keys %$fixed_param) {
1736 $opts->{$p} = $fixed_param->{$p};
1742 # A way to parse configuration data by giving a json schema
1744 my ($schema, $filename, $raw) = @_;
1746 # do fast check (avoid validate_schema($schema))
1747 die "got strange schema" if !$schema->{type
} ||
1748 !$schema->{properties
} || $schema->{type
} ne 'object';
1752 while ($raw =~ /^\s*(.+?)\s*$/gm) {
1755 next if $line =~ /^#/;
1757 if ($line =~ m/^(\S+?):\s*(.*)$/) {
1760 if ($schema->{properties
}->{$key} &&
1761 $schema->{properties
}->{$key}->{type
} eq 'boolean') {
1763 $value = parse_boolean
($value) // $value;
1765 $cfg->{$key} = $value;
1767 warn "ignore config line: $line\n"
1772 check_prop
($cfg, $schema, '', $errors);
1774 foreach my $k (keys %$errors) {
1775 warn "parse error in '$filename' - '$k': $errors->{$k}\n";
1782 # generate simple key/value file
1784 my ($schema, $filename, $cfg) = @_;
1786 # do fast check (avoid validate_schema($schema))
1787 die "got strange schema" if !$schema->{type
} ||
1788 !$schema->{properties
} || $schema->{type
} ne 'object';
1790 validate
($cfg, $schema, "validation error in '$filename'\n");
1794 foreach my $k (sort keys %$cfg) {
1795 $data .= "$k: $cfg->{$k}\n";
1801 # helpers used to generate our manual pages
1803 my $find_schema_default_key = sub {
1807 my $keyAliasProps = {};
1809 foreach my $key (keys %$format) {
1810 my $phash = $format->{$key};
1811 if ($phash->{default_key
}) {
1812 die "multiple default keys in schema ($default_key, $key)\n"
1813 if defined($default_key);
1814 die "default key '$key' is an alias - this is not allowed\n"
1815 if defined($phash->{alias
});
1816 die "default key '$key' with keyAlias attribute is not allowed\n"
1817 if $phash->{keyAlias
};
1818 $default_key = $key;
1820 my $key_alias = $phash->{keyAlias
};
1821 die "found keyAlias without 'alias definition for '$key'\n"
1822 if $key_alias && !$phash->{alias
};
1824 if ($phash->{alias
} && $key_alias) {
1825 die "inconsistent keyAlias '$key_alias' definition"
1826 if defined($keyAliasProps->{$key_alias}) &&
1827 $keyAliasProps->{$key_alias} ne $phash->{alias
};
1828 $keyAliasProps->{$key_alias} = $phash->{alias
};
1832 return wantarray ?
($default_key, $keyAliasProps) : $default_key;
1835 sub generate_typetext
{
1836 my ($format, $list_enums) = @_;
1838 my ($default_key, $keyAliasProps) = &$find_schema_default_key($format);
1843 my $add_option_string = sub {
1844 my ($text, $optional) = @_;
1850 $text = "[$text]" if $optional;
1855 my $format_key_value = sub {
1856 my ($key, $phash) = @_;
1858 die "internal error" if defined($phash->{alias
});
1864 if (my $desc = $phash->{format_description
}) {
1865 $typetext .= "<$desc>";
1866 } elsif (my $text = $phash->{typetext
}) {
1868 } elsif (my $enum = $phash->{enum
}) {
1869 if ($list_enums || (scalar(@$enum) <= 3)) {
1870 $typetext .= '<' . join('|', @$enum) . '>';
1872 $typetext .= '<enum>';
1874 } elsif ($phash->{type
} eq 'boolean') {
1875 $typetext .= '<1|0>';
1876 } elsif ($phash->{type
} eq 'integer') {
1877 $typetext .= '<integer>';
1878 } elsif ($phash->{type
} eq 'number') {
1879 $typetext .= '<number>';
1881 die "internal error: neither format_description nor typetext found for option '$key'";
1884 if (defined($default_key) && ($default_key eq $key)) {
1885 &$add_option_string("[$keytext=]$typetext", $phash->{optional
});
1887 &$add_option_string("$keytext=$typetext", $phash->{optional
});
1893 my $cond_add_key = sub {
1896 return if $done->{$key}; # avoid duplicates
1900 my $phash = $format->{$key};
1902 return if !$phash; # should not happen
1904 return if $phash->{alias
};
1906 &$format_key_value($key, $phash);
1910 &$cond_add_key($default_key) if defined($default_key);
1912 # add required keys first
1913 foreach my $key (sort keys %$format) {
1914 my $phash = $format->{$key};
1915 &$cond_add_key($key) if $phash && !$phash->{optional
};
1919 foreach my $key (sort keys %$format) {
1920 &$cond_add_key($key);
1923 foreach my $keyAlias (sort keys %$keyAliasProps) {
1924 &$add_option_string("<$keyAlias>=<$keyAliasProps->{$keyAlias }>", 1);
1930 sub print_property_string
{
1931 my ($data, $format, $skip, $path) = @_;
1934 if (ref($format) ne 'HASH') {
1935 my $schema = get_format
($format);
1936 die "not a valid format: $format\n" if !$schema;
1937 # named formats can have validators attached
1938 $validator = $format_validators->{$format};
1943 check_object
($path, $format, $data, undef, $errors);
1944 if (scalar(%$errors)) {
1945 raise
"format error", errors
=> $errors;
1948 $data = $validator->($data) if $validator;
1950 my ($default_key, $keyAliasProps) = &$find_schema_default_key($format);
1955 my $add_option_string = sub {
1958 $res .= ',' if $add_sep;
1963 my $format_value = sub {
1964 my ($key, $value, $format) = @_;
1966 if (defined($format) && ($format eq 'disk-size')) {
1967 return format_size
($value);
1969 die "illegal value with commas for $key\n" if $value =~ /,/;
1974 my $done = { map { $_ => 1 } @$skip };
1976 my $cond_add_key = sub {
1977 my ($key, $isdefault) = @_;
1979 return if $done->{$key}; # avoid duplicates
1983 my $value = $data->{$key};
1985 return if !defined($value);
1987 my $phash = $format->{$key};
1989 # try to combine values if we have key aliases
1990 if (my $combine = $keyAliasProps->{$key}) {
1991 if (defined(my $combine_value = $data->{$combine})) {
1992 my $combine_format = $format->{$combine}->{format
};
1993 my $value_str = &$format_value($key, $value, $phash->{format
});
1994 my $combine_str = &$format_value($combine, $combine_value, $combine_format);
1995 &$add_option_string("${value_str}=${combine_str}");
1996 $done->{$combine} = 1;
2001 if ($phash && $phash->{alias
}) {
2002 $phash = $format->{$phash->{alias
}};
2005 die "invalid key '$key'\n" if !$phash;
2006 die "internal error" if defined($phash->{alias
});
2008 my $value_str = &$format_value($key, $value, $phash->{format
});
2010 &$add_option_string($value_str);
2012 &$add_option_string("$key=${value_str}");
2016 # add default key first
2017 &$cond_add_key($default_key, 1) if defined($default_key);
2019 # add required keys first
2020 foreach my $key (sort keys %$data) {
2021 my $phash = $format->{$key};
2022 &$cond_add_key($key) if $phash && !$phash->{optional
};
2026 foreach my $key (sort keys %$data) {
2027 &$cond_add_key($key);
2033 sub schema_get_type_text
{
2034 my ($phash, $style) = @_;
2036 my $type = $phash->{type
} || 'string';
2038 if ($phash->{typetext
}) {
2039 return $phash->{typetext
};
2040 } elsif ($phash->{format_description
}) {
2041 return "<$phash->{format_description}>";
2042 } elsif ($phash->{enum
}) {
2043 return "<" . join(' | ', sort @{$phash->{enum
}}) . ">";
2044 } elsif ($phash->{pattern
}) {
2045 return $phash->{pattern
};
2046 } elsif ($type eq 'integer' || $type eq 'number') {
2047 # NOTE: always access values as number (avoid converion to string)
2048 if (defined($phash->{minimum
}) && defined($phash->{maximum
})) {
2049 return "<$type> (" . ($phash->{minimum
} + 0) . " - " .
2050 ($phash->{maximum
} + 0) . ")";
2051 } elsif (defined($phash->{minimum
})) {
2052 return "<$type> (" . ($phash->{minimum
} + 0) . " - N)";
2053 } elsif (defined($phash->{maximum
})) {
2054 return "<$type> (-N - " . ($phash->{maximum
} + 0) . ")";
2056 } elsif ($type eq 'string') {
2057 if (my $format = $phash->{format
}) {
2058 $format = get_format
($format) if ref($format) ne 'HASH';
2059 if (ref($format) eq 'HASH') {
2061 $list_enums = 1 if $style && $style eq 'config-sub';
2062 return generate_typetext
($format, $list_enums);