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/
});
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 = {};
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";
}
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) = @_;
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) = @_;
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,
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 {
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 {
$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";
}
raise "format error\n", errors => $errors;
}
+ return $validator->($res) if $validator;
return $res;
}
$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);
}
$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;
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);
}
}
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;
}
raise "format error", errors => $errors;
}
+ $data = $validator->($data) if $validator;
+
my ($default_key, $keyAliasProps) = &$find_schema_default_key($format);
my $res = '';