]> git.proxmox.com Git - pve-common.git/blobdiff - src/PVE/JSONSchema.pm
bump version to 8.2.1
[pve-common.git] / src / PVE / JSONSchema.pm
index 54c149dc7a7f326acc36763ca17a75390ed7cf70..115f811043360204c2ab07e86b8feb5278f2d594 100644 (file)
@@ -10,6 +10,7 @@ use Devel::Cycle -quiet; # todo: remove?
 use PVE::Tools qw(split_list $IPV6RE $IPV4RE);
 use PVE::Exception qw(raise);
 use HTTP::Status qw(:constants);
+use JSON;
 use Net::IP qw(:PROC);
 use Data::Dumper;
 
@@ -58,8 +59,10 @@ sub get_standard_option {
 
 register_standard_option('pve-vmid', {
     description => "The (unique) ID of the VM.",
-    type => 'integer', format => 'pve-vmid',
-    minimum => 1
+    type => 'integer',
+    format => 'pve-vmid',
+    minimum => 100,
+    maximum => 999_999_999,
 });
 
 register_standard_option('pve-node', {
@@ -81,6 +84,7 @@ register_standard_option('pve-iface', {
 register_standard_option('pve-storage-id', {
     description => "The storage identifier.",
     type => 'string', format => 'pve-storage-id',
+    format_description => 'storage ID',
 });
 
 register_standard_option('pve-bridge-id', {
@@ -90,10 +94,13 @@ register_standard_option('pve-bridge-id', {
 });
 
 register_standard_option('pve-config-digest', {
-    description => 'Prevent changes if current configuration file has different SHA1 digest. This can be used to prevent concurrent modifications.',
+    description => 'Prevent changes if current configuration file has a different digest. '
+       . 'This can be used to prevent concurrent modifications.',
     type => 'string',
     optional => 1,
-    maxLength => 40, # sha1 hex digest length is 40
+    # sha1 hex digests are 40 characters long
+    # sha256 hex digests are 64 characters long (sha256 is used in our Rust code)
+    maxLength => 64,
 });
 
 register_standard_option('skiplock', {
@@ -318,6 +325,13 @@ my $verify_idpair = sub {
     return $input;
 };
 
+PVE::JSONSchema::register_standard_option('pve-targetstorage', {
+    description => "Mapping from source to target storages. Providing only a single storage ID maps all source storages to that storage. Providing the special value '1' will map each source storage to itself.",
+    type => 'string',
+    format => 'storage-pair-list',
+    optional => 1,
+});
+
 # note: this only checks a single list entry
 # when using a storage-pair-list map, you need to pass the full parameter to
 # parse_idmap
@@ -663,7 +677,7 @@ my $bwlimit_format = {
 };
 register_format('bwlimit', $bwlimit_format);
 register_standard_option('bwlimit', {
-    description => "Set bandwidth/io limits various operations.",
+    description => "Set I/O bandwidth limit for various operations (in KiB/s).",
     optional => 1,
     type => 'string',
     format => $bwlimit_format,
@@ -672,21 +686,26 @@ register_standard_option('bwlimit', {
 my $remote_format = {
     host => {
        type => 'string',
-       format_description => 'Remote Proxmox hostname or IP',
+       description => 'Remote Proxmox hostname or IP',
+       format_description => 'ADDRESS',
     },
     port => {
        type => 'integer',
        optional => 1,
+       description => 'Port to connect to',
+       format_description => 'PORT',
     },
     apitoken => {
        type => 'string',
-       format_description => 'A full Proxmox API token including the secret value.',
+       description => 'A full Proxmox API token including the secret value.',
+       format_description => 'PVEAPIToken=user@realm!token=SECRET',
     },
     fingerprint => get_standard_option(
        'fingerprint-sha256',
        {
            optional => 1,
-           format_description => 'Remote host\'s certificate fingerprint, if not trusted by system store.',
+           description => 'Remote host\'s certificate fingerprint, if not trusted by system store.',
+           format_description => 'FINGERPRINT',
        }
     ),
 };
@@ -788,7 +807,7 @@ sub check_format {
     return if $format eq 'regex';
 
     my $parsed;
-    $format =~ m/^(.*?)(?:-a?(list|opt))?$/;
+    $format =~ m/^(.*?)(?:-(list|opt))?$/;
     my ($format_name, $format_type) = ($1, $2 // 'none');
     my $registered = get_format($format_name);
     die "undefined format '$format'\n" if !$registered;
@@ -1033,6 +1052,9 @@ sub check_type {
            return 1;
        } else {
            if ($vt) {
+               if ($type eq 'boolean' && JSON::is_bool($value)) {
+                   return 1;
+               }
                add_error($errors, $path, "type check ('$type') failed - got $vt");
                return undef;
            } else {
@@ -1071,6 +1093,16 @@ sub check_type {
     return undef;
 }
 
+my sub get_instance_type {
+    my ($schema, $key, $value) = @_;
+
+    if (my $type_property = $schema->{$key}->{'type-property'}) {
+       return $value->{$type_property};
+    }
+
+    return undef;
+}
+
 sub check_object {
     my ($path, $schema, $value, $additional_properties, $errors) = @_;
 
@@ -1089,7 +1121,8 @@ sub check_object {
     }
 
     foreach my $k (keys %$schema) {
-       check_prop($value->{$k}, $schema->{$k}, $path ? "$path.$k" : $k, $errors);
+       my $instance_type = get_instance_type($schema, $k, $value);
+       check_prop($value->{$k}, $schema->{$k}, $path ? "$path.$k" : $k, $errors, $instance_type);
     }
 
     foreach my $k (keys %$value) {
@@ -1107,7 +1140,23 @@ sub check_object {
                }
            }
 
-           next; # value is already checked above
+           # if it's a oneOf, check if there is a matching type
+           my $matched_type = 1;
+           if ($subschema->{oneOf}) {
+               my $instance_type = get_instance_type($schema, $k, $value);
+               $matched_type = 0;
+               for my $alternative ($subschema->{oneOf}->@*) {
+                   if (my $instance_types = $alternative->{'instance-types'}) {
+                       if (!grep { $instance_type eq $_ } $instance_types->@*) {
+                           next;
+                       }
+                   }
+                   $matched_type = 1;
+                   last;
+               }
+           }
+
+           next if $matched_type; # value is already checked above
        }
 
        if (defined ($additional_properties) && !$additional_properties) {
@@ -1134,7 +1183,7 @@ sub check_object_warn {
 }
 
 sub check_prop {
-    my ($value, $schema, $path, $errors) = @_;
+    my ($value, $schema, $path, $errors, $instance_type) = @_;
 
     die "internal error - no schema" if !$schema;
     die "internal error" if !$errors;
@@ -1147,6 +1196,58 @@ sub check_prop {
        return;
     }
 
+    # must pass any of the given schemas
+    my $optional_for_type = 0;
+    if ($schema->{oneOf}) {
+       # in case we have an instance_type given, just check for that variant
+       if ($schema->{'type-property'}) {
+           $optional_for_type = 1;
+           for (my $i = 0; $i < scalar($schema->{oneOf}->@*); $i++) {
+               last if !$instance_type; # treat as optional if we don't have a type
+               my $inner_schema = $schema->{oneOf}->[$i];
+
+               if (!defined($inner_schema->{'instance-types'})) {
+                   add_error($errors, $path, "missing 'instance-types' in oneOf alternative");
+                   return;
+               }
+
+               next if !grep { $_ eq $instance_type } $inner_schema->{'instance-types'}->@*;
+               $optional_for_type = $inner_schema->{optional} // 0;
+               check_prop($value, $inner_schema, $path, $errors);
+           }
+       } else {
+           my $is_valid = 0;
+           my $collected_errors = {};
+           for (my $i = 0; $i < scalar($schema->{oneOf}->@*); $i++) {
+               my $inner_schema = $schema->{oneOf}->[$i];
+               my $inner_errors = {};
+               check_prop($value, $inner_schema, "$path.oneOf[$i]", $inner_errors);
+               if (!$inner_errors->%*) {
+                   $is_valid = 1;
+                   last;
+               }
+
+               for my $inner_path (keys $inner_errors->%*) {
+                   add_error($collected_errors, $inner_path, $inner_errors->{$path});
+               }
+           }
+
+           if (!$is_valid) {
+               for my $inner_path (keys $collected_errors->%*) {
+                   add_error($errors, $inner_path, $collected_errors->{$path});
+               }
+           }
+       }
+    } elsif ($instance_type) {
+       if (!defined($schema->{'instance-types'})) {
+           add_error($errors, $path, "missing 'instance-types'");
+           return;
+       }
+       if (grep { $_ eq $instance_type} $schema->{'instance_types'}->@*) {
+           $optional_for_type = 1;
+       }
+    }
+
     # if it extends another schema, it must pass that schema as well
     if($schema->{extends}) {
        check_prop($value, $schema->{extends}, $path, $errors);
@@ -1154,7 +1255,7 @@ sub check_prop {
 
     if (!defined ($value)) {
        return if $schema->{type} && $schema->{type} eq 'null';
-       if (!$schema->{optional} && !$schema->{alias} && !$schema->{group}) {
+       if (!$schema->{optional} && !$schema->{alias} && !$schema->{group} && !$optional_for_type) {
            add_error($errors, $path, "property is missing and it is not optional");
        }
        return;
@@ -1301,6 +1402,28 @@ my $default_schema_noref = {
            },
            enum => $schema_valid_types,
        },
+       oneOf => {
+           type => 'array',
+           description => "This represents the alternative options for this Schema instance.",
+           optional => 1,
+           items => {
+               type => 'object',
+               description => "A valid option of the properties",
+           },
+       },
+       'instance-types' => {
+           type => 'array',
+           description => "Indicate to which type the parameter (or variant if inside a oneOf) belongs.",
+           optional => 1,
+           items => {
+               type => 'string',
+           },
+       },
+       'type-property' => {
+           type => 'string',
+           description => "The property to check for instance types.",
+           optional => 1,
+       },
        optional => {
            type => "boolean",
            description => "This indicates that the instance property in the instance object is not required.",
@@ -1475,6 +1598,7 @@ my $default_schema = Storable::dclone($default_schema_noref);
 
 $default_schema->{properties}->{properties}->{additionalProperties} = $default_schema;
 $default_schema->{properties}->{additionalProperties}->{properties} = $default_schema->{properties};
+$default_schema->{properties}->{oneOf}->{items}->{properties} = $default_schema->{properties};
 
 $default_schema->{properties}->{items}->{properties} = $default_schema->{properties};
 $default_schema->{properties}->{items}->{additionalProperties} = 0;
@@ -1697,10 +1821,12 @@ sub get_options {
            # optional and call the mapping function afterwards.
            push @getopt, "$prop:s";
            push @interactive, [$prop, $mapping->{func}];
-       } elsif ($pd->{type} eq 'boolean') {
+       } elsif ($pd->{type} && $pd->{type} eq 'boolean') {
            push @getopt, "$prop:s";
        } else {
-           if ($pd->{format} && $pd->{format} =~ m/-a?list/) {
+           if ($pd->{format} && $pd->{format} =~ m/-list/) {
+               push @getopt, "$prop=s@";
+           } elsif ($pd->{type} && $pd->{type} eq 'array') {
                push @getopt, "$prop=s@";
            } else {
                push @getopt, "$prop=s";
@@ -1789,7 +1915,7 @@ sub get_options {
 
     foreach my $p (keys %$opts) {
        if (my $pd = $schema->{properties}->{$p}) {
-           if ($pd->{type} eq 'boolean') {
+           if ($pd->{type} && $pd->{type} eq 'boolean') {
                if ($opts->{$p} eq '') {
                    $opts->{$p} = 1;
                } elsif (defined(my $bool = parse_boolean($opts->{$p}))) {
@@ -1803,16 +1929,6 @@ sub get_options {
                    # allow --vmid 100 --vmid 101 and --vmid 100,101
                    # allow --dow mon --dow fri and --dow mon,fri
                    $opts->{$p} = join(",", @{$opts->{$p}}) if ref($opts->{$p}) eq 'ARRAY';
-               } elsif ($pd->{format} =~ m/-alist/) {
-                   # we encode array as \0 separated strings
-                   # Note: CGI.pm also use this encoding
-                   if (scalar(@{$opts->{$p}}) != 1) {
-                       $opts->{$p} = join("\0", @{$opts->{$p}});
-                   } else {
-                       # st that split_list knows it is \0 terminated
-                       my $v = $opts->{$p}->[0];
-                       $opts->{$p} = "$v\0";
-                   }
                }
            }
        }
@@ -1862,6 +1978,15 @@ sub parse_config : prototype($$$;$) {
 
                $value = parse_boolean($value) // $value;
            }
+           if (
+               $schema->{properties}->{$key}
+               && $schema->{properties}->{$key}->{type} eq 'array'
+           ) {
+
+               $cfg->{$key} //= [];
+               push $cfg->{$key}->@*, $value;
+               next;
+           }
            $cfg->{$key} = $value;
        } else {
            warn "ignore config line: $line\n"