X-Git-Url: https://git.proxmox.com/?a=blobdiff_plain;f=src%2FPVE%2FJSONSchema.pm;h=29ada5bc06570ea6853ca0736e303f5fd89befa5;hb=eeafe02d81bb06108ca4dc51cd2fb515ce41010c;hp=d332a3dd1c21dbbd8c86721045005c665e4b4461;hpb=18f93ddf6385a017f5dcc7989952cc8462c423b5;p=pve-common.git diff --git a/src/PVE/JSONSchema.pm b/src/PVE/JSONSchema.pm index d332a3d..29ada5b 100644 --- a/src/PVE/JSONSchema.pm +++ b/src/PVE/JSONSchema.pm @@ -16,10 +16,13 @@ use Data::Dumper; use base 'Exporter'; our @EXPORT_OK = qw( -register_standard_option get_standard_option +parse_property_string +register_standard_option ); +our $CONFIGID_RE = qr/[a-z][a-z0-9_-]+/i; + # Note: This class implements something similar to JSON schema, but it is not 100% complete. # see: http://tools.ietf.org/html/draft-zyp-json-schema-02 # see: http://json-schema.org/ @@ -120,19 +123,26 @@ register_standard_option('pve-snapshot-name', { }); my $format_list = {}; +my $format_validators = {}; sub register_format { - my ($format, $code) = @_; + my ($name, $format, $validator) = @_; - die "JSON schema format '$format' already registered\n" - if $format_list->{$format}; + die "JSON schema format '$name' already registered\n" + if $format_list->{$name}; + + if ($validator) { + die "A \$validator function can only be specified for hash-based formats\n" + if ref($format) ne 'HASH'; + $format_validators->{$name} = $validator; + } - $format_list->{$format} = $code; + $format_list->{$name} = $format; } sub get_format { - my ($format) = @_; - return $format_list->{$format}; + my ($name) = @_; + return $format_list->{$name}; } my $renderer_hash = {}; @@ -169,7 +179,7 @@ register_format('pve-configid', \&pve_verify_configid); sub pve_verify_configid { my ($id, $noerr) = @_; - if ($id !~ m/^[a-z][a-z0-9_]+$/i) { + if ($id !~ m/^$CONFIGID_RE$/) { return undef if $noerr; die "invalid configuration ID '$id'\n"; } @@ -235,33 +245,30 @@ sub parse_idmap { } elsif ($entry =~ m/^([^:]+):([^:]+)$/) { my ($source, $target) = ($1, $2); eval { - PVE::JSONSchema::check_format($idformat, $source, ''); - PVE::JSONSchema::check_format($idformat, $target, ''); + check_format($idformat, $source, ''); + check_format($idformat, $target, ''); }; - die "entry '$entry' contains invalid ID - $@\n" - if $@; + die "entry '$entry' contains invalid ID - $@\n" if $@; die "duplicate mapping for source '$source'\n" - if $map->{entries}->{$source}; + if exists $map->{entries}->{$source}; $map->{entries}->{$source} = $target; } else { eval { - PVE::JSONSchema::check_format($idformat, $entry); + check_format($idformat, $entry); }; - - die "entry '$entry' contains invalid ID - $@\n" - if $@; + die "entry '$entry' contains invalid ID - $@\n" if $@; die "default target ID can only be provided once\n" - if $map->{default}; + if exists $map->{default}; $map->{default} = $entry; } } die "identity mapping cannot be combined with other mappings\n" - if $map->{identity} && ($map->{default} || $map->{entries}); + if $map->{identity} && ($map->{default} || exists $map->{entries}); return $map; } @@ -484,6 +491,25 @@ sub pve_verify_dns_name { return $name; } +register_format('timezone', \&pve_verify_timezone); +sub pve_verify_timezone { + my ($timezone, $noerr) = @_; + + return $timezone if $timezone eq 'UTC'; + + open(my $fh, "<", "/usr/share/zoneinfo/zone.tab"); + while (my $line = <$fh>) { + next if $line =~ /^\s*#/; + chomp $line; + my $zone = (split /\t/, $line)[2]; + return $timezone if $timezone eq $zone; # found + } + close $fh; + + return undef if $noerr; + die "invalid time zone '$timezone'\n"; +} + # network interface name register_format('pve-iface', \&pve_verify_iface); sub pve_verify_iface { @@ -649,39 +675,47 @@ sub pve_verify_tfa_secret { sub check_format { my ($format, $value, $path) = @_; - return parse_property_string($format, $value, $path) if ref($format) eq 'HASH'; - return if $format eq 'regex'; + if (ref($format) eq 'HASH') { + # hash ref cannot have validator/list/opt handling attached + return parse_property_string($format, $value, $path); + } - if ($format =~ m/^(.*)-a?list$/) { + if (ref($format) eq 'CODE') { + # we are the (sole, old-style) validator + return $format->($value); + } + + return if $format eq 'regex'; - my $code = $format_list->{$1}; + my $parsed; + $format =~ m/^(.*?)(?:-a?(list|opt))?$/; + my ($format_name, $format_type) = ($1, $2 // 'none'); + my $registered = get_format($format_name); + die "undefined format '$format'\n" if !$registered; - die "undefined format '$format'\n" if !$code; + die "'-$format_type' format must have code ref, not hash\n" + if $format_type ne 'none' && ref($registered) ne 'CODE'; + if ($format_type eq 'list') { # Note: we allow empty lists foreach my $v (split_list($value)) { - &$code($v); + $parsed = $registered->($v); } - - } elsif ($format =~ m/^(.*)-opt$/) { - - my $code = $format_list->{$1}; - - die "undefined format '$format'\n" if !$code; - - return if !$value; # allow empty string - - &$code($value); - + } elsif ($format_type eq 'opt') { + $parsed = $registered->($value) if $value; } else { - - my $code = $format_list->{$format}; - - die "undefined format '$format'\n" if !$code; - - return parse_property_string($code, $value, $path) if ref($code) eq 'HASH'; - &$code($value); + if (ref($registered) eq 'HASH') { + # Note: this is the only case where a validator function could be + # attached, hence it's safe to handle that in parse_property_string. + # We do however have to call it with $format_name instead of + # $registered, so it knows about the name (and thus any validators). + $parsed = parse_property_string($format, $value, $path); + } else { + $parsed = $registered->($value); + } } + + return $parsed; } sub parse_size { @@ -737,9 +771,16 @@ sub parse_property_string { $additional_properties = 0 if !defined($additional_properties); # Support named formats here, too: + my $validator; if (!ref($format)) { - if (my $desc = $format_list->{$format}) { - $format = $desc; + if (my $reg = get_format($format)) { + die "parse_property_string only accepts hash based named formats\n" + if ref($reg) ne 'HASH'; + + # named formats can have validators attached + $validator = $format_validators->{$format}; + + $format = $reg; } else { die "unknown format: $format\n"; } @@ -795,6 +836,7 @@ sub parse_property_string { raise "format error\n", errors => $errors; } + return $validator->($res) if $validator; return $res; } @@ -1573,7 +1615,8 @@ sub get_options { $opts->{$list_param} = $args; $args = []; } elsif (ref($arg_param)) { - foreach my $arg_name (@$arg_param) { + for (my $i = 0; $i < scalar(@$arg_param); $i++) { + my $arg_name = $arg_param->[$i]; if ($opts->{'extra-args'}) { raise("internal error: extra-args must be the last argument\n", code => HTTP_BAD_REQUEST); } @@ -1582,7 +1625,15 @@ sub get_options { $args = []; next; } - raise("not enough arguments\n", code => HTTP_BAD_REQUEST) if !@$args; + if (!@$args) { + # check if all left-over arg_param are optional, else we + # must die as the mapping is then ambigious + for (my $j = $i; $j < scalar(@$arg_param); $j++) { + my $prop = $arg_param->[$j]; + raise("not enough arguments\n", code => HTTP_BAD_REQUEST) + if !$schema->{properties}->{$prop}->{optional}; + } + } $opts->{$arg_name} = shift @$args; } raise("too many arguments\n", code => HTTP_BAD_REQUEST) if @$args; @@ -1595,7 +1646,7 @@ sub get_options { foreach my $arg_name (@$arg_param) { if ($arg_name eq 'extra-args') { $opts->{'extra-args'} = []; - } else { + } elsif (!$schema->{properties}->{$arg_name}->{optional}) { raise("not enough arguments\n", code => HTTP_BAD_REQUEST); } } @@ -1857,9 +1908,12 @@ sub generate_typetext { sub print_property_string { my ($data, $format, $skip, $path) = @_; + my $validator; if (ref($format) ne 'HASH') { my $schema = get_format($format); die "not a valid format: $format\n" if !$schema; + # named formats can have validators attached + $validator = $format_validators->{$format}; $format = $schema; } @@ -1869,6 +1923,8 @@ sub print_property_string { raise "format error", errors => $errors; } + $data = $validator->($data) if $validator; + my ($default_key, $keyAliasProps) = &$find_schema_default_key($format); my $res = '';