]> git.proxmox.com Git - pve-common.git/blame - src/PVE/JSONSchema.pm
generate_typetext: better handling of only-optional cases
[pve-common.git] / src / PVE / JSONSchema.pm
CommitLineData
e143e9d8
DM
1package PVE::JSONSchema;
2
e143e9d8 3use strict;
c36f332e 4use warnings;
e143e9d8
DM
5use Storable; # for dclone
6use Getopt::Long;
7use Devel::Cycle -quiet; # todo: remove?
e272bcb7 8use PVE::Tools qw(split_list $IPV6RE $IPV4RE);
e143e9d8
DM
9use PVE::Exception qw(raise);
10use HTTP::Status qw(:constants);
23b56245 11use Net::IP qw(:PROC);
e143e9d8
DM
12
13use base 'Exporter';
14
15our @EXPORT_OK = qw(
16register_standard_option
17get_standard_option
18);
19
20# Note: This class implements something similar to JSON schema, but it is not 100% complete.
21# see: http://tools.ietf.org/html/draft-zyp-json-schema-02
22# see: http://json-schema.org/
23
24# the code is similar to the javascript parser from http://code.google.com/p/jsonschema/
25
26my $standard_options = {};
27sub register_standard_option {
28 my ($name, $schema) = @_;
29
30 die "standard option '$name' already registered\n"
31 if $standard_options->{$name};
32
33 $standard_options->{$name} = $schema;
34}
35
36sub get_standard_option {
37 my ($name, $base) = @_;
38
39 my $std = $standard_options->{$name};
3432ae0c 40 die "no such standard option '$name'\n" if !$std;
e143e9d8
DM
41
42 my $res = $base || {};
43
44 foreach my $opt (keys %$std) {
c38ac70f 45 next if defined($res->{$opt});
e143e9d8
DM
46 $res->{$opt} = $std->{$opt};
47 }
48
49 return $res;
50};
51
52register_standard_option('pve-vmid', {
53 description => "The (unique) ID of the VM.",
54 type => 'integer', format => 'pve-vmid',
55 minimum => 1
56});
57
58register_standard_option('pve-node', {
59 description => "The cluster node name.",
60 type => 'string', format => 'pve-node',
61});
62
63register_standard_option('pve-node-list', {
64 description => "List of cluster node names.",
65 type => 'string', format => 'pve-node-list',
66});
67
68register_standard_option('pve-iface', {
69 description => "Network interface name.",
70 type => 'string', format => 'pve-iface',
71 minLength => 2, maxLength => 20,
72});
73
05e787c5
DM
74PVE::JSONSchema::register_standard_option('pve-storage-id', {
75 description => "The storage identifier.",
76 type => 'string', format => 'pve-storage-id',
77});
78
dc5eae7d
DM
79PVE::JSONSchema::register_standard_option('pve-config-digest', {
80 description => 'Prevent changes if current configuration file has different SHA1 digest. This can be used to prevent concurrent modifications.',
81 type => 'string',
82 optional => 1,
83 maxLength => 40, # sha1 hex digest lenght is 40
84});
85
5851be88
WB
86PVE::JSONSchema::register_standard_option('extra-args', {
87 description => "Extra arguments as array",
88 type => 'array',
89 items => { type => 'string' },
90 optional => 1
91});
92
e143e9d8
DM
93my $format_list = {};
94
95sub register_format {
96 my ($format, $code) = @_;
97
98 die "JSON schema format '$format' already registered\n"
99 if $format_list->{$format};
100
101 $format_list->{$format} = $code;
102}
103
104# register some common type for pve
8ba7c72b
DM
105
106register_format('string', sub {}); # allow format => 'string-list'
107
e143e9d8
DM
108register_format('pve-configid', \&pve_verify_configid);
109sub pve_verify_configid {
110 my ($id, $noerr) = @_;
111
112 if ($id !~ m/^[a-z][a-z0-9_]+$/i) {
113 return undef if $noerr;
39ed3462 114 die "invalid configuration ID '$id'\n";
e143e9d8
DM
115 }
116 return $id;
117}
118
05e787c5
DM
119PVE::JSONSchema::register_format('pve-storage-id', \&parse_storage_id);
120sub parse_storage_id {
121 my ($storeid, $noerr) = @_;
122
123 if ($storeid !~ m/^[a-z][a-z0-9\-\_\.]*[a-z0-9]$/i) {
124 return undef if $noerr;
125 die "storage ID '$storeid' contains illegal characters\n";
126 }
127 return $storeid;
128}
129
130
e143e9d8
DM
131register_format('pve-vmid', \&pve_verify_vmid);
132sub pve_verify_vmid {
133 my ($vmid, $noerr) = @_;
134
135 if ($vmid !~ m/^[1-9][0-9]+$/) {
136 return undef if $noerr;
137 die "value does not look like a valid VM ID\n";
138 }
139 return $vmid;
140}
141
142register_format('pve-node', \&pve_verify_node_name);
143sub pve_verify_node_name {
144 my ($node, $noerr) = @_;
145
e6db55c0 146 if ($node !~ m/^([a-zA-Z0-9]([a-zA-Z0-9\-]*[a-zA-Z0-9])?)$/) {
e143e9d8
DM
147 return undef if $noerr;
148 die "value does not look like a valid node name\n";
149 }
150 return $node;
151}
152
153register_format('ipv4', \&pve_verify_ipv4);
154sub pve_verify_ipv4 {
155 my ($ipv4, $noerr) = @_;
156
ed5880ac
DM
157 if ($ipv4 !~ m/^(?:$IPV4RE)$/) {
158 return undef if $noerr;
159 die "value does not look like a valid IPv4 address\n";
e143e9d8
DM
160 }
161 return $ipv4;
162}
a13c6f08 163
ed5880ac 164register_format('ipv6', \&pve_verify_ipv6);
93276209 165sub pve_verify_ipv6 {
ed5880ac
DM
166 my ($ipv6, $noerr) = @_;
167
168 if ($ipv6 !~ m/^(?:$IPV6RE)$/) {
169 return undef if $noerr;
170 die "value does not look like a valid IPv6 address\n";
171 }
172 return $ipv6;
173}
174
175register_format('ip', \&pve_verify_ip);
176sub pve_verify_ip {
177 my ($ip, $noerr) = @_;
178
179 if ($ip !~ m/^(?:(?:$IPV4RE)|(?:$IPV6RE))$/) {
180 return undef if $noerr;
181 die "value does not look like a valid IP address\n";
182 }
183 return $ip;
184}
185
a13c6f08
DM
186my $ipv4_mask_hash = {
187 '128.0.0.0' => 1,
188 '192.0.0.0' => 2,
189 '224.0.0.0' => 3,
190 '240.0.0.0' => 4,
191 '248.0.0.0' => 5,
192 '252.0.0.0' => 6,
193 '254.0.0.0' => 7,
194 '255.0.0.0' => 8,
195 '255.128.0.0' => 9,
196 '255.192.0.0' => 10,
197 '255.224.0.0' => 11,
198 '255.240.0.0' => 12,
199 '255.248.0.0' => 13,
200 '255.252.0.0' => 14,
201 '255.254.0.0' => 15,
202 '255.255.0.0' => 16,
203 '255.255.128.0' => 17,
204 '255.255.192.0' => 18,
205 '255.255.224.0' => 19,
206 '255.255.240.0' => 20,
207 '255.255.248.0' => 21,
208 '255.255.252.0' => 22,
209 '255.255.254.0' => 23,
210 '255.255.255.0' => 24,
211 '255.255.255.128' => 25,
212 '255.255.255.192' => 26,
213 '255.255.255.224' => 27,
214 '255.255.255.240' => 28,
215 '255.255.255.248' => 29,
216 '255.255.255.252' => 30
217};
218
e143e9d8
DM
219register_format('ipv4mask', \&pve_verify_ipv4mask);
220sub pve_verify_ipv4mask {
221 my ($mask, $noerr) = @_;
222
a13c6f08 223 if (!defined($ipv4_mask_hash->{$mask})) {
e143e9d8
DM
224 return undef if $noerr;
225 die "value does not look like a valid IP netmask\n";
226 }
227 return $mask;
228}
229
703c1f88
WB
230register_format('CIDRv6', \&pve_verify_cidrv6);
231sub pve_verify_cidrv6 {
e272bcb7
DM
232 my ($cidr, $noerr) = @_;
233
703c1f88 234 if ($cidr =~ m!^(?:$IPV6RE)(?:/(\d+))$! && ($1 > 7) && ($1 <= 120)) {
e272bcb7 235 return $cidr;
703c1f88
WB
236 }
237
238 return undef if $noerr;
239 die "value does not look like a valid IPv6 CIDR network\n";
240}
241
242register_format('CIDRv4', \&pve_verify_cidrv4);
243sub pve_verify_cidrv4 {
244 my ($cidr, $noerr) = @_;
245
246 if ($cidr =~ m!^(?:$IPV4RE)(?:/(\d+))$! && ($1 > 7) && ($1 < 32)) {
e272bcb7
DM
247 return $cidr;
248 }
249
250 return undef if $noerr;
703c1f88
WB
251 die "value does not look like a valid IPv4 CIDR network\n";
252}
253
254register_format('CIDR', \&pve_verify_cidr);
255sub pve_verify_cidr {
256 my ($cidr, $noerr) = @_;
257
258 if (!(pve_verify_cidrv4($cidr, 1) ||
259 pve_verify_cidrv6($cidr, 1)))
260 {
261 return undef if $noerr;
262 die "value does not look like a valid CIDR network\n";
263 }
264
265 return $cidr;
266}
267
268register_format('pve-ipv4-config', \&pve_verify_ipv4_config);
269sub pve_verify_ipv4_config {
270 my ($config, $noerr) = @_;
271
272 return $config if $config =~ /^(?:dhcp|manual)$/ ||
273 pve_verify_cidrv4($config, 1);
274 return undef if $noerr;
275 die "value does not look like a valid ipv4 network configuration\n";
276}
277
278register_format('pve-ipv6-config', \&pve_verify_ipv6_config);
279sub pve_verify_ipv6_config {
280 my ($config, $noerr) = @_;
281
282 return $config if $config =~ /^(?:auto|dhcp|manual)$/ ||
283 pve_verify_cidrv6($config, 1);
284 return undef if $noerr;
285 die "value does not look like a valid ipv6 network configuration\n";
e272bcb7
DM
286}
287
e143e9d8
DM
288register_format('email', \&pve_verify_email);
289sub pve_verify_email {
290 my ($email, $noerr) = @_;
291
87cb0e60
EK
292 # we use same regex as in Utils.js
293 if ($email !~ /^(\w+)([\-+.][\w]+)*@(\w[\-\w]*\.){1,5}([A-Za-z]){2,63}$/) {
e143e9d8
DM
294 return undef if $noerr;
295 die "value does not look like a valid email address\n";
296 }
297 return $email;
298}
299
34ebb226
DM
300register_format('dns-name', \&pve_verify_dns_name);
301sub pve_verify_dns_name {
302 my ($name, $noerr) = @_;
303
ce33e978 304 my $namere = "([a-zA-Z0-9]([a-zA-Z0-9\-]*[a-zA-Z0-9])?)";
34ebb226
DM
305
306 if ($name !~ /^(${namere}\.)*${namere}$/) {
307 return undef if $noerr;
308 die "value does not look like a valid DNS name\n";
309 }
310 return $name;
311}
312
e143e9d8
DM
313# network interface name
314register_format('pve-iface', \&pve_verify_iface);
315sub pve_verify_iface {
316 my ($id, $noerr) = @_;
317
318 if ($id !~ m/^[a-z][a-z0-9_]{1,20}([:\.]\d+)?$/i) {
319 return undef if $noerr;
320 die "invalid network interface name '$id'\n";
321 }
322 return $id;
323}
324
d07b7084
WB
325# general addresses by name or IP
326register_format('address', \&pve_verify_address);
327sub pve_verify_address {
328 my ($addr, $noerr) = @_;
329
330 if (!(pve_verify_ip($addr, 1) ||
331 pve_verify_dns_name($addr, 1)))
332 {
333 return undef if $noerr;
334 die "value does not look like a valid address: $addr\n";
335 }
336 return $addr;
337}
338
f0a10afc
DM
339register_standard_option('spice-proxy', {
340 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).",
d07b7084 341 type => 'string', format => 'address',
f0a10afc
DM
342});
343
344register_standard_option('remote-viewer-config', {
345 description => "Returned values can be directly passed to the 'remote-viewer' application.",
346 additionalProperties => 1,
347 properties => {
348 type => { type => 'string' },
349 password => { type => 'string' },
350 proxy => { type => 'string' },
351 host => { type => 'string' },
352 'tls-port' => { type => 'integer' },
353 },
354});
355
c70c3bbc 356register_format('pve-startup-order', \&pve_verify_startup_order);
b0edd8e6
DM
357sub pve_verify_startup_order {
358 my ($value, $noerr) = @_;
359
360 return $value if pve_parse_startup_order($value);
361
362 return undef if $noerr;
363
364 die "unable to parse startup options\n";
365}
366
367sub pve_parse_startup_order {
368 my ($value) = @_;
369
370 return undef if !$value;
371
372 my $res = {};
373
374 foreach my $p (split(/,/, $value)) {
375 next if $p =~ m/^\s*$/;
376
377 if ($p =~ m/^(order=)?(\d+)$/) {
378 $res->{order} = $2;
379 } elsif ($p =~ m/^up=(\d+)$/) {
380 $res->{up} = $1;
381 } elsif ($p =~ m/^down=(\d+)$/) {
382 $res->{down} = $1;
383 } else {
384 return undef;
385 }
386 }
387
388 return $res;
389}
390
391PVE::JSONSchema::register_standard_option('pve-startup-order', {
392 description => "Startup and shutdown behavior. Order is a non-negative number defining the general startup order. Shutdown in done with reverse ordering. Additionally you can set the 'up' or 'down' delay in seconds, which specifies a delay to wait before the next VM is started or stopped.",
393 optional => 1,
394 type => 'string', format => 'pve-startup-order',
395 typetext => '[[order=]\d+] [,up=\d+] [,down=\d+] ',
396});
397
e143e9d8
DM
398sub check_format {
399 my ($format, $value) = @_;
400
401 return if $format eq 'regex';
402
23dc9401 403 if ($format =~ m/^(.*)-a?list$/) {
e143e9d8
DM
404
405 my $code = $format_list->{$1};
406
407 die "undefined format '$format'\n" if !$code;
408
409 # Note: we allow empty lists
410 foreach my $v (split_list($value)) {
411 &$code($v);
412 }
413
414 } elsif ($format =~ m/^(.*)-opt$/) {
415
416 my $code = $format_list->{$1};
417
418 die "undefined format '$format'\n" if !$code;
419
420 return if !$value; # allow empty string
421
422 &$code($value);
423
424 } else {
425
426 my $code = $format_list->{$format};
427
428 die "undefined format '$format'\n" if !$code;
429
430 &$code($value);
431 }
432}
433
434sub add_error {
435 my ($errors, $path, $msg) = @_;
436
437 $path = '_root' if !$path;
438
439 if ($errors->{$path}) {
440 $errors->{$path} = join ('\n', $errors->{$path}, $msg);
441 } else {
442 $errors->{$path} = $msg;
443 }
444}
445
446sub is_number {
447 my $value = shift;
448
449 # see 'man perlretut'
450 return $value =~ /^[+-]?(\d+\.\d+|\d+\.|\.\d+|\d+)([eE][+-]?\d+)?$/;
451}
452
453sub is_integer {
454 my $value = shift;
455
456 return $value =~ m/^[+-]?\d+$/;
457}
458
459sub check_type {
460 my ($path, $type, $value, $errors) = @_;
461
462 return 1 if !$type;
463
464 if (!defined($value)) {
465 return 1 if $type eq 'null';
466 die "internal error"
467 }
468
469 if (my $tt = ref($type)) {
470 if ($tt eq 'ARRAY') {
471 foreach my $t (@$type) {
472 my $tmperr = {};
473 check_type($path, $t, $value, $tmperr);
474 return 1 if !scalar(%$tmperr);
475 }
476 my $ttext = join ('|', @$type);
477 add_error($errors, $path, "type check ('$ttext') failed");
478 return undef;
479 } elsif ($tt eq 'HASH') {
480 my $tmperr = {};
481 check_prop($value, $type, $path, $tmperr);
482 return 1 if !scalar(%$tmperr);
483 add_error($errors, $path, "type check failed");
484 return undef;
485 } else {
486 die "internal error - got reference type '$tt'";
487 }
488
489 } else {
490
491 return 1 if $type eq 'any';
492
493 if ($type eq 'null') {
494 if (defined($value)) {
495 add_error($errors, $path, "type check ('$type') failed - value is not null");
496 return undef;
497 }
498 return 1;
499 }
500
501 my $vt = ref($value);
502
503 if ($type eq 'array') {
504 if (!$vt || $vt ne 'ARRAY') {
505 add_error($errors, $path, "type check ('$type') failed");
506 return undef;
507 }
508 return 1;
509 } elsif ($type eq 'object') {
510 if (!$vt || $vt ne 'HASH') {
511 add_error($errors, $path, "type check ('$type') failed");
512 return undef;
513 }
514 return 1;
515 } elsif ($type eq 'coderef') {
516 if (!$vt || $vt ne 'CODE') {
517 add_error($errors, $path, "type check ('$type') failed");
518 return undef;
519 }
520 return 1;
521 } else {
522 if ($vt) {
523 add_error($errors, $path, "type check ('$type') failed - got $vt");
524 return undef;
525 } else {
526 if ($type eq 'string') {
527 return 1; # nothing to check ?
528 } elsif ($type eq 'boolean') {
529 #if ($value =~ m/^(1|true|yes|on)$/i) {
530 if ($value eq '1') {
531 return 1;
532 #} elsif ($value =~ m/^(0|false|no|off)$/i) {
533 } elsif ($value eq '0') {
534 return 0;
535 } else {
536 add_error($errors, $path, "type check ('$type') failed - got '$value'");
537 return undef;
538 }
539 } elsif ($type eq 'integer') {
540 if (!is_integer($value)) {
541 add_error($errors, $path, "type check ('$type') failed - got '$value'");
542 return undef;
543 }
544 return 1;
545 } elsif ($type eq 'number') {
546 if (!is_number($value)) {
547 add_error($errors, $path, "type check ('$type') failed - got '$value'");
548 return undef;
549 }
550 return 1;
551 } else {
552 return 1; # no need to verify unknown types
553 }
554 }
555 }
556 }
557
558 return undef;
559}
560
561sub check_object {
562 my ($path, $schema, $value, $additional_properties, $errors) = @_;
563
564 # print "Check Object " . Dumper($value) . "\nSchema: " . Dumper($schema);
565
566 my $st = ref($schema);
567 if (!$st || $st ne 'HASH') {
568 add_error($errors, $path, "Invalid schema definition.");
569 return;
570 }
571
572 my $vt = ref($value);
573 if (!$vt || $vt ne 'HASH') {
574 add_error($errors, $path, "an object is required");
575 return;
576 }
577
578 foreach my $k (keys %$schema) {
579 check_prop($value->{$k}, $schema->{$k}, $path ? "$path.$k" : $k, $errors);
580 }
581
582 foreach my $k (keys %$value) {
583
584 my $newpath = $path ? "$path.$k" : $k;
585
586 if (my $subschema = $schema->{$k}) {
587 if (my $requires = $subschema->{requires}) {
588 if (ref($requires)) {
589 #print "TEST: " . Dumper($value) . "\n", Dumper($requires) ;
590 check_prop($value, $requires, $path, $errors);
591 } elsif (!defined($value->{$requires})) {
592 add_error($errors, $path ? "$path.$requires" : $requires,
593 "missing property - '$newpath' requiers this property");
594 }
595 }
596
597 next; # value is already checked above
598 }
599
600 if (defined ($additional_properties) && !$additional_properties) {
601 add_error($errors, $newpath, "property is not defined in schema " .
602 "and the schema does not allow additional properties");
603 next;
604 }
605 check_prop($value->{$k}, $additional_properties, $newpath, $errors)
606 if ref($additional_properties);
607 }
608}
609
86425a09
WB
610sub check_object_warn {
611 my ($path, $schema, $value, $additional_properties) = @_;
612 my $errors = {};
613 check_object($path, $schema, $value, $additional_properties, $errors);
614 if (scalar(%$errors)) {
615 foreach my $k (keys %$errors) {
616 warn "parse error: $k: $errors->{$k}\n";
617 }
618 return 0;
619 }
620 return 1;
621}
622
e143e9d8
DM
623sub check_prop {
624 my ($value, $schema, $path, $errors) = @_;
625
626 die "internal error - no schema" if !$schema;
627 die "internal error" if !$errors;
628
629 #print "check_prop $path\n" if $value;
630
631 my $st = ref($schema);
632 if (!$st || $st ne 'HASH') {
633 add_error($errors, $path, "Invalid schema definition.");
634 return;
635 }
636
637 # if it extends another schema, it must pass that schema as well
638 if($schema->{extends}) {
639 check_prop($value, $schema->{extends}, $path, $errors);
640 }
641
642 if (!defined ($value)) {
643 return if $schema->{type} && $schema->{type} eq 'null';
644 if (!$schema->{optional}) {
645 add_error($errors, $path, "property is missing and it is not optional");
646 }
647 return;
648 }
649
650 return if !check_type($path, $schema->{type}, $value, $errors);
651
652 if ($schema->{disallow}) {
653 my $tmperr = {};
654 if (check_type($path, $schema->{disallow}, $value, $tmperr)) {
655 add_error($errors, $path, "disallowed value was matched");
656 return;
657 }
658 }
659
660 if (my $vt = ref($value)) {
661
662 if ($vt eq 'ARRAY') {
663 if ($schema->{items}) {
664 my $it = ref($schema->{items});
665 if ($it && $it eq 'ARRAY') {
666 #die "implement me $path: $vt " . Dumper($schema) ."\n". Dumper($value);
667 die "not implemented";
668 } else {
669 my $ind = 0;
670 foreach my $el (@$value) {
671 check_prop($el, $schema->{items}, "${path}[$ind]", $errors);
672 $ind++;
673 }
674 }
675 }
676 return;
677 } elsif ($schema->{properties} || $schema->{additionalProperties}) {
678 check_object($path, defined($schema->{properties}) ? $schema->{properties} : {},
679 $value, $schema->{additionalProperties}, $errors);
680 return;
681 }
682
683 } else {
684
685 if (my $format = $schema->{format}) {
686 eval { check_format($format, $value); };
687 if ($@) {
688 add_error($errors, $path, "invalid format - $@");
689 return;
690 }
691 }
692
693 if (my $pattern = $schema->{pattern}) {
694 if ($value !~ m/^$pattern$/) {
695 add_error($errors, $path, "value does not match the regex pattern");
696 return;
697 }
698 }
699
700 if (defined (my $max = $schema->{maxLength})) {
701 if (length($value) > $max) {
702 add_error($errors, $path, "value may only be $max characters long");
703 return;
704 }
705 }
706
707 if (defined (my $min = $schema->{minLength})) {
708 if (length($value) < $min) {
709 add_error($errors, $path, "value must be at least $min characters long");
710 return;
711 }
712 }
713
714 if (is_number($value)) {
715 if (defined (my $max = $schema->{maximum})) {
716 if ($value > $max) {
717 add_error($errors, $path, "value must have a maximum value of $max");
718 return;
719 }
720 }
721
722 if (defined (my $min = $schema->{minimum})) {
723 if ($value < $min) {
724 add_error($errors, $path, "value must have a minimum value of $min");
725 return;
726 }
727 }
728 }
729
730 if (my $ea = $schema->{enum}) {
731
732 my $found;
733 foreach my $ev (@$ea) {
734 if ($ev eq $value) {
735 $found = 1;
736 last;
737 }
738 }
739 if (!$found) {
740 add_error($errors, $path, "value '$value' does not have a value in the enumeration '" .
741 join(", ", @$ea) . "'");
742 }
743 }
744 }
745}
746
747sub validate {
748 my ($instance, $schema, $errmsg) = @_;
749
750 my $errors = {};
751 $errmsg = "Parameter verification failed.\n" if !$errmsg;
752
753 # todo: cycle detection is only needed for debugging, I guess
754 # we can disable that in the final release
755 # todo: is there a better/faster way to detect cycles?
756 my $cycles = 0;
757 find_cycle($instance, sub { $cycles = 1 });
758 if ($cycles) {
759 add_error($errors, undef, "data structure contains recursive cycles");
760 } elsif ($schema) {
761 check_prop($instance, $schema, '', $errors);
762 }
763
764 if (scalar(%$errors)) {
765 raise $errmsg, code => HTTP_BAD_REQUEST, errors => $errors;
766 }
767
768 return 1;
769}
770
771my $schema_valid_types = ["string", "object", "coderef", "array", "boolean", "number", "integer", "null", "any"];
772my $default_schema_noref = {
773 description => "This is the JSON Schema for JSON Schemas.",
774 type => [ "object" ],
775 additionalProperties => 0,
776 properties => {
777 type => {
778 type => ["string", "array"],
779 description => "This is a type definition value. This can be a simple type, or a union type",
780 optional => 1,
781 default => "any",
782 items => {
783 type => "string",
784 enum => $schema_valid_types,
785 },
786 enum => $schema_valid_types,
787 },
788 optional => {
789 type => "boolean",
790 description => "This indicates that the instance property in the instance object is not required.",
791 optional => 1,
792 default => 0
793 },
794 properties => {
795 type => "object",
796 description => "This is a definition for the properties of an object value",
797 optional => 1,
798 default => {},
799 },
800 items => {
801 type => "object",
802 description => "When the value is an array, this indicates the schema to use to validate each item in an array",
803 optional => 1,
804 default => {},
805 },
806 additionalProperties => {
807 type => [ "boolean", "object"],
808 description => "This provides a default property definition for all properties that are not explicitly defined in an object type definition.",
809 optional => 1,
810 default => {},
811 },
812 minimum => {
813 type => "number",
814 optional => 1,
815 description => "This indicates the minimum value for the instance property when the type of the instance value is a number.",
816 },
817 maximum => {
818 type => "number",
819 optional => 1,
820 description => "This indicates the maximum value for the instance property when the type of the instance value is a number.",
821 },
822 minLength => {
823 type => "integer",
824 description => "When the instance value is a string, this indicates minimum length of the string",
825 optional => 1,
826 minimum => 0,
827 default => 0,
828 },
829 maxLength => {
830 type => "integer",
831 description => "When the instance value is a string, this indicates maximum length of the string.",
832 optional => 1,
833 },
834 typetext => {
835 type => "string",
836 optional => 1,
837 description => "A text representation of the type (used to generate documentation).",
838 },
839 pattern => {
840 type => "string",
841 format => "regex",
842 description => "When the instance value is a string, this provides a regular expression that a instance string value should match in order to be valid.",
843 optional => 1,
844 default => ".*",
845 },
846
847 enum => {
848 type => "array",
849 optional => 1,
850 description => "This provides an enumeration of possible values that are valid for the instance property.",
851 },
852 description => {
853 type => "string",
854 optional => 1,
855 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).",
856 },
d5d10f85
WB
857 format_description => {
858 type => "string",
859 optional => 1,
860 description => "This provides a shorter (usually just one word) description for a property used to generate descriptions for comma separated list property strings.",
861 },
e143e9d8
DM
862 title => {
863 type => "string",
864 optional => 1,
865 description => "This provides the title of the property",
866 },
867 requires => {
868 type => [ "string", "object" ],
869 optional => 1,
870 description => "indicates a required property or a schema that must be validated if this property is present",
871 },
872 format => {
873 type => "string",
874 optional => 1,
875 description => "This indicates what format the data is among some predefined formats which may include:\n\ndate - a string following the ISO format \naddress \nschema - a schema definition object \nperson \npage \nhtml - a string representing HTML",
876 },
877 default => {
878 type => "any",
879 optional => 1,
880 description => "This indicates the default for the instance property."
881 },
7829989f
DM
882 completion => {
883 type => 'coderef',
884 description => "Bash completion function. This function should return a list of possible values.",
885 optional => 1,
886 },
e143e9d8
DM
887 disallow => {
888 type => "object",
889 optional => 1,
890 description => "This attribute may take the same values as the \"type\" attribute, however if the instance matches the type or if this value is an array and the instance matches any type or schema in the array, than this instance is not valid.",
891 },
892 extends => {
893 type => "object",
894 optional => 1,
895 description => "This indicates the schema extends the given schema. All instances of this schema must be valid to by the extended schema also.",
896 default => {},
897 },
898 # this is from hyper schema
899 links => {
900 type => "array",
901 description => "This defines the link relations of the instance objects",
902 optional => 1,
903 items => {
904 type => "object",
905 properties => {
906 href => {
907 type => "string",
908 description => "This defines the target URL for the relation and can be parameterized using {propertyName} notation. It should be resolved as a URI-reference relative to the URI that was used to retrieve the instance document",
909 },
910 rel => {
911 type => "string",
912 description => "This is the name of the link relation",
913 optional => 1,
914 default => "full",
915 },
916 method => {
917 type => "string",
918 description => "For submission links, this defines the method that should be used to access the target resource",
919 optional => 1,
920 default => "GET",
921 },
922 },
923 },
924 },
925 }
926};
927
928my $default_schema = Storable::dclone($default_schema_noref);
929
930$default_schema->{properties}->{properties}->{additionalProperties} = $default_schema;
931$default_schema->{properties}->{additionalProperties}->{properties} = $default_schema->{properties};
932
933$default_schema->{properties}->{items}->{properties} = $default_schema->{properties};
934$default_schema->{properties}->{items}->{additionalProperties} = 0;
935
936$default_schema->{properties}->{disallow}->{properties} = $default_schema->{properties};
937$default_schema->{properties}->{disallow}->{additionalProperties} = 0;
938
939$default_schema->{properties}->{requires}->{properties} = $default_schema->{properties};
940$default_schema->{properties}->{requires}->{additionalProperties} = 0;
941
942$default_schema->{properties}->{extends}->{properties} = $default_schema->{properties};
943$default_schema->{properties}->{extends}->{additionalProperties} = 0;
944
945my $method_schema = {
946 type => "object",
947 additionalProperties => 0,
948 properties => {
949 description => {
950 description => "This a description of the method",
951 optional => 1,
952 },
953 name => {
954 type => 'string',
955 description => "This indicates the name of the function to call.",
956 optional => 1,
957 requires => {
958 additionalProperties => 1,
959 properties => {
960 name => {},
961 description => {},
962 code => {},
963 method => {},
964 parameters => {},
965 path => {},
966 parameters => {},
967 returns => {},
968 }
969 },
970 },
971 method => {
972 type => 'string',
973 description => "The HTTP method name.",
974 enum => [ 'GET', 'POST', 'PUT', 'DELETE' ],
975 optional => 1,
976 },
977 protected => {
978 type => 'boolean',
979 description => "Method needs special privileges - only pvedaemon can execute it",
980 optional => 1,
981 },
982 proxyto => {
983 type => 'string',
984 description => "A parameter name. If specified, all calls to this method are proxied to the host contained in that parameter.",
985 optional => 1,
986 },
987 permissions => {
988 type => 'object',
989 description => "Required access permissions. By default only 'root' is allowed to access this method.",
990 optional => 1,
991 additionalProperties => 0,
992 properties => {
b18d1722
DM
993 description => {
994 description => "Describe access permissions.",
995 optional => 1,
996 },
e143e9d8 997 user => {
b18d1722 998 description => "A simply way to allow access for 'all' authenticated users. Value 'world' is used to allow access without credentials.",
e143e9d8 999 type => 'string',
b18d1722 1000 enum => ['all', 'world'],
e143e9d8
DM
1001 optional => 1,
1002 },
b18d1722
DM
1003 check => {
1004 description => "Array of permission checks (prefix notation).",
1005 type => 'array',
1006 optional => 1
1007 },
e143e9d8
DM
1008 },
1009 },
1010 match_name => {
1011 description => "Used internally",
1012 optional => 1,
1013 },
1014 match_re => {
1015 description => "Used internally",
1016 optional => 1,
1017 },
1018 path => {
1019 type => 'string',
1020 description => "path for URL matching (uri template)",
1021 },
1022 fragmentDelimiter => {
1023 type => 'string',
1024 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.",
1025 optional => 1,
1026 },
1027 parameters => {
1028 type => 'object',
1029 description => "JSON Schema for parameters.",
1030 optional => 1,
1031 },
638edfd4
DM
1032 formatter => {
1033 type => 'object',
1034 description => "Used to store page formatter information (set by PVE::RESTHandler->register_page_formatter).",
1035 optional => 1,
1036 },
e143e9d8
DM
1037 returns => {
1038 type => 'object',
1039 description => "JSON Schema for return value.",
1040 optional => 1,
1041 },
1042 code => {
1043 type => 'coderef',
1044 description => "method implementaion (code reference)",
1045 optional => 1,
1046 },
1047 subclass => {
1048 type => 'string',
1049 description => "Delegate call to this class (perl class string).",
1050 optional => 1,
1051 requires => {
1052 additionalProperties => 0,
1053 properties => {
1054 subclass => {},
1055 path => {},
1056 match_name => {},
1057 match_re => {},
1058 fragmentDelimiter => { optional => 1 }
1059 }
1060 },
1061 },
1062 },
1063
1064};
1065
1066sub validate_schema {
1067 my ($schema) = @_;
1068
1069 my $errmsg = "internal error - unable to verify schema\n";
1070 validate($schema, $default_schema, $errmsg);
1071}
1072
1073sub validate_method_info {
1074 my $info = shift;
1075
1076 my $errmsg = "internal error - unable to verify method info\n";
1077 validate($info, $method_schema, $errmsg);
1078
1079 validate_schema($info->{parameters}) if $info->{parameters};
1080 validate_schema($info->{returns}) if $info->{returns};
1081}
1082
1083# run a self test on load
1084# make sure we can verify the default schema
1085validate_schema($default_schema_noref);
1086validate_schema($method_schema);
1087
1088# and now some utility methods (used by pve api)
1089sub method_get_child_link {
1090 my ($info) = @_;
1091
1092 return undef if !$info;
1093
1094 my $schema = $info->{returns};
1095 return undef if !$schema || !$schema->{type} || $schema->{type} ne 'array';
1096
1097 my $links = $schema->{links};
1098 return undef if !$links;
1099
1100 my $found;
1101 foreach my $lnk (@$links) {
1102 if ($lnk->{href} && $lnk->{rel} && ($lnk->{rel} eq 'child')) {
1103 $found = $lnk;
1104 last;
1105 }
1106 }
1107
1108 return $found;
1109}
1110
1111# a way to parse command line parameters, using a
1112# schema to configure Getopt::Long
1113sub get_options {
0ce82909 1114 my ($schema, $args, $arg_param, $fixed_param, $pwcallback) = @_;
e143e9d8
DM
1115
1116 if (!$schema || !$schema->{properties}) {
1117 raise("too many arguments\n", code => HTTP_BAD_REQUEST)
1118 if scalar(@$args) != 0;
1119 return {};
1120 }
1121
0ce82909
DM
1122 my $list_param;
1123 if ($arg_param && !ref($arg_param)) {
1124 my $pd = $schema->{properties}->{$arg_param};
1125 die "expected list format $pd->{format}"
1126 if !($pd && $pd->{format} && $pd->{format} =~ m/-list/);
1127 $list_param = $arg_param;
1128 }
1129
e143e9d8
DM
1130 my @getopt = ();
1131 foreach my $prop (keys %{$schema->{properties}}) {
1132 my $pd = $schema->{properties}->{$prop};
aab47b58 1133 next if $list_param && $prop eq $list_param;
0ce82909 1134 next if defined($fixed_param->{$prop});
e143e9d8
DM
1135
1136 if ($prop eq 'password' && $pwcallback) {
1137 # we do not accept plain password on input line, instead
1138 # we turn this into a boolean option and ask for password below
1139 # using $pwcallback() (for security reasons).
1140 push @getopt, "$prop";
1141 } elsif ($pd->{type} eq 'boolean') {
1142 push @getopt, "$prop:s";
1143 } else {
23dc9401 1144 if ($pd->{format} && $pd->{format} =~ m/-a?list/) {
8ba7c72b
DM
1145 push @getopt, "$prop=s@";
1146 } else {
1147 push @getopt, "$prop=s";
1148 }
e143e9d8
DM
1149 }
1150 }
1151
1068aeb3
WB
1152 Getopt::Long::Configure('prefix_pattern=(--|-)');
1153
e143e9d8
DM
1154 my $opts = {};
1155 raise("unable to parse option\n", code => HTTP_BAD_REQUEST)
1156 if !Getopt::Long::GetOptionsFromArray($args, $opts, @getopt);
1d21344c 1157
5851be88 1158 if (@$args) {
0ce82909
DM
1159 if ($list_param) {
1160 $opts->{$list_param} = $args;
1161 $args = [];
1162 } elsif (ref($arg_param)) {
5851be88
WB
1163 foreach my $arg_name (@$arg_param) {
1164 if ($opts->{'extra-args'}) {
1165 raise("internal error: extra-args must be the last argument\n", code => HTTP_BAD_REQUEST);
1166 }
1167 if ($arg_name eq 'extra-args') {
1168 $opts->{'extra-args'} = $args;
1169 $args = [];
1170 next;
1171 }
1172 raise("not enough arguments\n", code => HTTP_BAD_REQUEST) if !@$args;
1173 $opts->{$arg_name} = shift @$args;
0ce82909 1174 }
5851be88 1175 raise("too many arguments\n", code => HTTP_BAD_REQUEST) if @$args;
0ce82909
DM
1176 } else {
1177 raise("too many arguments\n", code => HTTP_BAD_REQUEST)
1178 if scalar(@$args) != 0;
1179 }
1d21344c
DM
1180 }
1181
e143e9d8
DM
1182 if (my $pd = $schema->{properties}->{password}) {
1183 if ($pd->{type} ne 'boolean' && $pwcallback) {
1184 if ($opts->{password} || !$pd->{optional}) {
1185 $opts->{password} = &$pwcallback();
1186 }
1187 }
1188 }
815b2aba
DM
1189
1190 $opts = PVE::Tools::decode_utf8_parameters($opts);
815b2aba 1191
e143e9d8
DM
1192 foreach my $p (keys %$opts) {
1193 if (my $pd = $schema->{properties}->{$p}) {
1194 if ($pd->{type} eq 'boolean') {
1195 if ($opts->{$p} eq '') {
1196 $opts->{$p} = 1;
1197 } elsif ($opts->{$p} =~ m/^(1|true|yes|on)$/i) {
1198 $opts->{$p} = 1;
1199 } elsif ($opts->{$p} =~ m/^(0|false|no|off)$/i) {
1200 $opts->{$p} = 0;
1201 } else {
1202 raise("unable to parse boolean option\n", code => HTTP_BAD_REQUEST);
1203 }
23dc9401 1204 } elsif ($pd->{format}) {
8ba7c72b 1205
23dc9401 1206 if ($pd->{format} =~ m/-list/) {
8ba7c72b 1207 # allow --vmid 100 --vmid 101 and --vmid 100,101
23dc9401 1208 # allow --dow mon --dow fri and --dow mon,fri
43479146 1209 $opts->{$p} = join(",", @{$opts->{$p}}) if ref($opts->{$p}) eq 'ARRAY';
23dc9401 1210 } elsif ($pd->{format} =~ m/-alist/) {
8ba7c72b
DM
1211 # we encode array as \0 separated strings
1212 # Note: CGI.pm also use this encoding
1213 if (scalar(@{$opts->{$p}}) != 1) {
1214 $opts->{$p} = join("\0", @{$opts->{$p}});
1215 } else {
1216 # st that split_list knows it is \0 terminated
1217 my $v = $opts->{$p}->[0];
1218 $opts->{$p} = "$v\0";
1219 }
1220 }
e143e9d8
DM
1221 }
1222 }
1223 }
1224
0ce82909
DM
1225 foreach my $p (keys %$fixed_param) {
1226 $opts->{$p} = $fixed_param->{$p};
e143e9d8
DM
1227 }
1228
1229 return $opts;
1230}
1231
1232# A way to parse configuration data by giving a json schema
1233sub parse_config {
1234 my ($schema, $filename, $raw) = @_;
1235
1236 # do fast check (avoid validate_schema($schema))
1237 die "got strange schema" if !$schema->{type} ||
1238 !$schema->{properties} || $schema->{type} ne 'object';
1239
1240 my $cfg = {};
1241
3c4d612a 1242 while ($raw =~ /^\s*(.+?)\s*$/gm) {
e143e9d8 1243 my $line = $1;
e143e9d8 1244
3c4d612a
WB
1245 next if $line =~ /^#/;
1246
1247 if ($line =~ m/^(\S+?):\s*(.*)$/) {
e143e9d8
DM
1248 my $key = $1;
1249 my $value = $2;
1250 if ($schema->{properties}->{$key} &&
1251 $schema->{properties}->{$key}->{type} eq 'boolean') {
1252
1253 $value = 1 if $value =~ m/^(1|on|yes|true)$/i;
1254 $value = 0 if $value =~ m/^(0|off|no|false)$/i;
1255 }
1256 $cfg->{$key} = $value;
1257 } else {
1258 warn "ignore config line: $line\n"
1259 }
1260 }
1261
1262 my $errors = {};
1263 check_prop($cfg, $schema, '', $errors);
1264
1265 foreach my $k (keys %$errors) {
1266 warn "parse error in '$filename' - '$k': $errors->{$k}\n";
1267 delete $cfg->{$k};
1268 }
1269
1270 return $cfg;
1271}
1272
1273# generate simple key/value file
1274sub dump_config {
1275 my ($schema, $filename, $cfg) = @_;
1276
1277 # do fast check (avoid validate_schema($schema))
1278 die "got strange schema" if !$schema->{type} ||
1279 !$schema->{properties} || $schema->{type} ne 'object';
1280
1281 validate($cfg, $schema, "validation error in '$filename'\n");
1282
1283 my $data = '';
1284
1285 foreach my $k (keys %$cfg) {
1286 $data .= "$k: $cfg->{$k}\n";
1287 }
1288
1289 return $data;
1290}
1291
d5d10f85
WB
1292sub generate_typetext {
1293 my ($schema) = @_;
1294 my $typetext = '';
1295 my (@optional, @required);
1296 foreach my $key (sort keys %$schema) {
8c2d3ca1
WB
1297 next if !$schema->{$key}->{format_description} &&
1298 !$schema->{$key}->{typetext};
d5d10f85
WB
1299 if ($schema->{$key}->{optional}) {
1300 push @optional, $key;
1301 } else {
1302 push @required, $key;
1303 }
1304 }
1305 my ($pre, $post) = ('', '');
8c2d3ca1
WB
1306 my $add = sub {
1307 my ($key) = @_;
1308 if (my $desc = $schema->{$key}->{format_description}) {
1309 $typetext .= "$pre$key=<$desc>$post";
1310 } elsif (my $text = $schema->{$key}->{typetext}) {
1311 $typetext .= "$pre$text$post";
1312 } else {
1313 die "internal error: neither format_description nor typetext found";
1314 }
1315 };
d5d10f85 1316 foreach my $key (@required) {
8c2d3ca1 1317 &$add($key);
d5d10f85
WB
1318 $pre = ', ';
1319 }
40605176
WB
1320 $pre = $pre ? ' [,' : '[';
1321 $post = ']';
d5d10f85 1322 foreach my $key (@optional) {
8c2d3ca1 1323 &$add($key);
d5d10f85 1324 $pre = ' [,';
d5d10f85
WB
1325 }
1326 return $typetext;
1327}
1328
e143e9d8 13291;