]> git.proxmox.com Git - pve-common.git/blobdiff - src/PVE/JSONSchema.pm
JSONSchema: add get_netmask_bits and missing netmask
[pve-common.git] / src / PVE / JSONSchema.pm
index a2394f74ede21d9f49105f876f42e3b32bf05d0d..d458ec13f87d57eed2b6d1fab691479dac5cf0a5 100644 (file)
@@ -4,6 +4,8 @@ use strict;
 use warnings;
 use Storable; # for dclone
 use Getopt::Long;
+use Encode::Locale;
+use Encode;
 use Devel::Cycle -quiet; # todo: remove?
 use PVE::Tools qw(split_list $IPV6RE $IPV4RE);
 use PVE::Exception qw(raise);
@@ -81,7 +83,13 @@ 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.',
     type => 'string',
     optional => 1,
-    maxLength => 40, # sha1 hex digest lenght is 40
+    maxLength => 40, # sha1 hex digest length is 40
+});
+
+register_standard_option('skiplock', {
+    description => "Ignore locks - only root is allowed to use this option.",
+    type => 'boolean',
+    optional => 1,
 });
 
 register_standard_option('extra-args', {
@@ -91,6 +99,26 @@ register_standard_option('extra-args', {
     optional => 1
 });
 
+register_standard_option('fingerprint-sha256', {
+    description => "Certificate SHA 256 fingerprint.",
+    type => 'string',
+    pattern => '([A-Fa-f0-9]{2}:){31}[A-Fa-f0-9]{2}',
+});
+
+register_standard_option('pve-output-format', {
+    type => 'string',
+    description => 'Output format.',
+    enum => [ 'text', 'json', 'json-pretty', 'yaml' ],
+    optional => 1,
+    default => 'text',
+});
+
+register_standard_option('pve-snapshot-name', {
+    description => "The name of the snapshot.",
+    type => 'string', format => 'pve-configid',
+    maxLength => 40,
+});
+
 my $format_list = {};
 
 sub register_format {
@@ -107,6 +135,22 @@ sub get_format {
     return $format_list->{$format};
 }
 
+my $renderer_hash = {};
+
+sub register_renderer {
+    my ($name, $code) = @_;
+
+    die "renderer '$name' already registered\n"
+       if $renderer_hash->{$name};
+
+    $renderer_hash->{$name} = $code;
+}
+
+sub get_renderer {
+    my ($name) = @_;
+    return $renderer_hash->{$name};
+}
+
 # register some common type for pve
 
 register_format('string', sub {}); # allow format => 'string-list'
@@ -166,6 +210,28 @@ sub pve_verify_node_name {
     return $node;
 }
 
+register_format('mac-addr', \&pve_verify_mac_addr);
+sub pve_verify_mac_addr {
+    my ($mac_addr, $noerr) = @_;
+
+    # don't allow I/G bit to be set, most of the time it breaks things, see:
+    # https://pve.proxmox.com/pipermail/pve-devel/2019-March/035998.html
+    if ($mac_addr !~ m/^[a-f0-9][02468ace](?::[a-f0-9]{2}){5}$/i) {
+       return undef if $noerr;
+       die "value does not look like a valid unicast MAC address\n";
+    }
+    return $mac_addr;
+
+}
+register_standard_option('mac-addr', {
+    type => 'string',
+    description => 'Unicast MAC address.',
+    verbose_description => 'A common MAC address with the I/G (Individual/Group) bit not set.',
+    format_description => "XX:XX:XX:XX:XX:XX",
+    optional => 1,
+    format => 'mac-addr',
+});
+
 register_format('ipv4', \&pve_verify_ipv4);
 sub pve_verify_ipv4 {
     my ($ipv4, $noerr) = @_;
@@ -200,6 +266,7 @@ sub pve_verify_ip {
 }
 
 my $ipv4_mask_hash = {
+    '0.0.0.0' => 0,
     '128.0.0.0' => 1,
     '192.0.0.0' => 2,
     '224.0.0.0' => 3,
@@ -229,9 +296,16 @@ my $ipv4_mask_hash = {
     '255.255.255.224' => 27,
     '255.255.255.240' => 28,
     '255.255.255.248' => 29,
-    '255.255.255.252' => 30
+    '255.255.255.252' => 30,
+    '255.255.255.254' => 31,
+    '255.255.255.255' => 32,
 };
 
+sub get_netmask_bits {
+    my ($mask) = @_;
+    return $ipv4_mask_hash->{$mask};
+}
+
 register_format('ipv4mask', \&pve_verify_ipv4mask);
 sub pve_verify_ipv4mask {
     my ($mask, $noerr) = @_;
@@ -247,7 +321,7 @@ register_format('CIDRv6', \&pve_verify_cidrv6);
 sub pve_verify_cidrv6 {
     my ($cidr, $noerr) = @_;
 
-    if ($cidr =~ m!^(?:$IPV6RE)(?:/(\d+))$! && ($1 > 7) &&  ($1 <= 120)) {
+    if ($cidr =~ m!^(?:$IPV6RE)(?:/(\d+))$! && ($1 > 7) && ($1 <= 128)) {
        return $cidr;
     }
 
@@ -305,8 +379,7 @@ register_format('email', \&pve_verify_email);
 sub pve_verify_email {
     my ($email, $noerr) = @_;
 
-    # we use same regex as in Utils.js
-    if ($email !~ /^(\w+)([\-+.][\w]+)*@(\w[\-\w]*\.){1,5}([A-Za-z]){2,63}$/) {
+    if ($email !~ /^[\w\+\-\~]+(\.[\w\+\-\~]+)*@[a-zA-Z0-9\-]+(\.[a-zA-Z0-9\-]+)*$/) {
           return undef if $noerr;
           die "value does not look like a valid email address\n";
     }
@@ -363,7 +436,7 @@ sub pve_verify_disk_size {
 }
 
 register_standard_option('spice-proxy', {
-    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 resonable setting is to use same node you use to connect to the API (This is window.location.hostname for the JS GUI).",
+    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).",
     type => 'string', format => 'address',
 }); 
 
@@ -390,6 +463,42 @@ sub pve_verify_startup_order {
     die "unable to parse startup options\n";
 }
 
+my %bwlimit_opt = (
+    optional => 1,
+    type => 'number', minimum => '0',
+    format_description => 'LIMIT',
+);
+
+my $bwlimit_format = {
+       default => {
+           %bwlimit_opt,
+           description => 'default bandwidth limit in KiB/s',
+       },
+       restore => {
+           %bwlimit_opt,
+           description => 'bandwidth limit in KiB/s for restoring guests from backups',
+       },
+       migration => {
+           %bwlimit_opt,
+           description => 'bandwidth limit in KiB/s for migrating guests (including moving local disks)',
+       },
+       clone => {
+           %bwlimit_opt,
+           description => 'bandwidth limit in KiB/s for cloning disks',
+       },
+       move => {
+           %bwlimit_opt,
+           description => 'bandwidth limit in KiB/s for moving disks',
+       },
+};
+register_format('bwlimit', $bwlimit_format);
+register_standard_option('bwlimit', {
+    description => "Set bandwidth/io limits various operations.",
+    optional => 1,
+    type => 'string',
+    format => $bwlimit_format,
+});
+
 sub pve_parse_startup_order {
     my ($value) = @_;
 
@@ -498,12 +607,30 @@ sub format_size {
     return "${tb}T";
 };
 
+sub parse_boolean {
+    my ($bool) = @_;
+    return 1 if $bool =~ m/^(1|on|yes|true)$/i;
+    return 0 if $bool =~ m/^(0|off|no|false)$/i;
+    return undef;
+}
+
 sub parse_property_string {
     my ($format, $data, $path, $additional_properties) = @_;
 
     # In property strings we default to not allowing additional properties
     $additional_properties = 0 if !defined($additional_properties);
 
+    # Support named formats here, too:
+    if (!ref($format)) {
+       if (my $desc = $format_list->{$format}) {
+           $format = $desc;
+       } else {
+           die "unknown format: $format\n";
+       }
+    } elsif (ref($format) ne 'HASH') {
+       die "unexpected format value of type ".ref($format)."\n";
+    }
+
     my $default_key;
 
     my $res = {};
@@ -525,8 +652,7 @@ sub parse_property_string {
 
            die "invalid key in comma-separated list property: $k\n" if !$schema;
            if ($schema->{type} && $schema->{type} eq 'boolean') {
-               $v = 1 if $v =~ m/^(1|on|yes|true)$/i;
-               $v = 0 if $v =~ m/^(0|off|no|false)$/i;
+               $v = parse_boolean($v) // $v;
            }
            $res->{$k} = $v;
        } elsif ($part !~ /=/) {
@@ -643,6 +769,9 @@ sub check_type {
                return undef;
            }
            return 1;
+       } elsif ($type eq 'string' && $vt eq 'Regexp') {
+           # qr// regexes can be used as strings and make sense for format=regex
+           return 1;
        } else {
            if ($vt) {
                add_error($errors, $path, "type check ('$type') failed - got $vt");
@@ -656,7 +785,7 @@ sub check_type {
                        return 1;
                    #} elsif ($value =~ m/^(0|false|no|off)$/i) {
                    } elsif ($value eq '0') {
-                       return 0;
+                       return 1; # return success (not value)
                    } else {
                        add_error($errors, $path, "type check ('$type') failed - got '$value'");
                        return undef;
@@ -715,7 +844,7 @@ sub check_object {
                    check_prop($value, $requires, $path, $errors);
                } elsif (!defined($value->{$requires})) {
                    add_error($errors, $path ? "$path.$requires" : $requires, 
-                             "missing property - '$newpath' requiers this property");
+                             "missing property - '$newpath' requires this property");
                }
            }
 
@@ -993,6 +1122,11 @@ my $default_schema_noref = {
            optional => 1,
            description => "This provides the title of the property",
        },
+       renderer => {
+           type => "string",
+           optional => 1,
+           description => "This is used to provide rendering hints to format cli command output.",
+       },
        requires => {
            type => [ "string", "object" ],
            optional => 1,
@@ -1067,6 +1201,11 @@ my $default_schema_noref = {
                },
            },
        },
+       print_width => {
+           type => "integer",
+           description => "For CLI context, this defines the maximal width to print before truncating",
+           optional => 1,
+       },
     }  
 };
 
@@ -1124,11 +1263,21 @@ my $method_schema = {
            description => "Method needs special privileges - only pvedaemon can execute it",            
            optional => 1,
         },
+        download => {
+            type => 'boolean',
+           description => "Method downloads the file content (filename is the return value of the method).",
+           optional => 1,
+        },
        proxyto => {
            type =>  'string',
            description => "A parameter name. If specified, all calls to this method are proxied to the host contained in that parameter.",
            optional => 1,
        },
+       proxyto_callback => {
+           type =>  'coderef',
+           description => "A function which is called to resolve the proxyto attribute. The default implementation returns the value of the 'proxyto' parameter.",
+           optional => 1,
+       },
         permissions => {
            type => 'object',
            description => "Required access permissions. By default only 'root' is allowed to access this method.",
@@ -1166,7 +1315,7 @@ my $method_schema = {
        },
         fragmentDelimiter => {
             type => 'string',
-           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.",            
+           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.",
            optional => 1,
         },
        parameters => {
@@ -1174,11 +1323,6 @@ my $method_schema = {
            description => "JSON Schema for parameters.",
            optional => 1,
        },
-        formatter => {
-           type => 'object',
-           description => "Used to store page formatter information (set by PVE::RESTHandler->register_page_formatter).",
-           optional => 1,
-        },
        returns => {
            type => 'object',
            description => "JSON Schema for return value.",
@@ -1186,7 +1330,7 @@ my $method_schema = {
        },
         code => {
            type => 'coderef',
-           description => "method implementaion (code reference)",
+           description => "method implementation (code reference)",
            optional => 1,
         },
        subclass => {
@@ -1256,7 +1400,7 @@ sub method_get_child_link {
 # a way to parse command line parameters, using a 
 # schema to configure Getopt::Long
 sub get_options {
-    my ($schema, $args, $arg_param, $fixed_param, $pwcallback) = @_;
+    my ($schema, $args, $arg_param, $fixed_param, $param_mapping_hash) = @_;
 
     if (!$schema || !$schema->{properties}) {
        raise("too many arguments\n", code => HTTP_BAD_REQUEST)
@@ -1272,17 +1416,19 @@ sub get_options {
        $list_param = $arg_param;
     }
 
+    my @interactive = ();
     my @getopt = ();
     foreach my $prop (keys %{$schema->{properties}}) {
        my $pd = $schema->{properties}->{$prop};
        next if $list_param && $prop eq $list_param;
        next if defined($fixed_param->{$prop});
 
-       if ($prop eq 'password' && $pwcallback) {
-           # we do not accept plain password on input line, instead
-           # we turn this into a boolean option and ask for password below
-           # using $pwcallback() (for security reasons).
-           push @getopt, "$prop";
+       my $mapping = $param_mapping_hash->{$prop};
+       if ($mapping && $mapping->{interactive}) {
+           # interactive parameters such as passwords: make the argument
+           # optional and call the mapping function afterwards.
+           push @getopt, "$prop:s";
+           push @interactive, [$prop, $mapping->{func}];
        } elsif ($pd->{type} eq 'boolean') {
            push @getopt, "$prop:s";
        } else {
@@ -1322,27 +1468,51 @@ sub get_options {
            raise("too many arguments\n", code => HTTP_BAD_REQUEST)
                if scalar(@$args) != 0;
        }
+    } else {
+       if (ref($arg_param)) {
+           foreach my $arg_name (@$arg_param) {
+               if ($arg_name eq 'extra-args') {
+                   $opts->{'extra-args'} = [];
+               } else {
+                   raise("not enough arguments\n", code => HTTP_BAD_REQUEST);
+               }
+           }
+       }
     }
 
-    if (my $pd = $schema->{properties}->{password}) {
-       if ($pd->{type} ne 'boolean' && $pwcallback) {
-           if ($opts->{password} || !$pd->{optional}) {
-               $opts->{password} = &$pwcallback(); 
-           }
+    foreach my $entry (@interactive) {
+       my ($opt, $func) = @$entry;
+       my $pd = $schema->{properties}->{$opt};
+       my $value = $opts->{$opt};
+       if (defined($value) || !$pd->{optional}) {
+           $opts->{$opt} = $func->($value);
        }
     }
 
-    $opts = PVE::Tools::decode_utf8_parameters($opts);
+    # decode after Getopt as we are not sure how well it handles unicode
+    foreach my $p (keys %$opts) {
+       if (!ref($opts->{$p})) {
+           $opts->{$p} = decode('locale', $opts->{$p});
+       } elsif (ref($opts->{$p}) eq 'ARRAY') {
+           my $tmp = [];
+           foreach my $v (@{$opts->{$p}}) {
+               push @$tmp, decode('locale', $v);
+           }
+           $opts->{$p} = $tmp;
+       } elsif (ref($opts->{$p}) eq 'SCALAR') {
+           $opts->{$p} = decode('locale', $$opts->{$p});
+       } else {
+           raise("decoding options failed, unknown reference\n", code => HTTP_BAD_REQUEST);
+       }
+    }
 
     foreach my $p (keys %$opts) {
        if (my $pd = $schema->{properties}->{$p}) {
            if ($pd->{type} eq 'boolean') {
                if ($opts->{$p} eq '') {
                    $opts->{$p} = 1;
-               } elsif ($opts->{$p} =~ m/^(1|true|yes|on)$/i) {
-                   $opts->{$p} = 1;
-               } elsif ($opts->{$p} =~ m/^(0|false|no|off)$/i) {
-                   $opts->{$p} = 0;
+               } elsif (defined(my $bool = parse_boolean($opts->{$p}))) {
+                   $opts->{$p} = $bool;
                } else {
                    raise("unable to parse boolean option\n", code => HTTP_BAD_REQUEST);
                }
@@ -1395,8 +1565,7 @@ sub parse_config {
            if ($schema->{properties}->{$key} && 
                $schema->{properties}->{$key}->{type} eq 'boolean') {
 
-               $value = 1 if $value =~ m/^(1|on|yes|true)$/i; 
-               $value = 0 if $value =~ m/^(0|off|no|false)$/i; 
+               $value = parse_boolean($value) // $value;
            }
            $cfg->{$key} = $value;
        } else {
@@ -1469,7 +1638,7 @@ my $find_schema_default_key = sub {
 };
 
 sub generate_typetext {
-    my ($format) = @_;
+    my ($format, $list_enums) = @_;
 
     my ($default_key, $keyAliasProps) = &$find_schema_default_key($format);
 
@@ -1502,7 +1671,11 @@ sub generate_typetext {
        } elsif (my $text = $phash->{typetext}) {
            $typetext .= $text;
        } elsif (my $enum = $phash->{enum}) {
-           $typetext .= '<' . join('|', @$enum) . '>';
+           if ($list_enums || (scalar(@$enum) <= 3)) {
+               $typetext .= '<' . join('|', @$enum) . '>';
+           } else {
+               $typetext .= '<enum>';
+           }
        } elsif ($phash->{type} eq 'boolean') {
            $typetext .= '<1|0>';
        } elsif ($phash->{type} eq 'integer') {
@@ -1658,7 +1831,7 @@ sub print_property_string {
 }
 
 sub schema_get_type_text {
-    my ($phash) = @_;
+    my ($phash, $style) = @_;
 
     my $type = $phash->{type} || 'string';
 
@@ -1667,27 +1840,31 @@ sub schema_get_type_text {
     } elsif ($phash->{format_description}) {
        return "<$phash->{format_description}>";
     } elsif ($phash->{enum}) {
-       return "(" . join(' | ', sort @{$phash->{enum}}) . ")";
+       return "<" . join(' | ', sort @{$phash->{enum}}) . ">";
     } elsif ($phash->{pattern}) {
        return $phash->{pattern};
     } elsif ($type eq 'integer' || $type eq 'number') {
+       # NOTE: always access values as number (avoid converion to string)
        if (defined($phash->{minimum}) && defined($phash->{maximum})) {
-           return "$type ($phash->{minimum} - $phash->{maximum})";
+           return "<$type> (" . ($phash->{minimum} + 0) . " - " .
+               ($phash->{maximum} + 0) . ")";
        } elsif (defined($phash->{minimum})) {
-           return "$type ($phash->{minimum} - N)";
+           return "<$type> (" . ($phash->{minimum} + 0) . " - N)";
        } elsif (defined($phash->{maximum})) {
-           return "$type (-N - $phash->{maximum})";
+           return "<$type> (-N - " . ($phash->{maximum} + 0) . ")";
        }
     } elsif ($type eq 'string') {
        if (my $format = $phash->{format}) {
            $format = get_format($format) if ref($format) ne 'HASH';
            if (ref($format) eq 'HASH') {
-               return generate_typetext($format);
+               my $list_enums = 0;
+               $list_enums = 1 if $style && $style eq 'config-sub';
+               return generate_typetext($format, $list_enums);
            }
        }
     }
 
-    return $type;
+    return "<$type>";
 }
 
 1;