]>
git.proxmox.com Git - pve-common.git/blob - src/PVE/JSONSchema.pm
84fb694fcc1b2ce920036cdda2955667c1c229f5
1 package PVE
:: JSONSchema
;
5 use Storable
; # for dclone
9 use Devel
:: Cycle
- quiet
; # todo: remove?
10 use PVE
:: Tools
qw(split_list $IPV6RE $IPV4RE ) ;
11 use PVE
:: Exception
qw(raise) ;
12 use HTTP
:: Status
qw(:constants) ;
13 use Net
:: IP
qw(:PROC) ;
21 register_standard_option
24 # Note: This class implements something similar to JSON schema, but it is not 100% complete.
25 # see: http://tools.ietf.org/html/draft-zyp-json-schema-02
26 # see: http://json-schema.org/
28 # the code is similar to the javascript parser from http://code.google.com/p/jsonschema/
30 my $standard_options = {};
31 sub register_standard_option
{
32 my ( $name, $schema ) = @_ ;
34 die "standard option ' $name ' already registered \n "
35 if $standard_options ->{ $name };
37 $standard_options ->{ $name } = $schema ;
40 sub get_standard_option
{
41 my ( $name, $base ) = @_ ;
43 my $std = $standard_options ->{ $name };
44 die "no such standard option ' $name ' \n " if ! $std ;
46 my $res = $base || {};
48 foreach my $opt ( keys %$std ) {
49 next if defined ( $res ->{ $opt });
50 $res ->{ $opt } = $std ->{ $opt };
56 register_standard_option
( 'pve-vmid' , {
57 description
=> "The (unique) ID of the VM." ,
58 type
=> 'integer' , format
=> 'pve-vmid' ,
62 register_standard_option
( 'pve-node' , {
63 description
=> "The cluster node name." ,
64 type
=> 'string' , format
=> 'pve-node' ,
67 register_standard_option
( 'pve-node-list' , {
68 description
=> "List of cluster node names." ,
69 type
=> 'string' , format
=> 'pve-node-list' ,
72 register_standard_option
( 'pve-iface' , {
73 description
=> "Network interface name." ,
74 type
=> 'string' , format
=> 'pve-iface' ,
75 minLength
=> 2 , maxLength
=> 20 ,
78 register_standard_option
( 'pve-storage-id' , {
79 description
=> "The storage identifier." ,
80 type
=> 'string' , format
=> 'pve-storage-id' ,
83 register_standard_option
( 'pve-config-digest' , {
84 description
=> 'Prevent changes if current configuration file has different SHA1 digest. This can be used to prevent concurrent modifications.' ,
87 maxLength
=> 40 , # sha1 hex digest length is 40
90 register_standard_option
( 'skiplock' , {
91 description
=> "Ignore locks - only root is allowed to use this option." ,
96 register_standard_option
( 'extra-args' , {
97 description
=> "Extra arguments as array" ,
99 items
=> { type
=> 'string' },
103 register_standard_option
( 'fingerprint-sha256' , {
104 description
=> "Certificate SHA 256 fingerprint." ,
106 pattern
=> '([A-Fa-f0-9]{2}:){31}[A-Fa-f0-9]{2}' ,
109 register_standard_option
( 'pve-output-format' , {
111 description
=> 'Output format.' ,
112 enum
=> [ 'text' , 'json' , 'json-pretty' , 'yaml' ],
117 register_standard_option
( 'pve-snapshot-name' , {
118 description
=> "The name of the snapshot." ,
119 type
=> 'string' , format
=> 'pve-configid' ,
123 my $format_list = {};
125 sub register_format
{
126 my ( $format, $code ) = @_ ;
128 die "JSON schema format ' $format ' already registered \n "
129 if $format_list ->{ $format };
131 $format_list ->{ $format } = $code ;
136 return $format_list ->{ $format };
139 my $renderer_hash = {};
141 sub register_renderer
{
142 my ( $name, $code ) = @_ ;
144 die "renderer ' $name ' already registered \n "
145 if $renderer_hash ->{ $name };
147 $renderer_hash ->{ $name } = $code ;
152 return $renderer_hash ->{ $name };
155 # register some common type for pve
157 register_format
( 'string' , sub {}); # allow format => 'string-list'
159 register_format
( 'urlencoded' , \
& pve_verify_urlencoded
);
160 sub pve_verify_urlencoded
{
161 my ( $text, $noerr ) = @_ ;
162 if ( $text !~ /^[-%a-zA-Z0-9_.!~*'()]*$/ ) {
163 return undef if $noerr ;
164 die "invalid urlencoded string: $text\n " ;
169 register_format
( 'pve-configid' , \
& pve_verify_configid
);
170 sub pve_verify_configid
{
171 my ( $id, $noerr ) = @_ ;
173 if ( $id !~ m/^[a-z][a-z0-9_-]+$/i ) {
174 return undef if $noerr ;
175 die "invalid configuration ID ' $id ' \n " ;
180 PVE
:: JSONSchema
:: register_format
( 'pve-storage-id' , \
& parse_storage_id
);
181 sub parse_storage_id
{
182 my ( $storeid, $noerr ) = @_ ;
184 return parse_id
( $storeid, 'storage' , $noerr );
187 PVE
:: JSONSchema
:: register_format
( 'acme-plugin-id' , \
& parse_acme_plugin_id
);
188 sub parse_acme_plugin_id
{
189 my ( $pluginid, $noerr ) = @_ ;
191 return parse_id
( $pluginid, 'ACME plugin' , $noerr );
195 my ( $id, $type, $noerr ) = @_ ;
197 if ( $id !~ m/^[a-z][a-z0-9\-\_\.]*[a-z0-9]$/i ) {
198 return undef if $noerr ;
199 die " $type ID ' $id ' contains illegal characters \n " ;
204 register_format
( 'pve-vmid' , \
& pve_verify_vmid
);
205 sub pve_verify_vmid
{
206 my ( $vmid, $noerr ) = @_ ;
208 if ( $vmid !~ m/^[1-9][0-9]{2,8}$/ ) {
209 return undef if $noerr ;
210 die "value does not look like a valid VM ID \n " ;
215 register_format
( 'pve-node' , \
& pve_verify_node_name
);
216 sub pve_verify_node_name
{
217 my ( $node, $noerr ) = @_ ;
219 if ( $node !~ m/^([a-zA-Z0-9]([a-zA-Z0-9\-]*[a-zA-Z0-9])?)$/ ) {
220 return undef if $noerr ;
221 die "value does not look like a valid node name \n " ;
227 my ( $idmap, $idformat ) = @_ ;
229 return undef if ! $idmap ;
233 foreach my $entry ( PVE
:: Tools
:: split_list
( $idmap )) {
235 $map ->{ identity
} = 1 ;
236 } elsif ( $entry =~ m/^([^:]+):([^:]+)$/ ) {
237 my ( $source, $target ) = ( $1, $2 );
239 check_format
( $idformat, $source, '' );
240 check_format
( $idformat, $target, '' );
242 die "entry ' $entry ' contains invalid ID - $@\n " if $@ ;
244 die "duplicate mapping for source ' $source ' \n "
245 if exists $map ->{ entries
}->{ $source };
247 $map ->{ entries
}->{ $source } = $target ;
250 check_format
( $idformat, $entry );
252 die "entry ' $entry ' contains invalid ID - $@\n " if $@ ;
254 die "default target ID can only be provided once \n "
255 if exists $map ->{ default };
257 $map ->{ default } = $entry ;
261 die "identity mapping cannot be combined with other mappings \n "
262 if $map ->{ identity
} && ( $map ->{ default } || exists $map ->{ entries
});
267 register_format
( 'storagepair' , \
& verify_storagepair
);
268 sub verify_storagepair
{
269 my ( $storagepair, $noerr ) = @_ ;
271 # note: this only checks a single list entry
272 # when using a storagepair-list map, you need to pass the full
273 # parameter to parse_idmap
274 eval { parse_idmap
( $storagepair, 'pve-storage-id' ) };
276 return undef if $noerr ;
283 register_format
( 'mac-addr' , \
& pve_verify_mac_addr
);
284 sub pve_verify_mac_addr
{
285 my ( $mac_addr, $noerr ) = @_ ;
287 # don't allow I/G bit to be set, most of the time it breaks things, see:
288 # https://pve.proxmox.com/pipermail/pve-devel/2019-March/035998.html
289 if ( $mac_addr !~ m/^[a-f0-9][02468ace](?::[a-f0-9]{2}){5}$/i ) {
290 return undef if $noerr ;
291 die "value does not look like a valid unicast MAC address \n " ;
296 register_standard_option
( 'mac-addr' , {
298 description
=> 'Unicast MAC address.' ,
299 verbose_description
=> 'A common MAC address with the I/G (Individual/Group) bit not set.' ,
300 format_description
=> "XX:XX:XX:XX:XX:XX" ,
302 format
=> 'mac-addr' ,
305 register_format
( 'ipv4' , \
& pve_verify_ipv4
);
306 sub pve_verify_ipv4
{
307 my ( $ipv4, $noerr ) = @_ ;
309 if ( $ipv4 !~ m/^(?:$IPV4RE)$/ ) {
310 return undef if $noerr ;
311 die "value does not look like a valid IPv4 address \n " ;
316 register_format
( 'ipv6' , \
& pve_verify_ipv6
);
317 sub pve_verify_ipv6
{
318 my ( $ipv6, $noerr ) = @_ ;
320 if ( $ipv6 !~ m/^(?:$IPV6RE)$/ ) {
321 return undef if $noerr ;
322 die "value does not look like a valid IPv6 address \n " ;
327 register_format
( 'ip' , \
& pve_verify_ip
);
329 my ( $ip, $noerr ) = @_ ;
331 if ( $ip !~ m/^(?:(?:$IPV4RE)|(?:$IPV6RE))$/ ) {
332 return undef if $noerr ;
333 die "value does not look like a valid IP address \n " ;
338 PVE
:: JSONSchema
:: register_format
( 'ldap-simple-attr' , \
& verify_ldap_simple_attr
);
339 sub verify_ldap_simple_attr
{
340 my ( $attr, $noerr ) = @_ ;
342 if ( $attr =~ m/^[a-zA-Z0-9]+$/ ) {
346 die "value ' $attr ' does not look like a simple ldap attribute name \n " if ! $noerr ;
351 my $ipv4_mask_hash = {
369 '255.255.128.0' => 17 ,
370 '255.255.192.0' => 18 ,
371 '255.255.224.0' => 19 ,
372 '255.255.240.0' => 20 ,
373 '255.255.248.0' => 21 ,
374 '255.255.252.0' => 22 ,
375 '255.255.254.0' => 23 ,
376 '255.255.255.0' => 24 ,
377 '255.255.255.128' => 25 ,
378 '255.255.255.192' => 26 ,
379 '255.255.255.224' => 27 ,
380 '255.255.255.240' => 28 ,
381 '255.255.255.248' => 29 ,
382 '255.255.255.252' => 30 ,
383 '255.255.255.254' => 31 ,
384 '255.255.255.255' => 32 ,
387 sub get_netmask_bits
{
389 return $ipv4_mask_hash ->{ $mask };
392 register_format
( 'ipv4mask' , \
& pve_verify_ipv4mask
);
393 sub pve_verify_ipv4mask
{
394 my ( $mask, $noerr ) = @_ ;
396 if (! defined ( $ipv4_mask_hash ->{ $mask })) {
397 return undef if $noerr ;
398 die "value does not look like a valid IP netmask \n " ;
403 register_format
( 'CIDRv6' , \
& pve_verify_cidrv6
);
404 sub pve_verify_cidrv6
{
405 my ( $cidr, $noerr ) = @_ ;
407 if ( $cidr =~ m!^(?:$IPV6RE)(?:/(\d+))$! && ( $1 > 7 ) && ( $1 <= 128 )) {
411 return undef if $noerr ;
412 die "value does not look like a valid IPv6 CIDR network \n " ;
415 register_format
( 'CIDRv4' , \
& pve_verify_cidrv4
);
416 sub pve_verify_cidrv4
{
417 my ( $cidr, $noerr ) = @_ ;
419 if ( $cidr =~ m!^(?:$IPV4RE)(?:/(\d+))$! && ( $1 > 7 ) && ( $1 <= 32 )) {
423 return undef if $noerr ;
424 die "value does not look like a valid IPv4 CIDR network \n " ;
427 register_format
( 'CIDR' , \
& pve_verify_cidr
);
428 sub pve_verify_cidr
{
429 my ( $cidr, $noerr ) = @_ ;
431 if (!( pve_verify_cidrv4
( $cidr, 1 ) ||
432 pve_verify_cidrv6
( $cidr, 1 )))
434 return undef if $noerr ;
435 die "value does not look like a valid CIDR network \n " ;
441 register_format
( 'pve-ipv4-config' , \
& pve_verify_ipv4_config
);
442 sub pve_verify_ipv4_config
{
443 my ( $config, $noerr ) = @_ ;
445 return $config if $config =~ /^(?:dhcp|manual)$/ ||
446 pve_verify_cidrv4
( $config, 1 );
447 return undef if $noerr ;
448 die "value does not look like a valid ipv4 network configuration \n " ;
451 register_format
( 'pve-ipv6-config' , \
& pve_verify_ipv6_config
);
452 sub pve_verify_ipv6_config
{
453 my ( $config, $noerr ) = @_ ;
455 return $config if $config =~ /^(?:auto|dhcp|manual)$/ ||
456 pve_verify_cidrv6
( $config, 1 );
457 return undef if $noerr ;
458 die "value does not look like a valid ipv6 network configuration \n " ;
461 register_format
( 'email' , \
& pve_verify_email
);
462 sub pve_verify_email
{
463 my ( $email, $noerr ) = @_ ;
465 if ( $email !~ /^[\w\+\-\~]+(\.[\w\+\-\~]+)*@[a-zA-Z0-9\-]+(\.[a-zA-Z0-9\-]+)*$/ ) {
466 return undef if $noerr ;
467 die "value does not look like a valid email address \n " ;
472 register_format
( 'dns-name' , \
& pve_verify_dns_name
);
473 sub pve_verify_dns_name
{
474 my ( $name, $noerr ) = @_ ;
476 my $namere = "([a-zA-Z0-9]([a-zA-Z0-9\-]*[a-zA-Z0-9])?)" ;
478 if ( $name !~ /^(${namere}\.)*${namere}$/ ) {
479 return undef if $noerr ;
480 die "value does not look like a valid DNS name \n " ;
485 # network interface name
486 register_format
( 'pve-iface' , \
& pve_verify_iface
);
487 sub pve_verify_iface
{
488 my ( $id, $noerr ) = @_ ;
490 if ( $id !~ m/^[a-z][a-z0-9_]{1,20}([:\.]\d+)?$/i ) {
491 return undef if $noerr ;
492 die "invalid network interface name ' $id ' \n " ;
497 # general addresses by name or IP
498 register_format
( 'address' , \
& pve_verify_address
);
499 sub pve_verify_address
{
500 my ( $addr, $noerr ) = @_ ;
502 if (!( pve_verify_ip
( $addr, 1 ) ||
503 pve_verify_dns_name
( $addr, 1 )))
505 return undef if $noerr ;
506 die "value does not look like a valid address: $addr\n " ;
511 register_format
( 'disk-size' , \
& pve_verify_disk_size
);
512 sub pve_verify_disk_size
{
513 my ( $size, $noerr ) = @_ ;
514 if (! defined ( parse_size
( $size ))) {
515 return undef if $noerr ;
516 die "value does not look like a valid disk size: $size\n " ;
521 register_standard_option
( 'spice-proxy' , {
522 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)." ,
523 type
=> 'string' , format
=> 'address' ,
526 register_standard_option
( 'remote-viewer-config' , {
527 description
=> "Returned values can be directly passed to the 'remote-viewer' application." ,
528 additionalProperties
=> 1 ,
530 type
=> { type
=> 'string' },
531 password
=> { type
=> 'string' },
532 proxy
=> { type
=> 'string' },
533 host
=> { type
=> 'string' },
534 'tls-port' => { type
=> 'integer' },
538 register_format
( 'pve-startup-order' , \
& pve_verify_startup_order
);
539 sub pve_verify_startup_order
{
540 my ( $value, $noerr ) = @_ ;
542 return $value if pve_parse_startup_order
( $value );
544 return undef if $noerr ;
546 die "unable to parse startup options \n " ;
551 type
=> 'number' , minimum
=> '0' ,
552 format_description
=> 'LIMIT' ,
555 my $bwlimit_format = {
558 description
=> 'default bandwidth limit in KiB/s' ,
562 description
=> 'bandwidth limit in KiB/s for restoring guests from backups' ,
566 description
=> 'bandwidth limit in KiB/s for migrating guests (including moving local disks)' ,
570 description
=> 'bandwidth limit in KiB/s for cloning disks' ,
574 description
=> 'bandwidth limit in KiB/s for moving disks' ,
577 register_format
( 'bwlimit' , $bwlimit_format );
578 register_standard_option
( 'bwlimit' , {
579 description
=> "Set bandwidth/io limits various operations." ,
582 format
=> $bwlimit_format,
585 # used for pve-tag-list in e.g., guest configs
586 register_format
( 'pve-tag' , \
& pve_verify_tag
);
588 my ( $value, $noerr ) = @_ ;
590 return $value if $value =~ m/^[a-z0-9_][a-z0-9_\-\+\.]*$/i ;
592 return undef if $noerr ;
594 die "invalid characters in tag \n " ;
597 sub pve_parse_startup_order
{
600 return undef if ! $value ;
604 foreach my $p ( split ( /,/ , $value )) {
605 next if $p =~ m/^\s*$/ ;
607 if ( $p =~ m/^(order=)?(\d+)$/ ) {
609 } elsif ( $p =~ m/^up=(\d+)$/ ) {
611 } elsif ( $p =~ m/^down=(\d+)$/ ) {
621 PVE
:: JSONSchema
:: register_standard_option
( 'pve-startup-order' , {
622 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." ,
624 type
=> 'string' , format
=> 'pve-startup-order' ,
625 typetext
=> '[[order=]\d+] [,up=\d+] [,down=\d+] ' ,
628 register_format
( 'pve-tfa-secret' , \
& pve_verify_tfa_secret
);
629 sub pve_verify_tfa_secret
{
630 my ( $key, $noerr ) = @_ ;
632 # The old format used 16 base32 chars or 40 hex digits. Since they have a common subset it's
633 # hard to distinguish them without the our previous length constraints, so add a 'v2' of the
634 # format to support arbitrary lengths properly:
635 if ( $key =~ /^v2-0x[0-9a-fA-F]{16,128}$/ || # hex
636 $key =~ /^v2-[A-Z2-7=]{16,128}$/ || # base32
637 $key =~ /^(?:[A-Z2-7=]{16}|[A-Fa-f0-9]{40})$/ ) # and the old pattern copy&pasted
642 return undef if $noerr ;
644 die "unable to decode TFA secret \n " ;
648 my ( $format, $value, $path ) = @_ ;
650 return parse_property_string
( $format, $value, $path ) if ref ( $format ) eq 'HASH' ;
651 return if $format eq 'regex' ;
653 if ( $format =~ m/^(.*)-a?list$/ ) {
655 my $code = $format_list ->{ $1 };
657 die "undefined format ' $format ' \n " if ! $code ;
659 # Note: we allow empty lists
660 foreach my $v ( split_list
( $value )) {
664 } elsif ( $format =~ m/^(.*)-opt$/ ) {
666 my $code = $format_list ->{ $1 };
668 die "undefined format ' $format ' \n " if ! $code ;
670 return if ! $value ; # allow empty string
676 my $code = $format_list ->{ $format };
678 die "undefined format ' $format ' \n " if ! $code ;
680 return parse_property_string
( $code, $value, $path ) if ref ( $code ) eq 'HASH' ;
688 return undef if $value !~ m/^(\d+(\.\d+)?)([KMGT])?$/ ;
689 my ( $size, $unit ) = ( $1, $3 );
692 $size = $size * 1024 ;
693 } elsif ( $unit eq 'M' ) {
694 $size = $size * 1024 * 1024 ;
695 } elsif ( $unit eq 'G' ) {
696 $size = $size * 1024 * 1024 * 1024 ;
697 } elsif ( $unit eq 'T' ) {
698 $size = $size * 1024 * 1024 * 1024 * 1024 ;
709 my $kb = int ( $size/1024 );
710 return $size if $kb*1024 != $size ;
712 my $mb = int ( $kb/1024 );
713 return "${kb}K" if $mb*1024 != $kb ;
715 my $gb = int ( $mb/1024 );
716 return "${mb}M" if $gb*1024 != $mb ;
718 my $tb = int ( $gb/1024 );
719 return "${gb}G" if $tb*1024 != $gb ;
726 return 1 if $bool =~ m/^(1|on|yes|true)$/i ;
727 return 0 if $bool =~ m/^(0|off|no|false)$/i ;
731 sub parse_property_string
{
732 my ( $format, $data, $path, $additional_properties ) = @_ ;
734 # In property strings we default to not allowing additional properties
735 $additional_properties = 0 if ! defined ( $additional_properties );
737 # Support named formats here, too:
739 if ( my $desc = $format_list ->{ $format }) {
742 die "unknown format: $format\n " ;
744 } elsif ( ref ( $format ) ne 'HASH' ) {
745 die "unexpected format value of type " . ref ( $format ). " \n " ;
751 foreach my $part ( split ( /,/ , $data )) {
752 next if $part =~ /^\s*$/ ;
754 if ( $part =~ /^([^=]+)=(.+)$/ ) {
755 my ( $k, $v ) = ( $1, $2 );
756 die "duplicate key in comma-separated list property: $k\n " if defined ( $res ->{ $k });
757 my $schema = $format ->{ $k };
758 if ( my $alias = $schema ->{ alias
}) {
759 if ( my $key_alias = $schema ->{ keyAlias
}) {
760 die "key alias ' $key_alias ' is already defined \n " if defined ( $res ->{ $key_alias });
761 $res ->{ $key_alias } = $k ;
764 $schema = $format ->{ $k };
767 die "invalid key in comma-separated list property: $k\n " if ! $schema ;
768 if ( $schema ->{ type
} && $schema ->{ type
} eq 'boolean' ) {
769 $v = parse_boolean
( $v ) // $v ;
772 } elsif ( $part !~ /=/ ) {
773 die "duplicate key in comma-separated list property: $default_key\n " if $default_key ;
774 foreach my $key ( keys %$format ) {
775 if ( $format ->{ $key }->{ default_key
}) {
777 if (! $res ->{ $default_key }) {
778 $res ->{ $default_key } = $part ;
781 die "duplicate key in comma-separated list property: $default_key\n " ;
784 die "value without key, but schema does not define a default key \n " if ! $default_key ;
786 die "missing key in comma-separated list property \n " ;
791 check_object
( $path, $format, $res, $additional_properties, $errors );
792 if ( scalar ( %$errors )) {
793 raise
"format error \n " , errors
=> $errors ;
800 my ( $errors, $path, $msg ) = @_ ;
802 $path = '_root' if ! $path ;
804 if ( $errors ->{ $path }) {
805 $errors ->{ $path } = join ( ' \n ' , $errors ->{ $path }, $msg );
807 $errors ->{ $path } = $msg ;
814 # see 'man perlretut'
815 return $value =~ /^[+-]?(\d+\.\d+|\d+\.|\.\d+|\d+)([eE][+-]?\d+)?$/ ;
821 return $value =~ m/^[+-]?\d+$/ ;
825 my ( $path, $type, $value, $errors ) = @_ ;
829 if (! defined ( $value )) {
830 return 1 if $type eq 'null' ;
834 if ( my $tt = ref ( $type )) {
835 if ( $tt eq 'ARRAY' ) {
836 foreach my $t ( @$type ) {
838 check_type
( $path, $t, $value, $tmperr );
839 return 1 if ! scalar ( %$tmperr );
841 my $ttext = join ( '|' , @$type );
842 add_error
( $errors, $path, "type check (' $ttext ') failed" );
844 } elsif ( $tt eq 'HASH' ) {
846 check_prop
( $value, $type, $path, $tmperr );
847 return 1 if ! scalar ( %$tmperr );
848 add_error
( $errors, $path, "type check failed" );
851 die "internal error - got reference type ' $tt '" ;
856 return 1 if $type eq 'any' ;
858 if ( $type eq 'null' ) {
859 if ( defined ( $value )) {
860 add_error
( $errors, $path, "type check (' $type ') failed - value is not null" );
866 my $vt = ref ( $value );
868 if ( $type eq 'array' ) {
869 if (! $vt || $vt ne 'ARRAY' ) {
870 add_error
( $errors, $path, "type check (' $type ') failed" );
874 } elsif ( $type eq 'object' ) {
875 if (! $vt || $vt ne 'HASH' ) {
876 add_error
( $errors, $path, "type check (' $type ') failed" );
880 } elsif ( $type eq 'coderef' ) {
881 if (! $vt || $vt ne 'CODE' ) {
882 add_error
( $errors, $path, "type check (' $type ') failed" );
886 } elsif ( $type eq 'string' && $vt eq 'Regexp' ) {
887 # qr// regexes can be used as strings and make sense for format=regex
891 add_error
( $errors, $path, "type check (' $type ') failed - got $vt " );
894 if ( $type eq 'string' ) {
895 return 1 ; # nothing to check ?
896 } elsif ( $type eq 'boolean' ) {
897 #if ($value =~ m/^(1|true|yes|on)$/i) {
900 #} elsif ($value =~ m/^(0|false|no|off)$/i) {
901 } elsif ( $value eq '0' ) {
902 return 1 ; # return success (not value)
904 add_error
( $errors, $path, "type check (' $type ') failed - got ' $value '" );
907 } elsif ( $type eq 'integer' ) {
908 if (! is_integer
( $value )) {
909 add_error
( $errors, $path, "type check (' $type ') failed - got ' $value '" );
913 } elsif ( $type eq 'number' ) {
914 if (! is_number
( $value )) {
915 add_error
( $errors, $path, "type check (' $type ') failed - got ' $value '" );
920 return 1 ; # no need to verify unknown types
930 my ( $path, $schema, $value, $additional_properties, $errors ) = @_ ;
932 # print "Check Object " . Dumper($value) . "\nSchema: " . Dumper($schema);
934 my $st = ref ( $schema );
935 if (! $st || $st ne 'HASH' ) {
936 add_error
( $errors, $path, "Invalid schema definition." );
940 my $vt = ref ( $value );
941 if (! $vt || $vt ne 'HASH' ) {
942 add_error
( $errors, $path, "an object is required" );
946 foreach my $k ( keys %$schema ) {
947 check_prop
( $value ->{ $k }, $schema ->{ $k }, $path ?
" $path . $k " : $k, $errors );
950 foreach my $k ( keys %$value ) {
952 my $newpath = $path ?
" $path . $k " : $k ;
954 if ( my $subschema = $schema ->{ $k }) {
955 if ( my $requires = $subschema ->{ requires
}) {
956 if ( ref ( $requires )) {
957 #print "TEST: " . Dumper($value) . "\n", Dumper($requires) ;
958 check_prop
( $value, $requires, $path, $errors );
959 } elsif (! defined ( $value ->{ $requires })) {
960 add_error
( $errors, $path ?
" $path . $requires " : $requires,
961 "missing property - ' $newpath ' requires this property" );
965 next ; # value is already checked above
968 if ( defined ( $additional_properties ) && ! $additional_properties ) {
969 add_error
( $errors, $newpath, "property is not defined in schema " .
970 "and the schema does not allow additional properties" );
973 check_prop
( $value ->{ $k }, $additional_properties, $newpath, $errors )
974 if ref ( $additional_properties );
978 sub check_object_warn
{
979 my ( $path, $schema, $value, $additional_properties ) = @_ ;
981 check_object
( $path, $schema, $value, $additional_properties, $errors );
982 if ( scalar ( %$errors )) {
983 foreach my $k ( keys %$errors ) {
984 warn "parse error: $k : $errors ->{ $k } \n " ;
992 my ( $value, $schema, $path, $errors ) = @_ ;
994 die "internal error - no schema" if ! $schema ;
995 die "internal error" if ! $errors ;
997 #print "check_prop $path\n" if $value;
999 my $st = ref ( $schema );
1000 if (! $st || $st ne 'HASH' ) {
1001 add_error
( $errors, $path, "Invalid schema definition." );
1005 # if it extends another schema, it must pass that schema as well
1006 if ( $schema ->{ extends
}) {
1007 check_prop
( $value, $schema ->{ extends
}, $path, $errors );
1010 if (! defined ( $value )) {
1011 return if $schema ->{ type
} && $schema ->{ type
} eq 'null' ;
1012 if (! $schema ->{ optional
} && ! $schema ->{ alias
} && ! $schema ->{ group
}) {
1013 add_error
( $errors, $path, "property is missing and it is not optional" );
1018 return if ! check_type
( $path, $schema ->{ type
}, $value, $errors );
1020 if ( $schema ->{ disallow
}) {
1022 if ( check_type
( $path, $schema ->{ disallow
}, $value, $tmperr )) {
1023 add_error
( $errors, $path, "disallowed value was matched" );
1028 if ( my $vt = ref ( $value )) {
1030 if ( $vt eq 'ARRAY' ) {
1031 if ( $schema ->{ items
}) {
1032 my $it = ref ( $schema ->{ items
});
1033 if ( $it && $it eq 'ARRAY' ) {
1034 #die "implement me $path: $vt " . Dumper($schema) ."\n". Dumper($value);
1035 die "not implemented" ;
1038 foreach my $el ( @$value ) {
1039 check_prop
( $el, $schema ->{ items
}, "${path}[ $ind ]" , $errors );
1045 } elsif ( $schema ->{ properties
} || $schema ->{ additionalProperties
}) {
1046 check_object
( $path, defined ( $schema ->{ properties
}) ?
$schema ->{ properties
} : {},
1047 $value, $schema ->{ additionalProperties
}, $errors );
1053 if ( my $format = $schema ->{ format
}) {
1054 eval { check_format
( $format, $value, $path ); };
1056 add_error
( $errors, $path, "invalid format - $@ " );
1061 if ( my $pattern = $schema ->{ pattern
}) {
1062 if ( $value !~ m/^$pattern$/ ) {
1063 add_error
( $errors, $path, "value does not match the regex pattern" );
1068 if ( defined ( my $max = $schema ->{ maxLength
})) {
1069 if ( length ( $value ) > $max ) {
1070 add_error
( $errors, $path, "value may only be $max characters long" );
1075 if ( defined ( my $min = $schema ->{ minLength
})) {
1076 if ( length ( $value ) < $min ) {
1077 add_error
( $errors, $path, "value must be at least $min characters long" );
1082 if ( is_number
( $value )) {
1083 if ( defined ( my $max = $schema ->{ maximum
})) {
1084 if ( $value > $max ) {
1085 add_error
( $errors, $path, "value must have a maximum value of $max " );
1090 if ( defined ( my $min = $schema ->{ minimum
})) {
1091 if ( $value < $min ) {
1092 add_error
( $errors, $path, "value must have a minimum value of $min " );
1098 if ( my $ea = $schema ->{ enum
}) {
1101 foreach my $ev ( @$ea ) {
1102 if ( $ev eq $value ) {
1108 add_error
( $errors, $path, "value ' $value ' does not have a value in the enumeration '" .
1109 join ( ", " , @$ea ) . "'" );
1116 my ( $instance, $schema, $errmsg ) = @_ ;
1119 $errmsg = "Parameter verification failed. \n " if ! $errmsg ;
1121 # todo: cycle detection is only needed for debugging, I guess
1122 # we can disable that in the final release
1123 # todo: is there a better/faster way to detect cycles?
1125 find_cycle
( $instance, sub { $cycles = 1 });
1127 add_error
( $errors, undef , "data structure contains recursive cycles" );
1129 check_prop
( $instance, $schema, '' , $errors );
1132 if ( scalar ( %$errors )) {
1133 raise
$errmsg, code
=> HTTP_BAD_REQUEST
, errors
=> $errors ;
1139 my $schema_valid_types = [ "string" , "object" , "coderef" , "array" , "boolean" , "number" , "integer" , "null" , "any" ];
1140 my $default_schema_noref = {
1141 description
=> "This is the JSON Schema for JSON Schemas." ,
1142 type
=> [ "object" ],
1143 additionalProperties
=> 0 ,
1146 type
=> [ "string" , "array" ],
1147 description
=> "This is a type definition value. This can be a simple type, or a union type" ,
1152 enum
=> $schema_valid_types,
1154 enum
=> $schema_valid_types,
1158 description
=> "This indicates that the instance property in the instance object is not required." ,
1164 description
=> "This is a definition for the properties of an object value" ,
1170 description
=> "When the value is an array, this indicates the schema to use to validate each item in an array" ,
1174 additionalProperties
=> {
1175 type
=> [ "boolean" , "object" ],
1176 description
=> "This provides a default property definition for all properties that are not explicitly defined in an object type definition." ,
1183 description
=> "This indicates the minimum value for the instance property when the type of the instance value is a number." ,
1188 description
=> "This indicates the maximum value for the instance property when the type of the instance value is a number." ,
1192 description
=> "When the instance value is a string, this indicates minimum length of the string" ,
1199 description
=> "When the instance value is a string, this indicates maximum length of the string." ,
1205 description
=> "A text representation of the type (used to generate documentation)." ,
1210 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." ,
1217 description
=> "This provides an enumeration of possible values that are valid for the instance property." ,
1222 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)." ,
1224 verbose_description
=> {
1227 description
=> "This provides a more verbose description." ,
1229 format_description
=> {
1232 description
=> "This provides a shorter (usually just one word) description for a property used to generate descriptions for comma separated list property strings." ,
1237 description
=> "This provides the title of the property" ,
1242 description
=> "This is used to provide rendering hints to format cli command output." ,
1245 type
=> [ "string" , "object" ],
1247 description
=> "indicates a required property or a schema that must be validated if this property is present" ,
1250 type
=> [ "string" , "object" ],
1252 description
=> "This indicates what format the data is among some predefined formats which may include: \n\n date - a string following the ISO format \n address \n schema - a schema definition object \n person \n page \n html - a string representing HTML" ,
1257 description
=> "Whether this is the default key in a comma separated list property string." ,
1262 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." ,
1267 description
=> "Allows to store the current 'key' as value of another property. Only valid if used together with 'alias'." ,
1268 requires
=> 'alias' ,
1273 description
=> "This indicates the default for the instance property."
1277 description
=> "Bash completion function. This function should return a list of possible values." ,
1283 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." ,
1288 description
=> "This indicates the schema extends the given schema. All instances of this schema must be valid to by the extended schema also." ,
1291 # this is from hyper schema
1294 description
=> "This defines the link relations of the instance objects" ,
1301 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" ,
1305 description
=> "This is the name of the link relation" ,
1311 description
=> "For submission links, this defines the method that should be used to access the target resource" ,
1320 description
=> "For CLI context, this defines the maximal width to print before truncating" ,
1326 my $default_schema = Storable
:: dclone
( $default_schema_noref );
1328 $default_schema ->{ properties
}->{ properties
}->{ additionalProperties
} = $default_schema ;
1329 $default_schema ->{ properties
}->{ additionalProperties
}->{ properties
} = $default_schema ->{ properties
};
1331 $default_schema ->{ properties
}->{ items
}->{ properties
} = $default_schema ->{ properties
};
1332 $default_schema ->{ properties
}->{ items
}->{ additionalProperties
} = 0 ;
1334 $default_schema ->{ properties
}->{ disallow
}->{ properties
} = $default_schema ->{ properties
};
1335 $default_schema ->{ properties
}->{ disallow
}->{ additionalProperties
} = 0 ;
1337 $default_schema ->{ properties
}->{ requires
}->{ properties
} = $default_schema ->{ properties
};
1338 $default_schema ->{ properties
}->{ requires
}->{ additionalProperties
} = 0 ;
1340 $default_schema ->{ properties
}->{ extends
}->{ properties
} = $default_schema ->{ properties
};
1341 $default_schema ->{ properties
}->{ extends
}->{ additionalProperties
} = 0 ;
1343 my $method_schema = {
1345 additionalProperties
=> 0 ,
1348 description
=> "This a description of the method" ,
1353 description
=> "This indicates the name of the function to call." ,
1356 additionalProperties
=> 1 ,
1371 description
=> "The HTTP method name." ,
1372 enum
=> [ 'GET' , 'POST' , 'PUT' , 'DELETE' ],
1377 description
=> "Method needs special privileges - only pvedaemon can execute it" ,
1382 description
=> "Method is available for clients authenticated using an API token." ,
1388 description
=> "Method downloads the file content (filename is the return value of the method)." ,
1393 description
=> "A parameter name. If specified, all calls to this method are proxied to the host contained in that parameter." ,
1396 proxyto_callback
=> {
1398 description
=> "A function which is called to resolve the proxyto attribute. The default implementation returns the value of the 'proxyto' parameter." ,
1403 description
=> "Required access permissions. By default only 'root' is allowed to access this method." ,
1405 additionalProperties
=> 0 ,
1408 description
=> "Describe access permissions." ,
1412 description
=> "A simply way to allow access for 'all' authenticated users. Value 'world' is used to allow access without credentials." ,
1414 enum
=> [ 'all' , 'world' ],
1418 description
=> "Array of permission checks (prefix notation)." ,
1425 description
=> "Used internally" ,
1429 description
=> "Used internally" ,
1434 description
=> "path for URL matching (uri template)" ,
1436 fragmentDelimiter
=> {
1438 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." ,
1443 description
=> "JSON Schema for parameters." ,
1448 description
=> "JSON Schema for return value." ,
1453 description
=> "method implementation (code reference)" ,
1458 description
=> "Delegate call to this class (perl class string)." ,
1461 additionalProperties
=> 0 ,
1467 fragmentDelimiter
=> { optional
=> 1 }
1475 sub validate_schema
{
1478 my $errmsg = "internal error - unable to verify schema \n " ;
1479 validate
( $schema, $default_schema, $errmsg );
1482 sub validate_method_info
{
1485 my $errmsg = "internal error - unable to verify method info \n " ;
1486 validate
( $info, $method_schema, $errmsg );
1488 validate_schema
( $info ->{ parameters
}) if $info ->{ parameters
};
1489 validate_schema
( $info ->{ returns
}) if $info ->{ returns
};
1492 # run a self test on load
1493 # make sure we can verify the default schema
1494 validate_schema
( $default_schema_noref );
1495 validate_schema
( $method_schema );
1497 # and now some utility methods (used by pve api)
1498 sub method_get_child_link
{
1501 return undef if ! $info ;
1503 my $schema = $info ->{ returns
};
1504 return undef if ! $schema || ! $schema ->{ type
} || $schema ->{ type
} ne 'array' ;
1506 my $links = $schema ->{ links
};
1507 return undef if ! $links ;
1510 foreach my $lnk ( @$links ) {
1511 if ( $lnk ->{ href
} && $lnk ->{ rel
} && ( $lnk ->{ rel
} eq 'child' )) {
1520 # a way to parse command line parameters, using a
1521 # schema to configure Getopt::Long
1523 my ( $schema, $args, $arg_param, $fixed_param, $param_mapping_hash ) = @_ ;
1525 if (! $schema || ! $schema ->{ properties
}) {
1526 raise
( "too many arguments \n " , code
=> HTTP_BAD_REQUEST
)
1527 if scalar ( @$args ) != 0 ;
1532 if ( $arg_param && ! ref ( $arg_param )) {
1533 my $pd = $schema ->{ properties
}->{ $arg_param };
1534 die "expected list format $pd ->{format}"
1535 if !( $pd && $pd ->{ format
} && $pd ->{ format
} =~ m/-list/ );
1536 $list_param = $arg_param ;
1539 my @interactive = ();
1541 foreach my $prop ( keys %{ $schema ->{ properties
}}) {
1542 my $pd = $schema ->{ properties
}->{ $prop };
1543 next if $list_param && $prop eq $list_param ;
1544 next if defined ( $fixed_param ->{ $prop });
1546 my $mapping = $param_mapping_hash ->{ $prop };
1547 if ( $mapping && $mapping ->{ interactive
}) {
1548 # interactive parameters such as passwords: make the argument
1549 # optional and call the mapping function afterwards.
1550 push @getopt, " $prop :s" ;
1551 push @interactive, [ $prop, $mapping ->{ func
}];
1552 } elsif ( $pd ->{ type
} eq 'boolean' ) {
1553 push @getopt, " $prop :s" ;
1555 if ( $pd ->{ format
} && $pd ->{ format
} =~ m/-a?list/ ) {
1556 push @getopt, " $prop =s@" ;
1558 push @getopt, " $prop =s" ;
1563 Getopt
:: Long
:: Configure
( 'prefix_pattern=(--|-)' );
1566 raise
( "unable to parse option \n " , code
=> HTTP_BAD_REQUEST
)
1567 if ! Getopt
:: Long
:: GetOptionsFromArray
( $args, $opts, @getopt );
1571 $opts ->{ $list_param } = $args ;
1573 } elsif ( ref ( $arg_param )) {
1574 foreach my $arg_name ( @$arg_param ) {
1575 if ( $opts ->{ 'extra-args' }) {
1576 raise
( "internal error: extra-args must be the last argument \n " , code
=> HTTP_BAD_REQUEST
);
1578 if ( $arg_name eq 'extra-args' ) {
1579 $opts ->{ 'extra-args' } = $args ;
1583 raise
( "not enough arguments \n " , code
=> HTTP_BAD_REQUEST
) if ! @$args ;
1584 $opts ->{ $arg_name } = shift @$args ;
1586 raise
( "too many arguments \n " , code
=> HTTP_BAD_REQUEST
) if @$args ;
1588 raise
( "too many arguments \n " , code
=> HTTP_BAD_REQUEST
)
1589 if scalar ( @$args ) != 0 ;
1592 if ( ref ( $arg_param )) {
1593 foreach my $arg_name ( @$arg_param ) {
1594 if ( $arg_name eq 'extra-args' ) {
1595 $opts ->{ 'extra-args' } = [];
1597 raise
( "not enough arguments \n " , code
=> HTTP_BAD_REQUEST
);
1603 foreach my $entry ( @interactive ) {
1604 my ( $opt, $func ) = @$entry ;
1605 my $pd = $schema ->{ properties
}->{ $opt };
1606 my $value = $opts ->{ $opt };
1607 if ( defined ( $value ) || ! $pd ->{ optional
}) {
1608 $opts ->{ $opt } = $func ->( $value );
1612 # decode after Getopt as we are not sure how well it handles unicode
1613 foreach my $p ( keys %$opts ) {
1614 if (! ref ( $opts ->{ $p })) {
1615 $opts ->{ $p } = decode
( 'locale' , $opts ->{ $p });
1616 } elsif ( ref ( $opts ->{ $p }) eq 'ARRAY' ) {
1618 foreach my $v (@{ $opts ->{ $p }}) {
1619 push @$tmp, decode
( 'locale' , $v );
1622 } elsif ( ref ( $opts ->{ $p }) eq 'SCALAR' ) {
1623 $opts ->{ $p } = decode
( 'locale' , $$opts ->{ $p });
1625 raise
( "decoding options failed, unknown reference \n " , code
=> HTTP_BAD_REQUEST
);
1629 foreach my $p ( keys %$opts ) {
1630 if ( my $pd = $schema ->{ properties
}->{ $p }) {
1631 if ( $pd ->{ type
} eq 'boolean' ) {
1632 if ( $opts ->{ $p } eq '' ) {
1634 } elsif ( defined ( my $bool = parse_boolean
( $opts ->{ $p }))) {
1635 $opts ->{ $p } = $bool ;
1637 raise
( "unable to parse boolean option \n " , code
=> HTTP_BAD_REQUEST
);
1639 } elsif ( $pd ->{ format
}) {
1641 if ( $pd ->{ format
} =~ m/-list/ ) {
1642 # allow --vmid 100 --vmid 101 and --vmid 100,101
1643 # allow --dow mon --dow fri and --dow mon,fri
1644 $opts ->{ $p } = join ( "," , @{ $opts ->{ $p }}) if ref ( $opts ->{ $p }) eq 'ARRAY' ;
1645 } elsif ( $pd ->{ format
} =~ m/-alist/ ) {
1646 # we encode array as \0 separated strings
1647 # Note: CGI.pm also use this encoding
1648 if ( scalar (@{ $opts ->{ $p }}) != 1 ) {
1649 $opts ->{ $p } = join ( "\0" , @{ $opts ->{ $p }});
1651 # st that split_list knows it is \0 terminated
1652 my $v = $opts ->{ $p }->[ 0 ];
1653 $opts ->{ $p } = " $v\0 " ;
1660 foreach my $p ( keys %$fixed_param ) {
1661 $opts ->{ $p } = $fixed_param ->{ $p };
1667 # A way to parse configuration data by giving a json schema
1669 my ( $schema, $filename, $raw ) = @_ ;
1671 # do fast check (avoid validate_schema($schema))
1672 die "got strange schema" if ! $schema ->{ type
} ||
1673 ! $schema ->{ properties
} || $schema ->{ type
} ne 'object' ;
1677 while ( $raw =~ /^\s*(.+?)\s*$/gm ) {
1680 next if $line =~ /^#/ ;
1682 if ( $line =~ m/^(\S+?):\s*(.*)$/ ) {
1685 if ( $schema ->{ properties
}->{ $key } &&
1686 $schema ->{ properties
}->{ $key }->{ type
} eq 'boolean' ) {
1688 $value = parse_boolean
( $value ) // $value ;
1690 $cfg ->{ $key } = $value ;
1692 warn "ignore config line: $line\n "
1697 check_prop
( $cfg, $schema, '' , $errors );
1699 foreach my $k ( keys %$errors ) {
1700 warn "parse error in ' $filename ' - ' $k ': $errors ->{ $k } \n " ;
1707 # generate simple key/value file
1709 my ( $schema, $filename, $cfg ) = @_ ;
1711 # do fast check (avoid validate_schema($schema))
1712 die "got strange schema" if ! $schema ->{ type
} ||
1713 ! $schema ->{ properties
} || $schema ->{ type
} ne 'object' ;
1715 validate
( $cfg, $schema, "validation error in ' $filename ' \n " );
1719 foreach my $k ( sort keys %$cfg ) {
1720 $data .= " $k : $cfg ->{ $k } \n " ;
1726 # helpers used to generate our manual pages
1728 my $find_schema_default_key = sub {
1732 my $keyAliasProps = {};
1734 foreach my $key ( keys %$format ) {
1735 my $phash = $format ->{ $key };
1736 if ( $phash ->{ default_key
}) {
1737 die "multiple default keys in schema ( $default_key, $key ) \n "
1738 if defined ( $default_key );
1739 die "default key ' $key ' is an alias - this is not allowed \n "
1740 if defined ( $phash ->{ alias
});
1741 die "default key ' $key ' with keyAlias attribute is not allowed \n "
1742 if $phash ->{ keyAlias
};
1743 $default_key = $key ;
1745 my $key_alias = $phash ->{ keyAlias
};
1746 die "found keyAlias without 'alias definition for ' $key ' \n "
1747 if $key_alias && ! $phash ->{ alias
};
1749 if ( $phash ->{ alias
} && $key_alias ) {
1750 die "inconsistent keyAlias ' $key_alias ' definition"
1751 if defined ( $keyAliasProps ->{ $key_alias }) &&
1752 $keyAliasProps ->{ $key_alias } ne $phash ->{ alias
};
1753 $keyAliasProps ->{ $key_alias } = $phash ->{ alias
};
1757 return wantarray ?
( $default_key, $keyAliasProps ) : $default_key ;
1760 sub generate_typetext
{
1761 my ( $format, $list_enums ) = @_ ;
1763 my ( $default_key, $keyAliasProps ) = & $find_schema_default_key ( $format );
1768 my $add_option_string = sub {
1769 my ( $text, $optional ) = @_ ;
1775 $text = "[ $text ]" if $optional ;
1780 my $format_key_value = sub {
1781 my ( $key, $phash ) = @_ ;
1783 die "internal error" if defined ( $phash ->{ alias
});
1789 if ( my $desc = $phash ->{ format_description
}) {
1790 $typetext .= "< $desc >" ;
1791 } elsif ( my $text = $phash ->{ typetext
}) {
1793 } elsif ( my $enum = $phash ->{ enum
}) {
1794 if ( $list_enums || ( scalar ( @$enum ) <= 3 )) {
1795 $typetext .= '<' . join ( '|' , @$enum ) . '>' ;
1797 $typetext .= '<enum>' ;
1799 } elsif ( $phash ->{ type
} eq 'boolean' ) {
1800 $typetext .= '<1|0>' ;
1801 } elsif ( $phash ->{ type
} eq 'integer' ) {
1802 $typetext .= '<integer>' ;
1803 } elsif ( $phash ->{ type
} eq 'number' ) {
1804 $typetext .= '<number>' ;
1806 die "internal error: neither format_description nor typetext found for option ' $key '" ;
1809 if ( defined ( $default_key ) && ( $default_key eq $key )) {
1810 & $add_option_string ( "[ $keytext =] $typetext " , $phash ->{ optional
});
1812 & $add_option_string ( " $keytext = $typetext " , $phash ->{ optional
});
1818 my $cond_add_key = sub {
1821 return if $done ->{ $key }; # avoid duplicates
1825 my $phash = $format ->{ $key };
1827 return if ! $phash ; # should not happen
1829 return if $phash ->{ alias
};
1831 & $format_key_value ( $key, $phash );
1835 & $cond_add_key ( $default_key ) if defined ( $default_key );
1837 # add required keys first
1838 foreach my $key ( sort keys %$format ) {
1839 my $phash = $format ->{ $key };
1840 & $cond_add_key ( $key ) if $phash && ! $phash ->{ optional
};
1844 foreach my $key ( sort keys %$format ) {
1845 & $cond_add_key ( $key );
1848 foreach my $keyAlias ( sort keys %$keyAliasProps ) {
1849 & $add_option_string ( "< $keyAlias >=< $keyAliasProps ->{ $keyAlias }>" , 1 );
1855 sub print_property_string
{
1856 my ( $data, $format, $skip, $path ) = @_ ;
1858 if ( ref ( $format ) ne 'HASH' ) {
1859 my $schema = get_format
( $format );
1860 die "not a valid format: $format\n " if ! $schema ;
1865 check_object
( $path, $format, $data, undef , $errors );
1866 if ( scalar ( %$errors )) {
1867 raise
"format error" , errors
=> $errors ;
1870 my ( $default_key, $keyAliasProps ) = & $find_schema_default_key ( $format );
1875 my $add_option_string = sub {
1878 $res .= ',' if $add_sep ;
1883 my $format_value = sub {
1884 my ( $key, $value, $format ) = @_ ;
1886 if ( defined ( $format ) && ( $format eq 'disk-size' )) {
1887 return format_size
( $value );
1889 die "illegal value with commas for $key\n " if $value =~ /,/ ;
1894 my $done = { map { $_ => 1 } @$skip };
1896 my $cond_add_key = sub {
1897 my ( $key, $isdefault ) = @_ ;
1899 return if $done ->{ $key }; # avoid duplicates
1903 my $value = $data ->{ $key };
1905 return if ! defined ( $value );
1907 my $phash = $format ->{ $key };
1909 # try to combine values if we have key aliases
1910 if ( my $combine = $keyAliasProps ->{ $key }) {
1911 if ( defined ( my $combine_value = $data ->{ $combine })) {
1912 my $combine_format = $format ->{ $combine }->{ format
};
1913 my $value_str = & $format_value ( $key, $value, $phash ->{ format
});
1914 my $combine_str = & $format_value ( $combine, $combine_value, $combine_format );
1915 & $add_option_string ( "${value_str}=${combine_str}" );
1916 $done ->{ $combine } = 1 ;
1921 if ( $phash && $phash ->{ alias
}) {
1922 $phash = $format ->{ $phash ->{ alias
}};
1925 die "invalid key ' $key ' \n " if ! $phash ;
1926 die "internal error" if defined ( $phash ->{ alias
});
1928 my $value_str = & $format_value ( $key, $value, $phash ->{ format
});
1930 & $add_option_string ( $value_str );
1932 & $add_option_string ( " $key =${value_str}" );
1936 # add default key first
1937 & $cond_add_key ( $default_key, 1 ) if defined ( $default_key );
1939 # add required keys first
1940 foreach my $key ( sort keys %$data ) {
1941 my $phash = $format ->{ $key };
1942 & $cond_add_key ( $key ) if $phash && ! $phash ->{ optional
};
1946 foreach my $key ( sort keys %$data ) {
1947 & $cond_add_key ( $key );
1953 sub schema_get_type_text
{
1954 my ( $phash, $style ) = @_ ;
1956 my $type = $phash ->{ type
} || 'string' ;
1958 if ( $phash ->{ typetext
}) {
1959 return $phash ->{ typetext
};
1960 } elsif ( $phash ->{ format_description
}) {
1961 return "< $phash ->{format_description}>" ;
1962 } elsif ( $phash ->{ enum
}) {
1963 return "<" . join ( ' | ' , sort @{ $phash ->{ enum
}}) . ">" ;
1964 } elsif ( $phash ->{ pattern
}) {
1965 return $phash ->{ pattern
};
1966 } elsif ( $type eq 'integer' || $type eq 'number' ) {
1967 # NOTE: always access values as number (avoid converion to string)
1968 if ( defined ( $phash ->{ minimum
}) && defined ( $phash ->{ maximum
})) {
1969 return "< $type > (" . ( $phash ->{ minimum
} + 0 ) . " - " .
1970 ( $phash ->{ maximum
} + 0 ) . ")" ;
1971 } elsif ( defined ( $phash ->{ minimum
})) {
1972 return "< $type > (" . ( $phash ->{ minimum
} + 0 ) . " - N)" ;
1973 } elsif ( defined ( $phash ->{ maximum
})) {
1974 return "< $type > (-N - " . ( $phash ->{ maximum
} + 0 ) . ")" ;
1976 } elsif ( $type eq 'string' ) {
1977 if ( my $format = $phash ->{ format
}) {
1978 $format = get_format
( $format ) if ref ( $format ) ne 'HASH' ;
1979 if ( ref ( $format ) eq 'HASH' ) {
1981 $list_enums = 1 if $style && $style eq 'config-sub' ;
1982 return generate_typetext
( $format, $list_enums );