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