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);
14 use Net
::IP
qw(:PROC);
20 register_standard_option
26 our $CONFIGID_RE = qr/[a-z][a-z0-9_-]+/i;
28 # Note: This class implements something similar to JSON schema, but it is not 100% complete.
29 # see: http://tools.ietf.org/html/draft-zyp-json-schema-02
30 # see: http://json-schema.org/
32 # the code is similar to the javascript parser from http://code.google.com/p/jsonschema/
34 my $standard_options = {};
35 sub register_standard_option
{
36 my ($name, $schema) = @_;
38 die "standard option '$name' already registered\n"
39 if $standard_options->{$name};
41 $standard_options->{$name} = $schema;
44 sub get_standard_option
{
45 my ($name, $base) = @_;
47 my $std = $standard_options->{$name};
48 die "no such standard option '$name'\n" if !$std;
50 my $res = $base || {};
52 foreach my $opt (keys %$std) {
53 next if defined($res->{$opt});
54 $res->{$opt} = $std->{$opt};
60 register_standard_option
('pve-vmid', {
61 description
=> "The (unique) ID of the VM.",
65 maximum
=> 999_999_999,
68 register_standard_option
('pve-node', {
69 description
=> "The cluster node name.",
70 type
=> 'string', format
=> 'pve-node',
73 register_standard_option
('pve-node-list', {
74 description
=> "List of cluster node names.",
75 type
=> 'string', format
=> 'pve-node-list',
78 register_standard_option
('pve-iface', {
79 description
=> "Network interface name.",
80 type
=> 'string', format
=> 'pve-iface',
81 minLength
=> 2, maxLength
=> 20,
84 register_standard_option
('pve-storage-id', {
85 description
=> "The storage identifier.",
86 type
=> 'string', format
=> 'pve-storage-id',
89 register_standard_option
('pve-bridge-id', {
90 description
=> "Bridge to attach guest network devices to.",
91 type
=> 'string', format
=> 'pve-bridge-id',
92 format_description
=> 'bridge',
95 register_standard_option
('pve-config-digest', {
96 description
=> 'Prevent changes if current configuration file has different SHA1 digest. This can be used to prevent concurrent modifications.',
99 maxLength
=> 40, # sha1 hex digest length is 40
102 register_standard_option
('skiplock', {
103 description
=> "Ignore locks - only root is allowed to use this option.",
108 register_standard_option
('extra-args', {
109 description
=> "Extra arguments as array",
111 items
=> { type
=> 'string' },
115 register_standard_option
('fingerprint-sha256', {
116 description
=> "Certificate SHA 256 fingerprint.",
118 pattern
=> '([A-Fa-f0-9]{2}:){31}[A-Fa-f0-9]{2}',
121 register_standard_option
('pve-output-format', {
123 description
=> 'Output format.',
124 enum
=> [ 'text', 'json', 'json-pretty', 'yaml' ],
129 register_standard_option
('pve-snapshot-name', {
130 description
=> "The name of the snapshot.",
131 type
=> 'string', format
=> 'pve-configid',
135 my $format_list = {};
136 my $format_validators = {};
138 sub register_format
{
139 my ($name, $format, $validator) = @_;
141 die "JSON schema format '$name' already registered\n"
142 if $format_list->{$name};
145 die "A \$validator function can only be specified for hash-based formats\n"
146 if ref($format) ne 'HASH';
147 $format_validators->{$name} = $validator;
150 $format_list->{$name} = $format;
155 return $format_list->{$name};
158 my $renderer_hash = {};
160 sub register_renderer
{
161 my ($name, $code) = @_;
163 die "renderer '$name' already registered\n"
164 if $renderer_hash->{$name};
166 $renderer_hash->{$name} = $code;
171 return $renderer_hash->{$name};
174 # register some common type for pve
176 register_format
('string', sub {}); # allow format => 'string-list'
178 register_format
('urlencoded', \
&pve_verify_urlencoded
);
179 sub pve_verify_urlencoded
{
180 my ($text, $noerr) = @_;
181 if ($text !~ /^[-%a-zA-Z0-9_.!~*'()]*$/) {
182 return undef if $noerr;
183 die "invalid urlencoded string: $text\n";
188 register_format
('pve-configid', \
&pve_verify_configid
);
189 sub pve_verify_configid
{
190 my ($id, $noerr) = @_;
192 if ($id !~ m/^$CONFIGID_RE$/) {
193 return undef if $noerr;
194 die "invalid configuration ID '$id'\n";
199 PVE
::JSONSchema
::register_format
('pve-storage-id', \
&parse_storage_id
);
200 sub parse_storage_id
{
201 my ($storeid, $noerr) = @_;
203 return parse_id
($storeid, 'storage', $noerr);
206 PVE
::JSONSchema
::register_format
('pve-bridge-id', \
&parse_bridge_id
);
207 sub parse_bridge_id
{
208 my ($id, $noerr) = @_;
210 if ($id !~ m/^[-_.\w\d]+$/) {
211 return undef if $noerr;
212 die "invalid bridge ID '$id'\n";
217 PVE
::JSONSchema
::register_format
('acme-plugin-id', \
&parse_acme_plugin_id
);
218 sub parse_acme_plugin_id
{
219 my ($pluginid, $noerr) = @_;
221 return parse_id
($pluginid, 'ACME plugin', $noerr);
225 my ($id, $type, $noerr) = @_;
227 if ($id !~ m/^[a-z][a-z0-9\-\_\.]*[a-z0-9]$/i) {
228 return undef if $noerr;
229 die "$type ID '$id' contains illegal characters\n";
234 register_format
('pve-vmid', \
&pve_verify_vmid
);
235 sub pve_verify_vmid
{
236 my ($vmid, $noerr) = @_;
238 if ($vmid !~ m/^[1-9][0-9]{2,8}$/) {
239 return undef if $noerr;
240 die "value does not look like a valid VM ID\n";
245 register_format
('pve-node', \
&pve_verify_node_name
);
246 sub pve_verify_node_name
{
247 my ($node, $noerr) = @_;
249 if ($node !~ m/^([a-zA-Z0-9]([a-zA-Z0-9\-]*[a-zA-Z0-9])?)$/) {
250 return undef if $noerr;
251 die "value does not look like a valid node name\n";
256 # maps source to target ID using an ID map
258 my ($map, $source) = @_;
260 return $source if !defined($map);
262 return $map->{entries
}->{$source}
263 if $map->{entries
} && defined($map->{entries
}->{$source});
265 return $map->{default} if $map->{default};
267 # identity (fallback)
272 my ($idmap, $idformat) = @_;
274 return undef if !$idmap;
278 foreach my $entry (PVE
::Tools
::split_list
($idmap)) {
280 $map->{identity
} = 1;
281 } elsif ($entry =~ m/^([^:]+):([^:]+)$/) {
282 my ($source, $target) = ($1, $2);
284 check_format
($idformat, $source, '');
285 check_format
($idformat, $target, '');
287 die "entry '$entry' contains invalid ID - $@\n" if $@;
289 die "duplicate mapping for source '$source'\n"
290 if exists $map->{entries
}->{$source};
292 $map->{entries
}->{$source} = $target;
295 check_format
($idformat, $entry);
297 die "entry '$entry' contains invalid ID - $@\n" if $@;
299 die "default target ID can only be provided once\n"
300 if exists $map->{default};
302 $map->{default} = $entry;
306 die "identity mapping cannot be combined with other mappings\n"
307 if $map->{identity
} && ($map->{default} || exists $map->{entries
});
312 my $verify_idpair = sub {
313 my ($input, $noerr, $format) = @_;
315 eval { parse_idmap
($input, $format) };
317 return undef if $noerr;
324 PVE
::JSONSchema
::register_standard_option
('pve-targetstorage', {
325 description
=> "Mapping from source to target storages. Providing only a single storage ID maps all source storages to that storage. Providing the special value '1' will map each source storage to itself.",
327 format
=> 'storage-pair-list',
331 # note: this only checks a single list entry
332 # when using a storage-pair-list map, you need to pass the full parameter to
334 register_format
('storage-pair', \
&verify_storagepair
);
335 sub verify_storagepair
{
336 my ($storagepair, $noerr) = @_;
337 return $verify_idpair->($storagepair, $noerr, 'pve-storage-id');
340 # note: this only checks a single list entry
341 # when using a bridge-pair-list map, you need to pass the full parameter to
343 register_format
('bridge-pair', \
&verify_bridgepair
);
344 sub verify_bridgepair
{
345 my ($bridgepair, $noerr) = @_;
346 return $verify_idpair->($bridgepair, $noerr, 'pve-bridge-id');
349 register_format
('mac-addr', \
&pve_verify_mac_addr
);
350 sub pve_verify_mac_addr
{
351 my ($mac_addr, $noerr) = @_;
353 # don't allow I/G bit to be set, most of the time it breaks things, see:
354 # https://pve.proxmox.com/pipermail/pve-devel/2019-March/035998.html
355 if ($mac_addr !~ m/^[a-f0-9][02468ace](?::[a-f0-9]{2}){5}$/i) {
356 return undef if $noerr;
357 die "value does not look like a valid unicast MAC address\n";
362 register_standard_option
('mac-addr', {
364 description
=> 'Unicast MAC address.',
365 verbose_description
=> 'A common MAC address with the I/G (Individual/Group) bit not set.',
366 format_description
=> "XX:XX:XX:XX:XX:XX",
368 format
=> 'mac-addr',
371 register_format
('ipv4', \
&pve_verify_ipv4
);
372 sub pve_verify_ipv4
{
373 my ($ipv4, $noerr) = @_;
375 if ($ipv4 !~ m/^(?:$IPV4RE)$/) {
376 return undef if $noerr;
377 die "value does not look like a valid IPv4 address\n";
382 register_format
('ipv6', \
&pve_verify_ipv6
);
383 sub pve_verify_ipv6
{
384 my ($ipv6, $noerr) = @_;
386 if ($ipv6 !~ m/^(?:$IPV6RE)$/) {
387 return undef if $noerr;
388 die "value does not look like a valid IPv6 address\n";
393 register_format
('ip', \
&pve_verify_ip
);
395 my ($ip, $noerr) = @_;
397 if ($ip !~ m/^(?:(?:$IPV4RE)|(?:$IPV6RE))$/) {
398 return undef if $noerr;
399 die "value does not look like a valid IP address\n";
404 PVE
::JSONSchema
::register_format
('ldap-simple-attr', \
&verify_ldap_simple_attr
);
405 sub verify_ldap_simple_attr
{
406 my ($attr, $noerr) = @_;
408 if ($attr =~ m/^[a-zA-Z0-9]+$/) {
412 die "value '$attr' does not look like a simple ldap attribute name\n" if !$noerr;
417 my $ipv4_mask_hash = {
435 '255.255.128.0' => 17,
436 '255.255.192.0' => 18,
437 '255.255.224.0' => 19,
438 '255.255.240.0' => 20,
439 '255.255.248.0' => 21,
440 '255.255.252.0' => 22,
441 '255.255.254.0' => 23,
442 '255.255.255.0' => 24,
443 '255.255.255.128' => 25,
444 '255.255.255.192' => 26,
445 '255.255.255.224' => 27,
446 '255.255.255.240' => 28,
447 '255.255.255.248' => 29,
448 '255.255.255.252' => 30,
449 '255.255.255.254' => 31,
450 '255.255.255.255' => 32,
453 sub get_netmask_bits
{
455 return $ipv4_mask_hash->{$mask};
458 register_format
('ipv4mask', \
&pve_verify_ipv4mask
);
459 sub pve_verify_ipv4mask
{
460 my ($mask, $noerr) = @_;
462 if (!defined($ipv4_mask_hash->{$mask})) {
463 return undef if $noerr;
464 die "value does not look like a valid IP netmask\n";
469 register_format
('CIDRv6', \
&pve_verify_cidrv6
);
470 sub pve_verify_cidrv6
{
471 my ($cidr, $noerr) = @_;
473 if ($cidr =~ m!^(?:$IPV6RE)(?:/(\d+))$! && ($1 > 7) && ($1 <= 128)) {
477 return undef if $noerr;
478 die "value does not look like a valid IPv6 CIDR network\n";
481 register_format
('CIDRv4', \
&pve_verify_cidrv4
);
482 sub pve_verify_cidrv4
{
483 my ($cidr, $noerr) = @_;
485 if ($cidr =~ m!^(?:$IPV4RE)(?:/(\d+))$! && ($1 > 7) && ($1 <= 32)) {
489 return undef if $noerr;
490 die "value does not look like a valid IPv4 CIDR network\n";
493 register_format
('CIDR', \
&pve_verify_cidr
);
494 sub pve_verify_cidr
{
495 my ($cidr, $noerr) = @_;
497 if (!(pve_verify_cidrv4
($cidr, 1) ||
498 pve_verify_cidrv6
($cidr, 1)))
500 return undef if $noerr;
501 die "value does not look like a valid CIDR network\n";
507 register_format
('pve-ipv4-config', \
&pve_verify_ipv4_config
);
508 sub pve_verify_ipv4_config
{
509 my ($config, $noerr) = @_;
511 return $config if $config =~ /^(?:dhcp|manual)$/ ||
512 pve_verify_cidrv4
($config, 1);
513 return undef if $noerr;
514 die "value does not look like a valid ipv4 network configuration\n";
517 register_format
('pve-ipv6-config', \
&pve_verify_ipv6_config
);
518 sub pve_verify_ipv6_config
{
519 my ($config, $noerr) = @_;
521 return $config if $config =~ /^(?:auto|dhcp|manual)$/ ||
522 pve_verify_cidrv6
($config, 1);
523 return undef if $noerr;
524 die "value does not look like a valid ipv6 network configuration\n";
527 register_format
('email', \
&pve_verify_email
);
528 sub pve_verify_email
{
529 my ($email, $noerr) = @_;
531 if ($email !~ /^$PVE::Tools::EMAIL_RE$/) {
532 return undef if $noerr;
533 die "value does not look like a valid email address\n";
538 register_format
('email-or-username', \
&pve_verify_email_or_username
);
539 sub pve_verify_email_or_username
{
540 my ($email, $noerr) = @_;
542 if ($email !~ /^$PVE::Tools::EMAIL_RE$/ &&
543 $email !~ /^$PVE::Tools::EMAIL_USER_RE$/) {
544 return undef if $noerr;
545 die "value does not look like a valid email address or user name\n";
550 register_format
('dns-name', \
&pve_verify_dns_name
);
551 sub pve_verify_dns_name
{
552 my ($name, $noerr) = @_;
554 my $namere = "([a-zA-Z0-9]([a-zA-Z0-9\-]*[a-zA-Z0-9])?)";
556 if ($name !~ /^(${namere}\.)*${namere}$/) {
557 return undef if $noerr;
558 die "value does not look like a valid DNS name\n";
563 register_format
('timezone', \
&pve_verify_timezone
);
564 sub pve_verify_timezone
{
565 my ($timezone, $noerr) = @_;
567 return $timezone if $timezone eq 'UTC';
569 open(my $fh, "<", "/usr/share/zoneinfo/zone.tab");
570 while (my $line = <$fh>) {
571 next if $line =~ /^\s*#/;
573 my $zone = (split /\t/, $line)[2];
574 return $timezone if $timezone eq $zone; # found
578 return undef if $noerr;
579 die "invalid time zone '$timezone'\n";
582 # network interface name
583 register_format
('pve-iface', \
&pve_verify_iface
);
584 sub pve_verify_iface
{
585 my ($id, $noerr) = @_;
587 if ($id !~ m/^[a-z][a-z0-9_]{1,20}([:\.]\d+)?$/i) {
588 return undef if $noerr;
589 die "invalid network interface name '$id'\n";
594 # general addresses by name or IP
595 register_format
('address', \
&pve_verify_address
);
596 sub pve_verify_address
{
597 my ($addr, $noerr) = @_;
599 if (!(pve_verify_ip
($addr, 1) ||
600 pve_verify_dns_name
($addr, 1)))
602 return undef if $noerr;
603 die "value does not look like a valid address: $addr\n";
608 register_format
('disk-size', \
&pve_verify_disk_size
);
609 sub pve_verify_disk_size
{
610 my ($size, $noerr) = @_;
611 if (!defined(parse_size
($size))) {
612 return undef if $noerr;
613 die "value does not look like a valid disk size: $size\n";
618 register_standard_option
('spice-proxy', {
619 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).",
620 type
=> 'string', format
=> 'address',
623 register_standard_option
('remote-viewer-config', {
624 description
=> "Returned values can be directly passed to the 'remote-viewer' application.",
625 additionalProperties
=> 1,
627 type
=> { type
=> 'string' },
628 password
=> { type
=> 'string' },
629 proxy
=> { type
=> 'string' },
630 host
=> { type
=> 'string' },
631 'tls-port' => { type
=> 'integer' },
635 register_format
('pve-startup-order', \
&pve_verify_startup_order
);
636 sub pve_verify_startup_order
{
637 my ($value, $noerr) = @_;
639 return $value if pve_parse_startup_order
($value);
641 return undef if $noerr;
643 die "unable to parse startup options\n";
648 type
=> 'number', minimum
=> '0',
649 format_description
=> 'LIMIT',
652 my $bwlimit_format = {
655 description
=> 'default bandwidth limit in KiB/s',
659 description
=> 'bandwidth limit in KiB/s for restoring guests from backups',
663 description
=> 'bandwidth limit in KiB/s for migrating guests (including moving local disks)',
667 description
=> 'bandwidth limit in KiB/s for cloning disks',
671 description
=> 'bandwidth limit in KiB/s for moving disks',
674 register_format
('bwlimit', $bwlimit_format);
675 register_standard_option
('bwlimit', {
676 description
=> "Set I/O bandwidth limit for various operations (in KiB/s).",
679 format
=> $bwlimit_format,
682 my $remote_format = {
685 format_description
=> 'Remote Proxmox hostname or IP',
693 format_description
=> 'A full Proxmox API token including the secret value.',
695 fingerprint
=> get_standard_option
(
696 'fingerprint-sha256',
699 format_description
=> 'Remote host\'s certificate fingerprint, if not trusted by system store.',
703 register_format
('proxmox-remote', $remote_format);
704 register_standard_option
('proxmox-remote', {
705 description
=> "Specification of a remote endpoint.",
706 type
=> 'string', format
=> 'proxmox-remote',
709 our $PVE_TAG_RE = qr/[a-z0-9_][a-z0-9_\-\+\.]*/i;
711 # used for pve-tag-list in e.g., guest configs
712 register_format
('pve-tag', \
&pve_verify_tag
);
714 my ($value, $noerr) = @_;
716 return $value if $value =~ m/^${PVE_TAG_RE}$/i;
718 return undef if $noerr;
720 die "invalid characters in tag\n";
723 sub pve_parse_startup_order
{
726 return undef if !$value;
730 foreach my $p (split(/,/, $value)) {
731 next if $p =~ m/^\s*$/;
733 if ($p =~ m/^(order=)?(\d+)$/) {
735 } elsif ($p =~ m/^up=(\d+)$/) {
737 } elsif ($p =~ m/^down=(\d+)$/) {
747 PVE
::JSONSchema
::register_standard_option
('pve-startup-order', {
748 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.",
750 type
=> 'string', format
=> 'pve-startup-order',
751 typetext
=> '[[order=]\d+] [,up=\d+] [,down=\d+] ',
754 register_format
('pve-tfa-secret', \
&pve_verify_tfa_secret
);
755 sub pve_verify_tfa_secret
{
756 my ($key, $noerr) = @_;
758 # The old format used 16 base32 chars or 40 hex digits. Since they have a common subset it's
759 # hard to distinguish them without the our previous length constraints, so add a 'v2' of the
760 # format to support arbitrary lengths properly:
761 if ($key =~ /^v2-0x[0-9a-fA-F]{16,128}$/ || # hex
762 $key =~ /^v2-[A-Z2-7=]{16,128}$/ || # base32
763 $key =~ /^(?:[A-Z2-7=]{16}|[A-Fa-f0-9]{40})$/) # and the old pattern copy&pasted
768 return undef if $noerr;
770 die "unable to decode TFA secret\n";
774 PVE
::JSONSchema
::register_format
('pve-task-status-type', \
&verify_task_status_type
);
775 sub verify_task_status_type
{
776 my ($value, $noerr) = @_;
778 return $value if $value =~ m/^(ok|error|warning|unknown)$/i;
780 return undef if $noerr;
782 die "invalid status '$value'\n";
786 my ($format, $value, $path) = @_;
788 if (ref($format) eq 'HASH') {
789 # hash ref cannot have validator/list/opt handling attached
790 return parse_property_string
($format, $value, $path);
793 if (ref($format) eq 'CODE') {
794 # we are the (sole, old-style) validator
795 return $format->($value);
798 return if $format eq 'regex';
801 $format =~ m/^(.*?)(?:-(list|opt))?$/;
802 my ($format_name, $format_type) = ($1, $2 // 'none');
803 my $registered = get_format
($format_name);
804 die "undefined format '$format'\n" if !$registered;
806 die "'-$format_type' format must have code ref, not hash\n"
807 if $format_type ne 'none' && ref($registered) ne 'CODE';
809 if ($format_type eq 'list') {
811 # Note: we allow empty lists
812 foreach my $v (split_list
($value)) {
813 push @{$parsed}, $registered->($v);
815 } elsif ($format_type eq 'opt') {
816 $parsed = $registered->($value) if $value;
818 if (ref($registered) eq 'HASH') {
819 # Note: this is the only case where a validator function could be
820 # attached, hence it's safe to handle that in parse_property_string.
821 # We do however have to call it with $format_name instead of
822 # $registered, so it knows about the name (and thus any validators).
823 $parsed = parse_property_string
($format, $value, $path);
825 $parsed = $registered->($value);
835 return undef if $value !~ m/^(\d+(\.\d+)?)([KMGT])?$/;
836 my ($size, $unit) = ($1, $3);
839 $size = $size * 1024;
840 } elsif ($unit eq 'M') {
841 $size = $size * 1024 * 1024;
842 } elsif ($unit eq 'G') {
843 $size = $size * 1024 * 1024 * 1024;
844 } elsif ($unit eq 'T') {
845 $size = $size * 1024 * 1024 * 1024 * 1024;
856 my $kb = int($size/1024);
857 return $size if $kb*1024 != $size;
859 my $mb = int($kb/1024);
860 return "${kb}K" if $mb*1024 != $kb;
862 my $gb = int($mb/1024);
863 return "${mb}M" if $gb*1024 != $mb;
865 my $tb = int($gb/1024);
866 return "${gb}G" if $tb*1024 != $gb;
873 return 1 if $bool =~ m/^(1|on|yes|true)$/i;
874 return 0 if $bool =~ m/^(0|off|no|false)$/i;
878 sub parse_property_string
{
879 my ($format, $data, $path, $additional_properties) = @_;
881 # In property strings we default to not allowing additional properties
882 $additional_properties = 0 if !defined($additional_properties);
884 # Support named formats here, too:
887 if (my $reg = get_format
($format)) {
888 die "parse_property_string only accepts hash based named formats\n"
889 if ref($reg) ne 'HASH';
891 # named formats can have validators attached
892 $validator = $format_validators->{$format};
896 die "unknown format: $format\n";
898 } elsif (ref($format) ne 'HASH') {
899 die "unexpected format value of type ".ref($format)."\n";
905 foreach my $part (split(/,/, $data)) {
906 next if $part =~ /^\s*$/;
908 if ($part =~ /^([^=]+)=(.+)$/) {
909 my ($k, $v) = ($1, $2);
910 die "duplicate key in comma-separated list property: $k\n" if defined($res->{$k});
911 my $schema = $format->{$k};
912 if (my $alias = $schema->{alias
}) {
913 if (my $key_alias = $schema->{keyAlias
}) {
914 die "key alias '$key_alias' is already defined\n" if defined($res->{$key_alias});
915 $res->{$key_alias} = $k;
918 $schema = $format->{$k};
921 die "invalid key in comma-separated list property: $k\n" if !$schema;
922 if ($schema->{type
} && $schema->{type
} eq 'boolean') {
923 $v = parse_boolean
($v) // $v;
926 } elsif ($part !~ /=/) {
927 die "duplicate key in comma-separated list property: $default_key\n" if $default_key;
928 foreach my $key (keys %$format) {
929 if ($format->{$key}->{default_key
}) {
931 if (!$res->{$default_key}) {
932 $res->{$default_key} = $part;
935 die "duplicate key in comma-separated list property: $default_key\n";
938 die "value without key, but schema does not define a default key\n" if !$default_key;
940 die "missing key in comma-separated list property\n";
945 check_object
($path, $format, $res, $additional_properties, $errors);
946 if (scalar(%$errors)) {
947 raise
"format error\n", errors
=> $errors;
950 return $validator->($res) if $validator;
955 my ($errors, $path, $msg) = @_;
957 $path = '_root' if !$path;
959 if ($errors->{$path}) {
960 $errors->{$path} = join ('\n', $errors->{$path}, $msg);
962 $errors->{$path} = $msg;
969 # see 'man perlretut'
970 return $value =~ /^[+-]?(\d+\.\d+|\d+\.|\.\d+|\d+)([eE][+-]?\d+)?$/;
976 return $value =~ m/^[+-]?\d+$/;
980 my ($path, $type, $value, $errors) = @_;
984 if (!defined($value)) {
985 return 1 if $type eq 'null';
989 if (my $tt = ref($type)) {
990 if ($tt eq 'ARRAY') {
991 foreach my $t (@$type) {
993 check_type
($path, $t, $value, $tmperr);
994 return 1 if !scalar(%$tmperr);
996 my $ttext = join ('|', @$type);
997 add_error
($errors, $path, "type check ('$ttext') failed");
999 } elsif ($tt eq 'HASH') {
1001 check_prop
($value, $type, $path, $tmperr);
1002 return 1 if !scalar(%$tmperr);
1003 add_error
($errors, $path, "type check failed");
1006 die "internal error - got reference type '$tt'";
1011 return 1 if $type eq 'any';
1013 if ($type eq 'null') {
1014 if (defined($value)) {
1015 add_error
($errors, $path, "type check ('$type') failed - value is not null");
1021 my $vt = ref($value);
1023 if ($type eq 'array') {
1024 if (!$vt || $vt ne 'ARRAY') {
1025 add_error
($errors, $path, "type check ('$type') failed");
1029 } elsif ($type eq 'object') {
1030 if (!$vt || $vt ne 'HASH') {
1031 add_error
($errors, $path, "type check ('$type') failed");
1035 } elsif ($type eq 'coderef') {
1036 if (!$vt || $vt ne 'CODE') {
1037 add_error
($errors, $path, "type check ('$type') failed");
1041 } elsif ($type eq 'string' && $vt eq 'Regexp') {
1042 # qr// regexes can be used as strings and make sense for format=regex
1046 if ($type eq 'boolean' && JSON
::is_bool
($value)) {
1049 add_error
($errors, $path, "type check ('$type') failed - got $vt");
1052 if ($type eq 'string') {
1053 return 1; # nothing to check ?
1054 } elsif ($type eq 'boolean') {
1055 #if ($value =~ m/^(1|true|yes|on)$/i) {
1056 if ($value eq '1') {
1058 #} elsif ($value =~ m/^(0|false|no|off)$/i) {
1059 } elsif ($value eq '0') {
1060 return 1; # return success (not value)
1062 add_error
($errors, $path, "type check ('$type') failed - got '$value'");
1065 } elsif ($type eq 'integer') {
1066 if (!is_integer
($value)) {
1067 add_error
($errors, $path, "type check ('$type') failed - got '$value'");
1071 } elsif ($type eq 'number') {
1072 if (!is_number
($value)) {
1073 add_error
($errors, $path, "type check ('$type') failed - got '$value'");
1078 return 1; # no need to verify unknown types
1088 my ($path, $schema, $value, $additional_properties, $errors) = @_;
1090 # print "Check Object " . Dumper($value) . "\nSchema: " . Dumper($schema);
1092 my $st = ref($schema);
1093 if (!$st || $st ne 'HASH') {
1094 add_error
($errors, $path, "Invalid schema definition.");
1098 my $vt = ref($value);
1099 if (!$vt || $vt ne 'HASH') {
1100 add_error
($errors, $path, "an object is required");
1104 foreach my $k (keys %$schema) {
1105 check_prop
($value->{$k}, $schema->{$k}, $path ?
"$path.$k" : $k, $errors);
1108 foreach my $k (keys %$value) {
1110 my $newpath = $path ?
"$path.$k" : $k;
1112 if (my $subschema = $schema->{$k}) {
1113 if (my $requires = $subschema->{requires
}) {
1114 if (ref($requires)) {
1115 #print "TEST: " . Dumper($value) . "\n", Dumper($requires) ;
1116 check_prop
($value, $requires, $path, $errors);
1117 } elsif (!defined($value->{$requires})) {
1118 add_error
($errors, $path ?
"$path.$requires" : $requires,
1119 "missing property - '$newpath' requires this property");
1123 next; # value is already checked above
1126 if (defined ($additional_properties) && !$additional_properties) {
1127 add_error
($errors, $newpath, "property is not defined in schema " .
1128 "and the schema does not allow additional properties");
1131 check_prop
($value->{$k}, $additional_properties, $newpath, $errors)
1132 if ref($additional_properties);
1136 sub check_object_warn
{
1137 my ($path, $schema, $value, $additional_properties) = @_;
1139 check_object
($path, $schema, $value, $additional_properties, $errors);
1140 if (scalar(%$errors)) {
1141 foreach my $k (keys %$errors) {
1142 warn "parse error: $k: $errors->{$k}\n";
1150 my ($value, $schema, $path, $errors) = @_;
1152 die "internal error - no schema" if !$schema;
1153 die "internal error" if !$errors;
1155 #print "check_prop $path\n" if $value;
1157 my $st = ref($schema);
1158 if (!$st || $st ne 'HASH') {
1159 add_error
($errors, $path, "Invalid schema definition.");
1163 # if it extends another schema, it must pass that schema as well
1164 if($schema->{extends
}) {
1165 check_prop
($value, $schema->{extends
}, $path, $errors);
1168 if (!defined ($value)) {
1169 return if $schema->{type
} && $schema->{type
} eq 'null';
1170 if (!$schema->{optional
} && !$schema->{alias
} && !$schema->{group
}) {
1171 add_error
($errors, $path, "property is missing and it is not optional");
1176 return if !check_type
($path, $schema->{type
}, $value, $errors);
1178 if ($schema->{disallow
}) {
1180 if (check_type
($path, $schema->{disallow
}, $value, $tmperr)) {
1181 add_error
($errors, $path, "disallowed value was matched");
1186 if (my $vt = ref($value)) {
1188 if ($vt eq 'ARRAY') {
1189 if ($schema->{items
}) {
1190 my $it = ref($schema->{items
});
1191 if ($it && $it eq 'ARRAY') {
1192 #die "implement me $path: $vt " . Dumper($schema) ."\n". Dumper($value);
1193 die "not implemented";
1196 foreach my $el (@$value) {
1197 check_prop
($el, $schema->{items
}, "${path}[$ind]", $errors);
1203 } elsif ($schema->{properties
} || $schema->{additionalProperties
}) {
1204 check_object
($path, defined($schema->{properties
}) ?
$schema->{properties
} : {},
1205 $value, $schema->{additionalProperties
}, $errors);
1211 if (my $format = $schema->{format
}) {
1212 eval { check_format
($format, $value, $path); };
1214 add_error
($errors, $path, "invalid format - $@");
1219 if (my $pattern = $schema->{pattern
}) {
1220 if ($value !~ m/^$pattern$/) {
1221 add_error
($errors, $path, "value does not match the regex pattern");
1226 if (defined (my $max = $schema->{maxLength
})) {
1227 if (length($value) > $max) {
1228 add_error
($errors, $path, "value may only be $max characters long");
1233 if (defined (my $min = $schema->{minLength
})) {
1234 if (length($value) < $min) {
1235 add_error
($errors, $path, "value must be at least $min characters long");
1240 if (is_number
($value)) {
1241 if (defined (my $max = $schema->{maximum
})) {
1242 if ($value > $max) {
1243 add_error
($errors, $path, "value must have a maximum value of $max");
1248 if (defined (my $min = $schema->{minimum
})) {
1249 if ($value < $min) {
1250 add_error
($errors, $path, "value must have a minimum value of $min");
1256 if (my $ea = $schema->{enum
}) {
1259 foreach my $ev (@$ea) {
1260 if ($ev eq $value) {
1266 add_error
($errors, $path, "value '$value' does not have a value in the enumeration '" .
1267 join(", ", @$ea) . "'");
1274 my ($instance, $schema, $errmsg) = @_;
1277 $errmsg = "Parameter verification failed.\n" if !$errmsg;
1279 # todo: cycle detection is only needed for debugging, I guess
1280 # we can disable that in the final release
1281 # todo: is there a better/faster way to detect cycles?
1283 # 'download' responses can contain a filehandle, don't cycle-check that as
1284 # it produces a warning
1285 my $is_download = ref($instance) eq 'HASH' && exists($instance->{download
});
1286 find_cycle
($instance, sub { $cycles = 1 }) if !$is_download;
1288 add_error
($errors, undef, "data structure contains recursive cycles");
1290 check_prop
($instance, $schema, '', $errors);
1293 if (scalar(%$errors)) {
1294 raise
$errmsg, code
=> HTTP_BAD_REQUEST
, errors
=> $errors;
1300 my $schema_valid_types = ["string", "object", "coderef", "array", "boolean", "number", "integer", "null", "any"];
1301 my $default_schema_noref = {
1302 description
=> "This is the JSON Schema for JSON Schemas.",
1303 type
=> [ "object" ],
1304 additionalProperties
=> 0,
1307 type
=> ["string", "array"],
1308 description
=> "This is a type definition value. This can be a simple type, or a union type",
1313 enum
=> $schema_valid_types,
1315 enum
=> $schema_valid_types,
1319 description
=> "This indicates that the instance property in the instance object is not required.",
1325 description
=> "This is a definition for the properties of an object value",
1331 description
=> "When the value is an array, this indicates the schema to use to validate each item in an array",
1335 additionalProperties
=> {
1336 type
=> [ "boolean", "object"],
1337 description
=> "This provides a default property definition for all properties that are not explicitly defined in an object type definition.",
1344 description
=> "This indicates the minimum value for the instance property when the type of the instance value is a number.",
1349 description
=> "This indicates the maximum value for the instance property when the type of the instance value is a number.",
1353 description
=> "When the instance value is a string, this indicates minimum length of the string",
1360 description
=> "When the instance value is a string, this indicates maximum length of the string.",
1366 description
=> "A text representation of the type (used to generate documentation).",
1371 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.",
1378 description
=> "This provides an enumeration of possible values that are valid for the instance property.",
1383 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).",
1385 verbose_description
=> {
1388 description
=> "This provides a more verbose description.",
1390 format_description
=> {
1393 description
=> "This provides a shorter (usually just one word) description for a property used to generate descriptions for comma separated list property strings.",
1398 description
=> "This provides the title of the property",
1403 description
=> "This is used to provide rendering hints to format cli command output.",
1406 type
=> [ "string", "object" ],
1408 description
=> "indicates a required property or a schema that must be validated if this property is present",
1411 type
=> [ "string", "object" ],
1413 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",
1418 description
=> "Whether this is the default key in a comma separated list property string.",
1423 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.",
1428 description
=> "Allows to store the current 'key' as value of another property. Only valid if used together with 'alias'.",
1429 requires
=> 'alias',
1434 description
=> "This indicates the default for the instance property."
1438 description
=> "Bash completion function. This function should return a list of possible values.",
1444 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.",
1449 description
=> "This indicates the schema extends the given schema. All instances of this schema must be valid to by the extended schema also.",
1452 # this is from hyper schema
1455 description
=> "This defines the link relations of the instance objects",
1462 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",
1466 description
=> "This is the name of the link relation",
1472 description
=> "For submission links, this defines the method that should be used to access the target resource",
1481 description
=> "For CLI context, this defines the maximal width to print before truncating",
1487 my $default_schema = Storable
::dclone
($default_schema_noref);
1489 $default_schema->{properties
}->{properties
}->{additionalProperties
} = $default_schema;
1490 $default_schema->{properties
}->{additionalProperties
}->{properties
} = $default_schema->{properties
};
1492 $default_schema->{properties
}->{items
}->{properties
} = $default_schema->{properties
};
1493 $default_schema->{properties
}->{items
}->{additionalProperties
} = 0;
1495 $default_schema->{properties
}->{disallow
}->{properties
} = $default_schema->{properties
};
1496 $default_schema->{properties
}->{disallow
}->{additionalProperties
} = 0;
1498 $default_schema->{properties
}->{requires
}->{properties
} = $default_schema->{properties
};
1499 $default_schema->{properties
}->{requires
}->{additionalProperties
} = 0;
1501 $default_schema->{properties
}->{extends
}->{properties
} = $default_schema->{properties
};
1502 $default_schema->{properties
}->{extends
}->{additionalProperties
} = 0;
1504 my $method_schema = {
1506 additionalProperties
=> 0,
1509 description
=> "This a description of the method",
1514 description
=> "This indicates the name of the function to call.",
1517 additionalProperties
=> 1,
1532 description
=> "The HTTP method name.",
1533 enum
=> [ 'GET', 'POST', 'PUT', 'DELETE' ],
1538 description
=> "Method needs special privileges - only pvedaemon can execute it",
1543 description
=> "Method is available for clients authenticated using an API token.",
1549 description
=> "Method downloads the file content (filename is the return value of the method).",
1554 description
=> "A parameter name. If specified, all calls to this method are proxied to the host contained in that parameter.",
1557 proxyto_callback
=> {
1559 description
=> "A function which is called to resolve the proxyto attribute. The default implementation returns the value of the 'proxyto' parameter.",
1564 description
=> "Required access permissions. By default only 'root' is allowed to access this method.",
1566 additionalProperties
=> 0,
1569 description
=> "Describe access permissions.",
1573 description
=> "A simply way to allow access for 'all' authenticated users. Value 'world' is used to allow access without credentials.",
1575 enum
=> ['all', 'world'],
1579 description
=> "Array of permission checks (prefix notation).",
1586 description
=> "Used internally",
1590 description
=> "Used internally",
1595 description
=> "path for URL matching (uri template)",
1597 fragmentDelimiter
=> {
1599 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.",
1604 description
=> "JSON Schema for parameters.",
1609 description
=> "JSON Schema for return value.",
1614 description
=> "method implementation (code reference)",
1619 description
=> "Delegate call to this class (perl class string).",
1622 additionalProperties
=> 0,
1628 fragmentDelimiter
=> { optional
=> 1 }
1636 sub validate_schema
{
1639 my $errmsg = "internal error - unable to verify schema\n";
1640 validate
($schema, $default_schema, $errmsg);
1643 sub validate_method_info
{
1646 my $errmsg = "internal error - unable to verify method info\n";
1647 validate
($info, $method_schema, $errmsg);
1649 validate_schema
($info->{parameters
}) if $info->{parameters
};
1650 validate_schema
($info->{returns
}) if $info->{returns
};
1653 # run a self test on load
1654 # make sure we can verify the default schema
1655 validate_schema
($default_schema_noref);
1656 validate_schema
($method_schema);
1658 # and now some utility methods (used by pve api)
1659 sub method_get_child_link
{
1662 return undef if !$info;
1664 my $schema = $info->{returns
};
1665 return undef if !$schema || !$schema->{type
} || $schema->{type
} ne 'array';
1667 my $links = $schema->{links
};
1668 return undef if !$links;
1671 foreach my $lnk (@$links) {
1672 if ($lnk->{href
} && $lnk->{rel
} && ($lnk->{rel
} eq 'child')) {
1681 # a way to parse command line parameters, using a
1682 # schema to configure Getopt::Long
1684 my ($schema, $args, $arg_param, $fixed_param, $param_mapping_hash) = @_;
1686 if (!$schema || !$schema->{properties
}) {
1687 raise
("too many arguments\n", code
=> HTTP_BAD_REQUEST
)
1688 if scalar(@$args) != 0;
1693 if ($arg_param && !ref($arg_param)) {
1694 my $pd = $schema->{properties
}->{$arg_param};
1695 die "expected list format $pd->{format}"
1696 if !($pd && $pd->{format
} && $pd->{format
} =~ m/-list/);
1697 $list_param = $arg_param;
1700 my @interactive = ();
1702 foreach my $prop (keys %{$schema->{properties
}}) {
1703 my $pd = $schema->{properties
}->{$prop};
1704 next if $list_param && $prop eq $list_param;
1705 next if defined($fixed_param->{$prop});
1707 my $mapping = $param_mapping_hash->{$prop};
1708 if ($mapping && $mapping->{interactive
}) {
1709 # interactive parameters such as passwords: make the argument
1710 # optional and call the mapping function afterwards.
1711 push @getopt, "$prop:s";
1712 push @interactive, [$prop, $mapping->{func
}];
1713 } elsif ($pd->{type
} eq 'boolean') {
1714 push @getopt, "$prop:s";
1716 if ($pd->{format
} && $pd->{format
} =~ m/-list/) {
1717 push @getopt, "$prop=s@";
1718 } elsif ($pd->{type
} eq 'array') {
1719 push @getopt, "$prop=s@";
1721 push @getopt, "$prop=s";
1726 Getopt
::Long
::Configure
('prefix_pattern=(--|-)');
1729 raise
("unable to parse option\n", code
=> HTTP_BAD_REQUEST
)
1730 if !Getopt
::Long
::GetOptionsFromArray
($args, $opts, @getopt);
1734 $opts->{$list_param} = $args;
1736 } elsif (ref($arg_param)) {
1737 for (my $i = 0; $i < scalar(@$arg_param); $i++) {
1738 my $arg_name = $arg_param->[$i];
1739 if ($opts->{'extra-args'}) {
1740 raise
("internal error: extra-args must be the last argument\n", code
=> HTTP_BAD_REQUEST
);
1742 if ($arg_name eq 'extra-args') {
1743 $opts->{'extra-args'} = $args;
1748 # check if all left-over arg_param are optional, else we
1749 # must die as the mapping is then ambigious
1750 for (; $i < scalar(@$arg_param); $i++) {
1751 my $prop = $arg_param->[$i];
1752 raise
("not enough arguments\n", code
=> HTTP_BAD_REQUEST
)
1753 if !$schema->{properties
}->{$prop}->{optional
};
1755 if ($arg_param->[-1] eq 'extra-args') {
1756 $opts->{'extra-args'} = [];
1760 $opts->{$arg_name} = shift @$args;
1762 raise
("too many arguments\n", code
=> HTTP_BAD_REQUEST
) if @$args;
1764 raise
("too many arguments\n", code
=> HTTP_BAD_REQUEST
)
1765 if scalar(@$args) != 0;
1768 if (ref($arg_param)) {
1769 foreach my $arg_name (@$arg_param) {
1770 if ($arg_name eq 'extra-args') {
1771 $opts->{'extra-args'} = [];
1772 } elsif (!$schema->{properties
}->{$arg_name}->{optional
}) {
1773 raise
("not enough arguments\n", code
=> HTTP_BAD_REQUEST
);
1779 foreach my $entry (@interactive) {
1780 my ($opt, $func) = @$entry;
1781 my $pd = $schema->{properties
}->{$opt};
1782 my $value = $opts->{$opt};
1783 if (defined($value) || !$pd->{optional
}) {
1784 $opts->{$opt} = $func->($value);
1788 # decode after Getopt as we are not sure how well it handles unicode
1789 foreach my $p (keys %$opts) {
1790 if (!ref($opts->{$p})) {
1791 $opts->{$p} = decode
('locale', $opts->{$p});
1792 } elsif (ref($opts->{$p}) eq 'ARRAY') {
1794 foreach my $v (@{$opts->{$p}}) {
1795 push @$tmp, decode
('locale', $v);
1798 } elsif (ref($opts->{$p}) eq 'SCALAR') {
1799 $opts->{$p} = decode
('locale', $$opts->{$p});
1801 raise
("decoding options failed, unknown reference\n", code
=> HTTP_BAD_REQUEST
);
1805 foreach my $p (keys %$opts) {
1806 if (my $pd = $schema->{properties
}->{$p}) {
1807 if ($pd->{type
} eq 'boolean') {
1808 if ($opts->{$p} eq '') {
1810 } elsif (defined(my $bool = parse_boolean
($opts->{$p}))) {
1811 $opts->{$p} = $bool;
1813 raise
("unable to parse boolean option\n", code
=> HTTP_BAD_REQUEST
);
1815 } elsif ($pd->{format
}) {
1817 if ($pd->{format
} =~ m/-list/) {
1818 # allow --vmid 100 --vmid 101 and --vmid 100,101
1819 # allow --dow mon --dow fri and --dow mon,fri
1820 $opts->{$p} = join(",", @{$opts->{$p}}) if ref($opts->{$p}) eq 'ARRAY';
1826 foreach my $p (keys %$fixed_param) {
1827 $opts->{$p} = $fixed_param->{$p};
1833 # A way to parse configuration data by giving a json schema
1834 sub parse_config
: prototype($$$;$) {
1835 my ($schema, $filename, $raw, $comment_key) = @_;
1837 # do fast check (avoid validate_schema($schema))
1838 die "got strange schema" if !$schema->{type
} ||
1839 !$schema->{properties
} || $schema->{type
} ne 'object';
1844 my $handle_comment = sub { $_[0] =~ /^#/ };
1845 if (defined($comment_key)) {
1847 my $comment_re = qr/^\Q$comment_key\E:\s*(.*\S)\s*$/;
1848 $handle_comment = sub {
1849 if ($_[0] =~ /^\#(.*)\s*$/ || $_[0] =~ $comment_re) {
1850 $comment_data .= PVE
::Tools
::decode_text
($1) . "\n";
1857 while ($raw =~ /^\s*(.+?)\s*$/gm) {
1860 next if $handle_comment->($line);
1862 if ($line =~ m/^(\S+?):\s*(.*)$/) {
1865 if ($schema->{properties
}->{$key} &&
1866 $schema->{properties
}->{$key}->{type
} eq 'boolean') {
1868 $value = parse_boolean
($value) // $value;
1871 $schema->{properties
}->{$key}
1872 && $schema->{properties
}->{$key}->{type
} eq 'array'
1875 $cfg->{$key} //= [];
1876 push $cfg->{$key}->@*, $value;
1879 $cfg->{$key} = $value;
1881 warn "ignore config line: $line\n"
1885 if (defined($comment_data)) {
1886 $cfg->{$comment_key} = $comment_data;
1890 check_prop
($cfg, $schema, '', $errors);
1892 foreach my $k (keys %$errors) {
1893 warn "parse error in '$filename' - '$k': $errors->{$k}\n";
1900 # generate simple key/value file
1902 my ($schema, $filename, $cfg) = @_;
1904 # do fast check (avoid validate_schema($schema))
1905 die "got strange schema" if !$schema->{type
} ||
1906 !$schema->{properties
} || $schema->{type
} ne 'object';
1908 validate
($cfg, $schema, "validation error in '$filename'\n");
1912 foreach my $k (sort keys %$cfg) {
1913 $data .= "$k: $cfg->{$k}\n";
1919 # helpers used to generate our manual pages
1921 my $find_schema_default_key = sub {
1925 my $keyAliasProps = {};
1927 foreach my $key (keys %$format) {
1928 my $phash = $format->{$key};
1929 if ($phash->{default_key
}) {
1930 die "multiple default keys in schema ($default_key, $key)\n"
1931 if defined($default_key);
1932 die "default key '$key' is an alias - this is not allowed\n"
1933 if defined($phash->{alias
});
1934 die "default key '$key' with keyAlias attribute is not allowed\n"
1935 if $phash->{keyAlias
};
1936 $default_key = $key;
1938 my $key_alias = $phash->{keyAlias
};
1939 die "found keyAlias without 'alias definition for '$key'\n"
1940 if $key_alias && !$phash->{alias
};
1942 if ($phash->{alias
} && $key_alias) {
1943 die "inconsistent keyAlias '$key_alias' definition"
1944 if defined($keyAliasProps->{$key_alias}) &&
1945 $keyAliasProps->{$key_alias} ne $phash->{alias
};
1946 $keyAliasProps->{$key_alias} = $phash->{alias
};
1950 return wantarray ?
($default_key, $keyAliasProps) : $default_key;
1953 sub generate_typetext
{
1954 my ($format, $list_enums) = @_;
1956 my ($default_key, $keyAliasProps) = &$find_schema_default_key($format);
1961 my $add_option_string = sub {
1962 my ($text, $optional) = @_;
1968 $text = "[$text]" if $optional;
1973 my $format_key_value = sub {
1974 my ($key, $phash) = @_;
1976 die "internal error" if defined($phash->{alias
});
1982 if (my $desc = $phash->{format_description
}) {
1983 $typetext .= "<$desc>";
1984 } elsif (my $text = $phash->{typetext
}) {
1986 } elsif (my $enum = $phash->{enum
}) {
1987 if ($list_enums || (scalar(@$enum) <= 3)) {
1988 $typetext .= '<' . join('|', @$enum) . '>';
1990 $typetext .= '<enum>';
1992 } elsif ($phash->{type
} eq 'boolean') {
1993 $typetext .= '<1|0>';
1994 } elsif ($phash->{type
} eq 'integer') {
1995 $typetext .= '<integer>';
1996 } elsif ($phash->{type
} eq 'number') {
1997 $typetext .= '<number>';
1999 die "internal error: neither format_description nor typetext found for option '$key'";
2002 if (defined($default_key) && ($default_key eq $key)) {
2003 &$add_option_string("[$keytext=]$typetext", $phash->{optional
});
2005 &$add_option_string("$keytext=$typetext", $phash->{optional
});
2011 my $cond_add_key = sub {
2014 return if $done->{$key}; # avoid duplicates
2018 my $phash = $format->{$key};
2020 return if !$phash; # should not happen
2022 return if $phash->{alias
};
2024 &$format_key_value($key, $phash);
2028 &$cond_add_key($default_key) if defined($default_key);
2030 # add required keys first
2031 foreach my $key (sort keys %$format) {
2032 my $phash = $format->{$key};
2033 &$cond_add_key($key) if $phash && !$phash->{optional
};
2037 foreach my $key (sort keys %$format) {
2038 &$cond_add_key($key);
2041 foreach my $keyAlias (sort keys %$keyAliasProps) {
2042 &$add_option_string("<$keyAlias>=<$keyAliasProps->{$keyAlias }>", 1);
2048 sub print_property_string
{
2049 my ($data, $format, $skip, $path) = @_;
2052 if (ref($format) ne 'HASH') {
2053 my $schema = get_format
($format);
2054 die "not a valid format: $format\n" if !$schema;
2055 # named formats can have validators attached
2056 $validator = $format_validators->{$format};
2061 check_object
($path, $format, $data, undef, $errors);
2062 if (scalar(%$errors)) {
2063 raise
"format error", errors
=> $errors;
2066 $data = $validator->($data) if $validator;
2068 my ($default_key, $keyAliasProps) = &$find_schema_default_key($format);
2073 my $add_option_string = sub {
2076 $res .= ',' if $add_sep;
2081 my $format_value = sub {
2082 my ($key, $value, $format) = @_;
2084 if (defined($format) && ($format eq 'disk-size')) {
2085 return format_size
($value);
2087 die "illegal value with commas for $key\n" if $value =~ /,/;
2092 my $done = { map { $_ => 1 } @$skip };
2094 my $cond_add_key = sub {
2095 my ($key, $isdefault) = @_;
2097 return if $done->{$key}; # avoid duplicates
2101 my $value = $data->{$key};
2103 return if !defined($value);
2105 my $phash = $format->{$key};
2107 # try to combine values if we have key aliases
2108 if (my $combine = $keyAliasProps->{$key}) {
2109 if (defined(my $combine_value = $data->{$combine})) {
2110 my $combine_format = $format->{$combine}->{format
};
2111 my $value_str = &$format_value($key, $value, $phash->{format
});
2112 my $combine_str = &$format_value($combine, $combine_value, $combine_format);
2113 &$add_option_string("${value_str}=${combine_str}");
2114 $done->{$combine} = 1;
2119 if ($phash && $phash->{alias
}) {
2120 $phash = $format->{$phash->{alias
}};
2123 die "invalid key '$key'\n" if !$phash;
2124 die "internal error" if defined($phash->{alias
});
2126 my $value_str = &$format_value($key, $value, $phash->{format
});
2128 &$add_option_string($value_str);
2130 &$add_option_string("$key=${value_str}");
2134 # add default key first
2135 &$cond_add_key($default_key, 1) if defined($default_key);
2137 # add required keys first
2138 foreach my $key (sort keys %$data) {
2139 my $phash = $format->{$key};
2140 &$cond_add_key($key) if $phash && !$phash->{optional
};
2144 foreach my $key (sort keys %$data) {
2145 &$cond_add_key($key);
2151 sub schema_get_type_text
{
2152 my ($phash, $style) = @_;
2154 my $type = $phash->{type
} || 'string';
2156 if ($phash->{typetext
}) {
2157 return $phash->{typetext
};
2158 } elsif ($phash->{format_description
}) {
2159 return "<$phash->{format_description}>";
2160 } elsif ($phash->{enum
}) {
2161 return "<" . join(' | ', sort @{$phash->{enum
}}) . ">";
2162 } elsif ($phash->{pattern
}) {
2163 return $phash->{pattern
};
2164 } elsif ($type eq 'integer' || $type eq 'number') {
2165 # NOTE: always access values as number (avoid converion to string)
2166 if (defined($phash->{minimum
}) && defined($phash->{maximum
})) {
2167 return "<$type> (" . ($phash->{minimum
} + 0) . " - " .
2168 ($phash->{maximum
} + 0) . ")";
2169 } elsif (defined($phash->{minimum
})) {
2170 return "<$type> (" . ($phash->{minimum
} + 0) . " - N)";
2171 } elsif (defined($phash->{maximum
})) {
2172 return "<$type> (-N - " . ($phash->{maximum
} + 0) . ")";
2174 } elsif ($type eq 'string') {
2175 if (my $format = $phash->{format
}) {
2176 $format = get_format
($format) if ref($format) ne 'HASH';
2177 if (ref($format) eq 'HASH') {
2179 $list_enums = 1 if $style && $style eq 'config-sub';
2180 return generate_typetext
($format, $list_enums);