]> git.proxmox.com Git - pve-common.git/blame - src/PVE/SectionConfig.pm
fix SectionConfig updateSchema for classes without plugins.
[pve-common.git] / src / PVE / SectionConfig.pm
CommitLineData
212b08e8
DM
1package PVE::SectionConfig;
2
3use strict;
4use warnings;
5use Digest::SHA;
6use PVE::Exception qw(raise_param_exc);
7use PVE::JSONSchema qw(get_standard_option);
8
9use Data::Dumper;
10
11my $defaultData = {
12 options => {},
13 plugins => {},
14 plugindata => {},
15 propertyList => {},
16};
17
18sub private {
19 die "overwrite me";
20 return $defaultData;
21}
22
23sub register {
24 my ($class) = @_;
25
26 my $type = $class->type();
27 my $pdata = $class->private();
28
29 my $plugindata = $class->plugindata();
30 $pdata->{plugindata}->{$type} = $plugindata;
31 $pdata->{plugins}->{$type} = $class;
32}
33
34sub type {
35 die "overwrite me";
36}
37
38sub properties {
39 return {};
40}
41
42sub options {
43 return {};
44}
45
46sub plugindata {
47 return {};
48}
49
50sub createSchema {
51 my ($class) = @_;
52
53 my $pdata = $class->private();
54 my $propertyList = $pdata->{propertyList};
a3530f55
DM
55 my $plugins = $pdata->{plugins};
56
57 my $props = {};
58
59 my $copy_property = sub {
60 my ($src) = @_;
61
62 my $res = {};
63 foreach my $k (keys %$src) {
64 $res->{$k} = $src->{$k};
65 }
66
67 return $res;
68 };
69
70 foreach my $p (keys %$propertyList) {
71 next if $p eq 'type';
72 if (!$propertyList->{$p}->{optional}) {
73 $props->{$p} = $propertyList->{$p};
74 next;
75 }
76
77 my $required = 1;
78
79 my $copts = $class->options();
80 $required = 0 if defined($copts->{$p}) && $copts->{$p}->{optional};
81
82 foreach my $t (keys %$plugins) {
83 my $opts = $pdata->{options}->{$t} || {};
84 $required = 0 if !defined($opts->{$p}) || $opts->{$p}->{optional};
85 }
86
87 if ($required) {
88 # make a copy, because we modify the optional property
89 my $res = &$copy_property($propertyList->{$p});
90 $res->{optional} = 0;
91 $props->{$p} = $res;
92 } else {
93 $props->{$p} = $propertyList->{$p};
94 }
95 }
212b08e8
DM
96
97 return {
98 type => "object",
99 additionalProperties => 0,
a3530f55 100 properties => $props,
212b08e8
DM
101 };
102}
103
104sub updateSchema {
105 my ($class) = @_;
106
107 my $pdata = $class->private();
108 my $propertyList = $pdata->{propertyList};
109 my $plugins = $pdata->{plugins};
110
111 my $props = {};
112
113 foreach my $p (keys %$propertyList) {
114 next if $p eq 'type';
115 if (!$propertyList->{$p}->{optional}) {
116 $props->{$p} = $propertyList->{$p};
117 next;
118 }
516dfb55
DM
119
120 my $modifyable = 0;
121
0d838035
DM
122 my $copts = $class->options();
123 $modifyable = 1 if defined($copts->{$p}) && !$copts->{$p}->{fixed};
124
212b08e8 125 foreach my $t (keys %$plugins) {
516dfb55 126 my $opts = $pdata->{options}->{$t} || {};
212b08e8 127 next if !defined($opts->{$p});
516dfb55 128 $modifyable = 1 if !$opts->{$p}->{fixed};
212b08e8 129 }
516dfb55
DM
130 next if !$modifyable;
131
132 $props->{$p} = $propertyList->{$p};
212b08e8
DM
133 }
134
dc5eae7d 135 $props->{digest} = get_standard_option('pve-config-digest');
212b08e8
DM
136
137 $props->{delete} = {
138 type => 'string', format => 'pve-configid-list',
139 description => "A list of settings you want to delete.",
140 maxLength => 4096,
141 optional => 1,
142 };
143
144 return {
145 type => "object",
146 additionalProperties => 0,
147 properties => $props,
148 };
149}
150
151sub init {
152 my ($class) = @_;
153
154 my $pdata = $class->private();
155
156 foreach my $k (qw(options plugins plugindata propertyList)) {
157 $pdata->{$k} = {} if !$pdata->{$k};
158 }
159
160 my $plugins = $pdata->{plugins};
161 my $propertyList = $pdata->{propertyList};
162
163 foreach my $type (keys %$plugins) {
164 my $props = $plugins->{$type}->properties();
165 foreach my $p (keys %$props) {
b732bea7 166 die "duplicate property '$p'" if defined($propertyList->{$p});
212b08e8
DM
167 my $res = $propertyList->{$p} = {};
168 my $data = $props->{$p};
169 for my $a (keys %$data) {
170 $res->{$a} = $data->{$a};
171 }
172 $res->{optional} = 1;
173 }
174 }
175
176 foreach my $type (keys %$plugins) {
177 my $opts = $plugins->{$type}->options();
178 foreach my $p (keys %$opts) {
179 die "undefined property '$p'" if !$propertyList->{$p};
180 }
181 $pdata->{options}->{$type} = $opts;
182 }
183
184 $propertyList->{type}->{type} = 'string';
185 $propertyList->{type}->{enum} = [keys %$plugins];
186}
187
188sub lookup {
189 my ($class, $type) = @_;
190
191 my $pdata = $class->private();
192 my $plugin = $pdata->{plugins}->{$type};
193
194 die "unknown section type '$type'\n" if !$plugin;
195
196 return $plugin;
197}
198
199sub lookup_types {
200 my ($class) = @_;
201
202 my $pdata = $class->private();
203
204 return [ keys %{$pdata->{plugins}} ];
205}
206
207sub decode_value {
208 my ($class, $type, $key, $value) = @_;
209
210 return $value;
211}
212
213sub encode_value {
214 my ($class, $type, $key, $value) = @_;
215
216 return $value;
217}
218
219sub check_value {
220 my ($class, $type, $key, $value, $storeid, $skipSchemaCheck) = @_;
221
222 my $pdata = $class->private();
223
224 return $value if $key eq 'type' && $type eq $value;
225
226 my $opts = $pdata->{options}->{$type};
227 die "unknown section type '$type'\n" if !$opts;
228
229 die "unexpected property '$key'\n" if !defined($opts->{$key});
230
231 my $schema = $pdata->{propertyList}->{$key};
232 die "unknown property type\n" if !$schema;
233
234 my $ct = $schema->{type};
235
236 $value = 1 if $ct eq 'boolean' && !defined($value);
237
238 die "got undefined value\n" if !defined($value);
239
240 die "property contains a line feed\n" if $value =~ m/[\n\r]/;
241
242 if (!$skipSchemaCheck) {
243 my $errors = {};
244 PVE::JSONSchema::check_prop($value, $schema, '', $errors);
245 if (scalar(keys %$errors)) {
246 die "$errors->{$key}\n" if $errors->{$key};
247 die "$errors->{_root}\n" if $errors->{_root};
248 die "unknown error\n";
249 }
250 }
251
252 return $value;
253}
254
255sub parse_section_header {
256 my ($class, $line) = @_;
257
258 if ($line =~ m/^(\S+):\s*(\S+)\s*$/) {
259 my ($type, $sectionId) = ($1, $2);
260 my $errmsg = undef; # set if you want to skip whole section
261 my $config = {}; # to return additional attributes
262 return ($type, $sectionId, $errmsg, $config);
263 }
264 return undef;
265}
266
267sub format_section_header {
268 my ($class, $type, $sectionId) = @_;
269
270 return "$type: $sectionId\n";
271}
272
273
274sub parse_config {
275 my ($class, $filename, $raw) = @_;
276
277 my $pdata = $class->private();
278
279 my $ids = {};
280 my $order = {};
281
3c4d612a
WB
282 $raw = '' if !defined($raw);
283
284 my $digest = Digest::SHA::sha1_hex($raw);
212b08e8
DM
285
286 my $pri = 1;
287
288 my $lineno = 0;
3c4d612a
WB
289 my @lines = split(/\n/, $raw);
290 my $nextline = sub {
291 while (my $line = shift @lines) {
292 $lineno++;
293 return $line if $line !~ /^\s*(?:#|$)/;
294 }
295 };
212b08e8 296
3c4d612a 297 while (my $line = &$nextline()) {
212b08e8
DM
298 my $errprefix = "file $filename line $lineno";
299
300 my ($type, $sectionId, $errmsg, $config) = $class->parse_section_header($line);
301 if ($config) {
302 my $ignore = 0;
303
304 my $plugin;
305
306 if ($errmsg) {
307 $ignore = 1;
308 chomp $errmsg;
309 warn "$errprefix (skip section '$sectionId'): $errmsg\n";
310 } elsif (!$type) {
311 $ignore = 1;
312 warn "$errprefix (skip section '$sectionId'): missing type - internal error\n";
313 } else {
314 if (!($plugin = $pdata->{plugins}->{$type})) {
315 $ignore = 1;
316 warn "$errprefix (skip section '$sectionId'): unsupported type '$type'\n";
317 }
318 }
319
3c4d612a 320 while ($line = &$nextline()) {
212b08e8
DM
321 next if $ignore; # skip
322
323 $errprefix = "file $filename line $lineno";
324
325 if ($line =~ m/^\s+(\S+)(\s+(.*\S))?\s*$/) {
326 my ($k, $v) = ($1, $3);
327
328 eval {
329 die "duplicate attribute\n" if defined($config->{$k});
330 $config->{$k} = $plugin->check_value($type, $k, $v, $sectionId);
331 };
332 warn "$errprefix (section '$sectionId') - unable to parse value of '$k': $@" if $@;
333
334 } else {
335 warn "$errprefix (section '$sectionId') - ignore config line: $line\n";
336 }
337 }
338
339 if (!$ignore && $type && $plugin && $config) {
340 $config->{type} = $type;
341 eval { $ids->{$sectionId} = $plugin->check_config($sectionId, $config, 1, 1); };
342 warn "$errprefix (skip section '$sectionId'): $@" if $@;
343 $order->{$sectionId} = $pri++;
344 }
345
346 } else {
347 warn "$errprefix - ignore config line: $line\n";
348 }
349 }
350
351
352 my $cfg = { ids => $ids, order => $order, digest => $digest};
353
354 return $cfg;
355}
356
357sub check_config {
358 my ($class, $sectionId, $config, $create, $skipSchemaCheck) = @_;
359
360 my $type = $class->type();
361 my $pdata = $class->private();
362 my $opts = $pdata->{options}->{$type};
363
364 my $settings = { type => $type };
365
366 foreach my $k (keys %$config) {
367 my $value = $config->{$k};
368
369 die "can't change value of fixed parameter '$k'\n"
370 if !$create && $opts->{$k}->{fixed};
371
372 if (defined($value)) {
373 my $tmp = $class->check_value($type, $k, $value, $sectionId, $skipSchemaCheck);
374 $settings->{$k} = $class->decode_value($type, $k, $tmp);
375 } else {
376 die "got undefined value for option '$k'\n";
377 }
378 }
379
380 if ($create) {
381 # check if we have a value for all required options
382 foreach my $k (keys %$opts) {
383 next if $opts->{$k}->{optional};
384 die "missing value for required option '$k'\n"
385 if !defined($config->{$k});
386 }
387 }
388
389 return $settings;
390}
391
392my $format_config_line = sub {
393 my ($schema, $key, $value) = @_;
394
395 my $ct = $schema->{type};
396
397 if ($ct eq 'boolean') {
398 return $value ? "\t$key\n" : '';
399 } else {
400 return "\t$key $value\n" if "$value" ne '';
401 }
402};
403
404sub write_config {
405 my ($class, $filename, $cfg) = @_;
406
407 my $pdata = $class->private();
408 my $propertyList = $pdata->{propertyList};
409
410 my $out = '';
411
412 my $ids = $cfg->{ids};
413 my $order = $cfg->{order};
414
415 my $maxpri = 0;
416 foreach my $sectionId (keys %$ids) {
417 my $pri = $order->{$sectionId};
418 $maxpri = $pri if $pri && $pri > $maxpri;
419 }
420 foreach my $sectionId (keys %$ids) {
421 if (!defined ($order->{$sectionId})) {
422 $order->{$sectionId} = ++$maxpri;
423 }
424 }
425
426 foreach my $sectionId (sort {$order->{$a} <=> $order->{$b}} keys %$ids) {
427 my $scfg = $ids->{$sectionId};
428 my $type = $scfg->{type};
429 my $opts = $pdata->{options}->{$type};
430
431 die "unknown section type '$type'\n" if !$opts;
432
433 my $data = $class->format_section_header($type, $sectionId);
434 if ($scfg->{comment}) {
435 my $k = 'comment';
436 my $v = $class->encode_value($type, $k, $scfg->{$k});
437 $data .= &$format_config_line($propertyList->{$k}, $k, $v);
438 }
439
440 $data .= "\tdisable\n" if $scfg->{disable};
441
442 my $done_hash = { comment => 1, disable => 1};
443
444 foreach my $k (keys %$opts) {
445 next if $opts->{$k}->{optional};
446 $done_hash->{$k} = 1;
447 my $v = $scfg->{$k};
448 die "section '$sectionId' - missing value for required option '$k'\n"
449 if !defined ($v);
450 $v = $class->encode_value($type, $k, $v);
451 $data .= &$format_config_line($propertyList->{$k}, $k, $v);
452 }
453
454 foreach my $k (keys %$opts) {
455 next if defined($done_hash->{$k});
456 my $v = $scfg->{$k};
457 next if !defined($v);
458 $v = $class->encode_value($type, $k, $v);
459 $data .= &$format_config_line($propertyList->{$k}, $k, $v);
460 }
461
462 $out .= "$data\n";
463 }
464
465 return $out;
466}
467
468sub assert_if_modified {
469 my ($cfg, $digest) = @_;
470
a345b9d5 471 PVE::Tools::assert_if_modified($cfg->{digest}, $digest);
212b08e8
DM
472}
473
4741;