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