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