remove + from getopt's prefix_pattern
[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 $raw = '' if !defined($raw);
235
236 my $digest = Digest::SHA::sha1_hex($raw);
237
238 my $pri = 1;
239
240 my $lineno = 0;
241 my @lines = split(/\n/, $raw);
242 my $nextline = sub {
243 while (my $line = shift @lines) {
244 $lineno++;
245 return $line if $line !~ /^\s*(?:#|$)/;
246 }
247 };
248
249 while (my $line = &$nextline()) {
250 my $errprefix = "file $filename line $lineno";
251
252 my ($type, $sectionId, $errmsg, $config) = $class->parse_section_header($line);
253 if ($config) {
254 my $ignore = 0;
255
256 my $plugin;
257
258 if ($errmsg) {
259 $ignore = 1;
260 chomp $errmsg;
261 warn "$errprefix (skip section '$sectionId'): $errmsg\n";
262 } elsif (!$type) {
263 $ignore = 1;
264 warn "$errprefix (skip section '$sectionId'): missing type - internal error\n";
265 } else {
266 if (!($plugin = $pdata->{plugins}->{$type})) {
267 $ignore = 1;
268 warn "$errprefix (skip section '$sectionId'): unsupported type '$type'\n";
269 }
270 }
271
272 while ($line = &$nextline()) {
273 next if $ignore; # skip
274
275 $errprefix = "file $filename line $lineno";
276
277 if ($line =~ m/^\s+(\S+)(\s+(.*\S))?\s*$/) {
278 my ($k, $v) = ($1, $3);
279
280 eval {
281 die "duplicate attribute\n" if defined($config->{$k});
282 $config->{$k} = $plugin->check_value($type, $k, $v, $sectionId);
283 };
284 warn "$errprefix (section '$sectionId') - unable to parse value of '$k': $@" if $@;
285
286 } else {
287 warn "$errprefix (section '$sectionId') - ignore config line: $line\n";
288 }
289 }
290
291 if (!$ignore && $type && $plugin && $config) {
292 $config->{type} = $type;
293 eval { $ids->{$sectionId} = $plugin->check_config($sectionId, $config, 1, 1); };
294 warn "$errprefix (skip section '$sectionId'): $@" if $@;
295 $order->{$sectionId} = $pri++;
296 }
297
298 } else {
299 warn "$errprefix - ignore config line: $line\n";
300 }
301 }
302
303
304 my $cfg = { ids => $ids, order => $order, digest => $digest};
305
306 return $cfg;
307 }
308
309 sub check_config {
310 my ($class, $sectionId, $config, $create, $skipSchemaCheck) = @_;
311
312 my $type = $class->type();
313 my $pdata = $class->private();
314 my $opts = $pdata->{options}->{$type};
315
316 my $settings = { type => $type };
317
318 foreach my $k (keys %$config) {
319 my $value = $config->{$k};
320
321 die "can't change value of fixed parameter '$k'\n"
322 if !$create && $opts->{$k}->{fixed};
323
324 if (defined($value)) {
325 my $tmp = $class->check_value($type, $k, $value, $sectionId, $skipSchemaCheck);
326 $settings->{$k} = $class->decode_value($type, $k, $tmp);
327 } else {
328 die "got undefined value for option '$k'\n";
329 }
330 }
331
332 if ($create) {
333 # check if we have a value for all required options
334 foreach my $k (keys %$opts) {
335 next if $opts->{$k}->{optional};
336 die "missing value for required option '$k'\n"
337 if !defined($config->{$k});
338 }
339 }
340
341 return $settings;
342 }
343
344 my $format_config_line = sub {
345 my ($schema, $key, $value) = @_;
346
347 my $ct = $schema->{type};
348
349 if ($ct eq 'boolean') {
350 return $value ? "\t$key\n" : '';
351 } else {
352 return "\t$key $value\n" if "$value" ne '';
353 }
354 };
355
356 sub write_config {
357 my ($class, $filename, $cfg) = @_;
358
359 my $pdata = $class->private();
360 my $propertyList = $pdata->{propertyList};
361
362 my $out = '';
363
364 my $ids = $cfg->{ids};
365 my $order = $cfg->{order};
366
367 my $maxpri = 0;
368 foreach my $sectionId (keys %$ids) {
369 my $pri = $order->{$sectionId};
370 $maxpri = $pri if $pri && $pri > $maxpri;
371 }
372 foreach my $sectionId (keys %$ids) {
373 if (!defined ($order->{$sectionId})) {
374 $order->{$sectionId} = ++$maxpri;
375 }
376 }
377
378 foreach my $sectionId (sort {$order->{$a} <=> $order->{$b}} keys %$ids) {
379 my $scfg = $ids->{$sectionId};
380 my $type = $scfg->{type};
381 my $opts = $pdata->{options}->{$type};
382
383 die "unknown section type '$type'\n" if !$opts;
384
385 my $data = $class->format_section_header($type, $sectionId);
386 if ($scfg->{comment}) {
387 my $k = 'comment';
388 my $v = $class->encode_value($type, $k, $scfg->{$k});
389 $data .= &$format_config_line($propertyList->{$k}, $k, $v);
390 }
391
392 $data .= "\tdisable\n" if $scfg->{disable};
393
394 my $done_hash = { comment => 1, disable => 1};
395
396 foreach my $k (keys %$opts) {
397 next if $opts->{$k}->{optional};
398 $done_hash->{$k} = 1;
399 my $v = $scfg->{$k};
400 die "section '$sectionId' - missing value for required option '$k'\n"
401 if !defined ($v);
402 $v = $class->encode_value($type, $k, $v);
403 $data .= &$format_config_line($propertyList->{$k}, $k, $v);
404 }
405
406 foreach my $k (keys %$opts) {
407 next if defined($done_hash->{$k});
408 my $v = $scfg->{$k};
409 next if !defined($v);
410 $v = $class->encode_value($type, $k, $v);
411 $data .= &$format_config_line($propertyList->{$k}, $k, $v);
412 }
413
414 $out .= "$data\n";
415 }
416
417 return $out;
418 }
419
420 sub assert_if_modified {
421 my ($cfg, $digest) = @_;
422
423 PVE::Tools::assert_if_modified($cfg->{digest}, $digest);
424 }
425
426 1;