]>
git.proxmox.com Git - pve-common.git/blob - src/PVE/JSONSchema.pm
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 );
187 my ( $id, $type, $noerr ) = @_ ;
189 if ( $id !~ m/^[a-z][a-z0-9\-\_\.]*[a-z0-9]$/i ) {
190 return undef if $noerr ;
191 die " $type ID ' $id ' contains illegal characters \n " ;
196 register_format
( 'pve-vmid' , \
& pve_verify_vmid
);
197 sub pve_verify_vmid
{
198 my ( $vmid, $noerr ) = @_ ;
200 if ( $vmid !~ m/^[1-9][0-9]{2,8}$/ ) {
201 return undef if $noerr ;
202 die "value does not look like a valid VM ID \n " ;
207 register_format
( 'pve-node' , \
& pve_verify_node_name
);
208 sub pve_verify_node_name
{
209 my ( $node, $noerr ) = @_ ;
211 if ( $node !~ m/^([a-zA-Z0-9]([a-zA-Z0-9\-]*[a-zA-Z0-9])?)$/ ) {
212 return undef if $noerr ;
213 die "value does not look like a valid node name \n " ;
218 register_format
( 'mac-addr' , \
& pve_verify_mac_addr
);
219 sub pve_verify_mac_addr
{
220 my ( $mac_addr, $noerr ) = @_ ;
222 # don't allow I/G bit to be set, most of the time it breaks things, see:
223 # https://pve.proxmox.com/pipermail/pve-devel/2019-March/035998.html
224 if ( $mac_addr !~ m/^[a-f0-9][02468ace](?::[a-f0-9]{2}){5}$/i ) {
225 return undef if $noerr ;
226 die "value does not look like a valid unicast MAC address \n " ;
231 register_standard_option
( 'mac-addr' , {
233 description
=> 'Unicast MAC address.' ,
234 verbose_description
=> 'A common MAC address with the I/G (Individual/Group) bit not set.' ,
235 format_description
=> "XX:XX:XX:XX:XX:XX" ,
237 format
=> 'mac-addr' ,
240 register_format
( 'ipv4' , \
& pve_verify_ipv4
);
241 sub pve_verify_ipv4
{
242 my ( $ipv4, $noerr ) = @_ ;
244 if ( $ipv4 !~ m/^(?:$IPV4RE)$/ ) {
245 return undef if $noerr ;
246 die "value does not look like a valid IPv4 address \n " ;
251 register_format
( 'ipv6' , \
& pve_verify_ipv6
);
252 sub pve_verify_ipv6
{
253 my ( $ipv6, $noerr ) = @_ ;
255 if ( $ipv6 !~ m/^(?:$IPV6RE)$/ ) {
256 return undef if $noerr ;
257 die "value does not look like a valid IPv6 address \n " ;
262 register_format
( 'ip' , \
& pve_verify_ip
);
264 my ( $ip, $noerr ) = @_ ;
266 if ( $ip !~ m/^(?:(?:$IPV4RE)|(?:$IPV6RE))$/ ) {
267 return undef if $noerr ;
268 die "value does not look like a valid IP address \n " ;
273 PVE
:: JSONSchema
:: register_format
( 'ldap-simple-attr' , \
& verify_ldap_simple_attr
);
274 sub verify_ldap_simple_attr
{
275 my ( $attr, $noerr ) = @_ ;
277 if ( $attr =~ m/^[a-zA-Z0-9]+$/ ) {
281 die "value ' $attr ' does not look like a simple ldap attribute name \n " if ! $noerr ;
286 my $ipv4_mask_hash = {
304 '255.255.128.0' => 17 ,
305 '255.255.192.0' => 18 ,
306 '255.255.224.0' => 19 ,
307 '255.255.240.0' => 20 ,
308 '255.255.248.0' => 21 ,
309 '255.255.252.0' => 22 ,
310 '255.255.254.0' => 23 ,
311 '255.255.255.0' => 24 ,
312 '255.255.255.128' => 25 ,
313 '255.255.255.192' => 26 ,
314 '255.255.255.224' => 27 ,
315 '255.255.255.240' => 28 ,
316 '255.255.255.248' => 29 ,
317 '255.255.255.252' => 30 ,
318 '255.255.255.254' => 31 ,
319 '255.255.255.255' => 32 ,
322 sub get_netmask_bits
{
324 return $ipv4_mask_hash ->{ $mask };
327 register_format
( 'ipv4mask' , \
& pve_verify_ipv4mask
);
328 sub pve_verify_ipv4mask
{
329 my ( $mask, $noerr ) = @_ ;
331 if (! defined ( $ipv4_mask_hash ->{ $mask })) {
332 return undef if $noerr ;
333 die "value does not look like a valid IP netmask \n " ;
338 register_format
( 'CIDRv6' , \
& pve_verify_cidrv6
);
339 sub pve_verify_cidrv6
{
340 my ( $cidr, $noerr ) = @_ ;
342 if ( $cidr =~ m!^(?:$IPV6RE)(?:/(\d+))$! && ( $1 > 7 ) && ( $1 <= 128 )) {
346 return undef if $noerr ;
347 die "value does not look like a valid IPv6 CIDR network \n " ;
350 register_format
( 'CIDRv4' , \
& pve_verify_cidrv4
);
351 sub pve_verify_cidrv4
{
352 my ( $cidr, $noerr ) = @_ ;
354 if ( $cidr =~ m!^(?:$IPV4RE)(?:/(\d+))$! && ( $1 > 7 ) && ( $1 <= 32 )) {
358 return undef if $noerr ;
359 die "value does not look like a valid IPv4 CIDR network \n " ;
362 register_format
( 'CIDR' , \
& pve_verify_cidr
);
363 sub pve_verify_cidr
{
364 my ( $cidr, $noerr ) = @_ ;
366 if (!( pve_verify_cidrv4
( $cidr, 1 ) ||
367 pve_verify_cidrv6
( $cidr, 1 )))
369 return undef if $noerr ;
370 die "value does not look like a valid CIDR network \n " ;
376 register_format
( 'pve-ipv4-config' , \
& pve_verify_ipv4_config
);
377 sub pve_verify_ipv4_config
{
378 my ( $config, $noerr ) = @_ ;
380 return $config if $config =~ /^(?:dhcp|manual)$/ ||
381 pve_verify_cidrv4
( $config, 1 );
382 return undef if $noerr ;
383 die "value does not look like a valid ipv4 network configuration \n " ;
386 register_format
( 'pve-ipv6-config' , \
& pve_verify_ipv6_config
);
387 sub pve_verify_ipv6_config
{
388 my ( $config, $noerr ) = @_ ;
390 return $config if $config =~ /^(?:auto|dhcp|manual)$/ ||
391 pve_verify_cidrv6
( $config, 1 );
392 return undef if $noerr ;
393 die "value does not look like a valid ipv6 network configuration \n " ;
396 register_format
( 'email' , \
& pve_verify_email
);
397 sub pve_verify_email
{
398 my ( $email, $noerr ) = @_ ;
400 if ( $email !~ /^[\w\+\-\~]+(\.[\w\+\-\~]+)*@[a-zA-Z0-9\-]+(\.[a-zA-Z0-9\-]+)*$/ ) {
401 return undef if $noerr ;
402 die "value does not look like a valid email address \n " ;
407 register_format
( 'dns-name' , \
& pve_verify_dns_name
);
408 sub pve_verify_dns_name
{
409 my ( $name, $noerr ) = @_ ;
411 my $namere = "([a-zA-Z0-9]([a-zA-Z0-9\-]*[a-zA-Z0-9])?)" ;
413 if ( $name !~ /^(${namere}\.)*${namere}$/ ) {
414 return undef if $noerr ;
415 die "value does not look like a valid DNS name \n " ;
420 # network interface name
421 register_format
( 'pve-iface' , \
& pve_verify_iface
);
422 sub pve_verify_iface
{
423 my ( $id, $noerr ) = @_ ;
425 if ( $id !~ m/^[a-z][a-z0-9_]{1,20}([:\.]\d+)?$/i ) {
426 return undef if $noerr ;
427 die "invalid network interface name ' $id ' \n " ;
432 # general addresses by name or IP
433 register_format
( 'address' , \
& pve_verify_address
);
434 sub pve_verify_address
{
435 my ( $addr, $noerr ) = @_ ;
437 if (!( pve_verify_ip
( $addr, 1 ) ||
438 pve_verify_dns_name
( $addr, 1 )))
440 return undef if $noerr ;
441 die "value does not look like a valid address: $addr\n " ;
446 register_format
( 'disk-size' , \
& pve_verify_disk_size
);
447 sub pve_verify_disk_size
{
448 my ( $size, $noerr ) = @_ ;
449 if (! defined ( parse_size
( $size ))) {
450 return undef if $noerr ;
451 die "value does not look like a valid disk size: $size\n " ;
456 register_standard_option
( 'spice-proxy' , {
457 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)." ,
458 type
=> 'string' , format
=> 'address' ,
461 register_standard_option
( 'remote-viewer-config' , {
462 description
=> "Returned values can be directly passed to the 'remote-viewer' application." ,
463 additionalProperties
=> 1 ,
465 type
=> { type
=> 'string' },
466 password
=> { type
=> 'string' },
467 proxy
=> { type
=> 'string' },
468 host
=> { type
=> 'string' },
469 'tls-port' => { type
=> 'integer' },
473 register_format
( 'pve-startup-order' , \
& pve_verify_startup_order
);
474 sub pve_verify_startup_order
{
475 my ( $value, $noerr ) = @_ ;
477 return $value if pve_parse_startup_order
( $value );
479 return undef if $noerr ;
481 die "unable to parse startup options \n " ;
486 type
=> 'number' , minimum
=> '0' ,
487 format_description
=> 'LIMIT' ,
490 my $bwlimit_format = {
493 description
=> 'default bandwidth limit in KiB/s' ,
497 description
=> 'bandwidth limit in KiB/s for restoring guests from backups' ,
501 description
=> 'bandwidth limit in KiB/s for migrating guests (including moving local disks)' ,
505 description
=> 'bandwidth limit in KiB/s for cloning disks' ,
509 description
=> 'bandwidth limit in KiB/s for moving disks' ,
512 register_format
( 'bwlimit' , $bwlimit_format );
513 register_standard_option
( 'bwlimit' , {
514 description
=> "Set bandwidth/io limits various operations." ,
517 format
=> $bwlimit_format,
520 # used for pve-tag-list in e.g., guest configs
521 register_format
( 'pve-tag' , \
& pve_verify_tag
);
523 my ( $value, $noerr ) = @_ ;
525 return $value if $value =~ m/^[a-z0-9_][a-z0-9_\-\+\.]*$/i ;
527 return undef if $noerr ;
529 die "invalid characters in tag \n " ;
532 sub pve_parse_startup_order
{
535 return undef if ! $value ;
539 foreach my $p ( split ( /,/ , $value )) {
540 next if $p =~ m/^\s*$/ ;
542 if ( $p =~ m/^(order=)?(\d+)$/ ) {
544 } elsif ( $p =~ m/^up=(\d+)$/ ) {
546 } elsif ( $p =~ m/^down=(\d+)$/ ) {
556 PVE
:: JSONSchema
:: register_standard_option
( 'pve-startup-order' , {
557 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." ,
559 type
=> 'string' , format
=> 'pve-startup-order' ,
560 typetext
=> '[[order=]\d+] [,up=\d+] [,down=\d+] ' ,
563 register_format
( 'pve-tfa-secret' , \
& pve_verify_tfa_secret
);
564 sub pve_verify_tfa_secret
{
565 my ( $key, $noerr ) = @_ ;
567 # The old format used 16 base32 chars or 40 hex digits. Since they have a common subset it's
568 # hard to distinguish them without the our previous length constraints, so add a 'v2' of the
569 # format to support arbitrary lengths properly:
570 if ( $key =~ /^v2-0x[0-9a-fA-F]{16,128}$/ || # hex
571 $key =~ /^v2-[A-Z2-7=]{16,128}$/ || # base32
572 $key =~ /^(?:[A-Z2-7=]{16}|[A-Fa-f0-9]{40})$/ ) # and the old pattern copy&pasted
577 return undef if $noerr ;
579 die "unable to decode TFA secret \n " ;
583 my ( $format, $value, $path ) = @_ ;
585 return parse_property_string
( $format, $value, $path ) if ref ( $format ) eq 'HASH' ;
586 return if $format eq 'regex' ;
588 if ( $format =~ m/^(.*)-a?list$/ ) {
590 my $code = $format_list ->{ $1 };
592 die "undefined format ' $format ' \n " if ! $code ;
594 # Note: we allow empty lists
595 foreach my $v ( split_list
( $value )) {
599 } elsif ( $format =~ m/^(.*)-opt$/ ) {
601 my $code = $format_list ->{ $1 };
603 die "undefined format ' $format ' \n " if ! $code ;
605 return if ! $value ; # allow empty string
611 my $code = $format_list ->{ $format };
613 die "undefined format ' $format ' \n " if ! $code ;
615 return parse_property_string
( $code, $value, $path ) if ref ( $code ) eq 'HASH' ;
623 return undef if $value !~ m/^(\d+(\.\d+)?)([KMGT])?$/ ;
624 my ( $size, $unit ) = ( $1, $3 );
627 $size = $size * 1024 ;
628 } elsif ( $unit eq 'M' ) {
629 $size = $size * 1024 * 1024 ;
630 } elsif ( $unit eq 'G' ) {
631 $size = $size * 1024 * 1024 * 1024 ;
632 } elsif ( $unit eq 'T' ) {
633 $size = $size * 1024 * 1024 * 1024 * 1024 ;
644 my $kb = int ( $size/1024 );
645 return $size if $kb*1024 != $size ;
647 my $mb = int ( $kb/1024 );
648 return "${kb}K" if $mb*1024 != $kb ;
650 my $gb = int ( $mb/1024 );
651 return "${mb}M" if $gb*1024 != $mb ;
653 my $tb = int ( $gb/1024 );
654 return "${gb}G" if $tb*1024 != $gb ;
661 return 1 if $bool =~ m/^(1|on|yes|true)$/i ;
662 return 0 if $bool =~ m/^(0|off|no|false)$/i ;
666 sub parse_property_string
{
667 my ( $format, $data, $path, $additional_properties ) = @_ ;
669 # In property strings we default to not allowing additional properties
670 $additional_properties = 0 if ! defined ( $additional_properties );
672 # Support named formats here, too:
674 if ( my $desc = $format_list ->{ $format }) {
677 die "unknown format: $format\n " ;
679 } elsif ( ref ( $format ) ne 'HASH' ) {
680 die "unexpected format value of type " . ref ( $format ). " \n " ;
686 foreach my $part ( split ( /,/ , $data )) {
687 next if $part =~ /^\s*$/ ;
689 if ( $part =~ /^([^=]+)=(.+)$/ ) {
690 my ( $k, $v ) = ( $1, $2 );
691 die "duplicate key in comma-separated list property: $k\n " if defined ( $res ->{ $k });
692 my $schema = $format ->{ $k };
693 if ( my $alias = $schema ->{ alias
}) {
694 if ( my $key_alias = $schema ->{ keyAlias
}) {
695 die "key alias ' $key_alias ' is already defined \n " if defined ( $res ->{ $key_alias });
696 $res ->{ $key_alias } = $k ;
699 $schema = $format ->{ $k };
702 die "invalid key in comma-separated list property: $k\n " if ! $schema ;
703 if ( $schema ->{ type
} && $schema ->{ type
} eq 'boolean' ) {
704 $v = parse_boolean
( $v ) // $v ;
707 } elsif ( $part !~ /=/ ) {
708 die "duplicate key in comma-separated list property: $default_key\n " if $default_key ;
709 foreach my $key ( keys %$format ) {
710 if ( $format ->{ $key }->{ default_key
}) {
712 if (! $res ->{ $default_key }) {
713 $res ->{ $default_key } = $part ;
716 die "duplicate key in comma-separated list property: $default_key\n " ;
719 die "value without key, but schema does not define a default key \n " if ! $default_key ;
721 die "missing key in comma-separated list property \n " ;
726 check_object
( $path, $format, $res, $additional_properties, $errors );
727 if ( scalar ( %$errors )) {
728 raise
"format error \n " , errors
=> $errors ;
735 my ( $errors, $path, $msg ) = @_ ;
737 $path = '_root' if ! $path ;
739 if ( $errors ->{ $path }) {
740 $errors ->{ $path } = join ( ' \n ' , $errors ->{ $path }, $msg );
742 $errors ->{ $path } = $msg ;
749 # see 'man perlretut'
750 return $value =~ /^[+-]?(\d+\.\d+|\d+\.|\.\d+|\d+)([eE][+-]?\d+)?$/ ;
756 return $value =~ m/^[+-]?\d+$/ ;
760 my ( $path, $type, $value, $errors ) = @_ ;
764 if (! defined ( $value )) {
765 return 1 if $type eq 'null' ;
769 if ( my $tt = ref ( $type )) {
770 if ( $tt eq 'ARRAY' ) {
771 foreach my $t ( @$type ) {
773 check_type
( $path, $t, $value, $tmperr );
774 return 1 if ! scalar ( %$tmperr );
776 my $ttext = join ( '|' , @$type );
777 add_error
( $errors, $path, "type check (' $ttext ') failed" );
779 } elsif ( $tt eq 'HASH' ) {
781 check_prop
( $value, $type, $path, $tmperr );
782 return 1 if ! scalar ( %$tmperr );
783 add_error
( $errors, $path, "type check failed" );
786 die "internal error - got reference type ' $tt '" ;
791 return 1 if $type eq 'any' ;
793 if ( $type eq 'null' ) {
794 if ( defined ( $value )) {
795 add_error
( $errors, $path, "type check (' $type ') failed - value is not null" );
801 my $vt = ref ( $value );
803 if ( $type eq 'array' ) {
804 if (! $vt || $vt ne 'ARRAY' ) {
805 add_error
( $errors, $path, "type check (' $type ') failed" );
809 } elsif ( $type eq 'object' ) {
810 if (! $vt || $vt ne 'HASH' ) {
811 add_error
( $errors, $path, "type check (' $type ') failed" );
815 } elsif ( $type eq 'coderef' ) {
816 if (! $vt || $vt ne 'CODE' ) {
817 add_error
( $errors, $path, "type check (' $type ') failed" );
821 } elsif ( $type eq 'string' && $vt eq 'Regexp' ) {
822 # qr// regexes can be used as strings and make sense for format=regex
826 add_error
( $errors, $path, "type check (' $type ') failed - got $vt " );
829 if ( $type eq 'string' ) {
830 return 1 ; # nothing to check ?
831 } elsif ( $type eq 'boolean' ) {
832 #if ($value =~ m/^(1|true|yes|on)$/i) {
835 #} elsif ($value =~ m/^(0|false|no|off)$/i) {
836 } elsif ( $value eq '0' ) {
837 return 1 ; # return success (not value)
839 add_error
( $errors, $path, "type check (' $type ') failed - got ' $value '" );
842 } elsif ( $type eq 'integer' ) {
843 if (! is_integer
( $value )) {
844 add_error
( $errors, $path, "type check (' $type ') failed - got ' $value '" );
848 } elsif ( $type eq 'number' ) {
849 if (! is_number
( $value )) {
850 add_error
( $errors, $path, "type check (' $type ') failed - got ' $value '" );
855 return 1 ; # no need to verify unknown types
865 my ( $path, $schema, $value, $additional_properties, $errors ) = @_ ;
867 # print "Check Object " . Dumper($value) . "\nSchema: " . Dumper($schema);
869 my $st = ref ( $schema );
870 if (! $st || $st ne 'HASH' ) {
871 add_error
( $errors, $path, "Invalid schema definition." );
875 my $vt = ref ( $value );
876 if (! $vt || $vt ne 'HASH' ) {
877 add_error
( $errors, $path, "an object is required" );
881 foreach my $k ( keys %$schema ) {
882 check_prop
( $value ->{ $k }, $schema ->{ $k }, $path ?
" $path . $k " : $k, $errors );
885 foreach my $k ( keys %$value ) {
887 my $newpath = $path ?
" $path . $k " : $k ;
889 if ( my $subschema = $schema ->{ $k }) {
890 if ( my $requires = $subschema ->{ requires
}) {
891 if ( ref ( $requires )) {
892 #print "TEST: " . Dumper($value) . "\n", Dumper($requires) ;
893 check_prop
( $value, $requires, $path, $errors );
894 } elsif (! defined ( $value ->{ $requires })) {
895 add_error
( $errors, $path ?
" $path . $requires " : $requires,
896 "missing property - ' $newpath ' requires this property" );
900 next ; # value is already checked above
903 if ( defined ( $additional_properties ) && ! $additional_properties ) {
904 add_error
( $errors, $newpath, "property is not defined in schema " .
905 "and the schema does not allow additional properties" );
908 check_prop
( $value ->{ $k }, $additional_properties, $newpath, $errors )
909 if ref ( $additional_properties );
913 sub check_object_warn
{
914 my ( $path, $schema, $value, $additional_properties ) = @_ ;
916 check_object
( $path, $schema, $value, $additional_properties, $errors );
917 if ( scalar ( %$errors )) {
918 foreach my $k ( keys %$errors ) {
919 warn "parse error: $k : $errors ->{ $k } \n " ;
927 my ( $value, $schema, $path, $errors ) = @_ ;
929 die "internal error - no schema" if ! $schema ;
930 die "internal error" if ! $errors ;
932 #print "check_prop $path\n" if $value;
934 my $st = ref ( $schema );
935 if (! $st || $st ne 'HASH' ) {
936 add_error
( $errors, $path, "Invalid schema definition." );
940 # if it extends another schema, it must pass that schema as well
941 if ( $schema ->{ extends
}) {
942 check_prop
( $value, $schema ->{ extends
}, $path, $errors );
945 if (! defined ( $value )) {
946 return if $schema ->{ type
} && $schema ->{ type
} eq 'null' ;
947 if (! $schema ->{ optional
} && ! $schema ->{ alias
} && ! $schema ->{ group
}) {
948 add_error
( $errors, $path, "property is missing and it is not optional" );
953 return if ! check_type
( $path, $schema ->{ type
}, $value, $errors );
955 if ( $schema ->{ disallow
}) {
957 if ( check_type
( $path, $schema ->{ disallow
}, $value, $tmperr )) {
958 add_error
( $errors, $path, "disallowed value was matched" );
963 if ( my $vt = ref ( $value )) {
965 if ( $vt eq 'ARRAY' ) {
966 if ( $schema ->{ items
}) {
967 my $it = ref ( $schema ->{ items
});
968 if ( $it && $it eq 'ARRAY' ) {
969 #die "implement me $path: $vt " . Dumper($schema) ."\n". Dumper($value);
970 die "not implemented" ;
973 foreach my $el ( @$value ) {
974 check_prop
( $el, $schema ->{ items
}, "${path}[ $ind ]" , $errors );
980 } elsif ( $schema ->{ properties
} || $schema ->{ additionalProperties
}) {
981 check_object
( $path, defined ( $schema ->{ properties
}) ?
$schema ->{ properties
} : {},
982 $value, $schema ->{ additionalProperties
}, $errors );
988 if ( my $format = $schema ->{ format
}) {
989 eval { check_format
( $format, $value, $path ); };
991 add_error
( $errors, $path, "invalid format - $@ " );
996 if ( my $pattern = $schema ->{ pattern
}) {
997 if ( $value !~ m/^$pattern$/ ) {
998 add_error
( $errors, $path, "value does not match the regex pattern" );
1003 if ( defined ( my $max = $schema ->{ maxLength
})) {
1004 if ( length ( $value ) > $max ) {
1005 add_error
( $errors, $path, "value may only be $max characters long" );
1010 if ( defined ( my $min = $schema ->{ minLength
})) {
1011 if ( length ( $value ) < $min ) {
1012 add_error
( $errors, $path, "value must be at least $min characters long" );
1017 if ( is_number
( $value )) {
1018 if ( defined ( my $max = $schema ->{ maximum
})) {
1019 if ( $value > $max ) {
1020 add_error
( $errors, $path, "value must have a maximum value of $max " );
1025 if ( defined ( my $min = $schema ->{ minimum
})) {
1026 if ( $value < $min ) {
1027 add_error
( $errors, $path, "value must have a minimum value of $min " );
1033 if ( my $ea = $schema ->{ enum
}) {
1036 foreach my $ev ( @$ea ) {
1037 if ( $ev eq $value ) {
1043 add_error
( $errors, $path, "value ' $value ' does not have a value in the enumeration '" .
1044 join ( ", " , @$ea ) . "'" );
1051 my ( $instance, $schema, $errmsg ) = @_ ;
1054 $errmsg = "Parameter verification failed. \n " if ! $errmsg ;
1056 # todo: cycle detection is only needed for debugging, I guess
1057 # we can disable that in the final release
1058 # todo: is there a better/faster way to detect cycles?
1060 find_cycle
( $instance, sub { $cycles = 1 });
1062 add_error
( $errors, undef , "data structure contains recursive cycles" );
1064 check_prop
( $instance, $schema, '' , $errors );
1067 if ( scalar ( %$errors )) {
1068 raise
$errmsg, code
=> HTTP_BAD_REQUEST
, errors
=> $errors ;
1074 my $schema_valid_types = [ "string" , "object" , "coderef" , "array" , "boolean" , "number" , "integer" , "null" , "any" ];
1075 my $default_schema_noref = {
1076 description
=> "This is the JSON Schema for JSON Schemas." ,
1077 type
=> [ "object" ],
1078 additionalProperties
=> 0 ,
1081 type
=> [ "string" , "array" ],
1082 description
=> "This is a type definition value. This can be a simple type, or a union type" ,
1087 enum
=> $schema_valid_types,
1089 enum
=> $schema_valid_types,
1093 description
=> "This indicates that the instance property in the instance object is not required." ,
1099 description
=> "This is a definition for the properties of an object value" ,
1105 description
=> "When the value is an array, this indicates the schema to use to validate each item in an array" ,
1109 additionalProperties
=> {
1110 type
=> [ "boolean" , "object" ],
1111 description
=> "This provides a default property definition for all properties that are not explicitly defined in an object type definition." ,
1118 description
=> "This indicates the minimum value for the instance property when the type of the instance value is a number." ,
1123 description
=> "This indicates the maximum value for the instance property when the type of the instance value is a number." ,
1127 description
=> "When the instance value is a string, this indicates minimum length of the string" ,
1134 description
=> "When the instance value is a string, this indicates maximum length of the string." ,
1140 description
=> "A text representation of the type (used to generate documentation)." ,
1145 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." ,
1152 description
=> "This provides an enumeration of possible values that are valid for the instance property." ,
1157 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)." ,
1159 verbose_description
=> {
1162 description
=> "This provides a more verbose description." ,
1164 format_description
=> {
1167 description
=> "This provides a shorter (usually just one word) description for a property used to generate descriptions for comma separated list property strings." ,
1172 description
=> "This provides the title of the property" ,
1177 description
=> "This is used to provide rendering hints to format cli command output." ,
1180 type
=> [ "string" , "object" ],
1182 description
=> "indicates a required property or a schema that must be validated if this property is present" ,
1185 type
=> [ "string" , "object" ],
1187 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" ,
1192 description
=> "Whether this is the default key in a comma separated list property string." ,
1197 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." ,
1202 description
=> "Allows to store the current 'key' as value of another property. Only valid if used together with 'alias'." ,
1203 requires
=> 'alias' ,
1208 description
=> "This indicates the default for the instance property."
1212 description
=> "Bash completion function. This function should return a list of possible values." ,
1218 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." ,
1223 description
=> "This indicates the schema extends the given schema. All instances of this schema must be valid to by the extended schema also." ,
1226 # this is from hyper schema
1229 description
=> "This defines the link relations of the instance objects" ,
1236 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" ,
1240 description
=> "This is the name of the link relation" ,
1246 description
=> "For submission links, this defines the method that should be used to access the target resource" ,
1255 description
=> "For CLI context, this defines the maximal width to print before truncating" ,
1261 my $default_schema = Storable
:: dclone
( $default_schema_noref );
1263 $default_schema ->{ properties
}->{ properties
}->{ additionalProperties
} = $default_schema ;
1264 $default_schema ->{ properties
}->{ additionalProperties
}->{ properties
} = $default_schema ->{ properties
};
1266 $default_schema ->{ properties
}->{ items
}->{ properties
} = $default_schema ->{ properties
};
1267 $default_schema ->{ properties
}->{ items
}->{ additionalProperties
} = 0 ;
1269 $default_schema ->{ properties
}->{ disallow
}->{ properties
} = $default_schema ->{ properties
};
1270 $default_schema ->{ properties
}->{ disallow
}->{ additionalProperties
} = 0 ;
1272 $default_schema ->{ properties
}->{ requires
}->{ properties
} = $default_schema ->{ properties
};
1273 $default_schema ->{ properties
}->{ requires
}->{ additionalProperties
} = 0 ;
1275 $default_schema ->{ properties
}->{ extends
}->{ properties
} = $default_schema ->{ properties
};
1276 $default_schema ->{ properties
}->{ extends
}->{ additionalProperties
} = 0 ;
1278 my $method_schema = {
1280 additionalProperties
=> 0 ,
1283 description
=> "This a description of the method" ,
1288 description
=> "This indicates the name of the function to call." ,
1291 additionalProperties
=> 1 ,
1306 description
=> "The HTTP method name." ,
1307 enum
=> [ 'GET' , 'POST' , 'PUT' , 'DELETE' ],
1312 description
=> "Method needs special privileges - only pvedaemon can execute it" ,
1317 description
=> "Method is available for clients authenticated using an API token." ,
1323 description
=> "Method downloads the file content (filename is the return value of the method)." ,
1328 description
=> "A parameter name. If specified, all calls to this method are proxied to the host contained in that parameter." ,
1331 proxyto_callback
=> {
1333 description
=> "A function which is called to resolve the proxyto attribute. The default implementation returns the value of the 'proxyto' parameter." ,
1338 description
=> "Required access permissions. By default only 'root' is allowed to access this method." ,
1340 additionalProperties
=> 0 ,
1343 description
=> "Describe access permissions." ,
1347 description
=> "A simply way to allow access for 'all' authenticated users. Value 'world' is used to allow access without credentials." ,
1349 enum
=> [ 'all' , 'world' ],
1353 description
=> "Array of permission checks (prefix notation)." ,
1360 description
=> "Used internally" ,
1364 description
=> "Used internally" ,
1369 description
=> "path for URL matching (uri template)" ,
1371 fragmentDelimiter
=> {
1373 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." ,
1378 description
=> "JSON Schema for parameters." ,
1383 description
=> "JSON Schema for return value." ,
1388 description
=> "method implementation (code reference)" ,
1393 description
=> "Delegate call to this class (perl class string)." ,
1396 additionalProperties
=> 0 ,
1402 fragmentDelimiter
=> { optional
=> 1 }
1410 sub validate_schema
{
1413 my $errmsg = "internal error - unable to verify schema \n " ;
1414 validate
( $schema, $default_schema, $errmsg );
1417 sub validate_method_info
{
1420 my $errmsg = "internal error - unable to verify method info \n " ;
1421 validate
( $info, $method_schema, $errmsg );
1423 validate_schema
( $info ->{ parameters
}) if $info ->{ parameters
};
1424 validate_schema
( $info ->{ returns
}) if $info ->{ returns
};
1427 # run a self test on load
1428 # make sure we can verify the default schema
1429 validate_schema
( $default_schema_noref );
1430 validate_schema
( $method_schema );
1432 # and now some utility methods (used by pve api)
1433 sub method_get_child_link
{
1436 return undef if ! $info ;
1438 my $schema = $info ->{ returns
};
1439 return undef if ! $schema || ! $schema ->{ type
} || $schema ->{ type
} ne 'array' ;
1441 my $links = $schema ->{ links
};
1442 return undef if ! $links ;
1445 foreach my $lnk ( @$links ) {
1446 if ( $lnk ->{ href
} && $lnk ->{ rel
} && ( $lnk ->{ rel
} eq 'child' )) {
1455 # a way to parse command line parameters, using a
1456 # schema to configure Getopt::Long
1458 my ( $schema, $args, $arg_param, $fixed_param, $param_mapping_hash ) = @_ ;
1460 if (! $schema || ! $schema ->{ properties
}) {
1461 raise
( "too many arguments \n " , code
=> HTTP_BAD_REQUEST
)
1462 if scalar ( @$args ) != 0 ;
1467 if ( $arg_param && ! ref ( $arg_param )) {
1468 my $pd = $schema ->{ properties
}->{ $arg_param };
1469 die "expected list format $pd ->{format}"
1470 if !( $pd && $pd ->{ format
} && $pd ->{ format
} =~ m/-list/ );
1471 $list_param = $arg_param ;
1474 my @interactive = ();
1476 foreach my $prop ( keys %{ $schema ->{ properties
}}) {
1477 my $pd = $schema ->{ properties
}->{ $prop };
1478 next if $list_param && $prop eq $list_param ;
1479 next if defined ( $fixed_param ->{ $prop });
1481 my $mapping = $param_mapping_hash ->{ $prop };
1482 if ( $mapping && $mapping ->{ interactive
}) {
1483 # interactive parameters such as passwords: make the argument
1484 # optional and call the mapping function afterwards.
1485 push @getopt, " $prop :s" ;
1486 push @interactive, [ $prop, $mapping ->{ func
}];
1487 } elsif ( $pd ->{ type
} eq 'boolean' ) {
1488 push @getopt, " $prop :s" ;
1490 if ( $pd ->{ format
} && $pd ->{ format
} =~ m/-a?list/ ) {
1491 push @getopt, " $prop =s@" ;
1493 push @getopt, " $prop =s" ;
1498 Getopt
:: Long
:: Configure
( 'prefix_pattern=(--|-)' );
1501 raise
( "unable to parse option \n " , code
=> HTTP_BAD_REQUEST
)
1502 if ! Getopt
:: Long
:: GetOptionsFromArray
( $args, $opts, @getopt );
1506 $opts ->{ $list_param } = $args ;
1508 } elsif ( ref ( $arg_param )) {
1509 foreach my $arg_name ( @$arg_param ) {
1510 if ( $opts ->{ 'extra-args' }) {
1511 raise
( "internal error: extra-args must be the last argument \n " , code
=> HTTP_BAD_REQUEST
);
1513 if ( $arg_name eq 'extra-args' ) {
1514 $opts ->{ 'extra-args' } = $args ;
1518 raise
( "not enough arguments \n " , code
=> HTTP_BAD_REQUEST
) if ! @$args ;
1519 $opts ->{ $arg_name } = shift @$args ;
1521 raise
( "too many arguments \n " , code
=> HTTP_BAD_REQUEST
) if @$args ;
1523 raise
( "too many arguments \n " , code
=> HTTP_BAD_REQUEST
)
1524 if scalar ( @$args ) != 0 ;
1527 if ( ref ( $arg_param )) {
1528 foreach my $arg_name ( @$arg_param ) {
1529 if ( $arg_name eq 'extra-args' ) {
1530 $opts ->{ 'extra-args' } = [];
1532 raise
( "not enough arguments \n " , code
=> HTTP_BAD_REQUEST
);
1538 foreach my $entry ( @interactive ) {
1539 my ( $opt, $func ) = @$entry ;
1540 my $pd = $schema ->{ properties
}->{ $opt };
1541 my $value = $opts ->{ $opt };
1542 if ( defined ( $value ) || ! $pd ->{ optional
}) {
1543 $opts ->{ $opt } = $func ->( $value );
1547 # decode after Getopt as we are not sure how well it handles unicode
1548 foreach my $p ( keys %$opts ) {
1549 if (! ref ( $opts ->{ $p })) {
1550 $opts ->{ $p } = decode
( 'locale' , $opts ->{ $p });
1551 } elsif ( ref ( $opts ->{ $p }) eq 'ARRAY' ) {
1553 foreach my $v (@{ $opts ->{ $p }}) {
1554 push @$tmp, decode
( 'locale' , $v );
1557 } elsif ( ref ( $opts ->{ $p }) eq 'SCALAR' ) {
1558 $opts ->{ $p } = decode
( 'locale' , $$opts ->{ $p });
1560 raise
( "decoding options failed, unknown reference \n " , code
=> HTTP_BAD_REQUEST
);
1564 foreach my $p ( keys %$opts ) {
1565 if ( my $pd = $schema ->{ properties
}->{ $p }) {
1566 if ( $pd ->{ type
} eq 'boolean' ) {
1567 if ( $opts ->{ $p } eq '' ) {
1569 } elsif ( defined ( my $bool = parse_boolean
( $opts ->{ $p }))) {
1570 $opts ->{ $p } = $bool ;
1572 raise
( "unable to parse boolean option \n " , code
=> HTTP_BAD_REQUEST
);
1574 } elsif ( $pd ->{ format
}) {
1576 if ( $pd ->{ format
} =~ m/-list/ ) {
1577 # allow --vmid 100 --vmid 101 and --vmid 100,101
1578 # allow --dow mon --dow fri and --dow mon,fri
1579 $opts ->{ $p } = join ( "," , @{ $opts ->{ $p }}) if ref ( $opts ->{ $p }) eq 'ARRAY' ;
1580 } elsif ( $pd ->{ format
} =~ m/-alist/ ) {
1581 # we encode array as \0 separated strings
1582 # Note: CGI.pm also use this encoding
1583 if ( scalar (@{ $opts ->{ $p }}) != 1 ) {
1584 $opts ->{ $p } = join ( "\0" , @{ $opts ->{ $p }});
1586 # st that split_list knows it is \0 terminated
1587 my $v = $opts ->{ $p }->[ 0 ];
1588 $opts ->{ $p } = " $v\0 " ;
1595 foreach my $p ( keys %$fixed_param ) {
1596 $opts ->{ $p } = $fixed_param ->{ $p };
1602 # A way to parse configuration data by giving a json schema
1604 my ( $schema, $filename, $raw ) = @_ ;
1606 # do fast check (avoid validate_schema($schema))
1607 die "got strange schema" if ! $schema ->{ type
} ||
1608 ! $schema ->{ properties
} || $schema ->{ type
} ne 'object' ;
1612 while ( $raw =~ /^\s*(.+?)\s*$/gm ) {
1615 next if $line =~ /^#/ ;
1617 if ( $line =~ m/^(\S+?):\s*(.*)$/ ) {
1620 if ( $schema ->{ properties
}->{ $key } &&
1621 $schema ->{ properties
}->{ $key }->{ type
} eq 'boolean' ) {
1623 $value = parse_boolean
( $value ) // $value ;
1625 $cfg ->{ $key } = $value ;
1627 warn "ignore config line: $line\n "
1632 check_prop
( $cfg, $schema, '' , $errors );
1634 foreach my $k ( keys %$errors ) {
1635 warn "parse error in ' $filename ' - ' $k ': $errors ->{ $k } \n " ;
1642 # generate simple key/value file
1644 my ( $schema, $filename, $cfg ) = @_ ;
1646 # do fast check (avoid validate_schema($schema))
1647 die "got strange schema" if ! $schema ->{ type
} ||
1648 ! $schema ->{ properties
} || $schema ->{ type
} ne 'object' ;
1650 validate
( $cfg, $schema, "validation error in ' $filename ' \n " );
1654 foreach my $k ( sort keys %$cfg ) {
1655 $data .= " $k : $cfg ->{ $k } \n " ;
1661 # helpers used to generate our manual pages
1663 my $find_schema_default_key = sub {
1667 my $keyAliasProps = {};
1669 foreach my $key ( keys %$format ) {
1670 my $phash = $format ->{ $key };
1671 if ( $phash ->{ default_key
}) {
1672 die "multiple default keys in schema ( $default_key, $key ) \n "
1673 if defined ( $default_key );
1674 die "default key ' $key ' is an alias - this is not allowed \n "
1675 if defined ( $phash ->{ alias
});
1676 die "default key ' $key ' with keyAlias attribute is not allowed \n "
1677 if $phash ->{ keyAlias
};
1678 $default_key = $key ;
1680 my $key_alias = $phash ->{ keyAlias
};
1681 die "found keyAlias without 'alias definition for ' $key ' \n "
1682 if $key_alias && ! $phash ->{ alias
};
1684 if ( $phash ->{ alias
} && $key_alias ) {
1685 die "inconsistent keyAlias ' $key_alias ' definition"
1686 if defined ( $keyAliasProps ->{ $key_alias }) &&
1687 $keyAliasProps ->{ $key_alias } ne $phash ->{ alias
};
1688 $keyAliasProps ->{ $key_alias } = $phash ->{ alias
};
1692 return wantarray ?
( $default_key, $keyAliasProps ) : $default_key ;
1695 sub generate_typetext
{
1696 my ( $format, $list_enums ) = @_ ;
1698 my ( $default_key, $keyAliasProps ) = & $find_schema_default_key ( $format );
1703 my $add_option_string = sub {
1704 my ( $text, $optional ) = @_ ;
1710 $text = "[ $text ]" if $optional ;
1715 my $format_key_value = sub {
1716 my ( $key, $phash ) = @_ ;
1718 die "internal error" if defined ( $phash ->{ alias
});
1724 if ( my $desc = $phash ->{ format_description
}) {
1725 $typetext .= "< $desc >" ;
1726 } elsif ( my $text = $phash ->{ typetext
}) {
1728 } elsif ( my $enum = $phash ->{ enum
}) {
1729 if ( $list_enums || ( scalar ( @$enum ) <= 3 )) {
1730 $typetext .= '<' . join ( '|' , @$enum ) . '>' ;
1732 $typetext .= '<enum>' ;
1734 } elsif ( $phash ->{ type
} eq 'boolean' ) {
1735 $typetext .= '<1|0>' ;
1736 } elsif ( $phash ->{ type
} eq 'integer' ) {
1737 $typetext .= '<integer>' ;
1738 } elsif ( $phash ->{ type
} eq 'number' ) {
1739 $typetext .= '<number>' ;
1741 die "internal error: neither format_description nor typetext found for option ' $key '" ;
1744 if ( defined ( $default_key ) && ( $default_key eq $key )) {
1745 & $add_option_string ( "[ $keytext =] $typetext " , $phash ->{ optional
});
1747 & $add_option_string ( " $keytext = $typetext " , $phash ->{ optional
});
1753 my $cond_add_key = sub {
1756 return if $done ->{ $key }; # avoid duplicates
1760 my $phash = $format ->{ $key };
1762 return if ! $phash ; # should not happen
1764 return if $phash ->{ alias
};
1766 & $format_key_value ( $key, $phash );
1770 & $cond_add_key ( $default_key ) if defined ( $default_key );
1772 # add required keys first
1773 foreach my $key ( sort keys %$format ) {
1774 my $phash = $format ->{ $key };
1775 & $cond_add_key ( $key ) if $phash && ! $phash ->{ optional
};
1779 foreach my $key ( sort keys %$format ) {
1780 & $cond_add_key ( $key );
1783 foreach my $keyAlias ( sort keys %$keyAliasProps ) {
1784 & $add_option_string ( "< $keyAlias >=< $keyAliasProps ->{ $keyAlias }>" , 1 );
1790 sub print_property_string
{
1791 my ( $data, $format, $skip, $path ) = @_ ;
1793 if ( ref ( $format ) ne 'HASH' ) {
1794 my $schema = get_format
( $format );
1795 die "not a valid format: $format\n " if ! $schema ;
1800 check_object
( $path, $format, $data, undef , $errors );
1801 if ( scalar ( %$errors )) {
1802 raise
"format error" , errors
=> $errors ;
1805 my ( $default_key, $keyAliasProps ) = & $find_schema_default_key ( $format );
1810 my $add_option_string = sub {
1813 $res .= ',' if $add_sep ;
1818 my $format_value = sub {
1819 my ( $key, $value, $format ) = @_ ;
1821 if ( defined ( $format ) && ( $format eq 'disk-size' )) {
1822 return format_size
( $value );
1824 die "illegal value with commas for $key\n " if $value =~ /,/ ;
1829 my $done = { map { $_ => 1 } @$skip };
1831 my $cond_add_key = sub {
1832 my ( $key, $isdefault ) = @_ ;
1834 return if $done ->{ $key }; # avoid duplicates
1838 my $value = $data ->{ $key };
1840 return if ! defined ( $value );
1842 my $phash = $format ->{ $key };
1844 # try to combine values if we have key aliases
1845 if ( my $combine = $keyAliasProps ->{ $key }) {
1846 if ( defined ( my $combine_value = $data ->{ $combine })) {
1847 my $combine_format = $format ->{ $combine }->{ format
};
1848 my $value_str = & $format_value ( $key, $value, $phash ->{ format
});
1849 my $combine_str = & $format_value ( $combine, $combine_value, $combine_format );
1850 & $add_option_string ( "${value_str}=${combine_str}" );
1851 $done ->{ $combine } = 1 ;
1856 if ( $phash && $phash ->{ alias
}) {
1857 $phash = $format ->{ $phash ->{ alias
}};
1860 die "invalid key ' $key ' \n " if ! $phash ;
1861 die "internal error" if defined ( $phash ->{ alias
});
1863 my $value_str = & $format_value ( $key, $value, $phash ->{ format
});
1865 & $add_option_string ( $value_str );
1867 & $add_option_string ( " $key =${value_str}" );
1871 # add default key first
1872 & $cond_add_key ( $default_key, 1 ) if defined ( $default_key );
1874 # add required keys first
1875 foreach my $key ( sort keys %$data ) {
1876 my $phash = $format ->{ $key };
1877 & $cond_add_key ( $key ) if $phash && ! $phash ->{ optional
};
1881 foreach my $key ( sort keys %$data ) {
1882 & $cond_add_key ( $key );
1888 sub schema_get_type_text
{
1889 my ( $phash, $style ) = @_ ;
1891 my $type = $phash ->{ type
} || 'string' ;
1893 if ( $phash ->{ typetext
}) {
1894 return $phash ->{ typetext
};
1895 } elsif ( $phash ->{ format_description
}) {
1896 return "< $phash ->{format_description}>" ;
1897 } elsif ( $phash ->{ enum
}) {
1898 return "<" . join ( ' | ' , sort @{ $phash ->{ enum
}}) . ">" ;
1899 } elsif ( $phash ->{ pattern
}) {
1900 return $phash ->{ pattern
};
1901 } elsif ( $type eq 'integer' || $type eq 'number' ) {
1902 # NOTE: always access values as number (avoid converion to string)
1903 if ( defined ( $phash ->{ minimum
}) && defined ( $phash ->{ maximum
})) {
1904 return "< $type > (" . ( $phash ->{ minimum
} + 0 ) . " - " .
1905 ( $phash ->{ maximum
} + 0 ) . ")" ;
1906 } elsif ( defined ( $phash ->{ minimum
})) {
1907 return "< $type > (" . ( $phash ->{ minimum
} + 0 ) . " - N)" ;
1908 } elsif ( defined ( $phash ->{ maximum
})) {
1909 return "< $type > (-N - " . ( $phash ->{ maximum
} + 0 ) . ")" ;
1911 } elsif ( $type eq 'string' ) {
1912 if ( my $format = $phash ->{ format
}) {
1913 $format = get_format
( $format ) if ref ( $format ) ne 'HASH' ;
1914 if ( ref ( $format ) eq 'HASH' ) {
1916 $list_enums = 1 if $style && $style eq 'config-sub' ;
1917 return generate_typetext
( $format, $list_enums );