be13056eb1106468256fb05179f03b8b630d02b0
[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         my $copts = $class->options();
123         $modifyable = 1 if defined($copts->{$p}) && !$copts->{$p}->{fixed};
124
125         foreach my $t (keys %$plugins) {
126             my $opts = $pdata->{options}->{$t} || {};
127             next if !defined($opts->{$p});
128             $modifyable = 1 if !$opts->{$p}->{fixed};
129         }
130         next if !$modifyable;
131
132         $props->{$p} = $propertyList->{$p};
133     }
134
135     $props->{digest} = get_standard_option('pve-config-digest');
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
151 sub 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) {
166             die "duplicate property '$p'" if defined($propertyList->{$p});
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
188 sub 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
199 sub lookup_types {
200     my ($class) = @_;
201
202     my $pdata = $class->private();
203     
204     return [ keys %{$pdata->{plugins}} ];
205 }
206
207 sub decode_value {
208     my ($class, $type, $key, $value) = @_;
209
210     return $value;
211 }
212
213 sub encode_value {
214     my ($class, $type, $key, $value) = @_;
215
216     return $value;
217 }
218
219 sub 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
255 sub 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
267 sub format_section_header {
268     my ($class, $type, $sectionId) = @_;
269
270     return "$type: $sectionId\n";
271 }
272
273
274 sub parse_config {
275     my ($class, $filename, $raw) = @_;
276
277     my $pdata = $class->private();
278
279     my $ids = {};
280     my $order = {};
281
282     $raw = '' if !defined($raw);
283
284     my $digest = Digest::SHA::sha1_hex($raw);
285     
286     my $pri = 1;
287
288     my $lineno = 0;
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     };
296
297     while (my $line = &$nextline()) {
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
320             while ($line = &$nextline()) {
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
357 sub 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
392 my $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
404 sub 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
468 sub assert_if_modified {
469     my ($cfg, $digest) = @_;
470
471     PVE::Tools::assert_if_modified($cfg->{digest}, $digest);
472 }
473
474 1;