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