]>
git.proxmox.com Git - pve-common.git/blob - data/PVE/JSONSchema.pm
1 package PVE
:: JSONSchema
;
5 use Storable
; # for dclone
7 use Devel
:: Cycle
- quiet
; # todo: remove?
8 use PVE
:: Tools
qw(split_list) ;
9 use PVE
:: Exception
qw(raise) ;
10 use HTTP
:: Status
qw(:constants) ;
15 register_standard_option
19 # Note: This class implements something similar to JSON schema, but it is not 100% complete.
20 # see: http://tools.ietf.org/html/draft-zyp-json-schema-02
21 # see: http://json-schema.org/
23 # the code is similar to the javascript parser from http://code.google.com/p/jsonschema/
25 my $standard_options = {};
26 sub register_standard_option
{
27 my ( $name, $schema ) = @_ ;
29 die "standard option ' $name ' already registered \n "
30 if $standard_options ->{ $name };
32 $standard_options ->{ $name } = $schema ;
35 sub get_standard_option
{
36 my ( $name, $base ) = @_ ;
38 my $std = $standard_options ->{ $name };
39 die "no such standard option \n " if ! $std ;
41 my $res = $base || {};
43 foreach my $opt ( keys %$std ) {
45 $res ->{ $opt } = $std ->{ $opt };
51 register_standard_option
( 'pve-vmid' , {
52 description
=> "The (unique) ID of the VM." ,
53 type
=> 'integer' , format
=> 'pve-vmid' ,
57 register_standard_option
( 'pve-node' , {
58 description
=> "The cluster node name." ,
59 type
=> 'string' , format
=> 'pve-node' ,
62 register_standard_option
( 'pve-node-list' , {
63 description
=> "List of cluster node names." ,
64 type
=> 'string' , format
=> 'pve-node-list' ,
67 register_standard_option
( 'pve-iface' , {
68 description
=> "Network interface name." ,
69 type
=> 'string' , format
=> 'pve-iface' ,
70 minLength
=> 2 , maxLength
=> 20 ,
76 my ( $format, $code ) = @_ ;
78 die "JSON schema format ' $format ' already registered \n "
79 if $format_list ->{ $format };
81 $format_list ->{ $format } = $code ;
84 # register some common type for pve
85 register_format
( 'pve-configid' , \
& pve_verify_configid
);
86 sub pve_verify_configid
{
87 my ( $id, $noerr ) = @_ ;
89 if ( $id !~ m/^[a-z][a-z0-9_]+$/i ) {
90 return undef if $noerr ;
91 die "invalid cofiguration ID ' $id ' \n " ;
96 register_format
( 'pve-vmid' , \
& pve_verify_vmid
);
98 my ( $vmid, $noerr ) = @_ ;
100 if ( $vmid !~ m/^[1-9][0-9]+$/ ) {
101 return undef if $noerr ;
102 die "value does not look like a valid VM ID \n " ;
107 register_format
( 'pve-node' , \
& pve_verify_node_name
);
108 sub pve_verify_node_name
{
109 my ( $node, $noerr ) = @_ ;
111 # todo: use better regex ?
112 if ( $node !~ m/^[A-Za-z][[:alnum:]\-]*[[:alnum:]]+$/ ) {
113 return undef if $noerr ;
114 die "value does not look like a valid node name \n " ;
119 register_format
( 'ipv4' , \
& pve_verify_ipv4
);
120 sub pve_verify_ipv4
{
121 my ( $ipv4, $noerr ) = @_ ;
123 if ( $ipv4 !~ m/^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/ ||
124 !(( $1 > 0 ) && ( $1 < 255 ) &&
125 ( $2 <= 255 ) && ( $3 <= 255 ) &&
126 ( $4 > 0 ) && ( $4 < 255 ))) {
127 return undef if $noerr ;
128 die "value does not look like a valid IP address \n " ;
132 register_format
( 'ipv4mask' , \
& pve_verify_ipv4mask
);
133 sub pve_verify_ipv4mask
{
134 my ( $mask, $noerr ) = @_ ;
136 if ( $mask !~ m/^255\.255\.(\d{1,3})\.(\d{1,3})$/ ||
137 !(( $1 <= 255 ) && ( $2 <= 255 ))) {
138 return undef if $noerr ;
139 die "value does not look like a valid IP netmask \n " ;
144 register_format
( 'email' , \
& pve_verify_email
);
145 sub pve_verify_email
{
146 my ( $email, $noerr ) = @_ ;
148 # we use same regex as extjs Ext.form.VTypes.email
149 if ( $email !~ /^(\w+)([\-+.][\w]+)*@(\w[\-\w]*\.){1,5}([A-Za-z]){2,6}$/ ) {
150 return undef if $noerr ;
151 die "value does not look like a valid email address \n " ;
156 # network interface name
157 register_format
( 'pve-iface' , \
& pve_verify_iface
);
158 sub pve_verify_iface
{
159 my ( $id, $noerr ) = @_ ;
161 if ( $id !~ m/^[a-z][a-z0-9_]{1,20}([:\.]\d+)?$/i ) {
162 return undef if $noerr ;
163 die "invalid network interface name ' $id ' \n " ;
169 my ( $format, $value ) = @_ ;
171 return if $format eq 'regex' ;
173 if ( $format =~ m/^(.*)-list$/ ) {
175 my $code = $format_list ->{ $1 };
177 die "undefined format ' $format ' \n " if ! $code ;
179 # Note: we allow empty lists
180 foreach my $v ( split_list
( $value )) {
184 } elsif ( $format =~ m/^(.*)-opt$/ ) {
186 my $code = $format_list ->{ $1 };
188 die "undefined format ' $format ' \n " if ! $code ;
190 return if ! $value ; # allow empty string
196 my $code = $format_list ->{ $format };
198 die "undefined format ' $format ' \n " if ! $code ;
205 my ( $errors, $path, $msg ) = @_ ;
207 $path = '_root' if ! $path ;
209 if ( $errors ->{ $path }) {
210 $errors ->{ $path } = join ( ' \n ' , $errors ->{ $path }, $msg );
212 $errors ->{ $path } = $msg ;
219 # see 'man perlretut'
220 return $value =~ /^[+-]?(\d+\.\d+|\d+\.|\.\d+|\d+)([eE][+-]?\d+)?$/ ;
226 return $value =~ m/^[+-]?\d+$/ ;
230 my ( $path, $type, $value, $errors ) = @_ ;
234 if (! defined ( $value )) {
235 return 1 if $type eq 'null' ;
239 if ( my $tt = ref ( $type )) {
240 if ( $tt eq 'ARRAY' ) {
241 foreach my $t ( @$type ) {
243 check_type
( $path, $t, $value, $tmperr );
244 return 1 if ! scalar ( %$tmperr );
246 my $ttext = join ( '|' , @$type );
247 add_error
( $errors, $path, "type check (' $ttext ') failed" );
249 } elsif ( $tt eq 'HASH' ) {
251 check_prop
( $value, $type, $path, $tmperr );
252 return 1 if ! scalar ( %$tmperr );
253 add_error
( $errors, $path, "type check failed" );
256 die "internal error - got reference type ' $tt '" ;
261 return 1 if $type eq 'any' ;
263 if ( $type eq 'null' ) {
264 if ( defined ( $value )) {
265 add_error
( $errors, $path, "type check (' $type ') failed - value is not null" );
271 my $vt = ref ( $value );
273 if ( $type eq 'array' ) {
274 if (! $vt || $vt ne 'ARRAY' ) {
275 add_error
( $errors, $path, "type check (' $type ') failed" );
279 } elsif ( $type eq 'object' ) {
280 if (! $vt || $vt ne 'HASH' ) {
281 add_error
( $errors, $path, "type check (' $type ') failed" );
285 } elsif ( $type eq 'coderef' ) {
286 if (! $vt || $vt ne 'CODE' ) {
287 add_error
( $errors, $path, "type check (' $type ') failed" );
293 add_error
( $errors, $path, "type check (' $type ') failed - got $vt " );
296 if ( $type eq 'string' ) {
297 return 1 ; # nothing to check ?
298 } elsif ( $type eq 'boolean' ) {
299 #if ($value =~ m/^(1|true|yes|on)$/i) {
302 #} elsif ($value =~ m/^(0|false|no|off)$/i) {
303 } elsif ( $value eq '0' ) {
306 add_error
( $errors, $path, "type check (' $type ') failed - got ' $value '" );
309 } elsif ( $type eq 'integer' ) {
310 if (! is_integer
( $value )) {
311 add_error
( $errors, $path, "type check (' $type ') failed - got ' $value '" );
315 } elsif ( $type eq 'number' ) {
316 if (! is_number
( $value )) {
317 add_error
( $errors, $path, "type check (' $type ') failed - got ' $value '" );
322 return 1 ; # no need to verify unknown types
332 my ( $path, $schema, $value, $additional_properties, $errors ) = @_ ;
334 # print "Check Object " . Dumper($value) . "\nSchema: " . Dumper($schema);
336 my $st = ref ( $schema );
337 if (! $st || $st ne 'HASH' ) {
338 add_error
( $errors, $path, "Invalid schema definition." );
342 my $vt = ref ( $value );
343 if (! $vt || $vt ne 'HASH' ) {
344 add_error
( $errors, $path, "an object is required" );
348 foreach my $k ( keys %$schema ) {
349 check_prop
( $value ->{ $k }, $schema ->{ $k }, $path ?
" $path . $k " : $k, $errors );
352 foreach my $k ( keys %$value ) {
354 my $newpath = $path ?
" $path . $k " : $k ;
356 if ( my $subschema = $schema ->{ $k }) {
357 if ( my $requires = $subschema ->{ requires
}) {
358 if ( ref ( $requires )) {
359 #print "TEST: " . Dumper($value) . "\n", Dumper($requires) ;
360 check_prop
( $value, $requires, $path, $errors );
361 } elsif (! defined ( $value ->{ $requires })) {
362 add_error
( $errors, $path ?
" $path . $requires " : $requires,
363 "missing property - ' $newpath ' requiers this property" );
367 next ; # value is already checked above
370 if ( defined ( $additional_properties ) && ! $additional_properties ) {
371 add_error
( $errors, $newpath, "property is not defined in schema " .
372 "and the schema does not allow additional properties" );
375 check_prop
( $value ->{ $k }, $additional_properties, $newpath, $errors )
376 if ref ( $additional_properties );
381 my ( $value, $schema, $path, $errors ) = @_ ;
383 die "internal error - no schema" if ! $schema ;
384 die "internal error" if ! $errors ;
386 #print "check_prop $path\n" if $value;
388 my $st = ref ( $schema );
389 if (! $st || $st ne 'HASH' ) {
390 add_error
( $errors, $path, "Invalid schema definition." );
394 # if it extends another schema, it must pass that schema as well
395 if ( $schema ->{ extends
}) {
396 check_prop
( $value, $schema ->{ extends
}, $path, $errors );
399 if (! defined ( $value )) {
400 return if $schema ->{ type
} && $schema ->{ type
} eq 'null' ;
401 if (! $schema ->{ optional
}) {
402 add_error
( $errors, $path, "property is missing and it is not optional" );
407 return if ! check_type
( $path, $schema ->{ type
}, $value, $errors );
409 if ( $schema ->{ disallow
}) {
411 if ( check_type
( $path, $schema ->{ disallow
}, $value, $tmperr )) {
412 add_error
( $errors, $path, "disallowed value was matched" );
417 if ( my $vt = ref ( $value )) {
419 if ( $vt eq 'ARRAY' ) {
420 if ( $schema ->{ items
}) {
421 my $it = ref ( $schema ->{ items
});
422 if ( $it && $it eq 'ARRAY' ) {
423 #die "implement me $path: $vt " . Dumper($schema) ."\n". Dumper($value);
424 die "not implemented" ;
427 foreach my $el ( @$value ) {
428 check_prop
( $el, $schema ->{ items
}, "${path}[ $ind ]" , $errors );
434 } elsif ( $schema ->{ properties
} || $schema ->{ additionalProperties
}) {
435 check_object
( $path, defined ( $schema ->{ properties
}) ?
$schema ->{ properties
} : {},
436 $value, $schema ->{ additionalProperties
}, $errors );
442 if ( my $format = $schema ->{ format
}) {
443 eval { check_format
( $format, $value ); };
445 add_error
( $errors, $path, "invalid format - $@ " );
450 if ( my $pattern = $schema ->{ pattern
}) {
451 if ( $value !~ m/^$pattern$/ ) {
452 add_error
( $errors, $path, "value does not match the regex pattern" );
457 if ( defined ( my $max = $schema ->{ maxLength
})) {
458 if ( length ( $value ) > $max ) {
459 add_error
( $errors, $path, "value may only be $max characters long" );
464 if ( defined ( my $min = $schema ->{ minLength
})) {
465 if ( length ( $value ) < $min ) {
466 add_error
( $errors, $path, "value must be at least $min characters long" );
471 if ( is_number
( $value )) {
472 if ( defined ( my $max = $schema ->{ maximum
})) {
474 add_error
( $errors, $path, "value must have a maximum value of $max " );
479 if ( defined ( my $min = $schema ->{ minimum
})) {
481 add_error
( $errors, $path, "value must have a minimum value of $min " );
487 if ( my $ea = $schema ->{ enum
}) {
490 foreach my $ev ( @$ea ) {
497 add_error
( $errors, $path, "value ' $value ' does not have a value in the enumeration '" .
498 join ( ", " , @$ea ) . "'" );
505 my ( $instance, $schema, $errmsg ) = @_ ;
508 $errmsg = "Parameter verification failed. \n " if ! $errmsg ;
510 # todo: cycle detection is only needed for debugging, I guess
511 # we can disable that in the final release
512 # todo: is there a better/faster way to detect cycles?
514 find_cycle
( $instance, sub { $cycles = 1 });
516 add_error
( $errors, undef , "data structure contains recursive cycles" );
518 check_prop
( $instance, $schema, '' , $errors );
521 if ( scalar ( %$errors )) {
522 raise
$errmsg, code
=> HTTP_BAD_REQUEST
, errors
=> $errors ;
528 my $schema_valid_types = [ "string" , "object" , "coderef" , "array" , "boolean" , "number" , "integer" , "null" , "any" ];
529 my $default_schema_noref = {
530 description
=> "This is the JSON Schema for JSON Schemas." ,
531 type
=> [ "object" ],
532 additionalProperties
=> 0 ,
535 type
=> [ "string" , "array" ],
536 description
=> "This is a type definition value. This can be a simple type, or a union type" ,
541 enum
=> $schema_valid_types,
543 enum
=> $schema_valid_types,
547 description
=> "This indicates that the instance property in the instance object is not required." ,
553 description
=> "This is a definition for the properties of an object value" ,
559 description
=> "When the value is an array, this indicates the schema to use to validate each item in an array" ,
563 additionalProperties
=> {
564 type
=> [ "boolean" , "object" ],
565 description
=> "This provides a default property definition for all properties that are not explicitly defined in an object type definition." ,
572 description
=> "This indicates the minimum value for the instance property when the type of the instance value is a number." ,
577 description
=> "This indicates the maximum value for the instance property when the type of the instance value is a number." ,
581 description
=> "When the instance value is a string, this indicates minimum length of the string" ,
588 description
=> "When the instance value is a string, this indicates maximum length of the string." ,
594 description
=> "A text representation of the type (used to generate documentation)." ,
599 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." ,
607 description
=> "This provides an enumeration of possible values that are valid for the instance property." ,
612 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)." ,
617 description
=> "This provides the title of the property" ,
620 type
=> [ "string" , "object" ],
622 description
=> "indicates a required property or a schema that must be validated if this property is present" ,
627 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" ,
632 description
=> "This indicates the default for the instance property."
637 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, than this instance is not valid." ,
642 description
=> "This indicates the schema extends the given schema. All instances of this schema must be valid to by the extended schema also." ,
645 # this is from hyper schema
648 description
=> "This defines the link relations of the instance objects" ,
655 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" ,
659 description
=> "This is the name of the link relation" ,
665 description
=> "For submission links, this defines the method that should be used to access the target resource" ,
675 my $default_schema = Storable
:: dclone
( $default_schema_noref );
677 $default_schema ->{ properties
}->{ properties
}->{ additionalProperties
} = $default_schema ;
678 $default_schema ->{ properties
}->{ additionalProperties
}->{ properties
} = $default_schema ->{ properties
};
680 $default_schema ->{ properties
}->{ items
}->{ properties
} = $default_schema ->{ properties
};
681 $default_schema ->{ properties
}->{ items
}->{ additionalProperties
} = 0 ;
683 $default_schema ->{ properties
}->{ disallow
}->{ properties
} = $default_schema ->{ properties
};
684 $default_schema ->{ properties
}->{ disallow
}->{ additionalProperties
} = 0 ;
686 $default_schema ->{ properties
}->{ requires
}->{ properties
} = $default_schema ->{ properties
};
687 $default_schema ->{ properties
}->{ requires
}->{ additionalProperties
} = 0 ;
689 $default_schema ->{ properties
}->{ extends
}->{ properties
} = $default_schema ->{ properties
};
690 $default_schema ->{ properties
}->{ extends
}->{ additionalProperties
} = 0 ;
692 my $method_schema = {
694 additionalProperties
=> 0 ,
697 description
=> "This a description of the method" ,
702 description
=> "This indicates the name of the function to call." ,
705 additionalProperties
=> 1 ,
720 description
=> "The HTTP method name." ,
721 enum
=> [ 'GET' , 'POST' , 'PUT' , 'DELETE' ],
726 description
=> "Method needs special privileges - only pvedaemon can execute it" ,
731 description
=> "A parameter name. If specified, all calls to this method are proxied to the host contained in that parameter." ,
736 description
=> "Required access permissions. By default only 'root' is allowed to access this method." ,
738 additionalProperties
=> 0 ,
741 description
=> "A simply way to allow access for 'all' users. The special value 'arg' allows access for the user specified in the 'username' parameter. This is useful to allow access to things owned by a user, like changing the user password. Value 'world' is used to allow access without credentials." ,
743 enum
=> [ 'all' , 'arg' , 'world' ],
746 path
=> { type
=> 'string' , optional
=> 1 , requires
=> 'privs' },
747 privs
=> { type
=> 'array' , optional
=> 1 , requires
=> 'path' },
751 description
=> "Used internally" ,
755 description
=> "Used internally" ,
760 description
=> "path for URL matching (uri template)" ,
762 fragmentDelimiter
=> {
764 description
=> "A ways to override the default fragment delimiter '/'. This onyl works on a whole sub-class. You can set this to the empty string to match the whole rest of the URI." ,
769 description
=> "JSON Schema for parameters." ,
774 description
=> "JSON Schema for return value." ,
779 description
=> "method implementaion (code reference)" ,
784 description
=> "Delegate call to this class (perl class string)." ,
787 additionalProperties
=> 0 ,
793 fragmentDelimiter
=> { optional
=> 1 }
801 sub validate_schema
{
804 my $errmsg = "internal error - unable to verify schema \n " ;
805 validate
( $schema, $default_schema, $errmsg );
808 sub validate_method_info
{
811 my $errmsg = "internal error - unable to verify method info \n " ;
812 validate
( $info, $method_schema, $errmsg );
814 validate_schema
( $info ->{ parameters
}) if $info ->{ parameters
};
815 validate_schema
( $info ->{ returns
}) if $info ->{ returns
};
818 # run a self test on load
819 # make sure we can verify the default schema
820 validate_schema
( $default_schema_noref );
821 validate_schema
( $method_schema );
823 # and now some utility methods (used by pve api)
824 sub method_get_child_link
{
827 return undef if ! $info ;
829 my $schema = $info ->{ returns
};
830 return undef if ! $schema || ! $schema ->{ type
} || $schema ->{ type
} ne 'array' ;
832 my $links = $schema ->{ links
};
833 return undef if ! $links ;
836 foreach my $lnk ( @$links ) {
837 if ( $lnk ->{ href
} && $lnk ->{ rel
} && ( $lnk ->{ rel
} eq 'child' )) {
846 # a way to parse command line parameters, using a
847 # schema to configure Getopt::Long
849 my ( $schema, $args, $uri_param, $pwcallback ) = @_ ;
851 if (! $schema || ! $schema ->{ properties
}) {
852 raise
( "too many arguments \n " , code
=> HTTP_BAD_REQUEST
)
853 if scalar ( @$args ) != 0 ;
858 foreach my $prop ( keys %{ $schema ->{ properties
}}) {
859 my $pd = $schema ->{ properties
}->{ $prop };
860 next if defined ( $uri_param ->{ $prop });
862 if ( $prop eq 'password' && $pwcallback ) {
863 # we do not accept plain password on input line, instead
864 # we turn this into a boolean option and ask for password below
865 # using $pwcallback() (for security reasons).
866 push @getopt, " $prop " ;
867 } elsif ( $pd ->{ type
} eq 'boolean' ) {
868 push @getopt, " $prop :s" ;
870 push @getopt, " $prop =s" ;
875 raise
( "unable to parse option \n " , code
=> HTTP_BAD_REQUEST
)
876 if ! Getopt
:: Long
:: GetOptionsFromArray
( $args, $opts, @getopt );
878 raise
( "too many arguments \n " , code
=> HTTP_BAD_REQUEST
)
879 if scalar ( @$args ) != 0 ;
881 if ( my $pd = $schema ->{ properties
}->{ password
}) {
882 if ( $pd ->{ type
} ne 'boolean' && $pwcallback ) {
883 if ( $opts ->{ password
} || ! $pd ->{ optional
}) {
884 $opts ->{ password
} = & $pwcallback ();
889 foreach my $p ( keys %$opts ) {
890 if ( my $pd = $schema ->{ properties
}->{ $p }) {
891 if ( $pd ->{ type
} eq 'boolean' ) {
892 if ( $opts ->{ $p } eq '' ) {
894 } elsif ( $opts ->{ $p } =~ m/^(1|true|yes|on)$/i ) {
896 } elsif ( $opts ->{ $p } =~ m/^(0|false|no|off)$/i ) {
899 raise
( "unable to parse boolean option \n " , code
=> HTTP_BAD_REQUEST
);
905 foreach my $p ( keys %$uri_param ) {
906 $opts ->{ $p } = $uri_param ->{ $p };
912 # A way to parse configuration data by giving a json schema
914 my ( $schema, $filename, $raw ) = @_ ;
916 # do fast check (avoid validate_schema($schema))
917 die "got strange schema" if ! $schema ->{ type
} ||
918 ! $schema ->{ properties
} || $schema ->{ type
} ne 'object' ;
922 while ( $raw && $raw =~ s/^(.*?)(\n|$)// ) {
925 next if $line =~ m/^\#/ ; # skip comment lines
926 next if $line =~ m/^\s*$/ ; # skip empty lines
928 if ( $line =~ m/^(\S+):\s*(\S+)\s*$/ ) {
931 if ( $schema ->{ properties
}->{ $key } &&
932 $schema ->{ properties
}->{ $key }->{ type
} eq 'boolean' ) {
934 $value = 1 if $value =~ m/^(1|on|yes|true)$/i ;
935 $value = 0 if $value =~ m/^(0|off|no|false)$/i ;
937 $cfg ->{ $key } = $value ;
939 warn "ignore config line: $line\n "
944 check_prop
( $cfg, $schema, '' , $errors );
946 foreach my $k ( keys %$errors ) {
947 warn "parse error in ' $filename ' - ' $k ': $errors ->{ $k } \n " ;
954 # generate simple key/value file
956 my ( $schema, $filename, $cfg ) = @_ ;
958 # do fast check (avoid validate_schema($schema))
959 die "got strange schema" if ! $schema ->{ type
} ||
960 ! $schema ->{ properties
} || $schema ->{ type
} ne 'object' ;
962 validate
( $cfg, $schema, "validation error in ' $filename ' \n " );
966 foreach my $k ( keys %$cfg ) {
967 $data .= " $k : $cfg ->{ $k } \n " ;