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