minLength => 2, maxLength => 20,
});
-PVE::JSONSchema::register_standard_option('pve-storage-id', {
+register_standard_option('pve-storage-id', {
description => "The storage identifier.",
type => 'string', format => 'pve-storage-id',
});
-PVE::JSONSchema::register_standard_option('pve-config-digest', {
+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
});
-PVE::JSONSchema::register_standard_option('extra-args', {
+register_standard_option('extra-args', {
description => "Extra arguments as array",
type => 'array',
items => { type => 'string' },
register_format('string', sub {}); # allow format => 'string-list'
+register_format('urlencoded', \&pve_verify_urlencoded);
+sub pve_verify_urlencoded {
+ my ($text, $noerr) = @_;
+ if ($text !~ /^[-%a-zA-Z0-9_.!~*'()]*$/) {
+ return undef if $noerr;
+ die "invalid urlencoded string: $text\n";
+ }
+ return $text;
+}
+
register_format('pve-configid', \&pve_verify_configid);
sub pve_verify_configid {
my ($id, $noerr) = @_;
sub pve_verify_vmid {
my ($vmid, $noerr) = @_;
- if ($vmid !~ m/^[1-9][0-9]+$/) {
+ if ($vmid !~ m/^[1-9][0-9]{2,8}$/) {
return undef if $noerr;
die "value does not look like a valid VM ID\n";
}
sub pve_verify_cidrv4 {
my ($cidr, $noerr) = @_;
- if ($cidr =~ m!^(?:$IPV4RE)(?:/(\d+))$! && ($1 > 7) && ($1 < 32)) {
+ if ($cidr =~ m!^(?:$IPV4RE)(?:/(\d+))$! && ($1 > 7) && ($1 <= 32)) {
return $cidr;
}
return $addr;
}
+register_format('disk-size', \&pve_verify_disk_size);
+sub pve_verify_disk_size {
+ my ($size, $noerr) = @_;
+ if (!defined(parse_size($size))) {
+ return undef if $noerr;
+ die "value does not look like a valid disk size: $size\n";
+ }
+ return $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).",
type => 'string', format => 'address',
}
}
+sub parse_size {
+ my ($value) = @_;
+
+ return undef if $value !~ m/^(\d+(\.\d+)?)([KMGT])?$/;
+ my ($size, $unit) = ($1, $3);
+ if ($unit) {
+ if ($unit eq 'K') {
+ $size = $size * 1024;
+ } elsif ($unit eq 'M') {
+ $size = $size * 1024 * 1024;
+ } elsif ($unit eq 'G') {
+ $size = $size * 1024 * 1024 * 1024;
+ } elsif ($unit eq 'T') {
+ $size = $size * 1024 * 1024 * 1024 * 1024;
+ }
+ }
+ return int($size);
+};
+
+sub format_size {
+ my ($size) = @_;
+
+ $size = int($size);
+
+ my $kb = int($size/1024);
+ return $size if $kb*1024 != $size;
+
+ my $mb = int($kb/1024);
+ return "${kb}K" if $mb*1024 != $kb;
+
+ my $gb = int($mb/1024);
+ return "${mb}M" if $gb*1024 != $mb;
+
+ my $tb = int($gb/1024);
+ return "${gb}G" if $tb*1024 != $gb;
+
+ return "${tb}T";
+};
+
sub parse_property_string {
- my ($format, $data, $path) = @_;
+ my ($format, $data, $path, $additional_properties) = @_;
+
+ # In property strings we default to not allowing additional properties
+ $additional_properties = 0 if !defined($additional_properties);
my $default_key;
if ($part =~ /^([^=]+)=(.+)$/) {
my ($k, $v) = ($1, $2);
- die "duplicate key in comma-separated list property: $k" if defined($res->{$k});
+ die "duplicate key in comma-separated list property: $k\n" if defined($res->{$k});
my $schema = $format->{$k};
- die "invalid key in comma-separated list property: $k" if !$schema;
+ if (my $alias = $schema->{alias}) {
+ $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;
}
$res->{$k} = $v;
} elsif ($part !~ /=/) {
- die "duplicate key in comma-separated list property: $default_key" if $default_key;
+ die "duplicate key in comma-separated list property: $default_key\n" if $default_key;
foreach my $key (keys %$format) {
if ($format->{$key}->{default_key}) {
$default_key = $key;
$res->{$default_key} = $part;
last;
}
- die "duplicate key in comma-separated list property: $default_key";
+ die "duplicate key in comma-separated list property: $default_key\n";
}
}
+ die "value without key, but schema does not define a default key\n" if !$default_key;
} else {
- die "missing key in comma-separated list property";
+ die "missing key in comma-separated list property\n";
}
}
my $errors = {};
- check_object($path, $format, $res, undef, $errors);
+ check_object($path, $format, $res, $additional_properties, $errors);
if (scalar(%$errors)) {
- raise "format error", errors => $errors;
+ raise "format error\n", errors => $errors;
}
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
+ foreach my $key (keys %$format) {
+ $allowed{$key} = 1;
+ if (!$format->{$key}->{optional} && !$format->{$key}->{alias} && !$skipped{$key}) {
+ $required{$key} = 1;
+ }
+
+ # Skip default keys
+ if ($format->{$key}->{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) {
+ $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 $typeformat = $format->{$key}->{format};
+ my $value = $data->{$key};
+ next if !defined($value);
+ $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) = @_;
if (!defined ($value)) {
return if $schema->{type} && $schema->{type} eq 'null';
- if (!$schema->{optional}) {
+ if (!$schema->{optional} && !$schema->{alias}) {
add_error($errors, $path, "property is missing and it is not optional");
}
return;
optional => 1,
description => "Whether this is the default key in a comma separated list property string.",
},
+ alias => {
+ type => 'string',
+ 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.",
+ },
default => {
type => "any",
optional => 1,