]> git.proxmox.com Git - pve-common.git/blobdiff - src/PVE/JSONSchema.pm
Make configid regex public
[pve-common.git] / src / PVE / JSONSchema.pm
index 599dd099b0441a4bb8fd0d41406f55b928afb0d1..29ada5bc06570ea6853ca0736e303f5fd89befa5 100644 (file)
@@ -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";
     }
@@ -180,14 +190,26 @@ PVE::JSONSchema::register_format('pve-storage-id', \&parse_storage_id);
 sub parse_storage_id {
     my ($storeid, $noerr) = @_;
 
-    if ($storeid !~ m/^[a-z][a-z0-9\-\_\.]*[a-z0-9]$/i) {
+    return parse_id($storeid, 'storage', $noerr);
+}
+
+PVE::JSONSchema::register_format('acme-plugin-id', \&parse_acme_plugin_id);
+sub parse_acme_plugin_id {
+    my ($pluginid, $noerr) = @_;
+
+    return parse_id($pluginid, 'ACME plugin', $noerr);
+}
+
+sub parse_id {
+    my ($id, $type, $noerr) = @_;
+
+     if ($id !~ m/^[a-z][a-z0-9\-\_\.]*[a-z0-9]$/i) {
        return undef if $noerr;
-       die "storage ID '$storeid' contains illegal characters\n";
+       die "$type ID '$id' contains illegal characters\n";
     }
-    return $storeid;
+    return $id;
 }
 
-
 register_format('pve-vmid', \&pve_verify_vmid);
 sub pve_verify_vmid {
     my ($vmid, $noerr) = @_;
@@ -210,6 +232,63 @@ sub pve_verify_node_name {
     return $node;
 }
 
+sub parse_idmap {
+    my ($idmap, $idformat) = @_;
+
+    return undef if !$idmap;
+
+    my $map = {};
+
+    foreach my $entry (PVE::Tools::split_list($idmap)) {
+       if ($entry eq '1') {
+           $map->{identity} = 1;
+       } elsif ($entry =~ m/^([^:]+):([^:]+)$/) {
+           my ($source, $target) = ($1, $2);
+           eval {
+               check_format($idformat, $source, '');
+               check_format($idformat, $target, '');
+           };
+           die "entry '$entry' contains invalid ID - $@\n" if $@;
+
+           die "duplicate mapping for source '$source'\n"
+               if exists $map->{entries}->{$source};
+
+           $map->{entries}->{$source} = $target;
+       } else {
+           eval {
+               check_format($idformat, $entry);
+           };
+           die "entry '$entry' contains invalid ID - $@\n" if $@;
+
+           die "default target ID can only be provided once\n"
+               if exists $map->{default};
+
+           $map->{default} = $entry;
+       }
+    }
+
+    die "identity mapping cannot be combined with other mappings\n"
+       if $map->{identity} && ($map->{default} || exists $map->{entries});
+
+    return $map;
+}
+
+register_format('storagepair', \&verify_storagepair);
+sub verify_storagepair {
+    my ($storagepair, $noerr) = @_;
+
+    # note: this only checks a single list entry
+    # when using a storagepair-list map, you need to pass the full
+    # parameter to parse_idmap
+    eval { parse_idmap($storagepair, 'pve-storage-id') };
+    if ($@) {
+       return undef if $noerr;
+       die "$@\n";
+    }
+
+    return $storagepair;
+}
+
 register_format('mac-addr', \&pve_verify_mac_addr);
 sub pve_verify_mac_addr {
     my ($mac_addr, $noerr) = @_;
@@ -265,6 +344,19 @@ sub pve_verify_ip {
     return $ip;
 }
 
+PVE::JSONSchema::register_format('ldap-simple-attr', \&verify_ldap_simple_attr);
+sub verify_ldap_simple_attr {
+    my ($attr, $noerr) = @_;
+
+    if ($attr =~ m/^[a-zA-Z0-9]+$/) {
+       return $attr;
+    }
+
+    die "value '$attr' does not look like a simple ldap attribute name\n" if !$noerr;
+
+    return undef;
+}
+
 my $ipv4_mask_hash = {
     '0.0.0.0' => 0,
     '128.0.0.0' => 1,
@@ -399,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 {
@@ -564,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 {
@@ -652,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";
        }
@@ -710,6 +836,7 @@ sub parse_property_string {
        raise "format error\n", errors => $errors;
     }
 
+    return $validator->($res) if $validator;
     return $res;
 }
 
@@ -1488,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);
                }
@@ -1497,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;
@@ -1510,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);
                }
            }
@@ -1772,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;
     }
 
@@ -1784,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 = '';