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