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);
use HTTP::Status qw(:constants);
use Net::IP qw(:PROC);
+use Data::Dumper;
use base 'Exporter';
maxLength => 40, # sha1 hex digest lenght 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', {
description => "Extra arguments as array",
type => 'array',
'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,
};
register_format('ipv4mask', \&pve_verify_ipv4mask);
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 = {};
my ($k, $v) = ($1, $2);
die "duplicate key in comma-separated list property: $k\n" if defined($res->{$k});
my $schema = $format->{$k};
- if (my $group = $schema->{group}) {
- die "keys $res->{$group} and $k are part of the same group and cannot be used together\n"
- if defined($res->{$group});
- $res->{$group} = $k;
- $schema = $format->{$group};
- }
if (my $alias = $schema->{alias}) {
+ if (my $key_alias = $schema->{keyAlias}) {
+ die "key alias '$key_alias' is already defined\n" if defined($res->{$key_alias});
+ $res->{$key_alias} = $k;
+ }
$k = $alias;
$schema = $format->{$k};
}
+
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 !~ /=/) {
return $res;
}
-sub print_property_string {
- my ($data, $format, $skip, $path) = @_;
-
- if (ref($format) ne 'HASH') {
- my $schema = $format_list->{$format};
- die "not a valid format: $format\n" if !$schema;
- $format = $schema;
- }
-
- my $errors = {};
- check_object($path, $format, $data, undef, $errors);
- if (scalar(%$errors)) {
- raise "format error", errors => $errors;
- }
-
- my $default_key;
- my %skipped = map { $_ => 1 } @$skip;
- my %allowed;
- my %required; # this is a set, all present keys are required regardless of value
- my %group_for_key;
- foreach my $key (keys %$format) {
- $allowed{$key} = 1;
- my $keyfmt = $format->{$key};
- my $group = $keyfmt->{group};
- if (defined($group)) {
- $skipped{$group} = 1;
- if (defined(my $grpalias = $format->{$group}->{alias})) {
- $group_for_key{$grpalias} = $group;
- } else {
- $group_for_key{$key} = $group;
- }
- }
- if (!$keyfmt->{optional} && !$keyfmt->{alias} && !defined($group) && !$skipped{$key}) {
- $required{$key} = 1;
- }
-
- # Skip default keys
- if ($keyfmt->{default_key}) {
- if ($default_key) {
- warn "multiple default keys in schema ($default_key, $key)\n";
- } else {
- $default_key = $key;
- $skipped{$key} = 1;
- }
- }
- }
-
- my ($text, $comma);
- if ($default_key && !defined($format->{$default_key}->{alias})) {
- $text = "$data->{$default_key}";
- $comma = ',';
- } else {
- $text = '';
- $comma = '';
- }
-
- foreach my $key (sort keys %$data) {
- delete $required{$key};
- next if $skipped{$key};
- die "invalid key: $key\n" if !$allowed{$key};
-
- my $keyfmt = $format->{$key};
- my $typeformat = $keyfmt->{format};
- my $value = $data->{$key};
- next if !defined($value);
- if (my $group = $group_for_key{$key}) {
- $key = $data->{$group};
- }
- $text .= $comma;
- $comma = ',';
- if ($typeformat && $typeformat eq 'disk-size') {
- $text .= "$key=" . format_size($value);
- } else {
- die "illegal value with commas for $key\n" if $value =~ /,/;
- $text .= "$key=$value";
- }
- }
-
- if (my $missing = join(',', keys %required)) {
- die "missing properties: $missing\n";
- }
-
- return $text;
-}
-
sub add_error {
my ($errors, $path, $msg) = @_;
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");
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;
return;
}
- my %groups;
- foreach my $k (keys %$schema) {
- if (defined(my $group = $schema->{$k}->{group})) {
- # When a group is aliased then the key/value pair will match the
- # schema, but if it's not then the group key contains the key-name
- # which will not match the group key's defined schema and we have
- # to match it against that...
- if (!defined($schema->{$group}->{alias})) {
- $groups{$group} = 1;
- }
- }
- }
foreach my $k (keys %$schema) {
- my $orig_key = $k;
- my $v;
- if ($groups{$k}) {
- if (defined($orig_key = $value->{$k})) {
- $v = $value->{$orig_key};
- } else {
- $orig_key = $k; # now only used for the 'path' parameter
- }
- } else {
- $v = $value->{$k};
- }
- check_prop($v, $schema->{$k}, $path ? "$path.$orig_key" : $orig_key, $errors);
+ check_prop($value->{$k}, $schema->{$k}, $path ? "$path.$k" : $k, $errors);
}
foreach my $k (keys %$value) {
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");
}
}
optional => 1,
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).",
},
+ verbose_description => {
+ type => "string",
+ optional => 1,
+ description => "This provides a more verbose description.",
+ },
format_description => {
type => "string",
optional => 1,
optional => 1,
description => "When a key represents the same property as another it can be an alias to it, causing the parsed datastructure to use the other key to store the current value under.",
},
- group => {
+ keyAlias => {
type => 'string',
optional => 1,
- description => "If a key is part of a group then setting it will additionally set the group name in the resulting data structure to the key used to fill the group. Only one key of a group can be assigned.",
+ description => "Allows to store the current 'key' as value of another property. Only valid if used together with 'alias'.",
+ requires => 'alias',
},
default => {
type => "any",
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 implementaion 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.",
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.",
}
}
- $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);
}
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 {
return $data;
}
+# helpers used to generate our manual pages
+
+my $find_schema_default_key = sub {
+ my ($format) = @_;
+
+ my $default_key;
+ my $keyAliasProps = {};
+
+ foreach my $key (keys %$format) {
+ my $phash = $format->{$key};
+ if ($phash->{default_key}) {
+ die "multiple default keys in schema ($default_key, $key)\n"
+ if defined($default_key);
+ die "default key '$key' is an alias - this is not allowed\n"
+ if defined($phash->{alias});
+ die "default key '$key' with keyAlias attribute is not allowed\n"
+ if $phash->{keyAlias};
+ $default_key = $key;
+ }
+ my $key_alias = $phash->{keyAlias};
+ die "found keyAlias without 'alias definition for '$key'\n"
+ if $key_alias && !$phash->{alias};
+
+ if ($phash->{alias} && $key_alias) {
+ die "inconsistent keyAlias '$key_alias' definition"
+ if defined($keyAliasProps->{$key_alias}) &&
+ $keyAliasProps->{$key_alias} ne $phash->{alias};
+ $keyAliasProps->{$key_alias} = $phash->{alias};
+ }
+ }
+
+ return wantarray ? ($default_key, $keyAliasProps) : $default_key;
+};
+
+sub generate_typetext {
+ my ($format, $list_enums) = @_;
+
+ my ($default_key, $keyAliasProps) = &$find_schema_default_key($format);
+
+ my $res = '';
+ my $add_sep = 0;
+
+ my $add_option_string = sub {
+ my ($text, $optional) = @_;
+
+ if ($add_sep) {
+ $text = ",$text";
+ $res .= ' ';
+ }
+ $text = "[$text]" if $optional;
+ $res .= $text;
+ $add_sep = 1;
+ };
+
+ my $format_key_value = sub {
+ my ($key, $phash) = @_;
+
+ die "internal error" if defined($phash->{alias});
+
+ my $keytext = $key;
+
+ my $typetext = '';
+
+ if (my $desc = $phash->{format_description}) {
+ $typetext .= "<$desc>";
+ } elsif (my $text = $phash->{typetext}) {
+ $typetext .= $text;
+ } elsif (my $enum = $phash->{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') {
+ $typetext .= '<integer>';
+ } elsif ($phash->{type} eq 'number') {
+ $typetext .= '<number>';
+ } else {
+ die "internal error: neither format_description nor typetext found for option '$key'";
+ }
+
+ if (defined($default_key) && ($default_key eq $key)) {
+ &$add_option_string("[$keytext=]$typetext", $phash->{optional});
+ } else {
+ &$add_option_string("$keytext=$typetext", $phash->{optional});
+ }
+ };
+
+ my $done = {};
+
+ my $cond_add_key = sub {
+ my ($key) = @_;
+
+ return if $done->{$key}; # avoid duplicates
+
+ $done->{$key} = 1;
+
+ my $phash = $format->{$key};
+
+ return if !$phash; # should not happen
+
+ return if $phash->{alias};
+
+ &$format_key_value($key, $phash);
+
+ };
+
+ &$cond_add_key($default_key) if defined($default_key);
+
+ # add required keys first
+ foreach my $key (sort keys %$format) {
+ my $phash = $format->{$key};
+ &$cond_add_key($key) if $phash && !$phash->{optional};
+ }
+
+ # add the rest
+ foreach my $key (sort keys %$format) {
+ &$cond_add_key($key);
+ }
+
+ foreach my $keyAlias (sort keys %$keyAliasProps) {
+ &$add_option_string("<$keyAlias>=<$keyAliasProps->{$keyAlias }>", 1);
+ }
+
+ return $res;
+}
+
+sub print_property_string {
+ my ($data, $format, $skip, $path) = @_;
+
+ if (ref($format) ne 'HASH') {
+ my $schema = get_format($format);
+ die "not a valid format: $format\n" if !$schema;
+ $format = $schema;
+ }
+
+ my $errors = {};
+ check_object($path, $format, $data, undef, $errors);
+ if (scalar(%$errors)) {
+ raise "format error", errors => $errors;
+ }
+
+ my ($default_key, $keyAliasProps) = &$find_schema_default_key($format);
+
+ my $res = '';
+ my $add_sep = 0;
+
+ my $add_option_string = sub {
+ my ($text) = @_;
+
+ $res .= ',' if $add_sep;
+ $res .= $text;
+ $add_sep = 1;
+ };
+
+ my $format_value = sub {
+ my ($key, $value, $format) = @_;
+
+ if (defined($format) && ($format eq 'disk-size')) {
+ return format_size($value);
+ } else {
+ die "illegal value with commas for $key\n" if $value =~ /,/;
+ return $value;
+ }
+ };
+
+ my $done = { map { $_ => 1 } @$skip };
+
+ my $cond_add_key = sub {
+ my ($key, $isdefault) = @_;
+
+ return if $done->{$key}; # avoid duplicates
+
+ $done->{$key} = 1;
+
+ my $value = $data->{$key};
+
+ return if !defined($value);
+
+ my $phash = $format->{$key};
+
+ # try to combine values if we have key aliases
+ if (my $combine = $keyAliasProps->{$key}) {
+ if (defined(my $combine_value = $data->{$combine})) {
+ my $combine_format = $format->{$combine}->{format};
+ my $value_str = &$format_value($key, $value, $phash->{format});
+ my $combine_str = &$format_value($combine, $combine_value, $combine_format);
+ &$add_option_string("${value_str}=${combine_str}");
+ $done->{$combine} = 1;
+ return;
+ }
+ }
+
+ if ($phash && $phash->{alias}) {
+ $phash = $format->{$phash->{alias}};
+ }
+
+ die "invalid key '$key'\n" if !$phash;
+ die "internal error" if defined($phash->{alias});
+
+ my $value_str = &$format_value($key, $value, $phash->{format});
+ if ($isdefault) {
+ &$add_option_string($value_str);
+ } else {
+ &$add_option_string("$key=${value_str}");
+ }
+ };
+
+ # add default key first
+ &$cond_add_key($default_key, 1) if defined($default_key);
+
+ # add required keys first
+ foreach my $key (sort keys %$data) {
+ my $phash = $format->{$key};
+ &$cond_add_key($key) if $phash && !$phash->{optional};
+ }
+
+ # add the rest
+ foreach my $key (sort keys %$data) {
+ &$cond_add_key($key);
+ }
+
+ return $res;
+}
+
+sub schema_get_type_text {
+ my ($phash, $style) = @_;
+
+ my $type = $phash->{type} || 'string';
+
+ if ($phash->{typetext}) {
+ return $phash->{typetext};
+ } elsif ($phash->{format_description}) {
+ return "<$phash->{format_description}>";
+ } elsif ($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} + 0) . " - " .
+ ($phash->{maximum} + 0) . ")";
+ } elsif (defined($phash->{minimum})) {
+ return "<$type> (" . ($phash->{minimum} + 0) . " - N)";
+ } elsif (defined($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') {
+ my $list_enums = 0;
+ $list_enums = 1 if $style && $style eq 'config-sub';
+ return generate_typetext($format, $list_enums);
+ }
+ }
+ }
+
+ return "<$type>";
+}
+
1;