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.",
62 type
=> 'integer', format
=> 'pve-vmid',
66 register_standard_option
('pve-node', {
67 description
=> "The cluster node name.",
68 type
=> 'string', format
=> 'pve-node',
71 register_standard_option
('pve-node-list', {
72 description
=> "List of cluster node names.",
73 type
=> 'string', format
=> 'pve-node-list',
76 register_standard_option
('pve-iface', {
77 description
=> "Network interface name.",
78 type
=> 'string', format
=> 'pve-iface',
79 minLength
=> 2, maxLength
=> 20,
82 register_standard_option
('pve-storage-id', {
83 description
=> "The storage identifier.",
84 type
=> 'string', format
=> 'pve-storage-id',
87 register_standard_option
('pve-bridge-id', {
88 description
=> "Bridge to attach guest network devices to.",
89 type
=> 'string', format
=> 'pve-bridge-id',
90 format_description
=> 'bridge',
93 register_standard_option
('pve-config-digest', {
94 description
=> 'Prevent changes if current configuration file has different SHA1 digest. This can be used to prevent concurrent modifications.',
97 maxLength
=> 40, # sha1 hex digest length is 40
100 register_standard_option
('skiplock', {
101 description
=> "Ignore locks - only root is allowed to use this option.",
106 register_standard_option
('extra-args', {
107 description
=> "Extra arguments as array",
109 items
=> { type
=> 'string' },
113 register_standard_option
('fingerprint-sha256', {
114 description
=> "Certificate SHA 256 fingerprint.",
116 pattern
=> '([A-Fa-f0-9]{2}:){31}[A-Fa-f0-9]{2}',
119 register_standard_option
('pve-output-format', {
121 description
=> 'Output format.',
122 enum
=> [ 'text', 'json', 'json-pretty', 'yaml' ],
127 register_standard_option
('pve-snapshot-name', {
128 description
=> "The name of the snapshot.",
129 type
=> 'string', format
=> 'pve-configid',
133 my $format_list = {};
134 my $format_validators = {};
136 sub register_format
{
137 my ($name, $format, $validator) = @_;
139 die "JSON schema format '$name' already registered\n"
140 if $format_list->{$name};
143 die "A \$validator function can only be specified for hash-based formats\n"
144 if ref($format) ne 'HASH';
145 $format_validators->{$name} = $validator;
148 $format_list->{$name} = $format;
153 return $format_list->{$name};
156 my $renderer_hash = {};
158 sub register_renderer
{
159 my ($name, $code) = @_;
161 die "renderer '$name' already registered\n"
162 if $renderer_hash->{$name};
164 $renderer_hash->{$name} = $code;
169 return $renderer_hash->{$name};
172 # register some common type for pve
174 register_format
('string', sub {}); # allow format => 'string-list'
176 register_format
('urlencoded', \
&pve_verify_urlencoded
);
177 sub pve_verify_urlencoded
{
178 my ($text, $noerr) = @_;
179 if ($text !~ /^[-%a-zA-Z0-9_.!~*'()]*$/) {
180 return undef if $noerr;
181 die "invalid urlencoded string: $text\n";
186 register_format
('pve-configid', \
&pve_verify_configid
);
187 sub pve_verify_configid
{
188 my ($id, $noerr) = @_;
190 if ($id !~ m/^$CONFIGID_RE$/) {
191 return undef if $noerr;
192 die "invalid configuration ID '$id'\n";
197 PVE
::JSONSchema
::register_format
('pve-storage-id', \
&parse_storage_id
);
198 sub parse_storage_id
{
199 my ($storeid, $noerr) = @_;
201 return parse_id
($storeid, 'storage', $noerr);
204 PVE
::JSONSchema
::register_format
('pve-bridge-id', \
&parse_bridge_id
);
205 sub parse_bridge_id
{
206 my ($id, $noerr) = @_;
208 if ($id !~ m/^[-_.\w\d]+$/) {
209 return undef if $noerr;
210 die "invalid bridge ID '$id'\n";
215 PVE
::JSONSchema
::register_format
('acme-plugin-id', \
&parse_acme_plugin_id
);
216 sub parse_acme_plugin_id
{
217 my ($pluginid, $noerr) = @_;
219 return parse_id
($pluginid, 'ACME plugin', $noerr);
223 my ($id, $type, $noerr) = @_;
225 if ($id !~ m/^[a-z][a-z0-9\-\_\.]*[a-z0-9]$/i) {
226 return undef if $noerr;
227 die "$type ID '$id' contains illegal characters\n";
232 register_format
('pve-vmid', \
&pve_verify_vmid
);
233 sub pve_verify_vmid
{
234 my ($vmid, $noerr) = @_;
236 if ($vmid !~ m/^[1-9][0-9]{2,8}$/) {
237 return undef if $noerr;
238 die "value does not look like a valid VM ID\n";
243 register_format
('pve-node', \
&pve_verify_node_name
);
244 sub pve_verify_node_name
{
245 my ($node, $noerr) = @_;
247 if ($node !~ m/^([a-zA-Z0-9]([a-zA-Z0-9\-]*[a-zA-Z0-9])?)$/) {
248 return undef if $noerr;
249 die "value does not look like a valid node name\n";
254 # maps source to target ID using an ID map
256 my ($map, $source) = @_;
258 return $source if !defined($map);
260 return $map->{entries
}->{$source}
261 if $map->{entries
} && defined($map->{entries
}->{$source});
263 return $map->{default} if $map->{default};
265 # identity (fallback)
270 my ($idmap, $idformat) = @_;
272 return undef if !$idmap;
276 foreach my $entry (PVE
::Tools
::split_list
($idmap)) {
278 $map->{identity
} = 1;
279 } elsif ($entry =~ m/^([^:]+):([^:]+)$/) {
280 my ($source, $target) = ($1, $2);
282 check_format
($idformat, $source, '');
283 check_format
($idformat, $target, '');
285 die "entry '$entry' contains invalid ID - $@\n" if $@;
287 die "duplicate mapping for source '$source'\n"
288 if exists $map->{entries
}->{$source};
290 $map->{entries
}->{$source} = $target;
293 check_format
($idformat, $entry);
295 die "entry '$entry' contains invalid ID - $@\n" if $@;
297 die "default target ID can only be provided once\n"
298 if exists $map->{default};
300 $map->{default} = $entry;
304 die "identity mapping cannot be combined with other mappings\n"
305 if $map->{identity
} && ($map->{default} || exists $map->{entries
});
310 my $verify_idpair = sub {
311 my ($input, $noerr, $format) = @_;
313 eval { parse_idmap
($input, $format) };
315 return undef if $noerr;
322 PVE
::JSONSchema
::register_standard_option
('pve-targetstorage', {
323 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.",
325 format
=> 'storage-pair-list',
329 # note: this only checks a single list entry
330 # when using a storage-pair-list map, you need to pass the full parameter to
332 register_format
('storage-pair', \
&verify_storagepair
);
333 sub verify_storagepair
{
334 my ($storagepair, $noerr) = @_;
335 return $verify_idpair->($storagepair, $noerr, 'pve-storage-id');
338 # note: this only checks a single list entry
339 # when using a bridge-pair-list map, you need to pass the full parameter to
341 register_format
('bridge-pair', \
&verify_bridgepair
);
342 sub verify_bridgepair
{
343 my ($bridgepair, $noerr) = @_;
344 return $verify_idpair->($bridgepair, $noerr, 'pve-bridge-id');
347 register_format
('mac-addr', \
&pve_verify_mac_addr
);
348 sub pve_verify_mac_addr
{
349 my ($mac_addr, $noerr) = @_;
351 # don't allow I/G bit to be set, most of the time it breaks things, see:
352 # https://pve.proxmox.com/pipermail/pve-devel/2019-March/035998.html
353 if ($mac_addr !~ m/^[a-f0-9][02468ace](?::[a-f0-9]{2}){5}$/i) {
354 return undef if $noerr;
355 die "value does not look like a valid unicast MAC address\n";
360 register_standard_option
('mac-addr', {
362 description
=> 'Unicast MAC address.',
363 verbose_description
=> 'A common MAC address with the I/G (Individual/Group) bit not set.',
364 format_description
=> "XX:XX:XX:XX:XX:XX",
366 format
=> 'mac-addr',
369 register_format
('ipv4', \
&pve_verify_ipv4
);
370 sub pve_verify_ipv4
{
371 my ($ipv4, $noerr) = @_;
373 if ($ipv4 !~ m/^(?:$IPV4RE)$/) {
374 return undef if $noerr;
375 die "value does not look like a valid IPv4 address\n";
380 register_format
('ipv6', \
&pve_verify_ipv6
);
381 sub pve_verify_ipv6
{
382 my ($ipv6, $noerr) = @_;
384 if ($ipv6 !~ m/^(?:$IPV6RE)$/) {
385 return undef if $noerr;
386 die "value does not look like a valid IPv6 address\n";
391 register_format
('ip', \
&pve_verify_ip
);
393 my ($ip, $noerr) = @_;
395 if ($ip !~ m/^(?:(?:$IPV4RE)|(?:$IPV6RE))$/) {
396 return undef if $noerr;
397 die "value does not look like a valid IP address\n";
402 PVE
::JSONSchema
::register_format
('ldap-simple-attr', \
&verify_ldap_simple_attr
);
403 sub verify_ldap_simple_attr
{
404 my ($attr, $noerr) = @_;
406 if ($attr =~ m/^[a-zA-Z0-9]+$/) {
410 die "value '$attr' does not look like a simple ldap attribute name\n" if !$noerr;
415 my $ipv4_mask_hash = {
433 '255.255.128.0' => 17,
434 '255.255.192.0' => 18,
435 '255.255.224.0' => 19,
436 '255.255.240.0' => 20,
437 '255.255.248.0' => 21,
438 '255.255.252.0' => 22,
439 '255.255.254.0' => 23,
440 '255.255.255.0' => 24,
441 '255.255.255.128' => 25,
442 '255.255.255.192' => 26,
443 '255.255.255.224' => 27,
444 '255.255.255.240' => 28,
445 '255.255.255.248' => 29,
446 '255.255.255.252' => 30,
447 '255.255.255.254' => 31,
448 '255.255.255.255' => 32,
451 sub get_netmask_bits
{
453 return $ipv4_mask_hash->{$mask};
456 register_format
('ipv4mask', \
&pve_verify_ipv4mask
);
457 sub pve_verify_ipv4mask
{
458 my ($mask, $noerr) = @_;
460 if (!defined($ipv4_mask_hash->{$mask})) {
461 return undef if $noerr;
462 die "value does not look like a valid IP netmask\n";
467 register_format
('CIDRv6', \
&pve_verify_cidrv6
);
468 sub pve_verify_cidrv6
{
469 my ($cidr, $noerr) = @_;
471 if ($cidr =~ m!^(?:$IPV6RE)(?:/(\d+))$! && ($1 > 7) && ($1 <= 128)) {
475 return undef if $noerr;
476 die "value does not look like a valid IPv6 CIDR network\n";
479 register_format
('CIDRv4', \
&pve_verify_cidrv4
);
480 sub pve_verify_cidrv4
{
481 my ($cidr, $noerr) = @_;
483 if ($cidr =~ m!^(?:$IPV4RE)(?:/(\d+))$! && ($1 > 7) && ($1 <= 32)) {
487 return undef if $noerr;
488 die "value does not look like a valid IPv4 CIDR network\n";
491 register_format
('CIDR', \
&pve_verify_cidr
);
492 sub pve_verify_cidr
{
493 my ($cidr, $noerr) = @_;
495 if (!(pve_verify_cidrv4
($cidr, 1) ||
496 pve_verify_cidrv6
($cidr, 1)))
498 return undef if $noerr;
499 die "value does not look like a valid CIDR network\n";
505 register_format
('pve-ipv4-config', \
&pve_verify_ipv4_config
);
506 sub pve_verify_ipv4_config
{
507 my ($config, $noerr) = @_;
509 return $config if $config =~ /^(?:dhcp|manual)$/ ||
510 pve_verify_cidrv4
($config, 1);
511 return undef if $noerr;
512 die "value does not look like a valid ipv4 network configuration\n";
515 register_format
('pve-ipv6-config', \
&pve_verify_ipv6_config
);
516 sub pve_verify_ipv6_config
{
517 my ($config, $noerr) = @_;
519 return $config if $config =~ /^(?:auto|dhcp|manual)$/ ||
520 pve_verify_cidrv6
($config, 1);
521 return undef if $noerr;
522 die "value does not look like a valid ipv6 network configuration\n";
525 register_format
('email', \
&pve_verify_email
);
526 sub pve_verify_email
{
527 my ($email, $noerr) = @_;
529 if ($email !~ /^$PVE::Tools::EMAIL_RE$/) {
530 return undef if $noerr;
531 die "value does not look like a valid email address\n";
536 register_format
('email-or-username', \
&pve_verify_email_or_username
);
537 sub pve_verify_email_or_username
{
538 my ($email, $noerr) = @_;
540 if ($email !~ /^$PVE::Tools::EMAIL_RE$/ &&
541 $email !~ /^$PVE::Tools::EMAIL_USER_RE$/) {
542 return undef if $noerr;
543 die "value does not look like a valid email address or user name\n";
548 register_format
('dns-name', \
&pve_verify_dns_name
);
549 sub pve_verify_dns_name
{
550 my ($name, $noerr) = @_;
552 my $namere = "([a-zA-Z0-9]([a-zA-Z0-9\-]*[a-zA-Z0-9])?)";
554 if ($name !~ /^(${namere}\.)*${namere}$/) {
555 return undef if $noerr;
556 die "value does not look like a valid DNS name\n";
561 register_format
('timezone', \
&pve_verify_timezone
);
562 sub pve_verify_timezone
{
563 my ($timezone, $noerr) = @_;
565 return $timezone if $timezone eq 'UTC';
567 open(my $fh, "<", "/usr/share/zoneinfo/zone.tab");
568 while (my $line = <$fh>) {
569 next if $line =~ /^\s*#/;
571 my $zone = (split /\t/, $line)[2];
572 return $timezone if $timezone eq $zone; # found
576 return undef if $noerr;
577 die "invalid time zone '$timezone'\n";
580 # network interface name
581 register_format
('pve-iface', \
&pve_verify_iface
);
582 sub pve_verify_iface
{
583 my ($id, $noerr) = @_;
585 if ($id !~ m/^[a-z][a-z0-9_]{1,20}([:\.]\d+)?$/i) {
586 return undef if $noerr;
587 die "invalid network interface name '$id'\n";
592 # general addresses by name or IP
593 register_format
('address', \
&pve_verify_address
);
594 sub pve_verify_address
{
595 my ($addr, $noerr) = @_;
597 if (!(pve_verify_ip
($addr, 1) ||
598 pve_verify_dns_name
($addr, 1)))
600 return undef if $noerr;
601 die "value does not look like a valid address: $addr\n";
606 register_format
('disk-size', \
&pve_verify_disk_size
);
607 sub pve_verify_disk_size
{
608 my ($size, $noerr) = @_;
609 if (!defined(parse_size
($size))) {
610 return undef if $noerr;
611 die "value does not look like a valid disk size: $size\n";
616 register_standard_option
('spice-proxy', {
617 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).",
618 type
=> 'string', format
=> 'address',
621 register_standard_option
('remote-viewer-config', {
622 description
=> "Returned values can be directly passed to the 'remote-viewer' application.",
623 additionalProperties
=> 1,
625 type
=> { type
=> 'string' },
626 password
=> { type
=> 'string' },
627 proxy
=> { type
=> 'string' },
628 host
=> { type
=> 'string' },
629 'tls-port' => { type
=> 'integer' },
633 register_format
('pve-startup-order', \
&pve_verify_startup_order
);
634 sub pve_verify_startup_order
{
635 my ($value, $noerr) = @_;
637 return $value if pve_parse_startup_order
($value);
639 return undef if $noerr;
641 die "unable to parse startup options\n";
646 type
=> 'number', minimum
=> '0',
647 format_description
=> 'LIMIT',
650 my $bwlimit_format = {
653 description
=> 'default bandwidth limit in KiB/s',
657 description
=> 'bandwidth limit in KiB/s for restoring guests from backups',
661 description
=> 'bandwidth limit in KiB/s for migrating guests (including moving local disks)',
665 description
=> 'bandwidth limit in KiB/s for cloning disks',
669 description
=> 'bandwidth limit in KiB/s for moving disks',
672 register_format
('bwlimit', $bwlimit_format);
673 register_standard_option
('bwlimit', {
674 description
=> "Set I/O bandwidth limit for various operations (in KiB/s).",
677 format
=> $bwlimit_format,
680 my $remote_format = {
683 format_description
=> 'Remote Proxmox hostname or IP',
691 format_description
=> 'A full Proxmox API token including the secret value.',
693 fingerprint
=> get_standard_option
(
694 'fingerprint-sha256',
697 format_description
=> 'Remote host\'s certificate fingerprint, if not trusted by system store.',
701 register_format
('proxmox-remote', $remote_format);
702 register_standard_option
('proxmox-remote', {
703 description
=> "Specification of a remote endpoint.",
704 type
=> 'string', format
=> 'proxmox-remote',
707 our $PVE_TAG_RE = qr/[a-z0-9_][a-z0-9_\-\+\.]*/i;
709 # used for pve-tag-list in e.g., guest configs
710 register_format
('pve-tag', \
&pve_verify_tag
);
712 my ($value, $noerr) = @_;
714 return $value if $value =~ m/^${PVE_TAG_RE}$/i;
716 return undef if $noerr;
718 die "invalid characters in tag\n";
721 sub pve_parse_startup_order
{
724 return undef if !$value;
728 foreach my $p (split(/,/, $value)) {
729 next if $p =~ m/^\s*$/;
731 if ($p =~ m/^(order=)?(\d+)$/) {
733 } elsif ($p =~ m/^up=(\d+)$/) {
735 } elsif ($p =~ m/^down=(\d+)$/) {
745 PVE
::JSONSchema
::register_standard_option
('pve-startup-order', {
746 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.",
748 type
=> 'string', format
=> 'pve-startup-order',
749 typetext
=> '[[order=]\d+] [,up=\d+] [,down=\d+] ',
752 register_format
('pve-tfa-secret', \
&pve_verify_tfa_secret
);
753 sub pve_verify_tfa_secret
{
754 my ($key, $noerr) = @_;
756 # The old format used 16 base32 chars or 40 hex digits. Since they have a common subset it's
757 # hard to distinguish them without the our previous length constraints, so add a 'v2' of the
758 # format to support arbitrary lengths properly:
759 if ($key =~ /^v2-0x[0-9a-fA-F]{16,128}$/ || # hex
760 $key =~ /^v2-[A-Z2-7=]{16,128}$/ || # base32
761 $key =~ /^(?:[A-Z2-7=]{16}|[A-Fa-f0-9]{40})$/) # and the old pattern copy&pasted
766 return undef if $noerr;
768 die "unable to decode TFA secret\n";
772 PVE
::JSONSchema
::register_format
('pve-task-status-type', \
&verify_task_status_type
);
773 sub verify_task_status_type
{
774 my ($value, $noerr) = @_;
776 return $value if $value =~ m/^(ok|error|warning|unknown)$/i;
778 return undef if $noerr;
780 die "invalid status '$value'\n";
784 my ($format, $value, $path) = @_;
786 if (ref($format) eq 'HASH') {
787 # hash ref cannot have validator/list/opt handling attached
788 return parse_property_string
($format, $value, $path);
791 if (ref($format) eq 'CODE') {
792 # we are the (sole, old-style) validator
793 return $format->($value);
796 return if $format eq 'regex';
799 $format =~ m/^(.*?)(?:-(list|opt))?$/;
800 my ($format_name, $format_type) = ($1, $2 // 'none');
801 my $registered = get_format
($format_name);
802 die "undefined format '$format'\n" if !$registered;
804 die "'-$format_type' format must have code ref, not hash\n"
805 if $format_type ne 'none' && ref($registered) ne 'CODE';
807 if ($format_type eq 'list') {
809 # Note: we allow empty lists
810 foreach my $v (split_list
($value)) {
811 push @{$parsed}, $registered->($v);
813 } elsif ($format_type eq 'opt') {
814 $parsed = $registered->($value) if $value;
816 if (ref($registered) eq 'HASH') {
817 # Note: this is the only case where a validator function could be
818 # attached, hence it's safe to handle that in parse_property_string.
819 # We do however have to call it with $format_name instead of
820 # $registered, so it knows about the name (and thus any validators).
821 $parsed = parse_property_string
($format, $value, $path);
823 $parsed = $registered->($value);
833 return undef if $value !~ m/^(\d+(\.\d+)?)([KMGT])?$/;
834 my ($size, $unit) = ($1, $3);
837 $size = $size * 1024;
838 } elsif ($unit eq 'M') {
839 $size = $size * 1024 * 1024;
840 } elsif ($unit eq 'G') {
841 $size = $size * 1024 * 1024 * 1024;
842 } elsif ($unit eq 'T') {
843 $size = $size * 1024 * 1024 * 1024 * 1024;
854 my $kb = int($size/1024);
855 return $size if $kb*1024 != $size;
857 my $mb = int($kb/1024);
858 return "${kb}K" if $mb*1024 != $kb;
860 my $gb = int($mb/1024);
861 return "${mb}M" if $gb*1024 != $mb;
863 my $tb = int($gb/1024);
864 return "${gb}G" if $tb*1024 != $gb;
871 return 1 if $bool =~ m/^(1|on|yes|true)$/i;
872 return 0 if $bool =~ m/^(0|off|no|false)$/i;
876 sub parse_property_string
{
877 my ($format, $data, $path, $additional_properties) = @_;
879 # In property strings we default to not allowing additional properties
880 $additional_properties = 0 if !defined($additional_properties);
882 # Support named formats here, too:
885 if (my $reg = get_format
($format)) {
886 die "parse_property_string only accepts hash based named formats\n"
887 if ref($reg) ne 'HASH';
889 # named formats can have validators attached
890 $validator = $format_validators->{$format};
894 die "unknown format: $format\n";
896 } elsif (ref($format) ne 'HASH') {
897 die "unexpected format value of type ".ref($format)."\n";
903 foreach my $part (split(/,/, $data)) {
904 next if $part =~ /^\s*$/;
906 if ($part =~ /^([^=]+)=(.+)$/) {
907 my ($k, $v) = ($1, $2);
908 die "duplicate key in comma-separated list property: $k\n" if defined($res->{$k});
909 my $schema = $format->{$k};
910 if (my $alias = $schema->{alias
}) {
911 if (my $key_alias = $schema->{keyAlias
}) {
912 die "key alias '$key_alias' is already defined\n" if defined($res->{$key_alias});
913 $res->{$key_alias} = $k;
916 $schema = $format->{$k};
919 die "invalid key in comma-separated list property: $k\n" if !$schema;
920 if ($schema->{type
} && $schema->{type
} eq 'boolean') {
921 $v = parse_boolean
($v) // $v;
924 } elsif ($part !~ /=/) {
925 die "duplicate key in comma-separated list property: $default_key\n" if $default_key;
926 foreach my $key (keys %$format) {
927 if ($format->{$key}->{default_key
}) {
929 if (!$res->{$default_key}) {
930 $res->{$default_key} = $part;
933 die "duplicate key in comma-separated list property: $default_key\n";
936 die "value without key, but schema does not define a default key\n" if !$default_key;
938 die "missing key in comma-separated list property\n";
943 check_object
($path, $format, $res, $additional_properties, $errors);
944 if (scalar(%$errors)) {
945 raise
"format error\n", errors
=> $errors;
948 return $validator->($res) if $validator;
953 my ($errors, $path, $msg) = @_;
955 $path = '_root' if !$path;
957 if ($errors->{$path}) {
958 $errors->{$path} = join ('\n', $errors->{$path}, $msg);
960 $errors->{$path} = $msg;
967 # see 'man perlretut'
968 return $value =~ /^[+-]?(\d+\.\d+|\d+\.|\.\d+|\d+)([eE][+-]?\d+)?$/;
974 return $value =~ m/^[+-]?\d+$/;
978 my ($path, $type, $value, $errors) = @_;
982 if (!defined($value)) {
983 return 1 if $type eq 'null';
987 if (my $tt = ref($type)) {
988 if ($tt eq 'ARRAY') {
989 foreach my $t (@$type) {
991 check_type
($path, $t, $value, $tmperr);
992 return 1 if !scalar(%$tmperr);
994 my $ttext = join ('|', @$type);
995 add_error
($errors, $path, "type check ('$ttext') failed");
997 } elsif ($tt eq 'HASH') {
999 check_prop
($value, $type, $path, $tmperr);
1000 return 1 if !scalar(%$tmperr);
1001 add_error
($errors, $path, "type check failed");
1004 die "internal error - got reference type '$tt'";
1009 return 1 if $type eq 'any';
1011 if ($type eq 'null') {
1012 if (defined($value)) {
1013 add_error
($errors, $path, "type check ('$type') failed - value is not null");
1019 my $vt = ref($value);
1021 if ($type eq 'array') {
1022 if (!$vt || $vt ne 'ARRAY') {
1023 add_error
($errors, $path, "type check ('$type') failed");
1027 } elsif ($type eq 'object') {
1028 if (!$vt || $vt ne 'HASH') {
1029 add_error
($errors, $path, "type check ('$type') failed");
1033 } elsif ($type eq 'coderef') {
1034 if (!$vt || $vt ne 'CODE') {
1035 add_error
($errors, $path, "type check ('$type') failed");
1039 } elsif ($type eq 'string' && $vt eq 'Regexp') {
1040 # qr// regexes can be used as strings and make sense for format=regex
1044 if ($type eq 'boolean' && JSON
::is_bool
($value)) {
1047 add_error
($errors, $path, "type check ('$type') failed - got $vt");
1050 if ($type eq 'string') {
1051 return 1; # nothing to check ?
1052 } elsif ($type eq 'boolean') {
1053 #if ($value =~ m/^(1|true|yes|on)$/i) {
1054 if ($value eq '1') {
1056 #} elsif ($value =~ m/^(0|false|no|off)$/i) {
1057 } elsif ($value eq '0') {
1058 return 1; # return success (not value)
1060 add_error
($errors, $path, "type check ('$type') failed - got '$value'");
1063 } elsif ($type eq 'integer') {
1064 if (!is_integer
($value)) {
1065 add_error
($errors, $path, "type check ('$type') failed - got '$value'");
1069 } elsif ($type eq 'number') {
1070 if (!is_number
($value)) {
1071 add_error
($errors, $path, "type check ('$type') failed - got '$value'");
1076 return 1; # no need to verify unknown types
1086 my ($path, $schema, $value, $additional_properties, $errors) = @_;
1088 # print "Check Object " . Dumper($value) . "\nSchema: " . Dumper($schema);
1090 my $st = ref($schema);
1091 if (!$st || $st ne 'HASH') {
1092 add_error
($errors, $path, "Invalid schema definition.");
1096 my $vt = ref($value);
1097 if (!$vt || $vt ne 'HASH') {
1098 add_error
($errors, $path, "an object is required");
1102 foreach my $k (keys %$schema) {
1103 check_prop
($value->{$k}, $schema->{$k}, $path ?
"$path.$k" : $k, $errors);
1106 foreach my $k (keys %$value) {
1108 my $newpath = $path ?
"$path.$k" : $k;
1110 if (my $subschema = $schema->{$k}) {
1111 if (my $requires = $subschema->{requires
}) {
1112 if (ref($requires)) {
1113 #print "TEST: " . Dumper($value) . "\n", Dumper($requires) ;
1114 check_prop
($value, $requires, $path, $errors);
1115 } elsif (!defined($value->{$requires})) {
1116 add_error
($errors, $path ?
"$path.$requires" : $requires,
1117 "missing property - '$newpath' requires this property");
1121 next; # value is already checked above
1124 if (defined ($additional_properties) && !$additional_properties) {
1125 add_error
($errors, $newpath, "property is not defined in schema " .
1126 "and the schema does not allow additional properties");
1129 check_prop
($value->{$k}, $additional_properties, $newpath, $errors)
1130 if ref($additional_properties);
1134 sub check_object_warn
{
1135 my ($path, $schema, $value, $additional_properties) = @_;
1137 check_object
($path, $schema, $value, $additional_properties, $errors);
1138 if (scalar(%$errors)) {
1139 foreach my $k (keys %$errors) {
1140 warn "parse error: $k: $errors->{$k}\n";
1148 my ($value, $schema, $path, $errors) = @_;
1150 die "internal error - no schema" if !$schema;
1151 die "internal error" if !$errors;
1153 #print "check_prop $path\n" if $value;
1155 my $st = ref($schema);
1156 if (!$st || $st ne 'HASH') {
1157 add_error
($errors, $path, "Invalid schema definition.");
1161 # if it extends another schema, it must pass that schema as well
1162 if($schema->{extends
}) {
1163 check_prop
($value, $schema->{extends
}, $path, $errors);
1166 if (!defined ($value)) {
1167 return if $schema->{type
} && $schema->{type
} eq 'null';
1168 if (!$schema->{optional
} && !$schema->{alias
} && !$schema->{group
}) {
1169 add_error
($errors, $path, "property is missing and it is not optional");
1174 return if !check_type
($path, $schema->{type
}, $value, $errors);
1176 if ($schema->{disallow
}) {
1178 if (check_type
($path, $schema->{disallow
}, $value, $tmperr)) {
1179 add_error
($errors, $path, "disallowed value was matched");
1184 if (my $vt = ref($value)) {
1186 if ($vt eq 'ARRAY') {
1187 if ($schema->{items
}) {
1188 my $it = ref($schema->{items
});
1189 if ($it && $it eq 'ARRAY') {
1190 #die "implement me $path: $vt " . Dumper($schema) ."\n". Dumper($value);
1191 die "not implemented";
1194 foreach my $el (@$value) {
1195 check_prop
($el, $schema->{items
}, "${path}[$ind]", $errors);
1201 } elsif ($schema->{properties
} || $schema->{additionalProperties
}) {
1202 check_object
($path, defined($schema->{properties
}) ?
$schema->{properties
} : {},
1203 $value, $schema->{additionalProperties
}, $errors);
1209 if (my $format = $schema->{format
}) {
1210 eval { check_format
($format, $value, $path); };
1212 add_error
($errors, $path, "invalid format - $@");
1217 if (my $pattern = $schema->{pattern
}) {
1218 if ($value !~ m/^$pattern$/) {
1219 add_error
($errors, $path, "value does not match the regex pattern");
1224 if (defined (my $max = $schema->{maxLength
})) {
1225 if (length($value) > $max) {
1226 add_error
($errors, $path, "value may only be $max characters long");
1231 if (defined (my $min = $schema->{minLength
})) {
1232 if (length($value) < $min) {
1233 add_error
($errors, $path, "value must be at least $min characters long");
1238 if (is_number
($value)) {
1239 if (defined (my $max = $schema->{maximum
})) {
1240 if ($value > $max) {
1241 add_error
($errors, $path, "value must have a maximum value of $max");
1246 if (defined (my $min = $schema->{minimum
})) {
1247 if ($value < $min) {
1248 add_error
($errors, $path, "value must have a minimum value of $min");
1254 if (my $ea = $schema->{enum
}) {
1257 foreach my $ev (@$ea) {
1258 if ($ev eq $value) {
1264 add_error
($errors, $path, "value '$value' does not have a value in the enumeration '" .
1265 join(", ", @$ea) . "'");
1272 my ($instance, $schema, $errmsg) = @_;
1275 $errmsg = "Parameter verification failed.\n" if !$errmsg;
1277 # todo: cycle detection is only needed for debugging, I guess
1278 # we can disable that in the final release
1279 # todo: is there a better/faster way to detect cycles?
1281 # 'download' responses can contain a filehandle, don't cycle-check that as
1282 # it produces a warning
1283 my $is_download = ref($instance) eq 'HASH' && exists($instance->{download
});
1284 find_cycle
($instance, sub { $cycles = 1 }) if !$is_download;
1286 add_error
($errors, undef, "data structure contains recursive cycles");
1288 check_prop
($instance, $schema, '', $errors);
1291 if (scalar(%$errors)) {
1292 raise
$errmsg, code
=> HTTP_BAD_REQUEST
, errors
=> $errors;
1298 my $schema_valid_types = ["string", "object", "coderef", "array", "boolean", "number", "integer", "null", "any"];
1299 my $default_schema_noref = {
1300 description
=> "This is the JSON Schema for JSON Schemas.",
1301 type
=> [ "object" ],
1302 additionalProperties
=> 0,
1305 type
=> ["string", "array"],
1306 description
=> "This is a type definition value. This can be a simple type, or a union type",
1311 enum
=> $schema_valid_types,
1313 enum
=> $schema_valid_types,
1317 description
=> "This indicates that the instance property in the instance object is not required.",
1323 description
=> "This is a definition for the properties of an object value",
1329 description
=> "When the value is an array, this indicates the schema to use to validate each item in an array",
1333 additionalProperties
=> {
1334 type
=> [ "boolean", "object"],
1335 description
=> "This provides a default property definition for all properties that are not explicitly defined in an object type definition.",
1342 description
=> "This indicates the minimum value for the instance property when the type of the instance value is a number.",
1347 description
=> "This indicates the maximum value for the instance property when the type of the instance value is a number.",
1351 description
=> "When the instance value is a string, this indicates minimum length of the string",
1358 description
=> "When the instance value is a string, this indicates maximum length of the string.",
1364 description
=> "A text representation of the type (used to generate documentation).",
1369 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.",
1376 description
=> "This provides an enumeration of possible values that are valid for the instance property.",
1381 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).",
1383 verbose_description
=> {
1386 description
=> "This provides a more verbose description.",
1388 format_description
=> {
1391 description
=> "This provides a shorter (usually just one word) description for a property used to generate descriptions for comma separated list property strings.",
1396 description
=> "This provides the title of the property",
1401 description
=> "This is used to provide rendering hints to format cli command output.",
1404 type
=> [ "string", "object" ],
1406 description
=> "indicates a required property or a schema that must be validated if this property is present",
1409 type
=> [ "string", "object" ],
1411 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",
1416 description
=> "Whether this is the default key in a comma separated list property string.",
1421 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.",
1426 description
=> "Allows to store the current 'key' as value of another property. Only valid if used together with 'alias'.",
1427 requires
=> 'alias',
1432 description
=> "This indicates the default for the instance property."
1436 description
=> "Bash completion function. This function should return a list of possible values.",
1442 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.",
1447 description
=> "This indicates the schema extends the given schema. All instances of this schema must be valid to by the extended schema also.",
1450 # this is from hyper schema
1453 description
=> "This defines the link relations of the instance objects",
1460 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",
1464 description
=> "This is the name of the link relation",
1470 description
=> "For submission links, this defines the method that should be used to access the target resource",
1479 description
=> "For CLI context, this defines the maximal width to print before truncating",
1485 my $default_schema = Storable
::dclone
($default_schema_noref);
1487 $default_schema->{properties
}->{properties
}->{additionalProperties
} = $default_schema;
1488 $default_schema->{properties
}->{additionalProperties
}->{properties
} = $default_schema->{properties
};
1490 $default_schema->{properties
}->{items
}->{properties
} = $default_schema->{properties
};
1491 $default_schema->{properties
}->{items
}->{additionalProperties
} = 0;
1493 $default_schema->{properties
}->{disallow
}->{properties
} = $default_schema->{properties
};
1494 $default_schema->{properties
}->{disallow
}->{additionalProperties
} = 0;
1496 $default_schema->{properties
}->{requires
}->{properties
} = $default_schema->{properties
};
1497 $default_schema->{properties
}->{requires
}->{additionalProperties
} = 0;
1499 $default_schema->{properties
}->{extends
}->{properties
} = $default_schema->{properties
};
1500 $default_schema->{properties
}->{extends
}->{additionalProperties
} = 0;
1502 my $method_schema = {
1504 additionalProperties
=> 0,
1507 description
=> "This a description of the method",
1512 description
=> "This indicates the name of the function to call.",
1515 additionalProperties
=> 1,
1530 description
=> "The HTTP method name.",
1531 enum
=> [ 'GET', 'POST', 'PUT', 'DELETE' ],
1536 description
=> "Method needs special privileges - only pvedaemon can execute it",
1541 description
=> "Method is available for clients authenticated using an API token.",
1547 description
=> "Method downloads the file content (filename is the return value of the method).",
1552 description
=> "A parameter name. If specified, all calls to this method are proxied to the host contained in that parameter.",
1555 proxyto_callback
=> {
1557 description
=> "A function which is called to resolve the proxyto attribute. The default implementation returns the value of the 'proxyto' parameter.",
1562 description
=> "Required access permissions. By default only 'root' is allowed to access this method.",
1564 additionalProperties
=> 0,
1567 description
=> "Describe access permissions.",
1571 description
=> "A simply way to allow access for 'all' authenticated users. Value 'world' is used to allow access without credentials.",
1573 enum
=> ['all', 'world'],
1577 description
=> "Array of permission checks (prefix notation).",
1584 description
=> "Used internally",
1588 description
=> "Used internally",
1593 description
=> "path for URL matching (uri template)",
1595 fragmentDelimiter
=> {
1597 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.",
1602 description
=> "JSON Schema for parameters.",
1607 description
=> "JSON Schema for return value.",
1612 description
=> "method implementation (code reference)",
1617 description
=> "Delegate call to this class (perl class string).",
1620 additionalProperties
=> 0,
1626 fragmentDelimiter
=> { optional
=> 1 }
1634 sub validate_schema
{
1637 my $errmsg = "internal error - unable to verify schema\n";
1638 validate
($schema, $default_schema, $errmsg);
1641 sub validate_method_info
{
1644 my $errmsg = "internal error - unable to verify method info\n";
1645 validate
($info, $method_schema, $errmsg);
1647 validate_schema
($info->{parameters
}) if $info->{parameters
};
1648 validate_schema
($info->{returns
}) if $info->{returns
};
1651 # run a self test on load
1652 # make sure we can verify the default schema
1653 validate_schema
($default_schema_noref);
1654 validate_schema
($method_schema);
1656 # and now some utility methods (used by pve api)
1657 sub method_get_child_link
{
1660 return undef if !$info;
1662 my $schema = $info->{returns
};
1663 return undef if !$schema || !$schema->{type
} || $schema->{type
} ne 'array';
1665 my $links = $schema->{links
};
1666 return undef if !$links;
1669 foreach my $lnk (@$links) {
1670 if ($lnk->{href
} && $lnk->{rel
} && ($lnk->{rel
} eq 'child')) {
1679 # a way to parse command line parameters, using a
1680 # schema to configure Getopt::Long
1682 my ($schema, $args, $arg_param, $fixed_param, $param_mapping_hash) = @_;
1684 if (!$schema || !$schema->{properties
}) {
1685 raise
("too many arguments\n", code
=> HTTP_BAD_REQUEST
)
1686 if scalar(@$args) != 0;
1691 if ($arg_param && !ref($arg_param)) {
1692 my $pd = $schema->{properties
}->{$arg_param};
1693 die "expected list format $pd->{format}"
1694 if !($pd && $pd->{format
} && $pd->{format
} =~ m/-list/);
1695 $list_param = $arg_param;
1698 my @interactive = ();
1700 foreach my $prop (keys %{$schema->{properties
}}) {
1701 my $pd = $schema->{properties
}->{$prop};
1702 next if $list_param && $prop eq $list_param;
1703 next if defined($fixed_param->{$prop});
1705 my $mapping = $param_mapping_hash->{$prop};
1706 if ($mapping && $mapping->{interactive
}) {
1707 # interactive parameters such as passwords: make the argument
1708 # optional and call the mapping function afterwards.
1709 push @getopt, "$prop:s";
1710 push @interactive, [$prop, $mapping->{func
}];
1711 } elsif ($pd->{type
} eq 'boolean') {
1712 push @getopt, "$prop:s";
1714 if ($pd->{format
} && $pd->{format
} =~ m/-list/) {
1715 push @getopt, "$prop=s@";
1716 } elsif ($pd->{type
} eq 'array') {
1717 push @getopt, "$prop=s@";
1719 push @getopt, "$prop=s";
1724 Getopt
::Long
::Configure
('prefix_pattern=(--|-)');
1727 raise
("unable to parse option\n", code
=> HTTP_BAD_REQUEST
)
1728 if !Getopt
::Long
::GetOptionsFromArray
($args, $opts, @getopt);
1732 $opts->{$list_param} = $args;
1734 } elsif (ref($arg_param)) {
1735 for (my $i = 0; $i < scalar(@$arg_param); $i++) {
1736 my $arg_name = $arg_param->[$i];
1737 if ($opts->{'extra-args'}) {
1738 raise
("internal error: extra-args must be the last argument\n", code
=> HTTP_BAD_REQUEST
);
1740 if ($arg_name eq 'extra-args') {
1741 $opts->{'extra-args'} = $args;
1746 # check if all left-over arg_param are optional, else we
1747 # must die as the mapping is then ambigious
1748 for (; $i < scalar(@$arg_param); $i++) {
1749 my $prop = $arg_param->[$i];
1750 raise
("not enough arguments\n", code
=> HTTP_BAD_REQUEST
)
1751 if !$schema->{properties
}->{$prop}->{optional
};
1753 if ($arg_param->[-1] eq 'extra-args') {
1754 $opts->{'extra-args'} = [];
1758 $opts->{$arg_name} = shift @$args;
1760 raise
("too many arguments\n", code
=> HTTP_BAD_REQUEST
) if @$args;
1762 raise
("too many arguments\n", code
=> HTTP_BAD_REQUEST
)
1763 if scalar(@$args) != 0;
1766 if (ref($arg_param)) {
1767 foreach my $arg_name (@$arg_param) {
1768 if ($arg_name eq 'extra-args') {
1769 $opts->{'extra-args'} = [];
1770 } elsif (!$schema->{properties
}->{$arg_name}->{optional
}) {
1771 raise
("not enough arguments\n", code
=> HTTP_BAD_REQUEST
);
1777 foreach my $entry (@interactive) {
1778 my ($opt, $func) = @$entry;
1779 my $pd = $schema->{properties
}->{$opt};
1780 my $value = $opts->{$opt};
1781 if (defined($value) || !$pd->{optional
}) {
1782 $opts->{$opt} = $func->($value);
1786 # decode after Getopt as we are not sure how well it handles unicode
1787 foreach my $p (keys %$opts) {
1788 if (!ref($opts->{$p})) {
1789 $opts->{$p} = decode
('locale', $opts->{$p});
1790 } elsif (ref($opts->{$p}) eq 'ARRAY') {
1792 foreach my $v (@{$opts->{$p}}) {
1793 push @$tmp, decode
('locale', $v);
1796 } elsif (ref($opts->{$p}) eq 'SCALAR') {
1797 $opts->{$p} = decode
('locale', $$opts->{$p});
1799 raise
("decoding options failed, unknown reference\n", code
=> HTTP_BAD_REQUEST
);
1803 foreach my $p (keys %$opts) {
1804 if (my $pd = $schema->{properties
}->{$p}) {
1805 if ($pd->{type
} eq 'boolean') {
1806 if ($opts->{$p} eq '') {
1808 } elsif (defined(my $bool = parse_boolean
($opts->{$p}))) {
1809 $opts->{$p} = $bool;
1811 raise
("unable to parse boolean option\n", code
=> HTTP_BAD_REQUEST
);
1813 } elsif ($pd->{format
}) {
1815 if ($pd->{format
} =~ m/-list/) {
1816 # allow --vmid 100 --vmid 101 and --vmid 100,101
1817 # allow --dow mon --dow fri and --dow mon,fri
1818 $opts->{$p} = join(",", @{$opts->{$p}}) if ref($opts->{$p}) eq 'ARRAY';
1824 foreach my $p (keys %$fixed_param) {
1825 $opts->{$p} = $fixed_param->{$p};
1831 # A way to parse configuration data by giving a json schema
1832 sub parse_config
: prototype($$$;$) {
1833 my ($schema, $filename, $raw, $comment_key) = @_;
1835 # do fast check (avoid validate_schema($schema))
1836 die "got strange schema" if !$schema->{type
} ||
1837 !$schema->{properties
} || $schema->{type
} ne 'object';
1842 my $handle_comment = sub { $_[0] =~ /^#/ };
1843 if (defined($comment_key)) {
1845 my $comment_re = qr/^\Q$comment_key\E:\s*(.*\S)\s*$/;
1846 $handle_comment = sub {
1847 if ($_[0] =~ /^\#(.*)\s*$/ || $_[0] =~ $comment_re) {
1848 $comment_data .= PVE
::Tools
::decode_text
($1) . "\n";
1855 while ($raw =~ /^\s*(.+?)\s*$/gm) {
1858 next if $handle_comment->($line);
1860 if ($line =~ m/^(\S+?):\s*(.*)$/) {
1863 if ($schema->{properties
}->{$key} &&
1864 $schema->{properties
}->{$key}->{type
} eq 'boolean') {
1866 $value = parse_boolean
($value) // $value;
1869 $schema->{properties
}->{$key}
1870 && $schema->{properties
}->{$key}->{type
} eq 'array'
1873 $cfg->{$key} //= [];
1874 push $cfg->{$key}->@*, $value;
1877 $cfg->{$key} = $value;
1879 warn "ignore config line: $line\n"
1883 if (defined($comment_data)) {
1884 $cfg->{$comment_key} = $comment_data;
1888 check_prop
($cfg, $schema, '', $errors);
1890 foreach my $k (keys %$errors) {
1891 warn "parse error in '$filename' - '$k': $errors->{$k}\n";
1898 # generate simple key/value file
1900 my ($schema, $filename, $cfg) = @_;
1902 # do fast check (avoid validate_schema($schema))
1903 die "got strange schema" if !$schema->{type
} ||
1904 !$schema->{properties
} || $schema->{type
} ne 'object';
1906 validate
($cfg, $schema, "validation error in '$filename'\n");
1910 foreach my $k (sort keys %$cfg) {
1911 $data .= "$k: $cfg->{$k}\n";
1917 # helpers used to generate our manual pages
1919 my $find_schema_default_key = sub {
1923 my $keyAliasProps = {};
1925 foreach my $key (keys %$format) {
1926 my $phash = $format->{$key};
1927 if ($phash->{default_key
}) {
1928 die "multiple default keys in schema ($default_key, $key)\n"
1929 if defined($default_key);
1930 die "default key '$key' is an alias - this is not allowed\n"
1931 if defined($phash->{alias
});
1932 die "default key '$key' with keyAlias attribute is not allowed\n"
1933 if $phash->{keyAlias
};
1934 $default_key = $key;
1936 my $key_alias = $phash->{keyAlias
};
1937 die "found keyAlias without 'alias definition for '$key'\n"
1938 if $key_alias && !$phash->{alias
};
1940 if ($phash->{alias
} && $key_alias) {
1941 die "inconsistent keyAlias '$key_alias' definition"
1942 if defined($keyAliasProps->{$key_alias}) &&
1943 $keyAliasProps->{$key_alias} ne $phash->{alias
};
1944 $keyAliasProps->{$key_alias} = $phash->{alias
};
1948 return wantarray ?
($default_key, $keyAliasProps) : $default_key;
1951 sub generate_typetext
{
1952 my ($format, $list_enums) = @_;
1954 my ($default_key, $keyAliasProps) = &$find_schema_default_key($format);
1959 my $add_option_string = sub {
1960 my ($text, $optional) = @_;
1966 $text = "[$text]" if $optional;
1971 my $format_key_value = sub {
1972 my ($key, $phash) = @_;
1974 die "internal error" if defined($phash->{alias
});
1980 if (my $desc = $phash->{format_description
}) {
1981 $typetext .= "<$desc>";
1982 } elsif (my $text = $phash->{typetext
}) {
1984 } elsif (my $enum = $phash->{enum
}) {
1985 if ($list_enums || (scalar(@$enum) <= 3)) {
1986 $typetext .= '<' . join('|', @$enum) . '>';
1988 $typetext .= '<enum>';
1990 } elsif ($phash->{type
} eq 'boolean') {
1991 $typetext .= '<1|0>';
1992 } elsif ($phash->{type
} eq 'integer') {
1993 $typetext .= '<integer>';
1994 } elsif ($phash->{type
} eq 'number') {
1995 $typetext .= '<number>';
1997 die "internal error: neither format_description nor typetext found for option '$key'";
2000 if (defined($default_key) && ($default_key eq $key)) {
2001 &$add_option_string("[$keytext=]$typetext", $phash->{optional
});
2003 &$add_option_string("$keytext=$typetext", $phash->{optional
});
2009 my $cond_add_key = sub {
2012 return if $done->{$key}; # avoid duplicates
2016 my $phash = $format->{$key};
2018 return if !$phash; # should not happen
2020 return if $phash->{alias
};
2022 &$format_key_value($key, $phash);
2026 &$cond_add_key($default_key) if defined($default_key);
2028 # add required keys first
2029 foreach my $key (sort keys %$format) {
2030 my $phash = $format->{$key};
2031 &$cond_add_key($key) if $phash && !$phash->{optional
};
2035 foreach my $key (sort keys %$format) {
2036 &$cond_add_key($key);
2039 foreach my $keyAlias (sort keys %$keyAliasProps) {
2040 &$add_option_string("<$keyAlias>=<$keyAliasProps->{$keyAlias }>", 1);
2046 sub print_property_string
{
2047 my ($data, $format, $skip, $path) = @_;
2050 if (ref($format) ne 'HASH') {
2051 my $schema = get_format
($format);
2052 die "not a valid format: $format\n" if !$schema;
2053 # named formats can have validators attached
2054 $validator = $format_validators->{$format};
2059 check_object
($path, $format, $data, undef, $errors);
2060 if (scalar(%$errors)) {
2061 raise
"format error", errors
=> $errors;
2064 $data = $validator->($data) if $validator;
2066 my ($default_key, $keyAliasProps) = &$find_schema_default_key($format);
2071 my $add_option_string = sub {
2074 $res .= ',' if $add_sep;
2079 my $format_value = sub {
2080 my ($key, $value, $format) = @_;
2082 if (defined($format) && ($format eq 'disk-size')) {
2083 return format_size
($value);
2085 die "illegal value with commas for $key\n" if $value =~ /,/;
2090 my $done = { map { $_ => 1 } @$skip };
2092 my $cond_add_key = sub {
2093 my ($key, $isdefault) = @_;
2095 return if $done->{$key}; # avoid duplicates
2099 my $value = $data->{$key};
2101 return if !defined($value);
2103 my $phash = $format->{$key};
2105 # try to combine values if we have key aliases
2106 if (my $combine = $keyAliasProps->{$key}) {
2107 if (defined(my $combine_value = $data->{$combine})) {
2108 my $combine_format = $format->{$combine}->{format
};
2109 my $value_str = &$format_value($key, $value, $phash->{format
});
2110 my $combine_str = &$format_value($combine, $combine_value, $combine_format);
2111 &$add_option_string("${value_str}=${combine_str}");
2112 $done->{$combine} = 1;
2117 if ($phash && $phash->{alias
}) {
2118 $phash = $format->{$phash->{alias
}};
2121 die "invalid key '$key'\n" if !$phash;
2122 die "internal error" if defined($phash->{alias
});
2124 my $value_str = &$format_value($key, $value, $phash->{format
});
2126 &$add_option_string($value_str);
2128 &$add_option_string("$key=${value_str}");
2132 # add default key first
2133 &$cond_add_key($default_key, 1) if defined($default_key);
2135 # add required keys first
2136 foreach my $key (sort keys %$data) {
2137 my $phash = $format->{$key};
2138 &$cond_add_key($key) if $phash && !$phash->{optional
};
2142 foreach my $key (sort keys %$data) {
2143 &$cond_add_key($key);
2149 sub schema_get_type_text
{
2150 my ($phash, $style) = @_;
2152 my $type = $phash->{type
} || 'string';
2154 if ($phash->{typetext
}) {
2155 return $phash->{typetext
};
2156 } elsif ($phash->{format_description
}) {
2157 return "<$phash->{format_description}>";
2158 } elsif ($phash->{enum
}) {
2159 return "<" . join(' | ', sort @{$phash->{enum
}}) . ">";
2160 } elsif ($phash->{pattern
}) {
2161 return $phash->{pattern
};
2162 } elsif ($type eq 'integer' || $type eq 'number') {
2163 # NOTE: always access values as number (avoid converion to string)
2164 if (defined($phash->{minimum
}) && defined($phash->{maximum
})) {
2165 return "<$type> (" . ($phash->{minimum
} + 0) . " - " .
2166 ($phash->{maximum
} + 0) . ")";
2167 } elsif (defined($phash->{minimum
})) {
2168 return "<$type> (" . ($phash->{minimum
} + 0) . " - N)";
2169 } elsif (defined($phash->{maximum
})) {
2170 return "<$type> (-N - " . ($phash->{maximum
} + 0) . ")";
2172 } elsif ($type eq 'string') {
2173 if (my $format = $phash->{format
}) {
2174 $format = get_format
($format) if ref($format) ne 'HASH';
2175 if (ref($format) eq 'HASH') {
2177 $list_enums = 1 if $style && $style eq 'config-sub';
2178 return generate_typetext
($format, $list_enums);