]> git.proxmox.com Git - pve-cluster.git/blob - src/PVE/DataCenterConfig.pm
c80872ce5ce445e2c2bf14da1186e8ade3ff273e
[pve-cluster.git] / src / PVE / DataCenterConfig.pm
1 package PVE::DataCenterConfig;
2
3 use strict;
4 use warnings;
5
6 use PVE::JSONSchema qw(get_standard_option parse_property_string register_standard_option);
7 use PVE::Tools;
8 use PVE::Cluster;
9
10 # MA-L (large) assigned by IEEE
11 my $PROXMOX_OUI = 'BC:24:11';
12
13 my $crs_format = {
14 ha => {
15 type => 'string',
16 enum => ['basic', 'static'],
17 optional => 1,
18 default => 'basic',
19 description => "Use this resource scheduler mode for HA.",
20 verbose_description => "Configures how the HA manager should select nodes to start or ".
21 "recover services. With 'basic', only the number of services is used, with 'static', ".
22 "static CPU and memory configuration of services is considered.",
23 },
24 'ha-rebalance-on-start' => {
25 type => 'boolean',
26 optional => 1,
27 default => 0,
28 description => "Set to use CRS for selecting a suited node when a HA services request-state"
29 ." changes from stop to start.",
30 }
31 };
32
33 my $migration_format = {
34 type => {
35 default_key => 1,
36 type => 'string',
37 enum => ['secure', 'insecure'],
38 description => "Migration traffic is encrypted using an SSH tunnel by " .
39 "default. On secure, completely private networks this can be " .
40 "disabled to increase performance.",
41 default => 'secure',
42 },
43 network => {
44 optional => 1,
45 type => 'string', format => 'CIDR',
46 format_description => 'CIDR',
47 description => "CIDR of the (sub) network that is used for migration."
48 },
49 };
50
51 my $notification_format = {
52 'package-updates' => {
53 type => 'string',
54 enum => ['auto', 'always', 'never'],
55 description => "Control when the daily update job should send out notifications.",
56 verbose_description => "Control how often the daily update job should send out notifications:\n"
57 ."* 'auto' daily for systems with a valid subscription, as those are assumed to be "
58 ." production-ready and thus should know about pending updates.\n"
59 ."* 'always' every update, if there are new pending updates.\n"
60 ."* 'never' never send a notification for new pending updates.\n",
61 default => 'auto',
62 optional => 1,
63 },
64 'target-package-updates' => {
65 type => 'string',
66 format_description => 'TARGET',
67 description => "Control where notifications about available updates should be sent to.",
68 verbose_description => "Control where notifications about available"
69 . " updates should be sent to."
70 . " Has to be the name of a notification target (endpoint or notification group)."
71 . " If the 'target-package-updates' parameter is not set, the system will send mails"
72 . " to root via a 'sendmail' notification endpoint.",
73 optional => 1,
74 },
75 'fencing' => {
76 type => 'string',
77 enum => ['always', 'never'],
78 description => "Control if notifications about node fencing should be sent.",
79 verbose_description => "Control if notifications about node fencing should be sent.\n"
80 . "* 'always' always send out notifications\n"
81 . "* 'never' never send out notifications.\n"
82 . "For production systems, turning off node fencing notifications is not"
83 . "recommended!\n",
84 default => 'always',
85 optional => 1,
86 },
87 'target-fencing' => {
88 type => 'string',
89 format_description => 'TARGET',
90 description => "Control where notifications about fenced cluster nodes should be sent to.",
91 verbose_description => "Control where notifications about fenced cluster nodes"
92 . " should be sent to."
93 . " Has to be the name of a notification target (endpoint or notification group)."
94 . " If the 'target-fencing' parameter is not set, the system will send mails"
95 . " to root via a 'sendmail' notification endpoint.",
96 optional => 1,
97 },
98 'replication' => {
99 type => 'string',
100 enum => ['always', 'never'],
101 description => "Control if notifications for replication failures should be sent.",
102 verbose_description => "Control if notifications for replication failures should be sent.\n"
103 . "* 'always' always send out notifications\n"
104 . "* 'never' never send out notifications.\n"
105 . "For production systems, turning off replication notifications is not"
106 . "recommended!\n",
107 default => 'always',
108 optional => 1,
109 },
110 'target-replication' => {
111 type => 'string',
112 format_description => 'TARGET',
113 description => "Control where notifications for failed storage replication jobs should"
114 . " be sent to.",
115 verbose_description => "Control where notifications for failed storage replication jobs"
116 . " should be sent to."
117 . " Has to be the name of a notification target (endpoint or notification group)."
118 . " If the 'target-replication' parameter is not set, the system will send mails"
119 . " to root via a 'sendmail' notification endpoint.",
120 optional => 1,
121 },
122 };
123
124 register_standard_option('pve-ha-shutdown-policy', {
125 type => 'string',
126 enum => ['freeze', 'failover', 'conditional', 'migrate'],
127 description => "The policy for HA services on node shutdown. 'freeze' disables ".
128 "auto-recovery, 'failover' ensures recovery, 'conditional' recovers on ".
129 "poweroff and freezes on reboot. 'migrate' will migrate running services ".
130 "to other nodes, if possible. With 'freeze' or 'failover', HA Services will ".
131 "always get stopped first on shutdown.",
132 verbose_description => "Describes the policy for handling HA services on poweroff ".
133 "or reboot of a node. Freeze will always freeze services which are still located ".
134 "on the node on shutdown, those services won't be recovered by the HA manager. ".
135 "Failover will not mark the services as frozen and thus the services will get ".
136 "recovered to other nodes, if the shutdown node does not come up again quickly ".
137 "(< 1min). 'conditional' chooses automatically depending on the type of shutdown, ".
138 "i.e., on a reboot the service will be frozen but on a poweroff the service will ".
139 "stay as is, and thus get recovered after about 2 minutes. ".
140 "Migrate will try to move all running services to another node when a reboot or ".
141 "shutdown was triggered. The poweroff process will only continue once no running services ".
142 "are located on the node anymore. If the node comes up again, the service will ".
143 "be moved back to the previously powered-off node, at least if no other migration, ".
144 "reloaction or recovery took place.",
145 default => 'conditional',
146 });
147
148 my $ha_format = {
149 shutdown_policy => get_standard_option('pve-ha-shutdown-policy'),
150 };
151
152 my $next_id_format = {
153 lower => {
154 type => 'integer',
155 description => "Lower, inclusive boundary for free next-id API range.",
156 min => 100,
157 max => 1000 * 1000 * 1000 - 1,
158 default => 100,
159 optional => 1,
160 },
161 upper => {
162 type => 'integer',
163 description => "Upper, exclusive boundary for free next-id API range.",
164 min => 100,
165 max => 1000 * 1000 * 1000,
166 default => 1000 * 1000, # lower than the maximum on purpose
167 optional => 1,
168 },
169 };
170
171 my $u2f_format = {
172 appid => {
173 type => 'string',
174 description => "U2F AppId URL override. Defaults to the origin.",
175 format_description => 'APPID',
176 optional => 1,
177 },
178 origin => {
179 type => 'string',
180 description => "U2F Origin override. Mostly useful for single nodes with a single URL.",
181 format_description => 'URL',
182 optional => 1,
183 },
184 };
185
186 my $webauthn_format = {
187 rp => {
188 type => 'string',
189 description =>
190 'Relying party name. Any text identifier.'
191 .' Changing this *may* break existing credentials.',
192 format_description => 'RELYING_PARTY',
193 optional => 1,
194 },
195 origin => {
196 type => 'string',
197 description =>
198 'Site origin. Must be a `https://` URL (or `http://localhost`).'
199 .' Should contain the address users type in their browsers to access'
200 .' the web interface.'
201 .' Changing this *may* break existing credentials.',
202 format_description => 'URL',
203 optional => 1,
204 },
205 id => {
206 type => 'string',
207 description =>
208 'Relying party ID. Must be the domain name without protocol, port or location.'
209 .' Changing this *will* break existing credentials.',
210 format_description => 'DOMAINNAME',
211 optional => 1,
212 },
213 'allow-subdomains' => {
214 type => 'boolean',
215 description => 'Whether to allow the origin to be a subdomain, rather than the exact URL.',
216 optional => 1,
217 default => 1,
218 },
219 };
220
221 PVE::JSONSchema::register_format('mac-prefix', \&pve_verify_mac_prefix);
222 sub pve_verify_mac_prefix {
223 my ($mac_prefix, $noerr) = @_;
224
225 if ($mac_prefix !~ m/^[a-f0-9][02468ace](?::[a-f0-9]{2}){0,2}:?$/i) {
226 return undef if $noerr;
227 die "value is not a valid unicast MAC address prefix\n";
228 }
229 return $mac_prefix;
230 }
231
232 my $COLOR_RE = '[0-9a-fA-F]{6}';
233 my $TAG_COLOR_OVERRIDE_RE = "(?:${PVE::JSONSchema::PVE_TAG_RE}:${COLOR_RE}(?:\:${COLOR_RE})?)";
234
235 my $tag_style_format = {
236 'shape' => {
237 optional => 1,
238 type => 'string',
239 enum => ['full', 'circle', 'dense', 'none'],
240 default => 'circle',
241 description => "Tag shape for the web ui tree. 'full' draws the full tag. "
242 ."'circle' draws only a circle with the background color. "
243 ."'dense' only draws a small rectancle (useful when many tags are assigned to each guest)."
244 ."'none' disables showing the tags.",
245 },
246 'color-map' => {
247 optional => 1,
248 type => 'string',
249 pattern => "${TAG_COLOR_OVERRIDE_RE}(?:\;$TAG_COLOR_OVERRIDE_RE)*",
250 typetext => '<tag>:<hex-color>[:<hex-color-for-text>][;<tag>=...]',
251 description => "Manual color mapping for tags (semicolon separated).",
252 },
253 ordering => {
254 optional => 1,
255 type => 'string',
256 enum => ['config', 'alphabetical'],
257 default => 'alphabetical',
258 description => 'Controls the sorting of the tags in the web-interface and the API update.',
259 },
260 'case-sensitive' => {
261 type => 'boolean',
262 description => 'Controls if filtering for unique tags on update should check case-sensitive.',
263 optional => 1,
264 default => 0,
265 },
266 };
267
268 my $user_tag_privs_format = {
269 'user-allow' => {
270 optional => 1,
271 type => 'string',
272 enum => ['none', 'list', 'existing', 'free'],
273 default => 'free',
274 description => "Controls tag usage for users without `Sys.Modify` on `/` by either"
275 ." allowing `none`, a `list`, already `existing` or anything (`free`).",
276 verbose_description => "Controls which tags can be set or deleted on resources a user"
277 ." controls (such as guests). Users with the `Sys.Modify` privilege on `/` are always"
278 ."unrestricted.\n"
279 ."* 'none' no tags are usable.\n"
280 ."* 'list' tags from 'user-allow-list' are usable.\n"
281 ."* 'existing' like list, but already existing tags of resources are also usable.\n"
282 ."* 'free' no tag restrictions.\n",
283 },
284 'user-allow-list' => {
285 optional => 1,
286 type => 'string',
287 pattern => "${PVE::JSONSchema::PVE_TAG_RE}(?:\;${PVE::JSONSchema::PVE_TAG_RE})*",
288 typetext => "<tag>[;<tag>...]",
289 description => "List of tags users are allowed to set and delete (semicolon separated) "
290 ."for 'user-allow' values 'list' and 'existing'.",
291 },
292 };
293
294 my $datacenter_schema = {
295 type => "object",
296 additionalProperties => 0,
297 properties => {
298 crs => {
299 optional => 1,
300 type => 'string', format => $crs_format,
301 description => "Cluster resource scheduling settings.",
302 },
303 keyboard => {
304 optional => 1,
305 type => 'string',
306 description => "Default keybord layout for vnc server.",
307 enum => PVE::Tools::kvmkeymaplist(),
308 },
309 language => {
310 optional => 1,
311 type => 'string',
312 description => "Default GUI language.",
313 enum => [
314 'ar', # Arabic
315 'ca', # Catalan
316 'da', # Danish
317 'de', # German
318 'en', # English
319 'es', # Spanish
320 'eu', # Euskera (Basque)
321 'fa', # Persian (Farsi)
322 'fr', # French
323 'hr', # Croatian
324 'he', # Hebrew
325 'it', # Italian
326 'ja', # Japanese
327 'ka', # Georgian
328 'kr', # Korean
329 'nb', # Norwegian (Bokmal)
330 'nl', # Dutch
331 'nn', # Norwegian (Nynorsk)
332 'pl', # Polish
333 'pt_BR', # Portuguese (Brazil)
334 'ru', # Russian
335 'sl', # Slovenian
336 'sv', # Swedish
337 'tr', # Turkish
338 'ukr', # Ukrainian
339 'zh_CN', # Chinese (Simplified)
340 'zh_TW', # Chinese (Traditional)
341 ],
342 },
343 http_proxy => {
344 optional => 1,
345 type => 'string',
346 description => "Specify external http proxy which is used for downloads (example: 'http://username:password\@host:port/')",
347 pattern => "http://.*",
348 },
349 # FIXME: remove with 8.0 (add check to pve7to8!), merged into "migration" since 4.3
350 migration_unsecure => {
351 optional => 1,
352 type => 'boolean',
353 description => "Migration is secure using SSH tunnel by default. " .
354 "For secure private networks you can disable it to speed up " .
355 "migration. Deprecated, use the 'migration' property instead!",
356 },
357 'next-id' => {
358 optional => 1,
359 type => 'string',
360 format => $next_id_format,
361 description => "Control the range for the free VMID auto-selection pool.",
362 },
363 migration => {
364 optional => 1,
365 type => 'string', format => $migration_format,
366 description => "For cluster wide migration settings.",
367 },
368 console => {
369 optional => 1,
370 type => 'string',
371 description => "Select the default Console viewer. You can either use the builtin java"
372 ." applet (VNC; deprecated and maps to html5), an external virt-viewer comtatible application (SPICE), an HTML5 based vnc viewer (noVNC), or an HTML5 based console client (xtermjs). If the selected viewer is not available (e.g. SPICE not activated for the VM), the fallback is noVNC.",
373 # FIXME: remove 'applet' with 9.0 (add pve8to9 check!)
374 enum => ['applet', 'vv', 'html5', 'xtermjs'],
375 },
376 email_from => {
377 optional => 1,
378 type => 'string',
379 format => 'email-opt',
380 description => "Specify email address to send notification from (default is root@\$hostname)",
381 },
382 max_workers => {
383 optional => 1,
384 type => 'integer',
385 minimum => 1,
386 description => "Defines how many workers (per node) are maximal started ".
387 " on actions like 'stopall VMs' or task from the ha-manager.",
388 },
389 fencing => {
390 optional => 1,
391 type => 'string',
392 default => 'watchdog',
393 enum => [ 'watchdog', 'hardware', 'both' ],
394 description => "Set the fencing mode of the HA cluster. Hardware mode " .
395 "needs a valid configuration of fence devices in /etc/pve/ha/fence.cfg." .
396 " With both all two modes are used." .
397 "\n\nWARNING: 'hardware' and 'both' are EXPERIMENTAL & WIP",
398 },
399 ha => {
400 optional => 1,
401 type => 'string', format => $ha_format,
402 description => "Cluster wide HA settings.",
403 },
404 mac_prefix => {
405 optional => 1,
406 type => 'string',
407 format => 'mac-prefix',
408 default => $PROXMOX_OUI,
409 description => "Prefix for the auto-generated MAC addresses of virtual guests. The"
410 ." default '$PROXMOX_OUI' is the OUI assigned by the IEEE to Proxmox Server Solutions"
411 ." GmbH for a 24-bit large MAC block. You're allowed to use this in local networks,"
412 ." i.e., those not directly reachable by the public (e.g., in a LAN or behind NAT)."
413 ,
414 verbose_description => "Prefix for the auto-generated MAC addresses of virtual guests."
415 ." The default `BC:24:11` is the Organizationally Unique Identifier (OUI) assigned"
416 ." by the IEEE to Proxmox Server Solutions GmbH for a MAC Address Block Large (MA-L)."
417 ." You're allowed to use this in local networks, i.e., those not directly reachable"
418 ." by the public (e.g., in a LAN or NAT/Masquerading).\n"
419 ." \nNote that when you run multiple cluster that (partially) share the networks of"
420 ." their virtual guests, it's highly recommended that you extend the default MAC"
421 ." prefix, or generate a custom (valid) one, to reduce the chance of MAC collisions."
422 ." For example, add a separate extra hexadecimal to the Proxmox OUI for each cluster,"
423 ." like `$PROXMOX_OUI:0` for the first, `$PROXMOX_OUI:1` for the second, and so on.\n"
424 ." Alternatively, you can also separate the networks of the guests logically, e.g.,"
425 ." by using VLANs.\n\nFor publicly accessible guests it's recommended that you get"
426 ." your own https://standards.ieee.org/products-programs/regauth/[OUI from the IEEE]"
427 ." registered or coordinate with your, or your hosting providers, network admins."
428 ,
429 },
430 notify => {
431 optional => 1,
432 type => 'string', format => $notification_format,
433 description => "Cluster-wide notification settings.",
434 },
435 bwlimit => PVE::JSONSchema::get_standard_option('bwlimit'),
436 u2f => {
437 optional => 1,
438 type => 'string',
439 format => $u2f_format,
440 description => 'u2f',
441 },
442 webauthn => {
443 optional => 1,
444 type => 'string',
445 format => $webauthn_format,
446 description => 'webauthn configuration',
447 },
448 description => {
449 type => 'string',
450 description => "Datacenter description. Shown in the web-interface datacenter notes panel."
451 ." This is saved as comment inside the configuration file.",
452 maxLength => 64 * 1024,
453 optional => 1,
454 },
455 'tag-style' => {
456 optional => 1,
457 type => 'string',
458 description => "Tag style options.",
459 format => $tag_style_format,
460 },
461 'user-tag-access' => {
462 optional => 1,
463 type => 'string',
464 description => "Privilege options for user-settable tags",
465 format => $user_tag_privs_format,
466 },
467 'registered-tags' => {
468 optional => 1,
469 type => 'string',
470 description => "A list of tags that require a `Sys.Modify` on '/' to set and delete. "
471 ."Tags set here that are also in 'user-tag-access' also require `Sys.Modify`.",
472 pattern => "(?:${PVE::JSONSchema::PVE_TAG_RE};)*${PVE::JSONSchema::PVE_TAG_RE}",
473 typetext => "<tag>[;<tag>...]",
474 },
475 },
476 };
477
478 # make schema accessible from outside (for documentation)
479 sub get_datacenter_schema { return $datacenter_schema };
480
481 sub parse_datacenter_config {
482 my ($filename, $raw) = @_;
483
484 $raw = '' if !defined($raw);
485
486 # description may be comment or key-value pair (or both)
487 my $comment = '';
488 for my $line (split(/\n/, $raw)) {
489 if ($line =~ /^\#(.*)$/) {
490 $comment .= PVE::Tools::decode_text($1) . "\n";
491 }
492 }
493
494 # parse_config ignores lines with # => use $raw
495 my $res = PVE::JSONSchema::parse_config($datacenter_schema, $filename, $raw);
496
497 $res->{description} = $comment;
498
499 # it could be better to track that this is the default, and not explicitly set, but having
500 # no MAC prefix is really not ideal, and overriding that here centrally catches all call sites
501 $res->{mac_prefix} = $PROXMOX_OUI if !defined($res->{mac_prefix});
502
503 if (my $crs = $res->{crs}) {
504 $res->{crs} = parse_property_string($crs_format, $crs);
505 }
506
507 if (my $migration = $res->{migration}) {
508 $res->{migration} = parse_property_string($migration_format, $migration);
509 }
510
511 if (my $next_id = $res->{'next-id'}) {
512 $res->{'next-id'} = parse_property_string($next_id_format, $next_id);
513 }
514
515 if (my $ha = $res->{ha}) {
516 $res->{ha} = parse_property_string($ha_format, $ha);
517 }
518 if (my $notify = $res->{notify}) {
519 $res->{notify} = parse_property_string($notification_format, $notify);
520 }
521
522 if (my $u2f = $res->{u2f}) {
523 $res->{u2f} = parse_property_string($u2f_format, $u2f);
524 }
525
526 if (my $webauthn = $res->{webauthn}) {
527 $res->{webauthn} = parse_property_string($webauthn_format, $webauthn);
528 }
529
530 if (my $tag_style = $res->{'tag-style'}) {
531 $res->{'tag-style'} = parse_property_string($tag_style_format, $tag_style);
532 }
533
534 if (my $user_tag_privs = $res->{'user-tag-access'}) {
535 $res->{'user-tag-access'} =
536 parse_property_string($user_tag_privs_format, $user_tag_privs);
537
538 if (my $user_tags = $res->{'user-tag-access'}->{'user-allow-list'}) {
539 $res->{'user-tag-access'}->{'user-allow-list'} = [split(';', $user_tags)];
540 }
541 }
542
543 if (my $admin_tags = $res->{'registered-tags'}) {
544 $res->{'registered-tags'} = [split(';', $admin_tags)];
545 }
546
547 # for backwards compatibility only, new migration property has precedence
548 if (defined($res->{migration_unsecure})) {
549 if (defined($res->{migration}->{type})) {
550 warn "deprecated setting 'migration_unsecure' and new 'migration: type' " .
551 "set at same time! Ignore 'migration_unsecure'\n";
552 } else {
553 $res->{migration}->{type} = ($res->{migration_unsecure}) ? 'insecure' : 'secure';
554 }
555 }
556
557 # for backwards compatibility only, applet maps to html5
558 if (defined($res->{console}) && $res->{console} eq 'applet') {
559 $res->{console} = 'html5';
560 }
561
562 return $res;
563 }
564
565 sub write_datacenter_config {
566 my ($filename, $cfg) = @_;
567
568 # map deprecated setting to new one
569 if (defined($cfg->{migration_unsecure}) && !defined($cfg->{migration})) {
570 my $migration_unsecure = delete $cfg->{migration_unsecure};
571 $cfg->{migration}->{type} = ($migration_unsecure) ? 'insecure' : 'secure';
572 }
573
574 # map deprecated applet setting to html5
575 if (defined($cfg->{console}) && $cfg->{console} eq 'applet') {
576 $cfg->{console} = 'html5';
577 }
578
579 if (defined($cfg->{mac_prefix}) && uc($cfg->{mac_prefix}) eq $PROXMOX_OUI) {
580 delete $cfg->{mac_prefix}; # avoid writing out default prefix
581 }
582
583 if (ref(my $crs = $cfg->{crs})) {
584 $cfg->{crs} = PVE::JSONSchema::print_property_string($crs, $crs_format);
585 }
586
587 if (ref(my $migration = $cfg->{migration})) {
588 $cfg->{migration} = PVE::JSONSchema::print_property_string($migration, $migration_format);
589 }
590
591 if (defined(my $next_id = $cfg->{'next-id'})) {
592 $next_id = parse_property_string($next_id_format, $next_id) if !ref($next_id);
593
594 my $lower = int($next_id->{lower} // $next_id_format->{lower}->{default});
595 my $upper = int($next_id->{upper} // $next_id_format->{upper}->{default});
596
597 die "lower ($lower) <= upper ($upper) boundary rule broken\n" if $lower > $upper;
598
599 $cfg->{'next-id'} = PVE::JSONSchema::print_property_string($next_id, $next_id_format);
600 }
601
602 if (ref(my $ha = $cfg->{ha})) {
603 $cfg->{ha} = PVE::JSONSchema::print_property_string($ha, $ha_format);
604 }
605 if (ref(my $notify = $cfg->{notify})) {
606 $cfg->{notify} = PVE::JSONSchema::print_property_string($notify, $notification_format);
607 }
608
609 if (ref(my $u2f = $cfg->{u2f})) {
610 $cfg->{u2f} = PVE::JSONSchema::print_property_string($u2f, $u2f_format);
611 }
612
613 if (ref(my $webauthn = $cfg->{webauthn})) {
614 $cfg->{webauthn} = PVE::JSONSchema::print_property_string($webauthn, $webauthn_format);
615 }
616
617 if (ref(my $tag_style = $cfg->{'tag-style'})) {
618 $cfg->{'tag-style'} = PVE::JSONSchema::print_property_string($tag_style, $tag_style_format);
619 }
620
621 if (ref(my $user_tag_privs = $cfg->{'user-tag-access'})) {
622 if (my $user_tags = $user_tag_privs->{'user-allow-list'}) {
623 $user_tag_privs->{'user-allow-list'} = join(';', sort $user_tags->@*);
624 }
625 $cfg->{'user-tag-access'} =
626 PVE::JSONSchema::print_property_string($user_tag_privs, $user_tag_privs_format);
627 }
628
629 if (ref(my $admin_tags = $cfg->{'registered-tags'})) {
630 $cfg->{'registered-tags'} = join(';', sort $admin_tags->@*);
631 }
632
633 my $comment = '';
634 # add description as comment to top of file
635 my $description = $cfg->{description} || '';
636 foreach my $line (split(/\n/, $description)) {
637 $comment .= '#' . PVE::Tools::encode_text($line) . "\n";
638 }
639 delete $cfg->{description}; # add only as comment, no additional key-value pair
640 my $dump = PVE::JSONSchema::dump_config($datacenter_schema, $filename, $cfg);
641
642 return $comment . "\n" . $dump;
643 }
644
645 PVE::Cluster::cfs_register_file(
646 'datacenter.cfg',
647 \&parse_datacenter_config,
648 \&write_datacenter_config,
649 );
650
651 1;