]>
git.proxmox.com Git - pve-common.git/blob - src/PVE/JSONSchema.pm
2802e072128a9a2bc8fcc95190c08ca0cae7d2c2
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) ;
19 register_standard_option
23 # Note: This class implements something similar to JSON schema, but it is not 100% complete.
24 # see: http://tools.ietf.org/html/draft-zyp-json-schema-02
25 # see: http://json-schema.org/
27 # the code is similar to the javascript parser from http://code.google.com/p/jsonschema/
29 my $standard_options = {};
30 sub register_standard_option
{
31 my ( $name, $schema ) = @_ ;
33 die "standard option ' $name ' already registered \n "
34 if $standard_options ->{ $name };
36 $standard_options ->{ $name } = $schema ;
39 sub get_standard_option
{
40 my ( $name, $base ) = @_ ;
42 my $std = $standard_options ->{ $name };
43 die "no such standard option ' $name ' \n " if ! $std ;
45 my $res = $base || {};
47 foreach my $opt ( keys %$std ) {
48 next if defined ( $res ->{ $opt });
49 $res ->{ $opt } = $std ->{ $opt };
55 register_standard_option
( 'pve-vmid' , {
56 description
=> "The (unique) ID of the VM." ,
57 type
=> 'integer' , format
=> 'pve-vmid' ,
61 register_standard_option
( 'pve-node' , {
62 description
=> "The cluster node name." ,
63 type
=> 'string' , format
=> 'pve-node' ,
66 register_standard_option
( 'pve-node-list' , {
67 description
=> "List of cluster node names." ,
68 type
=> 'string' , format
=> 'pve-node-list' ,
71 register_standard_option
( 'pve-iface' , {
72 description
=> "Network interface name." ,
73 type
=> 'string' , format
=> 'pve-iface' ,
74 minLength
=> 2 , maxLength
=> 20 ,
77 register_standard_option
( 'pve-storage-id' , {
78 description
=> "The storage identifier." ,
79 type
=> 'string' , format
=> 'pve-storage-id' ,
82 register_standard_option
( 'pve-config-digest' , {
83 description
=> 'Prevent changes if current configuration file has different SHA1 digest. This can be used to prevent concurrent modifications.' ,
86 maxLength
=> 40 , # sha1 hex digest length is 40
89 register_standard_option
( 'skiplock' , {
90 description
=> "Ignore locks - only root is allowed to use this option." ,
95 register_standard_option
( 'extra-args' , {
96 description
=> "Extra arguments as array" ,
98 items
=> { type
=> 'string' },
102 register_standard_option
( 'fingerprint-sha256' , {
103 description
=> "Certificate SHA 256 fingerprint." ,
105 pattern
=> '([A-Fa-f0-9]{2}:){31}[A-Fa-f0-9]{2}' ,
108 register_standard_option
( 'pve-output-format' , {
110 description
=> 'Output format.' ,
111 enum
=> [ 'text' , 'json' , 'json-pretty' , 'yaml' ],
116 register_standard_option
( 'pve-snapshot-name' , {
117 description
=> "The name of the snapshot." ,
118 type
=> 'string' , format
=> 'pve-configid' ,
122 my $format_list = {};
124 sub register_format
{
125 my ( $format, $code ) = @_ ;
127 die "JSON schema format ' $format ' already registered \n "
128 if $format_list ->{ $format };
130 $format_list ->{ $format } = $code ;
135 return $format_list ->{ $format };
138 my $renderer_hash = {};
140 sub register_renderer
{
141 my ( $name, $code ) = @_ ;
143 die "renderer ' $name ' already registered \n "
144 if $renderer_hash ->{ $name };
146 $renderer_hash ->{ $name } = $code ;
151 return $renderer_hash ->{ $name };
154 # register some common type for pve
156 register_format
( 'string' , sub {}); # allow format => 'string-list'
158 register_format
( 'urlencoded' , \
& pve_verify_urlencoded
);
159 sub pve_verify_urlencoded
{
160 my ( $text, $noerr ) = @_ ;
161 if ( $text !~ /^[-%a-zA-Z0-9_.!~*'()]*$/ ) {
162 return undef if $noerr ;
163 die "invalid urlencoded string: $text\n " ;
168 register_format
( 'pve-configid' , \
& pve_verify_configid
);
169 sub pve_verify_configid
{
170 my ( $id, $noerr ) = @_ ;
172 if ( $id !~ m/^[a-z][a-z0-9_]+$/i ) {
173 return undef if $noerr ;
174 die "invalid configuration ID ' $id ' \n " ;
179 PVE
:: JSONSchema
:: register_format
( 'pve-storage-id' , \
& parse_storage_id
);
180 sub parse_storage_id
{
181 my ( $storeid, $noerr ) = @_ ;
183 return parse_id
( $storeid, 'storage' , $noerr );
186 PVE
:: JSONSchema
:: register_format
( 'acme-plugin-id' , \
& parse_acme_plugin_id
);
187 sub parse_acme_plugin_id
{
188 my ( $pluginid, $noerr ) = @_ ;
190 return parse_id
( $pluginid, 'ACME plugin' , $noerr );
194 my ( $id, $type, $noerr ) = @_ ;
196 if ( $id !~ m/^[a-z][a-z0-9\-\_\.]*[a-z0-9]$/i ) {
197 return undef if $noerr ;
198 die " $type ID ' $id ' contains illegal characters \n " ;
203 register_format
( 'pve-vmid' , \
& pve_verify_vmid
);
204 sub pve_verify_vmid
{
205 my ( $vmid, $noerr ) = @_ ;
207 if ( $vmid !~ m/^[1-9][0-9]{2,8}$/ ) {
208 return undef if $noerr ;
209 die "value does not look like a valid VM ID \n " ;
214 register_format
( 'pve-node' , \
& pve_verify_node_name
);
215 sub pve_verify_node_name
{
216 my ( $node, $noerr ) = @_ ;
218 if ( $node !~ m/^([a-zA-Z0-9]([a-zA-Z0-9\-]*[a-zA-Z0-9])?)$/ ) {
219 return undef if $noerr ;
220 die "value does not look like a valid node name \n " ;
225 register_format
( 'mac-addr' , \
& pve_verify_mac_addr
);
226 sub pve_verify_mac_addr
{
227 my ( $mac_addr, $noerr ) = @_ ;
229 # don't allow I/G bit to be set, most of the time it breaks things, see:
230 # https://pve.proxmox.com/pipermail/pve-devel/2019-March/035998.html
231 if ( $mac_addr !~ m/^[a-f0-9][02468ace](?::[a-f0-9]{2}){5}$/i ) {
232 return undef if $noerr ;
233 die "value does not look like a valid unicast MAC address \n " ;
238 register_standard_option
( 'mac-addr' , {
240 description
=> 'Unicast MAC address.' ,
241 verbose_description
=> 'A common MAC address with the I/G (Individual/Group) bit not set.' ,
242 format_description
=> "XX:XX:XX:XX:XX:XX" ,
244 format
=> 'mac-addr' ,
247 register_format
( 'ipv4' , \
& pve_verify_ipv4
);
248 sub pve_verify_ipv4
{
249 my ( $ipv4, $noerr ) = @_ ;
251 if ( $ipv4 !~ m/^(?:$IPV4RE)$/ ) {
252 return undef if $noerr ;
253 die "value does not look like a valid IPv4 address \n " ;
258 register_format
( 'ipv6' , \
& pve_verify_ipv6
);
259 sub pve_verify_ipv6
{
260 my ( $ipv6, $noerr ) = @_ ;
262 if ( $ipv6 !~ m/^(?:$IPV6RE)$/ ) {
263 return undef if $noerr ;
264 die "value does not look like a valid IPv6 address \n " ;
269 register_format
( 'ip' , \
& pve_verify_ip
);
271 my ( $ip, $noerr ) = @_ ;
273 if ( $ip !~ m/^(?:(?:$IPV4RE)|(?:$IPV6RE))$/ ) {
274 return undef if $noerr ;
275 die "value does not look like a valid IP address \n " ;
280 PVE
:: JSONSchema
:: register_format
( 'ldap-simple-attr' , \
& verify_ldap_simple_attr
);
281 sub verify_ldap_simple_attr
{
282 my ( $attr, $noerr ) = @_ ;
284 if ( $attr =~ m/^[a-zA-Z0-9]+$/ ) {
288 die "value ' $attr ' does not look like a simple ldap attribute name \n " if ! $noerr ;
293 my $ipv4_mask_hash = {
311 '255.255.128.0' => 17 ,
312 '255.255.192.0' => 18 ,
313 '255.255.224.0' => 19 ,
314 '255.255.240.0' => 20 ,
315 '255.255.248.0' => 21 ,
316 '255.255.252.0' => 22 ,
317 '255.255.254.0' => 23 ,
318 '255.255.255.0' => 24 ,
319 '255.255.255.128' => 25 ,
320 '255.255.255.192' => 26 ,
321 '255.255.255.224' => 27 ,
322 '255.255.255.240' => 28 ,
323 '255.255.255.248' => 29 ,
324 '255.255.255.252' => 30 ,
325 '255.255.255.254' => 31 ,
326 '255.255.255.255' => 32 ,
329 sub get_netmask_bits
{
331 return $ipv4_mask_hash ->{ $mask };
334 register_format
( 'ipv4mask' , \
& pve_verify_ipv4mask
);
335 sub pve_verify_ipv4mask
{
336 my ( $mask, $noerr ) = @_ ;
338 if (! defined ( $ipv4_mask_hash ->{ $mask })) {
339 return undef if $noerr ;
340 die "value does not look like a valid IP netmask \n " ;
345 register_format
( 'CIDRv6' , \
& pve_verify_cidrv6
);
346 sub pve_verify_cidrv6
{
347 my ( $cidr, $noerr ) = @_ ;
349 if ( $cidr =~ m!^(?:$IPV6RE)(?:/(\d+))$! && ( $1 > 7 ) && ( $1 <= 128 )) {
353 return undef if $noerr ;
354 die "value does not look like a valid IPv6 CIDR network \n " ;
357 register_format
( 'CIDRv4' , \
& pve_verify_cidrv4
);
358 sub pve_verify_cidrv4
{
359 my ( $cidr, $noerr ) = @_ ;
361 if ( $cidr =~ m!^(?:$IPV4RE)(?:/(\d+))$! && ( $1 > 7 ) && ( $1 <= 32 )) {
365 return undef if $noerr ;
366 die "value does not look like a valid IPv4 CIDR network \n " ;
369 register_format
( 'CIDR' , \
& pve_verify_cidr
);
370 sub pve_verify_cidr
{
371 my ( $cidr, $noerr ) = @_ ;
373 if (!( pve_verify_cidrv4
( $cidr, 1 ) ||
374 pve_verify_cidrv6
( $cidr, 1 )))
376 return undef if $noerr ;
377 die "value does not look like a valid CIDR network \n " ;
383 register_format
( 'pve-ipv4-config' , \
& pve_verify_ipv4_config
);
384 sub pve_verify_ipv4_config
{
385 my ( $config, $noerr ) = @_ ;
387 return $config if $config =~ /^(?:dhcp|manual)$/ ||
388 pve_verify_cidrv4
( $config, 1 );
389 return undef if $noerr ;
390 die "value does not look like a valid ipv4 network configuration \n " ;
393 register_format
( 'pve-ipv6-config' , \
& pve_verify_ipv6_config
);
394 sub pve_verify_ipv6_config
{
395 my ( $config, $noerr ) = @_ ;
397 return $config if $config =~ /^(?:auto|dhcp|manual)$/ ||
398 pve_verify_cidrv6
( $config, 1 );
399 return undef if $noerr ;
400 die "value does not look like a valid ipv6 network configuration \n " ;
403 register_format
( 'email' , \
& pve_verify_email
);
404 sub pve_verify_email
{
405 my ( $email, $noerr ) = @_ ;
407 if ( $email !~ /^[\w\+\-\~]+(\.[\w\+\-\~]+)*@[a-zA-Z0-9\-]+(\.[a-zA-Z0-9\-]+)*$/ ) {
408 return undef if $noerr ;
409 die "value does not look like a valid email address \n " ;
414 register_format
( 'dns-name' , \
& pve_verify_dns_name
);
415 sub pve_verify_dns_name
{
416 my ( $name, $noerr ) = @_ ;
418 my $namere = "([a-zA-Z0-9]([a-zA-Z0-9\-]*[a-zA-Z0-9])?)" ;
420 if ( $name !~ /^(${namere}\.)*${namere}$/ ) {
421 return undef if $noerr ;
422 die "value does not look like a valid DNS name \n " ;
427 # network interface name
428 register_format
( 'pve-iface' , \
& pve_verify_iface
);
429 sub pve_verify_iface
{
430 my ( $id, $noerr ) = @_ ;
432 if ( $id !~ m/^[a-z][a-z0-9_]{1,20}([:\.]\d+)?$/i ) {
433 return undef if $noerr ;
434 die "invalid network interface name ' $id ' \n " ;
439 # general addresses by name or IP
440 register_format
( 'address' , \
& pve_verify_address
);
441 sub pve_verify_address
{
442 my ( $addr, $noerr ) = @_ ;
444 if (!( pve_verify_ip
( $addr, 1 ) ||
445 pve_verify_dns_name
( $addr, 1 )))
447 return undef if $noerr ;
448 die "value does not look like a valid address: $addr\n " ;
453 register_format
( 'disk-size' , \
& pve_verify_disk_size
);
454 sub pve_verify_disk_size
{
455 my ( $size, $noerr ) = @_ ;
456 if (! defined ( parse_size
( $size ))) {
457 return undef if $noerr ;
458 die "value does not look like a valid disk size: $size\n " ;
463 register_standard_option
( 'spice-proxy' , {
464 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)." ,
465 type
=> 'string' , format
=> 'address' ,
468 register_standard_option
( 'remote-viewer-config' , {
469 description
=> "Returned values can be directly passed to the 'remote-viewer' application." ,
470 additionalProperties
=> 1 ,
472 type
=> { type
=> 'string' },
473 password
=> { type
=> 'string' },
474 proxy
=> { type
=> 'string' },
475 host
=> { type
=> 'string' },
476 'tls-port' => { type
=> 'integer' },
480 register_format
( 'pve-startup-order' , \
& pve_verify_startup_order
);
481 sub pve_verify_startup_order
{
482 my ( $value, $noerr ) = @_ ;
484 return $value if pve_parse_startup_order
( $value );
486 return undef if $noerr ;
488 die "unable to parse startup options \n " ;
493 type
=> 'number' , minimum
=> '0' ,
494 format_description
=> 'LIMIT' ,
497 my $bwlimit_format = {
500 description
=> 'default bandwidth limit in KiB/s' ,
504 description
=> 'bandwidth limit in KiB/s for restoring guests from backups' ,
508 description
=> 'bandwidth limit in KiB/s for migrating guests (including moving local disks)' ,
512 description
=> 'bandwidth limit in KiB/s for cloning disks' ,
516 description
=> 'bandwidth limit in KiB/s for moving disks' ,
519 register_format
( 'bwlimit' , $bwlimit_format );
520 register_standard_option
( 'bwlimit' , {
521 description
=> "Set bandwidth/io limits various operations." ,
524 format
=> $bwlimit_format,
527 # used for pve-tag-list in e.g., guest configs
528 register_format
( 'pve-tag' , \
& pve_verify_tag
);
530 my ( $value, $noerr ) = @_ ;
532 return $value if $value =~ m/^[a-z0-9_][a-z0-9_\-\+\.]*$/i ;
534 return undef if $noerr ;
536 die "invalid characters in tag \n " ;
539 sub pve_parse_startup_order
{
542 return undef if ! $value ;
546 foreach my $p ( split ( /,/ , $value )) {
547 next if $p =~ m/^\s*$/ ;
549 if ( $p =~ m/^(order=)?(\d+)$/ ) {
551 } elsif ( $p =~ m/^up=(\d+)$/ ) {
553 } elsif ( $p =~ m/^down=(\d+)$/ ) {
563 PVE
:: JSONSchema
:: register_standard_option
( 'pve-startup-order' , {
564 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." ,
566 type
=> 'string' , format
=> 'pve-startup-order' ,
567 typetext
=> '[[order=]\d+] [,up=\d+] [,down=\d+] ' ,
570 register_format
( 'pve-tfa-secret' , \
& pve_verify_tfa_secret
);
571 sub pve_verify_tfa_secret
{
572 my ( $key, $noerr ) = @_ ;
574 # The old format used 16 base32 chars or 40 hex digits. Since they have a common subset it's
575 # hard to distinguish them without the our previous length constraints, so add a 'v2' of the
576 # format to support arbitrary lengths properly:
577 if ( $key =~ /^v2-0x[0-9a-fA-F]{16,128}$/ || # hex
578 $key =~ /^v2-[A-Z2-7=]{16,128}$/ || # base32
579 $key =~ /^(?:[A-Z2-7=]{16}|[A-Fa-f0-9]{40})$/ ) # and the old pattern copy&pasted
584 return undef if $noerr ;
586 die "unable to decode TFA secret \n " ;
590 my ( $format, $value, $path ) = @_ ;
592 return parse_property_string
( $format, $value, $path ) if ref ( $format ) eq 'HASH' ;
593 return if $format eq 'regex' ;
595 if ( $format =~ m/^(.*)-a?list$/ ) {
597 my $code = $format_list ->{ $1 };
599 die "undefined format ' $format ' \n " if ! $code ;
601 # Note: we allow empty lists
602 foreach my $v ( split_list
( $value )) {
606 } elsif ( $format =~ m/^(.*)-opt$/ ) {
608 my $code = $format_list ->{ $1 };
610 die "undefined format ' $format ' \n " if ! $code ;
612 return if ! $value ; # allow empty string
618 my $code = $format_list ->{ $format };
620 die "undefined format ' $format ' \n " if ! $code ;
622 return parse_property_string
( $code, $value, $path ) if ref ( $code ) eq 'HASH' ;
630 return undef if $value !~ m/^(\d+(\.\d+)?)([KMGT])?$/ ;
631 my ( $size, $unit ) = ( $1, $3 );
634 $size = $size * 1024 ;
635 } elsif ( $unit eq 'M' ) {
636 $size = $size * 1024 * 1024 ;
637 } elsif ( $unit eq 'G' ) {
638 $size = $size * 1024 * 1024 * 1024 ;
639 } elsif ( $unit eq 'T' ) {
640 $size = $size * 1024 * 1024 * 1024 * 1024 ;
651 my $kb = int ( $size/1024 );
652 return $size if $kb*1024 != $size ;
654 my $mb = int ( $kb/1024 );
655 return "${kb}K" if $mb*1024 != $kb ;
657 my $gb = int ( $mb/1024 );
658 return "${mb}M" if $gb*1024 != $mb ;
660 my $tb = int ( $gb/1024 );
661 return "${gb}G" if $tb*1024 != $gb ;
668 return 1 if $bool =~ m/^(1|on|yes|true)$/i ;
669 return 0 if $bool =~ m/^(0|off|no|false)$/i ;
673 sub parse_property_string
{
674 my ( $format, $data, $path, $additional_properties ) = @_ ;
676 # In property strings we default to not allowing additional properties
677 $additional_properties = 0 if ! defined ( $additional_properties );
679 # Support named formats here, too:
681 if ( my $desc = $format_list ->{ $format }) {
684 die "unknown format: $format\n " ;
686 } elsif ( ref ( $format ) ne 'HASH' ) {
687 die "unexpected format value of type " . ref ( $format ). " \n " ;
693 foreach my $part ( split ( /,/ , $data )) {
694 next if $part =~ /^\s*$/ ;
696 if ( $part =~ /^([^=]+)=(.+)$/ ) {
697 my ( $k, $v ) = ( $1, $2 );
698 die "duplicate key in comma-separated list property: $k\n " if defined ( $res ->{ $k });
699 my $schema = $format ->{ $k };
700 if ( my $alias = $schema ->{ alias
}) {
701 if ( my $key_alias = $schema ->{ keyAlias
}) {
702 die "key alias ' $key_alias ' is already defined \n " if defined ( $res ->{ $key_alias });
703 $res ->{ $key_alias } = $k ;
706 $schema = $format ->{ $k };
709 die "invalid key in comma-separated list property: $k\n " if ! $schema ;
710 if ( $schema ->{ type
} && $schema ->{ type
} eq 'boolean' ) {
711 $v = parse_boolean
( $v ) // $v ;
714 } elsif ( $part !~ /=/ ) {
715 die "duplicate key in comma-separated list property: $default_key\n " if $default_key ;
716 foreach my $key ( keys %$format ) {
717 if ( $format ->{ $key }->{ default_key
}) {
719 if (! $res ->{ $default_key }) {
720 $res ->{ $default_key } = $part ;
723 die "duplicate key in comma-separated list property: $default_key\n " ;
726 die "value without key, but schema does not define a default key \n " if ! $default_key ;
728 die "missing key in comma-separated list property \n " ;
733 check_object
( $path, $format, $res, $additional_properties, $errors );
734 if ( scalar ( %$errors )) {
735 raise
"format error \n " , errors
=> $errors ;
742 my ( $errors, $path, $msg ) = @_ ;
744 $path = '_root' if ! $path ;
746 if ( $errors ->{ $path }) {
747 $errors ->{ $path } = join ( ' \n ' , $errors ->{ $path }, $msg );
749 $errors ->{ $path } = $msg ;
756 # see 'man perlretut'
757 return $value =~ /^[+-]?(\d+\.\d+|\d+\.|\.\d+|\d+)([eE][+-]?\d+)?$/ ;
763 return $value =~ m/^[+-]?\d+$/ ;
767 my ( $path, $type, $value, $errors ) = @_ ;
771 if (! defined ( $value )) {
772 return 1 if $type eq 'null' ;
776 if ( my $tt = ref ( $type )) {
777 if ( $tt eq 'ARRAY' ) {
778 foreach my $t ( @$type ) {
780 check_type
( $path, $t, $value, $tmperr );
781 return 1 if ! scalar ( %$tmperr );
783 my $ttext = join ( '|' , @$type );
784 add_error
( $errors, $path, "type check (' $ttext ') failed" );
786 } elsif ( $tt eq 'HASH' ) {
788 check_prop
( $value, $type, $path, $tmperr );
789 return 1 if ! scalar ( %$tmperr );
790 add_error
( $errors, $path, "type check failed" );
793 die "internal error - got reference type ' $tt '" ;
798 return 1 if $type eq 'any' ;
800 if ( $type eq 'null' ) {
801 if ( defined ( $value )) {
802 add_error
( $errors, $path, "type check (' $type ') failed - value is not null" );
808 my $vt = ref ( $value );
810 if ( $type eq 'array' ) {
811 if (! $vt || $vt ne 'ARRAY' ) {
812 add_error
( $errors, $path, "type check (' $type ') failed" );
816 } elsif ( $type eq 'object' ) {
817 if (! $vt || $vt ne 'HASH' ) {
818 add_error
( $errors, $path, "type check (' $type ') failed" );
822 } elsif ( $type eq 'coderef' ) {
823 if (! $vt || $vt ne 'CODE' ) {
824 add_error
( $errors, $path, "type check (' $type ') failed" );
828 } elsif ( $type eq 'string' && $vt eq 'Regexp' ) {
829 # qr// regexes can be used as strings and make sense for format=regex
833 add_error
( $errors, $path, "type check (' $type ') failed - got $vt " );
836 if ( $type eq 'string' ) {
837 return 1 ; # nothing to check ?
838 } elsif ( $type eq 'boolean' ) {
839 #if ($value =~ m/^(1|true|yes|on)$/i) {
842 #} elsif ($value =~ m/^(0|false|no|off)$/i) {
843 } elsif ( $value eq '0' ) {
844 return 1 ; # return success (not value)
846 add_error
( $errors, $path, "type check (' $type ') failed - got ' $value '" );
849 } elsif ( $type eq 'integer' ) {
850 if (! is_integer
( $value )) {
851 add_error
( $errors, $path, "type check (' $type ') failed - got ' $value '" );
855 } elsif ( $type eq 'number' ) {
856 if (! is_number
( $value )) {
857 add_error
( $errors, $path, "type check (' $type ') failed - got ' $value '" );
862 return 1 ; # no need to verify unknown types
872 my ( $path, $schema, $value, $additional_properties, $errors ) = @_ ;
874 # print "Check Object " . Dumper($value) . "\nSchema: " . Dumper($schema);
876 my $st = ref ( $schema );
877 if (! $st || $st ne 'HASH' ) {
878 add_error
( $errors, $path, "Invalid schema definition." );
882 my $vt = ref ( $value );
883 if (! $vt || $vt ne 'HASH' ) {
884 add_error
( $errors, $path, "an object is required" );
888 foreach my $k ( keys %$schema ) {
889 check_prop
( $value ->{ $k }, $schema ->{ $k }, $path ?
" $path . $k " : $k, $errors );
892 foreach my $k ( keys %$value ) {
894 my $newpath = $path ?
" $path . $k " : $k ;
896 if ( my $subschema = $schema ->{ $k }) {
897 if ( my $requires = $subschema ->{ requires
}) {
898 if ( ref ( $requires )) {
899 #print "TEST: " . Dumper($value) . "\n", Dumper($requires) ;
900 check_prop
( $value, $requires, $path, $errors );
901 } elsif (! defined ( $value ->{ $requires })) {
902 add_error
( $errors, $path ?
" $path . $requires " : $requires,
903 "missing property - ' $newpath ' requires this property" );
907 next ; # value is already checked above
910 if ( defined ( $additional_properties ) && ! $additional_properties ) {
911 add_error
( $errors, $newpath, "property is not defined in schema " .
912 "and the schema does not allow additional properties" );
915 check_prop
( $value ->{ $k }, $additional_properties, $newpath, $errors )
916 if ref ( $additional_properties );
920 sub check_object_warn
{
921 my ( $path, $schema, $value, $additional_properties ) = @_ ;
923 check_object
( $path, $schema, $value, $additional_properties, $errors );
924 if ( scalar ( %$errors )) {
925 foreach my $k ( keys %$errors ) {
926 warn "parse error: $k : $errors ->{ $k } \n " ;
934 my ( $value, $schema, $path, $errors ) = @_ ;
936 die "internal error - no schema" if ! $schema ;
937 die "internal error" if ! $errors ;
939 #print "check_prop $path\n" if $value;
941 my $st = ref ( $schema );
942 if (! $st || $st ne 'HASH' ) {
943 add_error
( $errors, $path, "Invalid schema definition." );
947 # if it extends another schema, it must pass that schema as well
948 if ( $schema ->{ extends
}) {
949 check_prop
( $value, $schema ->{ extends
}, $path, $errors );
952 if (! defined ( $value )) {
953 return if $schema ->{ type
} && $schema ->{ type
} eq 'null' ;
954 if (! $schema ->{ optional
} && ! $schema ->{ alias
} && ! $schema ->{ group
}) {
955 add_error
( $errors, $path, "property is missing and it is not optional" );
960 return if ! check_type
( $path, $schema ->{ type
}, $value, $errors );
962 if ( $schema ->{ disallow
}) {
964 if ( check_type
( $path, $schema ->{ disallow
}, $value, $tmperr )) {
965 add_error
( $errors, $path, "disallowed value was matched" );
970 if ( my $vt = ref ( $value )) {
972 if ( $vt eq 'ARRAY' ) {
973 if ( $schema ->{ items
}) {
974 my $it = ref ( $schema ->{ items
});
975 if ( $it && $it eq 'ARRAY' ) {
976 #die "implement me $path: $vt " . Dumper($schema) ."\n". Dumper($value);
977 die "not implemented" ;
980 foreach my $el ( @$value ) {
981 check_prop
( $el, $schema ->{ items
}, "${path}[ $ind ]" , $errors );
987 } elsif ( $schema ->{ properties
} || $schema ->{ additionalProperties
}) {
988 check_object
( $path, defined ( $schema ->{ properties
}) ?
$schema ->{ properties
} : {},
989 $value, $schema ->{ additionalProperties
}, $errors );
995 if ( my $format = $schema ->{ format
}) {
996 eval { check_format
( $format, $value, $path ); };
998 add_error
( $errors, $path, "invalid format - $@ " );
1003 if ( my $pattern = $schema ->{ pattern
}) {
1004 if ( $value !~ m/^$pattern$/ ) {
1005 add_error
( $errors, $path, "value does not match the regex pattern" );
1010 if ( defined ( my $max = $schema ->{ maxLength
})) {
1011 if ( length ( $value ) > $max ) {
1012 add_error
( $errors, $path, "value may only be $max characters long" );
1017 if ( defined ( my $min = $schema ->{ minLength
})) {
1018 if ( length ( $value ) < $min ) {
1019 add_error
( $errors, $path, "value must be at least $min characters long" );
1024 if ( is_number
( $value )) {
1025 if ( defined ( my $max = $schema ->{ maximum
})) {
1026 if ( $value > $max ) {
1027 add_error
( $errors, $path, "value must have a maximum value of $max " );
1032 if ( defined ( my $min = $schema ->{ minimum
})) {
1033 if ( $value < $min ) {
1034 add_error
( $errors, $path, "value must have a minimum value of $min " );
1040 if ( my $ea = $schema ->{ enum
}) {
1043 foreach my $ev ( @$ea ) {
1044 if ( $ev eq $value ) {
1050 add_error
( $errors, $path, "value ' $value ' does not have a value in the enumeration '" .
1051 join ( ", " , @$ea ) . "'" );
1058 my ( $instance, $schema, $errmsg ) = @_ ;
1061 $errmsg = "Parameter verification failed. \n " if ! $errmsg ;
1063 # todo: cycle detection is only needed for debugging, I guess
1064 # we can disable that in the final release
1065 # todo: is there a better/faster way to detect cycles?
1067 find_cycle
( $instance, sub { $cycles = 1 });
1069 add_error
( $errors, undef , "data structure contains recursive cycles" );
1071 check_prop
( $instance, $schema, '' , $errors );
1074 if ( scalar ( %$errors )) {
1075 raise
$errmsg, code
=> HTTP_BAD_REQUEST
, errors
=> $errors ;
1081 my $schema_valid_types = [ "string" , "object" , "coderef" , "array" , "boolean" , "number" , "integer" , "null" , "any" ];
1082 my $default_schema_noref = {
1083 description
=> "This is the JSON Schema for JSON Schemas." ,
1084 type
=> [ "object" ],
1085 additionalProperties
=> 0 ,
1088 type
=> [ "string" , "array" ],
1089 description
=> "This is a type definition value. This can be a simple type, or a union type" ,
1094 enum
=> $schema_valid_types,
1096 enum
=> $schema_valid_types,
1100 description
=> "This indicates that the instance property in the instance object is not required." ,
1106 description
=> "This is a definition for the properties of an object value" ,
1112 description
=> "When the value is an array, this indicates the schema to use to validate each item in an array" ,
1116 additionalProperties
=> {
1117 type
=> [ "boolean" , "object" ],
1118 description
=> "This provides a default property definition for all properties that are not explicitly defined in an object type definition." ,
1125 description
=> "This indicates the minimum value for the instance property when the type of the instance value is a number." ,
1130 description
=> "This indicates the maximum value for the instance property when the type of the instance value is a number." ,
1134 description
=> "When the instance value is a string, this indicates minimum length of the string" ,
1141 description
=> "When the instance value is a string, this indicates maximum length of the string." ,
1147 description
=> "A text representation of the type (used to generate documentation)." ,
1152 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." ,
1159 description
=> "This provides an enumeration of possible values that are valid for the instance property." ,
1164 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)." ,
1166 verbose_description
=> {
1169 description
=> "This provides a more verbose description." ,
1171 format_description
=> {
1174 description
=> "This provides a shorter (usually just one word) description for a property used to generate descriptions for comma separated list property strings." ,
1179 description
=> "This provides the title of the property" ,
1184 description
=> "This is used to provide rendering hints to format cli command output." ,
1187 type
=> [ "string" , "object" ],
1189 description
=> "indicates a required property or a schema that must be validated if this property is present" ,
1192 type
=> [ "string" , "object" ],
1194 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" ,
1199 description
=> "Whether this is the default key in a comma separated list property string." ,
1204 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." ,
1209 description
=> "Allows to store the current 'key' as value of another property. Only valid if used together with 'alias'." ,
1210 requires
=> 'alias' ,
1215 description
=> "This indicates the default for the instance property."
1219 description
=> "Bash completion function. This function should return a list of possible values." ,
1225 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." ,
1230 description
=> "This indicates the schema extends the given schema. All instances of this schema must be valid to by the extended schema also." ,
1233 # this is from hyper schema
1236 description
=> "This defines the link relations of the instance objects" ,
1243 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" ,
1247 description
=> "This is the name of the link relation" ,
1253 description
=> "For submission links, this defines the method that should be used to access the target resource" ,
1262 description
=> "For CLI context, this defines the maximal width to print before truncating" ,
1268 my $default_schema = Storable
:: dclone
( $default_schema_noref );
1270 $default_schema ->{ properties
}->{ properties
}->{ additionalProperties
} = $default_schema ;
1271 $default_schema ->{ properties
}->{ additionalProperties
}->{ properties
} = $default_schema ->{ properties
};
1273 $default_schema ->{ properties
}->{ items
}->{ properties
} = $default_schema ->{ properties
};
1274 $default_schema ->{ properties
}->{ items
}->{ additionalProperties
} = 0 ;
1276 $default_schema ->{ properties
}->{ disallow
}->{ properties
} = $default_schema ->{ properties
};
1277 $default_schema ->{ properties
}->{ disallow
}->{ additionalProperties
} = 0 ;
1279 $default_schema ->{ properties
}->{ requires
}->{ properties
} = $default_schema ->{ properties
};
1280 $default_schema ->{ properties
}->{ requires
}->{ additionalProperties
} = 0 ;
1282 $default_schema ->{ properties
}->{ extends
}->{ properties
} = $default_schema ->{ properties
};
1283 $default_schema ->{ properties
}->{ extends
}->{ additionalProperties
} = 0 ;
1285 my $method_schema = {
1287 additionalProperties
=> 0 ,
1290 description
=> "This a description of the method" ,
1295 description
=> "This indicates the name of the function to call." ,
1298 additionalProperties
=> 1 ,
1313 description
=> "The HTTP method name." ,
1314 enum
=> [ 'GET' , 'POST' , 'PUT' , 'DELETE' ],
1319 description
=> "Method needs special privileges - only pvedaemon can execute it" ,
1324 description
=> "Method is available for clients authenticated using an API token." ,
1330 description
=> "Method downloads the file content (filename is the return value of the method)." ,
1335 description
=> "A parameter name. If specified, all calls to this method are proxied to the host contained in that parameter." ,
1338 proxyto_callback
=> {
1340 description
=> "A function which is called to resolve the proxyto attribute. The default implementation returns the value of the 'proxyto' parameter." ,
1345 description
=> "Required access permissions. By default only 'root' is allowed to access this method." ,
1347 additionalProperties
=> 0 ,
1350 description
=> "Describe access permissions." ,
1354 description
=> "A simply way to allow access for 'all' authenticated users. Value 'world' is used to allow access without credentials." ,
1356 enum
=> [ 'all' , 'world' ],
1360 description
=> "Array of permission checks (prefix notation)." ,
1367 description
=> "Used internally" ,
1371 description
=> "Used internally" ,
1376 description
=> "path for URL matching (uri template)" ,
1378 fragmentDelimiter
=> {
1380 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." ,
1385 description
=> "JSON Schema for parameters." ,
1390 description
=> "JSON Schema for return value." ,
1395 description
=> "method implementation (code reference)" ,
1400 description
=> "Delegate call to this class (perl class string)." ,
1403 additionalProperties
=> 0 ,
1409 fragmentDelimiter
=> { optional
=> 1 }
1417 sub validate_schema
{
1420 my $errmsg = "internal error - unable to verify schema \n " ;
1421 validate
( $schema, $default_schema, $errmsg );
1424 sub validate_method_info
{
1427 my $errmsg = "internal error - unable to verify method info \n " ;
1428 validate
( $info, $method_schema, $errmsg );
1430 validate_schema
( $info ->{ parameters
}) if $info ->{ parameters
};
1431 validate_schema
( $info ->{ returns
}) if $info ->{ returns
};
1434 # run a self test on load
1435 # make sure we can verify the default schema
1436 validate_schema
( $default_schema_noref );
1437 validate_schema
( $method_schema );
1439 # and now some utility methods (used by pve api)
1440 sub method_get_child_link
{
1443 return undef if ! $info ;
1445 my $schema = $info ->{ returns
};
1446 return undef if ! $schema || ! $schema ->{ type
} || $schema ->{ type
} ne 'array' ;
1448 my $links = $schema ->{ links
};
1449 return undef if ! $links ;
1452 foreach my $lnk ( @$links ) {
1453 if ( $lnk ->{ href
} && $lnk ->{ rel
} && ( $lnk ->{ rel
} eq 'child' )) {
1462 # a way to parse command line parameters, using a
1463 # schema to configure Getopt::Long
1465 my ( $schema, $args, $arg_param, $fixed_param, $param_mapping_hash ) = @_ ;
1467 if (! $schema || ! $schema ->{ properties
}) {
1468 raise
( "too many arguments \n " , code
=> HTTP_BAD_REQUEST
)
1469 if scalar ( @$args ) != 0 ;
1474 if ( $arg_param && ! ref ( $arg_param )) {
1475 my $pd = $schema ->{ properties
}->{ $arg_param };
1476 die "expected list format $pd ->{format}"
1477 if !( $pd && $pd ->{ format
} && $pd ->{ format
} =~ m/-list/ );
1478 $list_param = $arg_param ;
1481 my @interactive = ();
1483 foreach my $prop ( keys %{ $schema ->{ properties
}}) {
1484 my $pd = $schema ->{ properties
}->{ $prop };
1485 next if $list_param && $prop eq $list_param ;
1486 next if defined ( $fixed_param ->{ $prop });
1488 my $mapping = $param_mapping_hash ->{ $prop };
1489 if ( $mapping && $mapping ->{ interactive
}) {
1490 # interactive parameters such as passwords: make the argument
1491 # optional and call the mapping function afterwards.
1492 push @getopt, " $prop :s" ;
1493 push @interactive, [ $prop, $mapping ->{ func
}];
1494 } elsif ( $pd ->{ type
} eq 'boolean' ) {
1495 push @getopt, " $prop :s" ;
1497 if ( $pd ->{ format
} && $pd ->{ format
} =~ m/-a?list/ ) {
1498 push @getopt, " $prop =s@" ;
1500 push @getopt, " $prop =s" ;
1505 Getopt
:: Long
:: Configure
( 'prefix_pattern=(--|-)' );
1508 raise
( "unable to parse option \n " , code
=> HTTP_BAD_REQUEST
)
1509 if ! Getopt
:: Long
:: GetOptionsFromArray
( $args, $opts, @getopt );
1513 $opts ->{ $list_param } = $args ;
1515 } elsif ( ref ( $arg_param )) {
1516 foreach my $arg_name ( @$arg_param ) {
1517 if ( $opts ->{ 'extra-args' }) {
1518 raise
( "internal error: extra-args must be the last argument \n " , code
=> HTTP_BAD_REQUEST
);
1520 if ( $arg_name eq 'extra-args' ) {
1521 $opts ->{ 'extra-args' } = $args ;
1525 raise
( "not enough arguments \n " , code
=> HTTP_BAD_REQUEST
) if ! @$args ;
1526 $opts ->{ $arg_name } = shift @$args ;
1528 raise
( "too many arguments \n " , code
=> HTTP_BAD_REQUEST
) if @$args ;
1530 raise
( "too many arguments \n " , code
=> HTTP_BAD_REQUEST
)
1531 if scalar ( @$args ) != 0 ;
1534 if ( ref ( $arg_param )) {
1535 foreach my $arg_name ( @$arg_param ) {
1536 if ( $arg_name eq 'extra-args' ) {
1537 $opts ->{ 'extra-args' } = [];
1539 raise
( "not enough arguments \n " , code
=> HTTP_BAD_REQUEST
);
1545 foreach my $entry ( @interactive ) {
1546 my ( $opt, $func ) = @$entry ;
1547 my $pd = $schema ->{ properties
}->{ $opt };
1548 my $value = $opts ->{ $opt };
1549 if ( defined ( $value ) || ! $pd ->{ optional
}) {
1550 $opts ->{ $opt } = $func ->( $value );
1554 # decode after Getopt as we are not sure how well it handles unicode
1555 foreach my $p ( keys %$opts ) {
1556 if (! ref ( $opts ->{ $p })) {
1557 $opts ->{ $p } = decode
( 'locale' , $opts ->{ $p });
1558 } elsif ( ref ( $opts ->{ $p }) eq 'ARRAY' ) {
1560 foreach my $v (@{ $opts ->{ $p }}) {
1561 push @$tmp, decode
( 'locale' , $v );
1564 } elsif ( ref ( $opts ->{ $p }) eq 'SCALAR' ) {
1565 $opts ->{ $p } = decode
( 'locale' , $$opts ->{ $p });
1567 raise
( "decoding options failed, unknown reference \n " , code
=> HTTP_BAD_REQUEST
);
1571 foreach my $p ( keys %$opts ) {
1572 if ( my $pd = $schema ->{ properties
}->{ $p }) {
1573 if ( $pd ->{ type
} eq 'boolean' ) {
1574 if ( $opts ->{ $p } eq '' ) {
1576 } elsif ( defined ( my $bool = parse_boolean
( $opts ->{ $p }))) {
1577 $opts ->{ $p } = $bool ;
1579 raise
( "unable to parse boolean option \n " , code
=> HTTP_BAD_REQUEST
);
1581 } elsif ( $pd ->{ format
}) {
1583 if ( $pd ->{ format
} =~ m/-list/ ) {
1584 # allow --vmid 100 --vmid 101 and --vmid 100,101
1585 # allow --dow mon --dow fri and --dow mon,fri
1586 $opts ->{ $p } = join ( "," , @{ $opts ->{ $p }}) if ref ( $opts ->{ $p }) eq 'ARRAY' ;
1587 } elsif ( $pd ->{ format
} =~ m/-alist/ ) {
1588 # we encode array as \0 separated strings
1589 # Note: CGI.pm also use this encoding
1590 if ( scalar (@{ $opts ->{ $p }}) != 1 ) {
1591 $opts ->{ $p } = join ( "\0" , @{ $opts ->{ $p }});
1593 # st that split_list knows it is \0 terminated
1594 my $v = $opts ->{ $p }->[ 0 ];
1595 $opts ->{ $p } = " $v\0 " ;
1602 foreach my $p ( keys %$fixed_param ) {
1603 $opts ->{ $p } = $fixed_param ->{ $p };
1609 # A way to parse configuration data by giving a json schema
1611 my ( $schema, $filename, $raw ) = @_ ;
1613 # do fast check (avoid validate_schema($schema))
1614 die "got strange schema" if ! $schema ->{ type
} ||
1615 ! $schema ->{ properties
} || $schema ->{ type
} ne 'object' ;
1619 while ( $raw =~ /^\s*(.+?)\s*$/gm ) {
1622 next if $line =~ /^#/ ;
1624 if ( $line =~ m/^(\S+?):\s*(.*)$/ ) {
1627 if ( $schema ->{ properties
}->{ $key } &&
1628 $schema ->{ properties
}->{ $key }->{ type
} eq 'boolean' ) {
1630 $value = parse_boolean
( $value ) // $value ;
1632 $cfg ->{ $key } = $value ;
1634 warn "ignore config line: $line\n "
1639 check_prop
( $cfg, $schema, '' , $errors );
1641 foreach my $k ( keys %$errors ) {
1642 warn "parse error in ' $filename ' - ' $k ': $errors ->{ $k } \n " ;
1649 # generate simple key/value file
1651 my ( $schema, $filename, $cfg ) = @_ ;
1653 # do fast check (avoid validate_schema($schema))
1654 die "got strange schema" if ! $schema ->{ type
} ||
1655 ! $schema ->{ properties
} || $schema ->{ type
} ne 'object' ;
1657 validate
( $cfg, $schema, "validation error in ' $filename ' \n " );
1661 foreach my $k ( sort keys %$cfg ) {
1662 $data .= " $k : $cfg ->{ $k } \n " ;
1668 # helpers used to generate our manual pages
1670 my $find_schema_default_key = sub {
1674 my $keyAliasProps = {};
1676 foreach my $key ( keys %$format ) {
1677 my $phash = $format ->{ $key };
1678 if ( $phash ->{ default_key
}) {
1679 die "multiple default keys in schema ( $default_key, $key ) \n "
1680 if defined ( $default_key );
1681 die "default key ' $key ' is an alias - this is not allowed \n "
1682 if defined ( $phash ->{ alias
});
1683 die "default key ' $key ' with keyAlias attribute is not allowed \n "
1684 if $phash ->{ keyAlias
};
1685 $default_key = $key ;
1687 my $key_alias = $phash ->{ keyAlias
};
1688 die "found keyAlias without 'alias definition for ' $key ' \n "
1689 if $key_alias && ! $phash ->{ alias
};
1691 if ( $phash ->{ alias
} && $key_alias ) {
1692 die "inconsistent keyAlias ' $key_alias ' definition"
1693 if defined ( $keyAliasProps ->{ $key_alias }) &&
1694 $keyAliasProps ->{ $key_alias } ne $phash ->{ alias
};
1695 $keyAliasProps ->{ $key_alias } = $phash ->{ alias
};
1699 return wantarray ?
( $default_key, $keyAliasProps ) : $default_key ;
1702 sub generate_typetext
{
1703 my ( $format, $list_enums ) = @_ ;
1705 my ( $default_key, $keyAliasProps ) = & $find_schema_default_key ( $format );
1710 my $add_option_string = sub {
1711 my ( $text, $optional ) = @_ ;
1717 $text = "[ $text ]" if $optional ;
1722 my $format_key_value = sub {
1723 my ( $key, $phash ) = @_ ;
1725 die "internal error" if defined ( $phash ->{ alias
});
1731 if ( my $desc = $phash ->{ format_description
}) {
1732 $typetext .= "< $desc >" ;
1733 } elsif ( my $text = $phash ->{ typetext
}) {
1735 } elsif ( my $enum = $phash ->{ enum
}) {
1736 if ( $list_enums || ( scalar ( @$enum ) <= 3 )) {
1737 $typetext .= '<' . join ( '|' , @$enum ) . '>' ;
1739 $typetext .= '<enum>' ;
1741 } elsif ( $phash ->{ type
} eq 'boolean' ) {
1742 $typetext .= '<1|0>' ;
1743 } elsif ( $phash ->{ type
} eq 'integer' ) {
1744 $typetext .= '<integer>' ;
1745 } elsif ( $phash ->{ type
} eq 'number' ) {
1746 $typetext .= '<number>' ;
1748 die "internal error: neither format_description nor typetext found for option ' $key '" ;
1751 if ( defined ( $default_key ) && ( $default_key eq $key )) {
1752 & $add_option_string ( "[ $keytext =] $typetext " , $phash ->{ optional
});
1754 & $add_option_string ( " $keytext = $typetext " , $phash ->{ optional
});
1760 my $cond_add_key = sub {
1763 return if $done ->{ $key }; # avoid duplicates
1767 my $phash = $format ->{ $key };
1769 return if ! $phash ; # should not happen
1771 return if $phash ->{ alias
};
1773 & $format_key_value ( $key, $phash );
1777 & $cond_add_key ( $default_key ) if defined ( $default_key );
1779 # add required keys first
1780 foreach my $key ( sort keys %$format ) {
1781 my $phash = $format ->{ $key };
1782 & $cond_add_key ( $key ) if $phash && ! $phash ->{ optional
};
1786 foreach my $key ( sort keys %$format ) {
1787 & $cond_add_key ( $key );
1790 foreach my $keyAlias ( sort keys %$keyAliasProps ) {
1791 & $add_option_string ( "< $keyAlias >=< $keyAliasProps ->{ $keyAlias }>" , 1 );
1797 sub print_property_string
{
1798 my ( $data, $format, $skip, $path ) = @_ ;
1800 if ( ref ( $format ) ne 'HASH' ) {
1801 my $schema = get_format
( $format );
1802 die "not a valid format: $format\n " if ! $schema ;
1807 check_object
( $path, $format, $data, undef , $errors );
1808 if ( scalar ( %$errors )) {
1809 raise
"format error" , errors
=> $errors ;
1812 my ( $default_key, $keyAliasProps ) = & $find_schema_default_key ( $format );
1817 my $add_option_string = sub {
1820 $res .= ',' if $add_sep ;
1825 my $format_value = sub {
1826 my ( $key, $value, $format ) = @_ ;
1828 if ( defined ( $format ) && ( $format eq 'disk-size' )) {
1829 return format_size
( $value );
1831 die "illegal value with commas for $key\n " if $value =~ /,/ ;
1836 my $done = { map { $_ => 1 } @$skip };
1838 my $cond_add_key = sub {
1839 my ( $key, $isdefault ) = @_ ;
1841 return if $done ->{ $key }; # avoid duplicates
1845 my $value = $data ->{ $key };
1847 return if ! defined ( $value );
1849 my $phash = $format ->{ $key };
1851 # try to combine values if we have key aliases
1852 if ( my $combine = $keyAliasProps ->{ $key }) {
1853 if ( defined ( my $combine_value = $data ->{ $combine })) {
1854 my $combine_format = $format ->{ $combine }->{ format
};
1855 my $value_str = & $format_value ( $key, $value, $phash ->{ format
});
1856 my $combine_str = & $format_value ( $combine, $combine_value, $combine_format );
1857 & $add_option_string ( "${value_str}=${combine_str}" );
1858 $done ->{ $combine } = 1 ;
1863 if ( $phash && $phash ->{ alias
}) {
1864 $phash = $format ->{ $phash ->{ alias
}};
1867 die "invalid key ' $key ' \n " if ! $phash ;
1868 die "internal error" if defined ( $phash ->{ alias
});
1870 my $value_str = & $format_value ( $key, $value, $phash ->{ format
});
1872 & $add_option_string ( $value_str );
1874 & $add_option_string ( " $key =${value_str}" );
1878 # add default key first
1879 & $cond_add_key ( $default_key, 1 ) if defined ( $default_key );
1881 # add required keys first
1882 foreach my $key ( sort keys %$data ) {
1883 my $phash = $format ->{ $key };
1884 & $cond_add_key ( $key ) if $phash && ! $phash ->{ optional
};
1888 foreach my $key ( sort keys %$data ) {
1889 & $cond_add_key ( $key );
1895 sub schema_get_type_text
{
1896 my ( $phash, $style ) = @_ ;
1898 my $type = $phash ->{ type
} || 'string' ;
1900 if ( $phash ->{ typetext
}) {
1901 return $phash ->{ typetext
};
1902 } elsif ( $phash ->{ format_description
}) {
1903 return "< $phash ->{format_description}>" ;
1904 } elsif ( $phash ->{ enum
}) {
1905 return "<" . join ( ' | ' , sort @{ $phash ->{ enum
}}) . ">" ;
1906 } elsif ( $phash ->{ pattern
}) {
1907 return $phash ->{ pattern
};
1908 } elsif ( $type eq 'integer' || $type eq 'number' ) {
1909 # NOTE: always access values as number (avoid converion to string)
1910 if ( defined ( $phash ->{ minimum
}) && defined ( $phash ->{ maximum
})) {
1911 return "< $type > (" . ( $phash ->{ minimum
} + 0 ) . " - " .
1912 ( $phash ->{ maximum
} + 0 ) . ")" ;
1913 } elsif ( defined ( $phash ->{ minimum
})) {
1914 return "< $type > (" . ( $phash ->{ minimum
} + 0 ) . " - N)" ;
1915 } elsif ( defined ( $phash ->{ maximum
})) {
1916 return "< $type > (-N - " . ( $phash ->{ maximum
} + 0 ) . ")" ;
1918 } elsif ( $type eq 'string' ) {
1919 if ( my $format = $phash ->{ format
}) {
1920 $format = get_format
( $format ) if ref ( $format ) ne 'HASH' ;
1921 if ( ref ( $format ) eq 'HASH' ) {
1923 $list_enums = 1 if $style && $style eq 'config-sub' ;
1924 return generate_typetext
( $format, $list_enums );