1 package PVE
::JSONSchema
;
5 use Storable
; # for dclone
9 use Devel
::Cycle
-quiet
; # todo: remove?
10 use PVE
::Tools
qw(split_list $IPV6RE $IPV4RE);
11 use PVE
::Exception
qw(raise);
12 use HTTP
::Status
qw(:constants);
14 use Net
::IP
qw(:PROC);
20 register_standard_option
26 our $CONFIGID_RE = qr/[a-z][a-z0-9_-]+/i;
28 # Note: This class implements something similar to JSON schema, but it is not 100% complete.
29 # see: http://tools.ietf.org/html/draft-zyp-json-schema-02
30 # see: http://json-schema.org/
32 # the code is similar to the javascript parser from http://code.google.com/p/jsonschema/
34 my $standard_options = {};
35 sub register_standard_option
{
36 my ($name, $schema) = @_;
38 die "standard option '$name' already registered\n"
39 if $standard_options->{$name};
41 $standard_options->{$name} = $schema;
44 sub get_standard_option
{
45 my ($name, $base) = @_;
47 my $std = $standard_options->{$name};
48 die "no such standard option '$name'\n" if !$std;
50 my $res = $base || {};
52 foreach my $opt (keys %$std) {
53 next if defined($res->{$opt});
54 $res->{$opt} = $std->{$opt};
60 register_standard_option
('pve-vmid', {
61 description
=> "The (unique) ID of the VM.",
65 maximum
=> 999_999_999,
68 register_standard_option
('pve-node', {
69 description
=> "The cluster node name.",
70 type
=> 'string', format
=> 'pve-node',
73 register_standard_option
('pve-node-list', {
74 description
=> "List of cluster node names.",
75 type
=> 'string', format
=> 'pve-node-list',
78 register_standard_option
('pve-iface', {
79 description
=> "Network interface name.",
80 type
=> 'string', format
=> 'pve-iface',
81 minLength
=> 2, maxLength
=> 20,
84 register_standard_option
('pve-storage-id', {
85 description
=> "The storage identifier.",
86 type
=> 'string', format
=> 'pve-storage-id',
89 register_standard_option
('pve-bridge-id', {
90 description
=> "Bridge to attach guest network devices to.",
91 type
=> 'string', format
=> 'pve-bridge-id',
92 format_description
=> 'bridge',
95 register_standard_option
('pve-config-digest', {
96 description
=> 'Prevent changes if current configuration file has a different digest. '
97 . 'This can be used to prevent concurrent modifications.',
100 # sha1 hex digests are 40 characters long
101 # sha256 hex digests are 64 characters long (sha256 is used in our Rust code)
105 register_standard_option
('skiplock', {
106 description
=> "Ignore locks - only root is allowed to use this option.",
111 register_standard_option
('extra-args', {
112 description
=> "Extra arguments as array",
114 items
=> { type
=> 'string' },
118 register_standard_option
('fingerprint-sha256', {
119 description
=> "Certificate SHA 256 fingerprint.",
121 pattern
=> '([A-Fa-f0-9]{2}:){31}[A-Fa-f0-9]{2}',
124 register_standard_option
('pve-output-format', {
126 description
=> 'Output format.',
127 enum
=> [ 'text', 'json', 'json-pretty', 'yaml' ],
132 register_standard_option
('pve-snapshot-name', {
133 description
=> "The name of the snapshot.",
134 type
=> 'string', format
=> 'pve-configid',
138 my $format_list = {};
139 my $format_validators = {};
141 sub register_format
{
142 my ($name, $format, $validator) = @_;
144 die "JSON schema format '$name' already registered\n"
145 if $format_list->{$name};
148 die "A \$validator function can only be specified for hash-based formats\n"
149 if ref($format) ne 'HASH';
150 $format_validators->{$name} = $validator;
153 $format_list->{$name} = $format;
158 return $format_list->{$name};
161 my $renderer_hash = {};
163 sub register_renderer
{
164 my ($name, $code) = @_;
166 die "renderer '$name' already registered\n"
167 if $renderer_hash->{$name};
169 $renderer_hash->{$name} = $code;
174 return $renderer_hash->{$name};
177 # register some common type for pve
179 register_format
('string', sub {}); # allow format => 'string-list'
181 register_format
('urlencoded', \
&pve_verify_urlencoded
);
182 sub pve_verify_urlencoded
{
183 my ($text, $noerr) = @_;
184 if ($text !~ /^[-%a-zA-Z0-9_.!~*'()]*$/) {
185 return undef if $noerr;
186 die "invalid urlencoded string: $text\n";
191 register_format
('pve-configid', \
&pve_verify_configid
);
192 sub pve_verify_configid
{
193 my ($id, $noerr) = @_;
195 if ($id !~ m/^$CONFIGID_RE$/) {
196 return undef if $noerr;
197 die "invalid configuration ID '$id'\n";
202 PVE
::JSONSchema
::register_format
('pve-storage-id', \
&parse_storage_id
);
203 sub parse_storage_id
{
204 my ($storeid, $noerr) = @_;
206 return parse_id
($storeid, 'storage', $noerr);
209 PVE
::JSONSchema
::register_format
('pve-bridge-id', \
&parse_bridge_id
);
210 sub parse_bridge_id
{
211 my ($id, $noerr) = @_;
213 if ($id !~ m/^[-_.\w\d]+$/) {
214 return undef if $noerr;
215 die "invalid bridge ID '$id'\n";
220 PVE
::JSONSchema
::register_format
('acme-plugin-id', \
&parse_acme_plugin_id
);
221 sub parse_acme_plugin_id
{
222 my ($pluginid, $noerr) = @_;
224 return parse_id
($pluginid, 'ACME plugin', $noerr);
228 my ($id, $type, $noerr) = @_;
230 if ($id !~ m/^[a-z][a-z0-9\-\_\.]*[a-z0-9]$/i) {
231 return undef if $noerr;
232 die "$type ID '$id' contains illegal characters\n";
237 register_format
('pve-vmid', \
&pve_verify_vmid
);
238 sub pve_verify_vmid
{
239 my ($vmid, $noerr) = @_;
241 if ($vmid !~ m/^[1-9][0-9]{2,8}$/) {
242 return undef if $noerr;
243 die "value does not look like a valid VM ID\n";
248 register_format
('pve-node', \
&pve_verify_node_name
);
249 sub pve_verify_node_name
{
250 my ($node, $noerr) = @_;
252 if ($node !~ m/^([a-zA-Z0-9]([a-zA-Z0-9\-]*[a-zA-Z0-9])?)$/) {
253 return undef if $noerr;
254 die "value does not look like a valid node name\n";
259 # maps source to target ID using an ID map
261 my ($map, $source) = @_;
263 return $source if !defined($map);
265 return $map->{entries
}->{$source}
266 if $map->{entries
} && defined($map->{entries
}->{$source});
268 return $map->{default} if $map->{default};
270 # identity (fallback)
275 my ($idmap, $idformat) = @_;
277 return undef if !$idmap;
281 foreach my $entry (PVE
::Tools
::split_list
($idmap)) {
283 $map->{identity
} = 1;
284 } elsif ($entry =~ m/^([^:]+):([^:]+)$/) {
285 my ($source, $target) = ($1, $2);
287 check_format
($idformat, $source, '');
288 check_format
($idformat, $target, '');
290 die "entry '$entry' contains invalid ID - $@\n" if $@;
292 die "duplicate mapping for source '$source'\n"
293 if exists $map->{entries
}->{$source};
295 $map->{entries
}->{$source} = $target;
298 check_format
($idformat, $entry);
300 die "entry '$entry' contains invalid ID - $@\n" if $@;
302 die "default target ID can only be provided once\n"
303 if exists $map->{default};
305 $map->{default} = $entry;
309 die "identity mapping cannot be combined with other mappings\n"
310 if $map->{identity
} && ($map->{default} || exists $map->{entries
});
315 my $verify_idpair = sub {
316 my ($input, $noerr, $format) = @_;
318 eval { parse_idmap
($input, $format) };
320 return undef if $noerr;
327 PVE
::JSONSchema
::register_standard_option
('pve-targetstorage', {
328 description
=> "Mapping from source to target storages. Providing only a single storage ID maps all source storages to that storage. Providing the special value '1' will map each source storage to itself.",
330 format
=> 'storage-pair-list',
334 # note: this only checks a single list entry
335 # when using a storage-pair-list map, you need to pass the full parameter to
337 register_format
('storage-pair', \
&verify_storagepair
);
338 sub verify_storagepair
{
339 my ($storagepair, $noerr) = @_;
340 return $verify_idpair->($storagepair, $noerr, 'pve-storage-id');
343 # note: this only checks a single list entry
344 # when using a bridge-pair-list map, you need to pass the full parameter to
346 register_format
('bridge-pair', \
&verify_bridgepair
);
347 sub verify_bridgepair
{
348 my ($bridgepair, $noerr) = @_;
349 return $verify_idpair->($bridgepair, $noerr, 'pve-bridge-id');
352 register_format
('mac-addr', \
&pve_verify_mac_addr
);
353 sub pve_verify_mac_addr
{
354 my ($mac_addr, $noerr) = @_;
356 # don't allow I/G bit to be set, most of the time it breaks things, see:
357 # https://pve.proxmox.com/pipermail/pve-devel/2019-March/035998.html
358 if ($mac_addr !~ m/^[a-f0-9][02468ace](?::[a-f0-9]{2}){5}$/i) {
359 return undef if $noerr;
360 die "value does not look like a valid unicast MAC address\n";
365 register_standard_option
('mac-addr', {
367 description
=> 'Unicast MAC address.',
368 verbose_description
=> 'A common MAC address with the I/G (Individual/Group) bit not set.',
369 format_description
=> "XX:XX:XX:XX:XX:XX",
371 format
=> 'mac-addr',
374 register_format
('ipv4', \
&pve_verify_ipv4
);
375 sub pve_verify_ipv4
{
376 my ($ipv4, $noerr) = @_;
378 if ($ipv4 !~ m/^(?:$IPV4RE)$/) {
379 return undef if $noerr;
380 die "value does not look like a valid IPv4 address\n";
385 register_format
('ipv6', \
&pve_verify_ipv6
);
386 sub pve_verify_ipv6
{
387 my ($ipv6, $noerr) = @_;
389 if ($ipv6 !~ m/^(?:$IPV6RE)$/) {
390 return undef if $noerr;
391 die "value does not look like a valid IPv6 address\n";
396 register_format
('ip', \
&pve_verify_ip
);
398 my ($ip, $noerr) = @_;
400 if ($ip !~ m/^(?:(?:$IPV4RE)|(?:$IPV6RE))$/) {
401 return undef if $noerr;
402 die "value does not look like a valid IP address\n";
407 PVE
::JSONSchema
::register_format
('ldap-simple-attr', \
&verify_ldap_simple_attr
);
408 sub verify_ldap_simple_attr
{
409 my ($attr, $noerr) = @_;
411 if ($attr =~ m/^[a-zA-Z0-9]+$/) {
415 die "value '$attr' does not look like a simple ldap attribute name\n" if !$noerr;
420 my $ipv4_mask_hash = {
438 '255.255.128.0' => 17,
439 '255.255.192.0' => 18,
440 '255.255.224.0' => 19,
441 '255.255.240.0' => 20,
442 '255.255.248.0' => 21,
443 '255.255.252.0' => 22,
444 '255.255.254.0' => 23,
445 '255.255.255.0' => 24,
446 '255.255.255.128' => 25,
447 '255.255.255.192' => 26,
448 '255.255.255.224' => 27,
449 '255.255.255.240' => 28,
450 '255.255.255.248' => 29,
451 '255.255.255.252' => 30,
452 '255.255.255.254' => 31,
453 '255.255.255.255' => 32,
456 sub get_netmask_bits
{
458 return $ipv4_mask_hash->{$mask};
461 register_format
('ipv4mask', \
&pve_verify_ipv4mask
);
462 sub pve_verify_ipv4mask
{
463 my ($mask, $noerr) = @_;
465 if (!defined($ipv4_mask_hash->{$mask})) {
466 return undef if $noerr;
467 die "value does not look like a valid IP netmask\n";
472 register_format
('CIDRv6', \
&pve_verify_cidrv6
);
473 sub pve_verify_cidrv6
{
474 my ($cidr, $noerr) = @_;
476 if ($cidr =~ m!^(?:$IPV6RE)(?:/(\d+))$! && ($1 > 7) && ($1 <= 128)) {
480 return undef if $noerr;
481 die "value does not look like a valid IPv6 CIDR network\n";
484 register_format
('CIDRv4', \
&pve_verify_cidrv4
);
485 sub pve_verify_cidrv4
{
486 my ($cidr, $noerr) = @_;
488 if ($cidr =~ m!^(?:$IPV4RE)(?:/(\d+))$! && ($1 > 7) && ($1 <= 32)) {
492 return undef if $noerr;
493 die "value does not look like a valid IPv4 CIDR network\n";
496 register_format
('CIDR', \
&pve_verify_cidr
);
497 sub pve_verify_cidr
{
498 my ($cidr, $noerr) = @_;
500 if (!(pve_verify_cidrv4
($cidr, 1) ||
501 pve_verify_cidrv6
($cidr, 1)))
503 return undef if $noerr;
504 die "value does not look like a valid CIDR network\n";
510 register_format
('pve-ipv4-config', \
&pve_verify_ipv4_config
);
511 sub pve_verify_ipv4_config
{
512 my ($config, $noerr) = @_;
514 return $config if $config =~ /^(?:dhcp|manual)$/ ||
515 pve_verify_cidrv4
($config, 1);
516 return undef if $noerr;
517 die "value does not look like a valid ipv4 network configuration\n";
520 register_format
('pve-ipv6-config', \
&pve_verify_ipv6_config
);
521 sub pve_verify_ipv6_config
{
522 my ($config, $noerr) = @_;
524 return $config if $config =~ /^(?:auto|dhcp|manual)$/ ||
525 pve_verify_cidrv6
($config, 1);
526 return undef if $noerr;
527 die "value does not look like a valid ipv6 network configuration\n";
530 register_format
('email', \
&pve_verify_email
);
531 sub pve_verify_email
{
532 my ($email, $noerr) = @_;
534 if ($email !~ /^$PVE::Tools::EMAIL_RE$/) {
535 return undef if $noerr;
536 die "value does not look like a valid email address\n";
541 register_format
('email-or-username', \
&pve_verify_email_or_username
);
542 sub pve_verify_email_or_username
{
543 my ($email, $noerr) = @_;
545 if ($email !~ /^$PVE::Tools::EMAIL_RE$/ &&
546 $email !~ /^$PVE::Tools::EMAIL_USER_RE$/) {
547 return undef if $noerr;
548 die "value does not look like a valid email address or user name\n";
553 register_format
('dns-name', \
&pve_verify_dns_name
);
554 sub pve_verify_dns_name
{
555 my ($name, $noerr) = @_;
557 my $namere = "([a-zA-Z0-9]([a-zA-Z0-9\-]*[a-zA-Z0-9])?)";
559 if ($name !~ /^(${namere}\.)*${namere}$/) {
560 return undef if $noerr;
561 die "value does not look like a valid DNS name\n";
566 register_format
('timezone', \
&pve_verify_timezone
);
567 sub pve_verify_timezone
{
568 my ($timezone, $noerr) = @_;
570 return $timezone if $timezone eq 'UTC';
572 open(my $fh, "<", "/usr/share/zoneinfo/zone.tab");
573 while (my $line = <$fh>) {
574 next if $line =~ /^\s*#/;
576 my $zone = (split /\t/, $line)[2];
577 return $timezone if $timezone eq $zone; # found
581 return undef if $noerr;
582 die "invalid time zone '$timezone'\n";
585 # network interface name
586 register_format
('pve-iface', \
&pve_verify_iface
);
587 sub pve_verify_iface
{
588 my ($id, $noerr) = @_;
590 if ($id !~ m/^[a-z][a-z0-9_]{1,20}([:\.]\d+)?$/i) {
591 return undef if $noerr;
592 die "invalid network interface name '$id'\n";
597 # general addresses by name or IP
598 register_format
('address', \
&pve_verify_address
);
599 sub pve_verify_address
{
600 my ($addr, $noerr) = @_;
602 if (!(pve_verify_ip
($addr, 1) ||
603 pve_verify_dns_name
($addr, 1)))
605 return undef if $noerr;
606 die "value does not look like a valid address: $addr\n";
611 register_format
('disk-size', \
&pve_verify_disk_size
);
612 sub pve_verify_disk_size
{
613 my ($size, $noerr) = @_;
614 if (!defined(parse_size
($size))) {
615 return undef if $noerr;
616 die "value does not look like a valid disk size: $size\n";
621 register_standard_option
('spice-proxy', {
622 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).",
623 type
=> 'string', format
=> 'address',
626 register_standard_option
('remote-viewer-config', {
627 description
=> "Returned values can be directly passed to the 'remote-viewer' application.",
628 additionalProperties
=> 1,
630 type
=> { type
=> 'string' },
631 password
=> { type
=> 'string' },
632 proxy
=> { type
=> 'string' },
633 host
=> { type
=> 'string' },
634 'tls-port' => { type
=> 'integer' },
638 register_format
('pve-startup-order', \
&pve_verify_startup_order
);
639 sub pve_verify_startup_order
{
640 my ($value, $noerr) = @_;
642 return $value if pve_parse_startup_order
($value);
644 return undef if $noerr;
646 die "unable to parse startup options\n";
651 type
=> 'number', minimum
=> '0',
652 format_description
=> 'LIMIT',
655 my $bwlimit_format = {
658 description
=> 'default bandwidth limit in KiB/s',
662 description
=> 'bandwidth limit in KiB/s for restoring guests from backups',
666 description
=> 'bandwidth limit in KiB/s for migrating guests (including moving local disks)',
670 description
=> 'bandwidth limit in KiB/s for cloning disks',
674 description
=> 'bandwidth limit in KiB/s for moving disks',
677 register_format
('bwlimit', $bwlimit_format);
678 register_standard_option
('bwlimit', {
679 description
=> "Set I/O bandwidth limit for various operations (in KiB/s).",
682 format
=> $bwlimit_format,
685 my $remote_format = {
688 description
=> 'Remote Proxmox hostname or IP',
689 format_description
=> 'ADDRESS',
694 description
=> 'Port to connect to',
695 format_description
=> 'PORT',
699 description
=> 'A full Proxmox API token including the secret value.',
700 format_description
=> 'user@realm!token=SECRET',
702 fingerprint
=> get_standard_option
(
703 'fingerprint-sha256',
706 description
=> 'Remote host\'s certificate fingerprint, if not trusted by system store.',
707 format_description
=> 'FINGERPRINT',
711 register_format
('proxmox-remote', $remote_format);
712 register_standard_option
('proxmox-remote', {
713 description
=> "Specification of a remote endpoint.",
714 type
=> 'string', format
=> 'proxmox-remote',
717 our $PVE_TAG_RE = qr/[a-z0-9_][a-z0-9_\-\+\.]*/i;
719 # used for pve-tag-list in e.g., guest configs
720 register_format
('pve-tag', \
&pve_verify_tag
);
722 my ($value, $noerr) = @_;
724 return $value if $value =~ m/^${PVE_TAG_RE}$/i;
726 return undef if $noerr;
728 die "invalid characters in tag\n";
731 sub pve_parse_startup_order
{
734 return undef if !$value;
738 foreach my $p (split(/,/, $value)) {
739 next if $p =~ m/^\s*$/;
741 if ($p =~ m/^(order=)?(\d+)$/) {
743 } elsif ($p =~ m/^up=(\d+)$/) {
745 } elsif ($p =~ m/^down=(\d+)$/) {
755 PVE
::JSONSchema
::register_standard_option
('pve-startup-order', {
756 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.",
758 type
=> 'string', format
=> 'pve-startup-order',
759 typetext
=> '[[order=]\d+] [,up=\d+] [,down=\d+] ',
762 register_format
('pve-tfa-secret', \
&pve_verify_tfa_secret
);
763 sub pve_verify_tfa_secret
{
764 my ($key, $noerr) = @_;
766 # The old format used 16 base32 chars or 40 hex digits. Since they have a common subset it's
767 # hard to distinguish them without the our previous length constraints, so add a 'v2' of the
768 # format to support arbitrary lengths properly:
769 if ($key =~ /^v2-0x[0-9a-fA-F]{16,128}$/ || # hex
770 $key =~ /^v2-[A-Z2-7=]{16,128}$/ || # base32
771 $key =~ /^(?:[A-Z2-7=]{16}|[A-Fa-f0-9]{40})$/) # and the old pattern copy&pasted
776 return undef if $noerr;
778 die "unable to decode TFA secret\n";
782 PVE
::JSONSchema
::register_format
('pve-task-status-type', \
&verify_task_status_type
);
783 sub verify_task_status_type
{
784 my ($value, $noerr) = @_;
786 return $value if $value =~ m/^(ok|error|warning|unknown)$/i;
788 return undef if $noerr;
790 die "invalid status '$value'\n";
794 my ($format, $value, $path) = @_;
796 if (ref($format) eq 'HASH') {
797 # hash ref cannot have validator/list/opt handling attached
798 return parse_property_string
($format, $value, $path);
801 if (ref($format) eq 'CODE') {
802 # we are the (sole, old-style) validator
803 return $format->($value);
806 return if $format eq 'regex';
809 $format =~ m/^(.*?)(?:-(list|opt))?$/;
810 my ($format_name, $format_type) = ($1, $2 // 'none');
811 my $registered = get_format
($format_name);
812 die "undefined format '$format'\n" if !$registered;
814 die "'-$format_type' format must have code ref, not hash\n"
815 if $format_type ne 'none' && ref($registered) ne 'CODE';
817 if ($format_type eq 'list') {
819 # Note: we allow empty lists
820 foreach my $v (split_list
($value)) {
821 push @{$parsed}, $registered->($v);
823 } elsif ($format_type eq 'opt') {
824 $parsed = $registered->($value) if $value;
826 if (ref($registered) eq 'HASH') {
827 # Note: this is the only case where a validator function could be
828 # attached, hence it's safe to handle that in parse_property_string.
829 # We do however have to call it with $format_name instead of
830 # $registered, so it knows about the name (and thus any validators).
831 $parsed = parse_property_string
($format, $value, $path);
833 $parsed = $registered->($value);
843 return undef if $value !~ m/^(\d+(\.\d+)?)([KMGT])?$/;
844 my ($size, $unit) = ($1, $3);
847 $size = $size * 1024;
848 } elsif ($unit eq 'M') {
849 $size = $size * 1024 * 1024;
850 } elsif ($unit eq 'G') {
851 $size = $size * 1024 * 1024 * 1024;
852 } elsif ($unit eq 'T') {
853 $size = $size * 1024 * 1024 * 1024 * 1024;
864 my $kb = int($size/1024);
865 return $size if $kb*1024 != $size;
867 my $mb = int($kb/1024);
868 return "${kb}K" if $mb*1024 != $kb;
870 my $gb = int($mb/1024);
871 return "${mb}M" if $gb*1024 != $mb;
873 my $tb = int($gb/1024);
874 return "${gb}G" if $tb*1024 != $gb;
881 return 1 if $bool =~ m/^(1|on|yes|true)$/i;
882 return 0 if $bool =~ m/^(0|off|no|false)$/i;
886 sub parse_property_string
{
887 my ($format, $data, $path, $additional_properties) = @_;
889 # In property strings we default to not allowing additional properties
890 $additional_properties = 0 if !defined($additional_properties);
892 # Support named formats here, too:
895 if (my $reg = get_format
($format)) {
896 die "parse_property_string only accepts hash based named formats\n"
897 if ref($reg) ne 'HASH';
899 # named formats can have validators attached
900 $validator = $format_validators->{$format};
904 die "unknown format: $format\n";
906 } elsif (ref($format) ne 'HASH') {
907 die "unexpected format value of type ".ref($format)."\n";
913 foreach my $part (split(/,/, $data)) {
914 next if $part =~ /^\s*$/;
916 if ($part =~ /^([^=]+)=(.+)$/) {
917 my ($k, $v) = ($1, $2);
918 die "duplicate key in comma-separated list property: $k\n" if defined($res->{$k});
919 my $schema = $format->{$k};
920 if (my $alias = $schema->{alias
}) {
921 if (my $key_alias = $schema->{keyAlias
}) {
922 die "key alias '$key_alias' is already defined\n" if defined($res->{$key_alias});
923 $res->{$key_alias} = $k;
926 $schema = $format->{$k};
929 die "invalid key in comma-separated list property: $k\n" if !$schema;
930 if ($schema->{type
} && $schema->{type
} eq 'boolean') {
931 $v = parse_boolean
($v) // $v;
934 } elsif ($part !~ /=/) {
935 die "duplicate key in comma-separated list property: $default_key\n" if $default_key;
936 foreach my $key (keys %$format) {
937 if ($format->{$key}->{default_key
}) {
939 if (!$res->{$default_key}) {
940 $res->{$default_key} = $part;
943 die "duplicate key in comma-separated list property: $default_key\n";
946 die "value without key, but schema does not define a default key\n" if !$default_key;
948 die "missing key in comma-separated list property\n";
953 check_object
($path, $format, $res, $additional_properties, $errors);
954 if (scalar(%$errors)) {
955 raise
"format error\n", errors
=> $errors;
958 return $validator->($res) if $validator;
963 my ($errors, $path, $msg) = @_;
965 $path = '_root' if !$path;
967 if ($errors->{$path}) {
968 $errors->{$path} = join ('\n', $errors->{$path}, $msg);
970 $errors->{$path} = $msg;
977 # see 'man perlretut'
978 return $value =~ /^[+-]?(\d+\.\d+|\d+\.|\.\d+|\d+)([eE][+-]?\d+)?$/;
984 return $value =~ m/^[+-]?\d+$/;
988 my ($path, $type, $value, $errors) = @_;
992 if (!defined($value)) {
993 return 1 if $type eq 'null';
997 if (my $tt = ref($type)) {
998 if ($tt eq 'ARRAY') {
999 foreach my $t (@$type) {
1001 check_type
($path, $t, $value, $tmperr);
1002 return 1 if !scalar(%$tmperr);
1004 my $ttext = join ('|', @$type);
1005 add_error
($errors, $path, "type check ('$ttext') failed");
1007 } elsif ($tt eq 'HASH') {
1009 check_prop
($value, $type, $path, $tmperr);
1010 return 1 if !scalar(%$tmperr);
1011 add_error
($errors, $path, "type check failed");
1014 die "internal error - got reference type '$tt'";
1019 return 1 if $type eq 'any';
1021 if ($type eq 'null') {
1022 if (defined($value)) {
1023 add_error
($errors, $path, "type check ('$type') failed - value is not null");
1029 my $vt = ref($value);
1031 if ($type eq 'array') {
1032 if (!$vt || $vt ne 'ARRAY') {
1033 add_error
($errors, $path, "type check ('$type') failed");
1037 } elsif ($type eq 'object') {
1038 if (!$vt || $vt ne 'HASH') {
1039 add_error
($errors, $path, "type check ('$type') failed");
1043 } elsif ($type eq 'coderef') {
1044 if (!$vt || $vt ne 'CODE') {
1045 add_error
($errors, $path, "type check ('$type') failed");
1049 } elsif ($type eq 'string' && $vt eq 'Regexp') {
1050 # qr// regexes can be used as strings and make sense for format=regex
1054 if ($type eq 'boolean' && JSON
::is_bool
($value)) {
1057 add_error
($errors, $path, "type check ('$type') failed - got $vt");
1060 if ($type eq 'string') {
1061 return 1; # nothing to check ?
1062 } elsif ($type eq 'boolean') {
1063 #if ($value =~ m/^(1|true|yes|on)$/i) {
1064 if ($value eq '1') {
1066 #} elsif ($value =~ m/^(0|false|no|off)$/i) {
1067 } elsif ($value eq '0') {
1068 return 1; # return success (not value)
1070 add_error
($errors, $path, "type check ('$type') failed - got '$value'");
1073 } elsif ($type eq 'integer') {
1074 if (!is_integer
($value)) {
1075 add_error
($errors, $path, "type check ('$type') failed - got '$value'");
1079 } elsif ($type eq 'number') {
1080 if (!is_number
($value)) {
1081 add_error
($errors, $path, "type check ('$type') failed - got '$value'");
1086 return 1; # no need to verify unknown types
1095 my sub get_instance_type
{
1096 my ($schema, $key, $value) = @_;
1098 if (my $type_property = $schema->{$key}->{'type-property'}) {
1099 return $value->{$type_property};
1106 my ($path, $schema, $value, $additional_properties, $errors) = @_;
1108 # print "Check Object " . Dumper($value) . "\nSchema: " . Dumper($schema);
1110 my $st = ref($schema);
1111 if (!$st || $st ne 'HASH') {
1112 add_error
($errors, $path, "Invalid schema definition.");
1116 my $vt = ref($value);
1117 if (!$vt || $vt ne 'HASH') {
1118 add_error
($errors, $path, "an object is required");
1122 foreach my $k (keys %$schema) {
1123 my $instance_type = get_instance_type
($schema, $k, $value);
1124 check_prop
($value->{$k}, $schema->{$k}, $path ?
"$path.$k" : $k, $errors, $instance_type);
1127 foreach my $k (keys %$value) {
1129 my $newpath = $path ?
"$path.$k" : $k;
1131 if (my $subschema = $schema->{$k}) {
1132 if (my $requires = $subschema->{requires
}) {
1133 if (ref($requires)) {
1134 #print "TEST: " . Dumper($value) . "\n", Dumper($requires) ;
1135 check_prop
($value, $requires, $path, $errors);
1136 } elsif (!defined($value->{$requires})) {
1137 add_error
($errors, $path ?
"$path.$requires" : $requires,
1138 "missing property - '$newpath' requires this property");
1142 # if it's a oneOf, check if there is a matching type
1143 my $matched_type = 1;
1144 if ($subschema->{oneOf
}) {
1145 my $instance_type = get_instance_type
($schema, $k, $value);
1147 for my $alternative ($subschema->{oneOf
}->@*) {
1148 if (my $instance_types = $alternative->{'instance-types'}) {
1149 if (!grep { $instance_type eq $_ } $instance_types->@*) {
1158 next if $matched_type; # value is already checked above
1161 if (defined ($additional_properties) && !$additional_properties) {
1162 add_error
($errors, $newpath, "property is not defined in schema " .
1163 "and the schema does not allow additional properties");
1166 check_prop
($value->{$k}, $additional_properties, $newpath, $errors)
1167 if ref($additional_properties);
1171 sub check_object_warn
{
1172 my ($path, $schema, $value, $additional_properties) = @_;
1174 check_object
($path, $schema, $value, $additional_properties, $errors);
1175 if (scalar(%$errors)) {
1176 foreach my $k (keys %$errors) {
1177 warn "parse error: $k: $errors->{$k}\n";
1185 my ($value, $schema, $path, $errors, $instance_type) = @_;
1187 die "internal error - no schema" if !$schema;
1188 die "internal error" if !$errors;
1190 #print "check_prop $path\n" if $value;
1192 my $st = ref($schema);
1193 if (!$st || $st ne 'HASH') {
1194 add_error
($errors, $path, "Invalid schema definition.");
1198 # must pass any of the given schemas
1199 my $optional_for_type = 0;
1200 if ($schema->{oneOf
}) {
1201 # in case we have an instance_type given, just check for that variant
1202 if ($schema->{'type-property'}) {
1203 $optional_for_type = 1;
1204 for (my $i = 0; $i < scalar($schema->{oneOf
}->@*); $i++) {
1205 last if !$instance_type; # treat as optional if we don't have a type
1206 my $inner_schema = $schema->{oneOf
}->[$i];
1208 if (!defined($inner_schema->{'instance-types'})) {
1209 add_error
($errors, $path, "missing 'instance-types' in oneOf alternative");
1213 next if !grep { $_ eq $instance_type } $inner_schema->{'instance-types'}->@*;
1214 $optional_for_type = $inner_schema->{optional
} // 0;
1215 check_prop
($value, $inner_schema, $path, $errors);
1219 my $collected_errors = {};
1220 for (my $i = 0; $i < scalar($schema->{oneOf
}->@*); $i++) {
1221 my $inner_schema = $schema->{oneOf
}->[$i];
1222 my $inner_errors = {};
1223 check_prop
($value, $inner_schema, "$path.oneOf[$i]", $inner_errors);
1224 if (!$inner_errors->%*) {
1229 for my $inner_path (keys $inner_errors->%*) {
1230 add_error
($collected_errors, $inner_path, $inner_errors->{$path});
1235 for my $inner_path (keys $collected_errors->%*) {
1236 add_error
($errors, $inner_path, $collected_errors->{$path});
1240 } elsif ($instance_type) {
1241 if (!defined($schema->{'instance-types'})) {
1242 add_error
($errors, $path, "missing 'instance-types'");
1245 if (grep { $_ eq $instance_type} $schema->{'instance_types'}->@*) {
1246 $optional_for_type = 1;
1250 # if it extends another schema, it must pass that schema as well
1251 if($schema->{extends
}) {
1252 check_prop
($value, $schema->{extends
}, $path, $errors);
1255 if (!defined ($value)) {
1256 return if $schema->{type
} && $schema->{type
} eq 'null';
1257 if (!$schema->{optional
} && !$schema->{alias
} && !$schema->{group
} && !$optional_for_type) {
1258 add_error
($errors, $path, "property is missing and it is not optional");
1263 return if !check_type
($path, $schema->{type
}, $value, $errors);
1265 if ($schema->{disallow
}) {
1267 if (check_type
($path, $schema->{disallow
}, $value, $tmperr)) {
1268 add_error
($errors, $path, "disallowed value was matched");
1273 if (my $vt = ref($value)) {
1275 if ($vt eq 'ARRAY') {
1276 if ($schema->{items
}) {
1277 my $it = ref($schema->{items
});
1278 if ($it && $it eq 'ARRAY') {
1279 #die "implement me $path: $vt " . Dumper($schema) ."\n". Dumper($value);
1280 die "not implemented";
1283 foreach my $el (@$value) {
1284 check_prop
($el, $schema->{items
}, "${path}[$ind]", $errors);
1290 } elsif ($schema->{properties
} || $schema->{additionalProperties
}) {
1291 check_object
($path, defined($schema->{properties
}) ?
$schema->{properties
} : {},
1292 $value, $schema->{additionalProperties
}, $errors);
1298 if (my $format = $schema->{format
}) {
1299 eval { check_format
($format, $value, $path); };
1301 add_error
($errors, $path, "invalid format - $@");
1306 if (my $pattern = $schema->{pattern
}) {
1307 if ($value !~ m/^$pattern$/) {
1308 add_error
($errors, $path, "value does not match the regex pattern");
1313 if (defined (my $max = $schema->{maxLength
})) {
1314 if (length($value) > $max) {
1315 add_error
($errors, $path, "value may only be $max characters long");
1320 if (defined (my $min = $schema->{minLength
})) {
1321 if (length($value) < $min) {
1322 add_error
($errors, $path, "value must be at least $min characters long");
1327 if (is_number
($value)) {
1328 if (defined (my $max = $schema->{maximum
})) {
1329 if ($value > $max) {
1330 add_error
($errors, $path, "value must have a maximum value of $max");
1335 if (defined (my $min = $schema->{minimum
})) {
1336 if ($value < $min) {
1337 add_error
($errors, $path, "value must have a minimum value of $min");
1343 if (my $ea = $schema->{enum
}) {
1346 foreach my $ev (@$ea) {
1347 if ($ev eq $value) {
1353 add_error
($errors, $path, "value '$value' does not have a value in the enumeration '" .
1354 join(", ", @$ea) . "'");
1361 my ($instance, $schema, $errmsg) = @_;
1364 $errmsg = "Parameter verification failed.\n" if !$errmsg;
1366 # todo: cycle detection is only needed for debugging, I guess
1367 # we can disable that in the final release
1368 # todo: is there a better/faster way to detect cycles?
1370 # 'download' responses can contain a filehandle, don't cycle-check that as
1371 # it produces a warning
1372 my $is_download = ref($instance) eq 'HASH' && exists($instance->{download
});
1373 find_cycle
($instance, sub { $cycles = 1 }) if !$is_download;
1375 add_error
($errors, undef, "data structure contains recursive cycles");
1377 check_prop
($instance, $schema, '', $errors);
1380 if (scalar(%$errors)) {
1381 raise
$errmsg, code
=> HTTP_BAD_REQUEST
, errors
=> $errors;
1387 my $schema_valid_types = ["string", "object", "coderef", "array", "boolean", "number", "integer", "null", "any"];
1388 my $default_schema_noref = {
1389 description
=> "This is the JSON Schema for JSON Schemas.",
1390 type
=> [ "object" ],
1391 additionalProperties
=> 0,
1394 type
=> ["string", "array"],
1395 description
=> "This is a type definition value. This can be a simple type, or a union type",
1400 enum
=> $schema_valid_types,
1402 enum
=> $schema_valid_types,
1406 description
=> "This represents the alternative options for this Schema instance.",
1410 description
=> "A valid option of the properties",
1413 'instance-types' => {
1415 description
=> "Indicate to which type the parameter (or variant if inside a oneOf) belongs.",
1421 'type-property' => {
1423 description
=> "The property to check for instance types.",
1428 description
=> "This indicates that the instance property in the instance object is not required.",
1434 description
=> "This is a definition for the properties of an object value",
1440 description
=> "When the value is an array, this indicates the schema to use to validate each item in an array",
1444 additionalProperties
=> {
1445 type
=> [ "boolean", "object"],
1446 description
=> "This provides a default property definition for all properties that are not explicitly defined in an object type definition.",
1453 description
=> "This indicates the minimum value for the instance property when the type of the instance value is a number.",
1458 description
=> "This indicates the maximum value for the instance property when the type of the instance value is a number.",
1462 description
=> "When the instance value is a string, this indicates minimum length of the string",
1469 description
=> "When the instance value is a string, this indicates maximum length of the string.",
1475 description
=> "A text representation of the type (used to generate documentation).",
1480 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.",
1487 description
=> "This provides an enumeration of possible values that are valid for the instance property.",
1492 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).",
1494 verbose_description
=> {
1497 description
=> "This provides a more verbose description.",
1499 format_description
=> {
1502 description
=> "This provides a shorter (usually just one word) description for a property used to generate descriptions for comma separated list property strings.",
1507 description
=> "This provides the title of the property",
1512 description
=> "This is used to provide rendering hints to format cli command output.",
1515 type
=> [ "string", "object" ],
1517 description
=> "indicates a required property or a schema that must be validated if this property is present",
1520 type
=> [ "string", "object" ],
1522 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",
1527 description
=> "Whether this is the default key in a comma separated list property string.",
1532 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.",
1537 description
=> "Allows to store the current 'key' as value of another property. Only valid if used together with 'alias'.",
1538 requires
=> 'alias',
1543 description
=> "This indicates the default for the instance property."
1547 description
=> "Bash completion function. This function should return a list of possible values.",
1553 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, then this instance is not valid.",
1558 description
=> "This indicates the schema extends the given schema. All instances of this schema must be valid to by the extended schema also.",
1561 # this is from hyper schema
1564 description
=> "This defines the link relations of the instance objects",
1571 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",
1575 description
=> "This is the name of the link relation",
1581 description
=> "For submission links, this defines the method that should be used to access the target resource",
1590 description
=> "For CLI context, this defines the maximal width to print before truncating",
1596 my $default_schema = Storable
::dclone
($default_schema_noref);
1598 $default_schema->{properties
}->{properties
}->{additionalProperties
} = $default_schema;
1599 $default_schema->{properties
}->{additionalProperties
}->{properties
} = $default_schema->{properties
};
1600 $default_schema->{properties
}->{oneOf
}->{items
}->{properties
} = $default_schema->{properties
};
1602 $default_schema->{properties
}->{items
}->{properties
} = $default_schema->{properties
};
1603 $default_schema->{properties
}->{items
}->{additionalProperties
} = 0;
1605 $default_schema->{properties
}->{disallow
}->{properties
} = $default_schema->{properties
};
1606 $default_schema->{properties
}->{disallow
}->{additionalProperties
} = 0;
1608 $default_schema->{properties
}->{requires
}->{properties
} = $default_schema->{properties
};
1609 $default_schema->{properties
}->{requires
}->{additionalProperties
} = 0;
1611 $default_schema->{properties
}->{extends
}->{properties
} = $default_schema->{properties
};
1612 $default_schema->{properties
}->{extends
}->{additionalProperties
} = 0;
1614 my $method_schema = {
1616 additionalProperties
=> 0,
1619 description
=> "This a description of the method",
1624 description
=> "This indicates the name of the function to call.",
1627 additionalProperties
=> 1,
1642 description
=> "The HTTP method name.",
1643 enum
=> [ 'GET', 'POST', 'PUT', 'DELETE' ],
1648 description
=> "Method needs special privileges - only pvedaemon can execute it",
1653 description
=> "Method is available for clients authenticated using an API token.",
1659 description
=> "Method downloads the file content (filename is the return value of the method).",
1664 description
=> "A parameter name. If specified, all calls to this method are proxied to the host contained in that parameter.",
1667 proxyto_callback
=> {
1669 description
=> "A function which is called to resolve the proxyto attribute. The default implementation returns the value of the 'proxyto' parameter.",
1674 description
=> "Required access permissions. By default only 'root' is allowed to access this method.",
1676 additionalProperties
=> 0,
1679 description
=> "Describe access permissions.",
1683 description
=> "A simply way to allow access for 'all' authenticated users. Value 'world' is used to allow access without credentials.",
1685 enum
=> ['all', 'world'],
1689 description
=> "Array of permission checks (prefix notation).",
1696 description
=> "Used internally",
1700 description
=> "Used internally",
1705 description
=> "path for URL matching (uri template)",
1707 fragmentDelimiter
=> {
1709 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.",
1714 description
=> "JSON Schema for parameters.",
1719 description
=> "JSON Schema for return value.",
1724 description
=> "method implementation (code reference)",
1729 description
=> "Delegate call to this class (perl class string).",
1732 additionalProperties
=> 0,
1738 fragmentDelimiter
=> { optional
=> 1 }
1746 sub validate_schema
{
1749 my $errmsg = "internal error - unable to verify schema\n";
1750 validate
($schema, $default_schema, $errmsg);
1753 sub validate_method_info
{
1756 my $errmsg = "internal error - unable to verify method info\n";
1757 validate
($info, $method_schema, $errmsg);
1759 validate_schema
($info->{parameters
}) if $info->{parameters
};
1760 validate_schema
($info->{returns
}) if $info->{returns
};
1763 # run a self test on load
1764 # make sure we can verify the default schema
1765 validate_schema
($default_schema_noref);
1766 validate_schema
($method_schema);
1768 # and now some utility methods (used by pve api)
1769 sub method_get_child_link
{
1772 return undef if !$info;
1774 my $schema = $info->{returns
};
1775 return undef if !$schema || !$schema->{type
} || $schema->{type
} ne 'array';
1777 my $links = $schema->{links
};
1778 return undef if !$links;
1781 foreach my $lnk (@$links) {
1782 if ($lnk->{href
} && $lnk->{rel
} && ($lnk->{rel
} eq 'child')) {
1791 # a way to parse command line parameters, using a
1792 # schema to configure Getopt::Long
1794 my ($schema, $args, $arg_param, $fixed_param, $param_mapping_hash) = @_;
1796 if (!$schema || !$schema->{properties
}) {
1797 raise
("too many arguments\n", code
=> HTTP_BAD_REQUEST
)
1798 if scalar(@$args) != 0;
1803 if ($arg_param && !ref($arg_param)) {
1804 my $pd = $schema->{properties
}->{$arg_param};
1805 die "expected list format $pd->{format}"
1806 if !($pd && $pd->{format
} && $pd->{format
} =~ m/-list/);
1807 $list_param = $arg_param;
1810 my @interactive = ();
1812 foreach my $prop (keys %{$schema->{properties
}}) {
1813 my $pd = $schema->{properties
}->{$prop};
1814 next if $list_param && $prop eq $list_param;
1815 next if defined($fixed_param->{$prop});
1817 my $mapping = $param_mapping_hash->{$prop};
1818 if ($mapping && $mapping->{interactive
}) {
1819 # interactive parameters such as passwords: make the argument
1820 # optional and call the mapping function afterwards.
1821 push @getopt, "$prop:s";
1822 push @interactive, [$prop, $mapping->{func
}];
1823 } elsif ($pd->{type
} && $pd->{type
} eq 'boolean') {
1824 push @getopt, "$prop:s";
1826 if ($pd->{format
} && $pd->{format
} =~ m/-list/) {
1827 push @getopt, "$prop=s@";
1828 } elsif ($pd->{type
} && $pd->{type
} eq 'array') {
1829 push @getopt, "$prop=s@";
1831 push @getopt, "$prop=s";
1836 Getopt
::Long
::Configure
('prefix_pattern=(--|-)');
1839 raise
("unable to parse option\n", code
=> HTTP_BAD_REQUEST
)
1840 if !Getopt
::Long
::GetOptionsFromArray
($args, $opts, @getopt);
1844 $opts->{$list_param} = $args;
1846 } elsif (ref($arg_param)) {
1847 for (my $i = 0; $i < scalar(@$arg_param); $i++) {
1848 my $arg_name = $arg_param->[$i];
1849 if ($opts->{'extra-args'}) {
1850 raise
("internal error: extra-args must be the last argument\n", code
=> HTTP_BAD_REQUEST
);
1852 if ($arg_name eq 'extra-args') {
1853 $opts->{'extra-args'} = $args;
1858 # check if all left-over arg_param are optional, else we
1859 # must die as the mapping is then ambigious
1860 for (; $i < scalar(@$arg_param); $i++) {
1861 my $prop = $arg_param->[$i];
1862 raise
("not enough arguments\n", code
=> HTTP_BAD_REQUEST
)
1863 if !$schema->{properties
}->{$prop}->{optional
};
1865 if ($arg_param->[-1] eq 'extra-args') {
1866 $opts->{'extra-args'} = [];
1870 $opts->{$arg_name} = shift @$args;
1872 raise
("too many arguments\n", code
=> HTTP_BAD_REQUEST
) if @$args;
1874 raise
("too many arguments\n", code
=> HTTP_BAD_REQUEST
)
1875 if scalar(@$args) != 0;
1878 if (ref($arg_param)) {
1879 foreach my $arg_name (@$arg_param) {
1880 if ($arg_name eq 'extra-args') {
1881 $opts->{'extra-args'} = [];
1882 } elsif (!$schema->{properties
}->{$arg_name}->{optional
}) {
1883 raise
("not enough arguments\n", code
=> HTTP_BAD_REQUEST
);
1889 foreach my $entry (@interactive) {
1890 my ($opt, $func) = @$entry;
1891 my $pd = $schema->{properties
}->{$opt};
1892 my $value = $opts->{$opt};
1893 if (defined($value) || !$pd->{optional
}) {
1894 $opts->{$opt} = $func->($value);
1898 # decode after Getopt as we are not sure how well it handles unicode
1899 foreach my $p (keys %$opts) {
1900 if (!ref($opts->{$p})) {
1901 $opts->{$p} = decode
('locale', $opts->{$p});
1902 } elsif (ref($opts->{$p}) eq 'ARRAY') {
1904 foreach my $v (@{$opts->{$p}}) {
1905 push @$tmp, decode
('locale', $v);
1908 } elsif (ref($opts->{$p}) eq 'SCALAR') {
1909 $opts->{$p} = decode
('locale', $$opts->{$p});
1911 raise
("decoding options failed, unknown reference\n", code
=> HTTP_BAD_REQUEST
);
1915 foreach my $p (keys %$opts) {
1916 if (my $pd = $schema->{properties
}->{$p}) {
1917 if ($pd->{type
} && $pd->{type
} eq 'boolean') {
1918 if ($opts->{$p} eq '') {
1920 } elsif (defined(my $bool = parse_boolean
($opts->{$p}))) {
1921 $opts->{$p} = $bool;
1923 raise
("unable to parse boolean option\n", code
=> HTTP_BAD_REQUEST
);
1925 } elsif ($pd->{format
}) {
1927 if ($pd->{format
} =~ m/-list/) {
1928 # allow --vmid 100 --vmid 101 and --vmid 100,101
1929 # allow --dow mon --dow fri and --dow mon,fri
1930 $opts->{$p} = join(",", @{$opts->{$p}}) if ref($opts->{$p}) eq 'ARRAY';
1936 foreach my $p (keys %$fixed_param) {
1937 $opts->{$p} = $fixed_param->{$p};
1943 # A way to parse configuration data by giving a json schema
1944 sub parse_config
: prototype($$$;$) {
1945 my ($schema, $filename, $raw, $comment_key) = @_;
1947 # do fast check (avoid validate_schema($schema))
1948 die "got strange schema" if !$schema->{type
} ||
1949 !$schema->{properties
} || $schema->{type
} ne 'object';
1954 my $handle_comment = sub { $_[0] =~ /^#/ };
1955 if (defined($comment_key)) {
1957 my $comment_re = qr/^\Q$comment_key\E:\s*(.*\S)\s*$/;
1958 $handle_comment = sub {
1959 if ($_[0] =~ /^\#(.*)\s*$/ || $_[0] =~ $comment_re) {
1960 $comment_data .= PVE
::Tools
::decode_text
($1) . "\n";
1967 while ($raw =~ /^\s*(.+?)\s*$/gm) {
1970 next if $handle_comment->($line);
1972 if ($line =~ m/^(\S+?):\s*(.*)$/) {
1975 if ($schema->{properties
}->{$key} &&
1976 $schema->{properties
}->{$key}->{type
} eq 'boolean') {
1978 $value = parse_boolean
($value) // $value;
1981 $schema->{properties
}->{$key}
1982 && $schema->{properties
}->{$key}->{type
} eq 'array'
1985 $cfg->{$key} //= [];
1986 push $cfg->{$key}->@*, $value;
1989 $cfg->{$key} = $value;
1991 warn "ignore config line: $line\n"
1995 if (defined($comment_data)) {
1996 $cfg->{$comment_key} = $comment_data;
2000 check_prop
($cfg, $schema, '', $errors);
2002 foreach my $k (keys %$errors) {
2003 warn "parse error in '$filename' - '$k': $errors->{$k}\n";
2010 # generate simple key/value file
2012 my ($schema, $filename, $cfg) = @_;
2014 # do fast check (avoid validate_schema($schema))
2015 die "got strange schema" if !$schema->{type
} ||
2016 !$schema->{properties
} || $schema->{type
} ne 'object';
2018 validate
($cfg, $schema, "validation error in '$filename'\n");
2022 foreach my $k (sort keys %$cfg) {
2023 $data .= "$k: $cfg->{$k}\n";
2029 # helpers used to generate our manual pages
2031 my $find_schema_default_key = sub {
2035 my $keyAliasProps = {};
2037 foreach my $key (keys %$format) {
2038 my $phash = $format->{$key};
2039 if ($phash->{default_key
}) {
2040 die "multiple default keys in schema ($default_key, $key)\n"
2041 if defined($default_key);
2042 die "default key '$key' is an alias - this is not allowed\n"
2043 if defined($phash->{alias
});
2044 die "default key '$key' with keyAlias attribute is not allowed\n"
2045 if $phash->{keyAlias
};
2046 $default_key = $key;
2048 my $key_alias = $phash->{keyAlias
};
2049 die "found keyAlias without 'alias definition for '$key'\n"
2050 if $key_alias && !$phash->{alias
};
2052 if ($phash->{alias
} && $key_alias) {
2053 die "inconsistent keyAlias '$key_alias' definition"
2054 if defined($keyAliasProps->{$key_alias}) &&
2055 $keyAliasProps->{$key_alias} ne $phash->{alias
};
2056 $keyAliasProps->{$key_alias} = $phash->{alias
};
2060 return wantarray ?
($default_key, $keyAliasProps) : $default_key;
2063 sub generate_typetext
{
2064 my ($format, $list_enums) = @_;
2066 my ($default_key, $keyAliasProps) = &$find_schema_default_key($format);
2071 my $add_option_string = sub {
2072 my ($text, $optional) = @_;
2078 $text = "[$text]" if $optional;
2083 my $format_key_value = sub {
2084 my ($key, $phash) = @_;
2086 die "internal error" if defined($phash->{alias
});
2092 if (my $desc = $phash->{format_description
}) {
2093 $typetext .= "<$desc>";
2094 } elsif (my $text = $phash->{typetext
}) {
2096 } elsif (my $enum = $phash->{enum
}) {
2097 if ($list_enums || (scalar(@$enum) <= 3)) {
2098 $typetext .= '<' . join('|', @$enum) . '>';
2100 $typetext .= '<enum>';
2102 } elsif ($phash->{type
} eq 'boolean') {
2103 $typetext .= '<1|0>';
2104 } elsif ($phash->{type
} eq 'integer') {
2105 $typetext .= '<integer>';
2106 } elsif ($phash->{type
} eq 'number') {
2107 $typetext .= '<number>';
2109 die "internal error: neither format_description nor typetext found for option '$key'";
2112 if (defined($default_key) && ($default_key eq $key)) {
2113 &$add_option_string("[$keytext=]$typetext", $phash->{optional
});
2115 &$add_option_string("$keytext=$typetext", $phash->{optional
});
2121 my $cond_add_key = sub {
2124 return if $done->{$key}; # avoid duplicates
2128 my $phash = $format->{$key};
2130 return if !$phash; # should not happen
2132 return if $phash->{alias
};
2134 &$format_key_value($key, $phash);
2138 &$cond_add_key($default_key) if defined($default_key);
2140 # add required keys first
2141 foreach my $key (sort keys %$format) {
2142 my $phash = $format->{$key};
2143 &$cond_add_key($key) if $phash && !$phash->{optional
};
2147 foreach my $key (sort keys %$format) {
2148 &$cond_add_key($key);
2151 foreach my $keyAlias (sort keys %$keyAliasProps) {
2152 &$add_option_string("<$keyAlias>=<$keyAliasProps->{$keyAlias }>", 1);
2158 sub print_property_string
{
2159 my ($data, $format, $skip, $path) = @_;
2162 if (ref($format) ne 'HASH') {
2163 my $schema = get_format
($format);
2164 die "not a valid format: $format\n" if !$schema;
2165 # named formats can have validators attached
2166 $validator = $format_validators->{$format};
2171 check_object
($path, $format, $data, undef, $errors);
2172 if (scalar(%$errors)) {
2173 raise
"format error", errors
=> $errors;
2176 $data = $validator->($data) if $validator;
2178 my ($default_key, $keyAliasProps) = &$find_schema_default_key($format);
2183 my $add_option_string = sub {
2186 $res .= ',' if $add_sep;
2191 my $format_value = sub {
2192 my ($key, $value, $format) = @_;
2194 if (defined($format) && ($format eq 'disk-size')) {
2195 return format_size
($value);
2197 die "illegal value with commas for $key\n" if $value =~ /,/;
2202 my $done = { map { $_ => 1 } @$skip };
2204 my $cond_add_key = sub {
2205 my ($key, $isdefault) = @_;
2207 return if $done->{$key}; # avoid duplicates
2211 my $value = $data->{$key};
2213 return if !defined($value);
2215 my $phash = $format->{$key};
2217 # try to combine values if we have key aliases
2218 if (my $combine = $keyAliasProps->{$key}) {
2219 if (defined(my $combine_value = $data->{$combine})) {
2220 my $combine_format = $format->{$combine}->{format
};
2221 my $value_str = &$format_value($key, $value, $phash->{format
});
2222 my $combine_str = &$format_value($combine, $combine_value, $combine_format);
2223 &$add_option_string("${value_str}=${combine_str}");
2224 $done->{$combine} = 1;
2229 if ($phash && $phash->{alias
}) {
2230 $phash = $format->{$phash->{alias
}};
2233 die "invalid key '$key'\n" if !$phash;
2234 die "internal error" if defined($phash->{alias
});
2236 my $value_str = &$format_value($key, $value, $phash->{format
});
2238 &$add_option_string($value_str);
2240 &$add_option_string("$key=${value_str}");
2244 # add default key first
2245 &$cond_add_key($default_key, 1) if defined($default_key);
2247 # add required keys first
2248 foreach my $key (sort keys %$data) {
2249 my $phash = $format->{$key};
2250 &$cond_add_key($key) if $phash && !$phash->{optional
};
2254 foreach my $key (sort keys %$data) {
2255 &$cond_add_key($key);
2261 sub schema_get_type_text
{
2262 my ($phash, $style) = @_;
2264 my $type = $phash->{type
} || 'string';
2266 if ($phash->{typetext
}) {
2267 return $phash->{typetext
};
2268 } elsif ($phash->{format_description
}) {
2269 return "<$phash->{format_description}>";
2270 } elsif ($phash->{enum
}) {
2271 return "<" . join(' | ', sort @{$phash->{enum
}}) . ">";
2272 } elsif ($phash->{pattern
}) {
2273 return $phash->{pattern
};
2274 } elsif ($type eq 'integer' || $type eq 'number') {
2275 # NOTE: always access values as number (avoid converion to string)
2276 if (defined($phash->{minimum
}) && defined($phash->{maximum
})) {
2277 return "<$type> (" . ($phash->{minimum
} + 0) . " - " .
2278 ($phash->{maximum
} + 0) . ")";
2279 } elsif (defined($phash->{minimum
})) {
2280 return "<$type> (" . ($phash->{minimum
} + 0) . " - N)";
2281 } elsif (defined($phash->{maximum
})) {
2282 return "<$type> (-N - " . ($phash->{maximum
} + 0) . ")";
2284 } elsif ($type eq 'string') {
2285 if (my $format = $phash->{format
}) {
2286 $format = get_format
($format) if ref($format) ne 'HASH';
2287 if (ref($format) eq 'HASH') {
2289 $list_enums = 1 if $style && $style eq 'config-sub';
2290 return generate_typetext
($format, $list_enums);