]> git.proxmox.com Git - pve-cluster.git/blob - src/PVE/DataCenterConfig.pm
datacenter config: fix descriptions for user-tag-access schema
[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 my $crs_format = {
11 ha => {
12 type => 'string',
13 enum => ['basic', 'static'],
14 optional => 1,
15 default => 'basic',
16 description => "Use this resource scheduler mode for HA.",
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 },
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 }
28 };
29
30 my $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
48 my $notification_format = {
49 'package-updates' => {
50 type => 'string',
51 enum => ['auto', 'always', 'never'],
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"
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',
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,
118 },
119 };
120
121 register_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
145 my $ha_format = {
146 shutdown_policy => get_standard_option('pve-ha-shutdown-policy'),
147 };
148
149 my $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',
160 description => "Upper, exclusive boundary for free next-id API range.",
161 min => 100,
162 max => 1000 * 1000 * 1000,
163 default => 1000 * 1000, # lower than the maximum on purpose
164 optional => 1,
165 },
166 };
167
168 my $u2f_format = {
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
183 my $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 =>
205 'Relying party ID. Must be the domain name without protocol, port or location.'
206 .' Changing this *will* break existing credentials.',
207 format_description => 'DOMAINNAME',
208 optional => 1,
209 },
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 },
216 };
217
218 PVE::JSONSchema::register_format('mac-prefix', \&pve_verify_mac_prefix);
219 sub 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
229 my $COLOR_RE = '[0-9a-fA-F]{6}';
230 my $TAG_COLOR_OVERRIDE_RE = "(?:${PVE::JSONSchema::PVE_TAG_RE}:${COLOR_RE}(?:\:${COLOR_RE})?)";
231
232 my $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 },
250 ordering => {
251 optional => 1,
252 type => 'string',
253 enum => ['config', 'alphabetical'],
254 default => 'alphabetical',
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 },
263 };
264
265 my $user_tag_privs_format = {
266 'user-allow' => {
267 optional => 1,
268 type => 'string',
269 enum => ['none', 'list', 'existing', 'free'],
270 default => 'free',
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",
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
291 my $datacenter_schema = {
292 type => "object",
293 additionalProperties => 0,
294 properties => {
295 crs => {
296 optional => 1,
297 type => 'string', format => $crs_format,
298 description => "Cluster resource scheduling settings.",
299 },
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 => [
311 'ca',
312 'da',
313 'de',
314 'en',
315 'es',
316 'eu',
317 'fa',
318 'fr',
319 'he',
320 'it',
321 'ja',
322 'nb',
323 'nn',
324 'pl',
325 'pt_BR',
326 'ru',
327 'sl',
328 'sv',
329 'tr',
330 'zh_CN',
331 'zh_TW',
332 ],
333 },
334 http_proxy => {
335 optional => 1,
336 type => 'string',
337 description => "Specify external http proxy which is used for downloads (example: 'http://username:password\@host:port/')",
338 pattern => "http://.*",
339 },
340 # FIXME: remove with 8.0 (add check to pve7to8!), merged into "migration" since 4.3
341 migration_unsecure => {
342 optional => 1,
343 type => 'boolean',
344 description => "Migration is secure using SSH tunnel by default. " .
345 "For secure private networks you can disable it to speed up " .
346 "migration. Deprecated, use the 'migration' property instead!",
347 },
348 'next-id' => {
349 optional => 1,
350 type => 'string',
351 format => $next_id_format,
352 description => "Control the range for the free VMID auto-selection pool.",
353 },
354 migration => {
355 optional => 1,
356 type => 'string', format => $migration_format,
357 description => "For cluster wide migration settings.",
358 },
359 console => {
360 optional => 1,
361 type => 'string',
362 description => "Select the default Console viewer. You can either use the builtin java"
363 ." 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.",
364 # FIXME: remove 'applet' with 9.0 (add pve8to9 check!)
365 enum => ['applet', 'vv', 'html5', 'xtermjs'],
366 },
367 email_from => {
368 optional => 1,
369 type => 'string',
370 format => 'email-opt',
371 description => "Specify email address to send notification from (default is root@\$hostname)",
372 },
373 max_workers => {
374 optional => 1,
375 type => 'integer',
376 minimum => 1,
377 description => "Defines how many workers (per node) are maximal started ".
378 " on actions like 'stopall VMs' or task from the ha-manager.",
379 },
380 fencing => {
381 optional => 1,
382 type => 'string',
383 default => 'watchdog',
384 enum => [ 'watchdog', 'hardware', 'both' ],
385 description => "Set the fencing mode of the HA cluster. Hardware mode " .
386 "needs a valid configuration of fence devices in /etc/pve/ha/fence.cfg." .
387 " With both all two modes are used." .
388 "\n\nWARNING: 'hardware' and 'both' are EXPERIMENTAL & WIP",
389 },
390 ha => {
391 optional => 1,
392 type => 'string', format => $ha_format,
393 description => "Cluster wide HA settings.",
394 },
395 mac_prefix => {
396 optional => 1,
397 type => 'string',
398 format => 'mac-prefix',
399 description => 'Prefix for autogenerated MAC addresses.',
400 },
401 notify => {
402 optional => 1,
403 type => 'string', format => $notification_format,
404 description => "Cluster-wide notification settings.",
405 },
406 bwlimit => PVE::JSONSchema::get_standard_option('bwlimit'),
407 u2f => {
408 optional => 1,
409 type => 'string',
410 format => $u2f_format,
411 description => 'u2f',
412 },
413 webauthn => {
414 optional => 1,
415 type => 'string',
416 format => $webauthn_format,
417 description => 'webauthn configuration',
418 },
419 description => {
420 type => 'string',
421 description => "Datacenter description. Shown in the web-interface datacenter notes panel."
422 ." This is saved as comment inside the configuration file.",
423 maxLength => 64 * 1024,
424 optional => 1,
425 },
426 'tag-style' => {
427 optional => 1,
428 type => 'string',
429 description => "Tag style options.",
430 format => $tag_style_format,
431 },
432 'user-tag-access' => {
433 optional => 1,
434 type => 'string',
435 description => "Privilege options for user-settable tags",
436 format => $user_tag_privs_format,
437 },
438 'registered-tags' => {
439 optional => 1,
440 type => 'string',
441 description => "A list of tags that require a `Sys.Modify` on '/' to set and delete. "
442 ."Tags set here that are also in 'user-tag-access' also require `Sys.Modify`.",
443 pattern => "(?:${PVE::JSONSchema::PVE_TAG_RE};)*${PVE::JSONSchema::PVE_TAG_RE}",
444 typetext => "<tag>[;<tag>...]",
445 },
446 },
447 };
448
449 # make schema accessible from outside (for documentation)
450 sub get_datacenter_schema { return $datacenter_schema };
451
452 sub parse_datacenter_config {
453 my ($filename, $raw) = @_;
454
455 $raw = '' if !defined($raw);
456
457 # description may be comment or key-value pair (or both)
458 my $comment = '';
459 for my $line (split(/\n/, $raw)) {
460 if ($line =~ /^\#(.*)$/) {
461 $comment .= PVE::Tools::decode_text($1) . "\n";
462 }
463 }
464
465 # parse_config ignores lines with # => use $raw
466 my $res = PVE::JSONSchema::parse_config($datacenter_schema, $filename, $raw);
467
468 $res->{description} = $comment;
469
470 if (my $crs = $res->{crs}) {
471 $res->{crs} = parse_property_string($crs_format, $crs);
472 }
473
474 if (my $migration = $res->{migration}) {
475 $res->{migration} = parse_property_string($migration_format, $migration);
476 }
477
478 if (my $next_id = $res->{'next-id'}) {
479 $res->{'next-id'} = parse_property_string($next_id_format, $next_id);
480 }
481
482 if (my $ha = $res->{ha}) {
483 $res->{ha} = parse_property_string($ha_format, $ha);
484 }
485 if (my $notify = $res->{notify}) {
486 $res->{notify} = parse_property_string($notification_format, $notify);
487 }
488
489 if (my $u2f = $res->{u2f}) {
490 $res->{u2f} = parse_property_string($u2f_format, $u2f);
491 }
492
493 if (my $webauthn = $res->{webauthn}) {
494 $res->{webauthn} = parse_property_string($webauthn_format, $webauthn);
495 }
496
497 if (my $tag_style = $res->{'tag-style'}) {
498 $res->{'tag-style'} = parse_property_string($tag_style_format, $tag_style);
499 }
500
501 if (my $user_tag_privs = $res->{'user-tag-access'}) {
502 $res->{'user-tag-access'} =
503 parse_property_string($user_tag_privs_format, $user_tag_privs);
504
505 if (my $user_tags = $res->{'user-tag-access'}->{'user-allow-list'}) {
506 $res->{'user-tag-access'}->{'user-allow-list'} = [split(';', $user_tags)];
507 }
508 }
509
510 if (my $admin_tags = $res->{'registered-tags'}) {
511 $res->{'registered-tags'} = [split(';', $admin_tags)];
512 }
513
514 # for backwards compatibility only, new migration property has precedence
515 if (defined($res->{migration_unsecure})) {
516 if (defined($res->{migration}->{type})) {
517 warn "deprecated setting 'migration_unsecure' and new 'migration: type' " .
518 "set at same time! Ignore 'migration_unsecure'\n";
519 } else {
520 $res->{migration}->{type} = ($res->{migration_unsecure}) ? 'insecure' : 'secure';
521 }
522 }
523
524 # for backwards compatibility only, applet maps to html5
525 if (defined($res->{console}) && $res->{console} eq 'applet') {
526 $res->{console} = 'html5';
527 }
528
529 return $res;
530 }
531
532 sub write_datacenter_config {
533 my ($filename, $cfg) = @_;
534
535 # map deprecated setting to new one
536 if (defined($cfg->{migration_unsecure}) && !defined($cfg->{migration})) {
537 my $migration_unsecure = delete $cfg->{migration_unsecure};
538 $cfg->{migration}->{type} = ($migration_unsecure) ? 'insecure' : 'secure';
539 }
540
541 # map deprecated applet setting to html5
542 if (defined($cfg->{console}) && $cfg->{console} eq 'applet') {
543 $cfg->{console} = 'html5';
544 }
545
546 if (ref(my $crs = $cfg->{crs})) {
547 $cfg->{crs} = PVE::JSONSchema::print_property_string($crs, $crs_format);
548 }
549
550 if (ref(my $migration = $cfg->{migration})) {
551 $cfg->{migration} = PVE::JSONSchema::print_property_string($migration, $migration_format);
552 }
553
554 if (defined(my $next_id = $cfg->{'next-id'})) {
555 $next_id = parse_property_string($next_id_format, $next_id) if !ref($next_id);
556
557 my $lower = int($next_id->{lower} // $next_id_format->{lower}->{default});
558 my $upper = int($next_id->{upper} // $next_id_format->{upper}->{default});
559
560 die "lower ($lower) <= upper ($upper) boundary rule broken\n" if $lower > $upper;
561
562 $cfg->{'next-id'} = PVE::JSONSchema::print_property_string($next_id, $next_id_format);
563 }
564
565 if (ref(my $ha = $cfg->{ha})) {
566 $cfg->{ha} = PVE::JSONSchema::print_property_string($ha, $ha_format);
567 }
568 if (ref(my $notify = $cfg->{notify})) {
569 $cfg->{notify} = PVE::JSONSchema::print_property_string($notify, $notification_format);
570 }
571
572 if (ref(my $u2f = $cfg->{u2f})) {
573 $cfg->{u2f} = PVE::JSONSchema::print_property_string($u2f, $u2f_format);
574 }
575
576 if (ref(my $webauthn = $cfg->{webauthn})) {
577 $cfg->{webauthn} = PVE::JSONSchema::print_property_string($webauthn, $webauthn_format);
578 }
579
580 if (ref(my $tag_style = $cfg->{'tag-style'})) {
581 $cfg->{'tag-style'} = PVE::JSONSchema::print_property_string($tag_style, $tag_style_format);
582 }
583
584 if (ref(my $user_tag_privs = $cfg->{'user-tag-access'})) {
585 if (my $user_tags = $user_tag_privs->{'user-allow-list'}) {
586 $user_tag_privs->{'user-allow-list'} = join(';', sort $user_tags->@*);
587 }
588 $cfg->{'user-tag-access'} =
589 PVE::JSONSchema::print_property_string($user_tag_privs, $user_tag_privs_format);
590 }
591
592 if (ref(my $admin_tags = $cfg->{'registered-tags'})) {
593 $cfg->{'registered-tags'} = join(';', sort $admin_tags->@*);
594 }
595
596 my $comment = '';
597 # add description as comment to top of file
598 my $description = $cfg->{description} || '';
599 foreach my $line (split(/\n/, $description)) {
600 $comment .= '#' . PVE::Tools::encode_text($line) . "\n";
601 }
602 delete $cfg->{description}; # add only as comment, no additional key-value pair
603 my $dump = PVE::JSONSchema::dump_config($datacenter_schema, $filename, $cfg);
604
605 return $comment . "\n" . $dump;
606 }
607
608 PVE::Cluster::cfs_register_file(
609 'datacenter.cfg',
610 \&parse_datacenter_config,
611 \&write_datacenter_config,
612 );
613
614 1;