]> git.proxmox.com Git - pve-cluster.git/blob - src/PVE/DataCenterConfig.pm
3d24e7886686cda44aa5b6d87d0abcd92b835326
[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 notification mails.",
53 verbose_description => "Control how often the daily update job should send out notification mails:\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 },
60 };
61
62 register_standard_option('pve-ha-shutdown-policy', {
63 type => 'string',
64 enum => ['freeze', 'failover', 'conditional', 'migrate'],
65 description => "The policy for HA services on node shutdown. 'freeze' disables ".
66 "auto-recovery, 'failover' ensures recovery, 'conditional' recovers on ".
67 "poweroff and freezes on reboot. 'migrate' will migrate running services ".
68 "to other nodes, if possible. With 'freeze' or 'failover', HA Services will ".
69 "always get stopped first on shutdown.",
70 verbose_description => "Describes the policy for handling HA services on poweroff ".
71 "or reboot of a node. Freeze will always freeze services which are still located ".
72 "on the node on shutdown, those services won't be recovered by the HA manager. ".
73 "Failover will not mark the services as frozen and thus the services will get ".
74 "recovered to other nodes, if the shutdown node does not come up again quickly ".
75 "(< 1min). 'conditional' chooses automatically depending on the type of shutdown, ".
76 "i.e., on a reboot the service will be frozen but on a poweroff the service will ".
77 "stay as is, and thus get recovered after about 2 minutes. ".
78 "Migrate will try to move all running services to another node when a reboot or ".
79 "shutdown was triggered. The poweroff process will only continue once no running services ".
80 "are located on the node anymore. If the node comes up again, the service will ".
81 "be moved back to the previously powered-off node, at least if no other migration, ".
82 "reloaction or recovery took place.",
83 default => 'conditional',
84 });
85
86 my $ha_format = {
87 shutdown_policy => get_standard_option('pve-ha-shutdown-policy'),
88 };
89
90 my $next_id_format = {
91 lower => {
92 type => 'integer',
93 description => "Lower, inclusive boundary for free next-id API range.",
94 min => 100,
95 max => 1000 * 1000 * 1000 - 1,
96 default => 100,
97 optional => 1,
98 },
99 upper => {
100 type => 'integer',
101 description => "Upper, exclusive boundary for free next-id API range.",
102 min => 100,
103 max => 1000 * 1000 * 1000,
104 default => 1000 * 1000, # lower than the maximum on purpose
105 optional => 1,
106 },
107 };
108
109 my $u2f_format = {
110 appid => {
111 type => 'string',
112 description => "U2F AppId URL override. Defaults to the origin.",
113 format_description => 'APPID',
114 optional => 1,
115 },
116 origin => {
117 type => 'string',
118 description => "U2F Origin override. Mostly useful for single nodes with a single URL.",
119 format_description => 'URL',
120 optional => 1,
121 },
122 };
123
124 my $webauthn_format = {
125 rp => {
126 type => 'string',
127 description =>
128 'Relying party name. Any text identifier.'
129 .' Changing this *may* break existing credentials.',
130 format_description => 'RELYING_PARTY',
131 optional => 1,
132 },
133 origin => {
134 type => 'string',
135 description =>
136 'Site origin. Must be a `https://` URL (or `http://localhost`).'
137 .' Should contain the address users type in their browsers to access'
138 .' the web interface.'
139 .' Changing this *may* break existing credentials.',
140 format_description => 'URL',
141 optional => 1,
142 },
143 id => {
144 type => 'string',
145 description =>
146 'Relying party ID. Must be the domain name without protocol, port or location.'
147 .' Changing this *will* break existing credentials.',
148 format_description => 'DOMAINNAME',
149 optional => 1,
150 },
151 'allow-subdomains' => {
152 type => 'boolean',
153 description => 'Whether to allow the origin to be a subdomain, rather than the exact URL.',
154 optional => 1,
155 default => 1,
156 },
157 };
158
159 PVE::JSONSchema::register_format('mac-prefix', \&pve_verify_mac_prefix);
160 sub pve_verify_mac_prefix {
161 my ($mac_prefix, $noerr) = @_;
162
163 if ($mac_prefix !~ m/^[a-f0-9][02468ace](?::[a-f0-9]{2}){0,2}:?$/i) {
164 return undef if $noerr;
165 die "value is not a valid unicast MAC address prefix\n";
166 }
167 return $mac_prefix;
168 }
169
170 my $COLOR_RE = '[0-9a-fA-F]{6}';
171 my $TAG_COLOR_OVERRIDE_RE = "(?:${PVE::JSONSchema::PVE_TAG_RE}:${COLOR_RE}(?:\:${COLOR_RE})?)";
172
173 my $tag_style_format = {
174 'shape' => {
175 optional => 1,
176 type => 'string',
177 enum => ['full', 'circle', 'dense', 'none'],
178 default => 'circle',
179 description => "Tag shape for the web ui tree. 'full' draws the full tag. "
180 ."'circle' draws only a circle with the background color. "
181 ."'dense' only draws a small rectancle (useful when many tags are assigned to each guest)."
182 ."'none' disables showing the tags.",
183 },
184 'color-map' => {
185 optional => 1,
186 type => 'string',
187 pattern => "${TAG_COLOR_OVERRIDE_RE}(?:\;$TAG_COLOR_OVERRIDE_RE)*",
188 typetext => '<tag>:<hex-color>[:<hex-color-for-text>][;<tag>=...]',
189 description => "Manual color mapping for tags (semicolon separated).",
190 },
191 ordering => {
192 optional => 1,
193 type => 'string',
194 enum => ['config', 'alphabetical'],
195 default => 'alphabetical',
196 description => 'Controls the sorting of the tags in the web-interface and the API update.',
197 },
198 'case-sensitive' => {
199 type => 'boolean',
200 description => 'Controls if filtering for unique tags on update should check case-sensitive.',
201 optional => 1,
202 default => 0,
203 },
204 };
205
206 my $user_tag_privs_format = {
207 'user-allow' => {
208 optional => 1,
209 type => 'string',
210 enum => ['none', 'list', 'existing', 'free'],
211 default => 'free',
212 description => "Controls tag usage for users without `Sys.Modify` on `/` by either "
213 ."allowing `none`, a `list`, already `existing` or anything (`free`).",
214 verbose_description => "Controls which tags can be set or deleted on resources a user "
215 ."controls (such as guests). Users with the `Sys.Modify` privilege on `/` are always "
216 ." unrestricted. "
217 ."* 'none' no tags are usable. "
218 ."* 'list' tags from 'user-allow-list' are usable. "
219 ."* 'existing' like list, but already existing tags of resources are also usable."
220 ."* 'free' no tag restrictions.",
221 },
222 'user-allow-list' => {
223 optional => 1,
224 type => 'string',
225 pattern => "${PVE::JSONSchema::PVE_TAG_RE}(?:\;${PVE::JSONSchema::PVE_TAG_RE})*",
226 typetext => "<tag>[;<tag>...]",
227 description => "List of tags users are allowed to set and delete (semicolon separated) "
228 ."for 'user-allow' values 'list' and 'existing'.",
229 },
230 };
231
232 my $datacenter_schema = {
233 type => "object",
234 additionalProperties => 0,
235 properties => {
236 crs => {
237 optional => 1,
238 type => 'string', format => $crs_format,
239 description => "Cluster resource scheduling settings.",
240 },
241 keyboard => {
242 optional => 1,
243 type => 'string',
244 description => "Default keybord layout for vnc server.",
245 enum => PVE::Tools::kvmkeymaplist(),
246 },
247 language => {
248 optional => 1,
249 type => 'string',
250 description => "Default GUI language.",
251 enum => [
252 'ca',
253 'da',
254 'de',
255 'en',
256 'es',
257 'eu',
258 'fa',
259 'fr',
260 'he',
261 'it',
262 'ja',
263 'nb',
264 'nn',
265 'pl',
266 'pt_BR',
267 'ru',
268 'sl',
269 'sv',
270 'tr',
271 'zh_CN',
272 'zh_TW',
273 ],
274 },
275 http_proxy => {
276 optional => 1,
277 type => 'string',
278 description => "Specify external http proxy which is used for downloads (example: 'http://username:password\@host:port/')",
279 pattern => "http://.*",
280 },
281 # FIXME: remove with 8.0 (add check to pve7to8!), merged into "migration" since 4.3
282 migration_unsecure => {
283 optional => 1,
284 type => 'boolean',
285 description => "Migration is secure using SSH tunnel by default. " .
286 "For secure private networks you can disable it to speed up " .
287 "migration. Deprecated, use the 'migration' property instead!",
288 },
289 'next-id' => {
290 optional => 1,
291 type => 'string',
292 format => $next_id_format,
293 description => "Control the range for the free VMID auto-selection pool.",
294 },
295 migration => {
296 optional => 1,
297 type => 'string', format => $migration_format,
298 description => "For cluster wide migration settings.",
299 },
300 console => {
301 optional => 1,
302 type => 'string',
303 description => "Select the default Console viewer. You can either use the builtin java"
304 ." 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.",
305 # FIXME: remove 'applet' with 8.0 (add pve7to8 check!)
306 enum => ['applet', 'vv', 'html5', 'xtermjs'],
307 },
308 email_from => {
309 optional => 1,
310 type => 'string',
311 format => 'email-opt',
312 description => "Specify email address to send notification from (default is root@\$hostname)",
313 },
314 max_workers => {
315 optional => 1,
316 type => 'integer',
317 minimum => 1,
318 description => "Defines how many workers (per node) are maximal started ".
319 " on actions like 'stopall VMs' or task from the ha-manager.",
320 },
321 fencing => {
322 optional => 1,
323 type => 'string',
324 default => 'watchdog',
325 enum => [ 'watchdog', 'hardware', 'both' ],
326 description => "Set the fencing mode of the HA cluster. Hardware mode " .
327 "needs a valid configuration of fence devices in /etc/pve/ha/fence.cfg." .
328 " With both all two modes are used." .
329 "\n\nWARNING: 'hardware' and 'both' are EXPERIMENTAL & WIP",
330 },
331 ha => {
332 optional => 1,
333 type => 'string', format => $ha_format,
334 description => "Cluster wide HA settings.",
335 },
336 mac_prefix => {
337 optional => 1,
338 type => 'string',
339 format => 'mac-prefix',
340 description => 'Prefix for autogenerated MAC addresses.',
341 },
342 notify => {
343 optional => 1,
344 type => 'string', format => $notification_format,
345 description => "Cluster-wide notification settings.",
346 },
347 bwlimit => PVE::JSONSchema::get_standard_option('bwlimit'),
348 u2f => {
349 optional => 1,
350 type => 'string',
351 format => $u2f_format,
352 description => 'u2f',
353 },
354 webauthn => {
355 optional => 1,
356 type => 'string',
357 format => $webauthn_format,
358 description => 'webauthn configuration',
359 },
360 description => {
361 type => 'string',
362 description => "Datacenter description. Shown in the web-interface datacenter notes panel."
363 ." This is saved as comment inside the configuration file.",
364 maxLength => 64 * 1024,
365 optional => 1,
366 },
367 'tag-style' => {
368 optional => 1,
369 type => 'string',
370 description => "Tag style options.",
371 format => $tag_style_format,
372 },
373 'user-tag-access' => {
374 optional => 1,
375 type => 'string',
376 description => "Privilege options for user-settable tags",
377 format => $user_tag_privs_format,
378 },
379 'registered-tags' => {
380 optional => 1,
381 type => 'string',
382 description => "A list of tags that require a `Sys.Modify` on '/' to set and delete. "
383 ."Tags set here that are also in 'user-tag-access' also require `Sys.Modify`.",
384 pattern => "(?:${PVE::JSONSchema::PVE_TAG_RE};)*${PVE::JSONSchema::PVE_TAG_RE}",
385 typetext => "<tag>[;<tag>...]",
386 },
387 },
388 };
389
390 # make schema accessible from outside (for documentation)
391 sub get_datacenter_schema { return $datacenter_schema };
392
393 sub parse_datacenter_config {
394 my ($filename, $raw) = @_;
395
396 $raw = '' if !defined($raw);
397
398 # description may be comment or key-value pair (or both)
399 my $comment = '';
400 for my $line (split(/\n/, $raw)) {
401 if ($line =~ /^\#(.*)$/) {
402 $comment .= PVE::Tools::decode_text($1) . "\n";
403 }
404 }
405
406 # parse_config ignores lines with # => use $raw
407 my $res = PVE::JSONSchema::parse_config($datacenter_schema, $filename, $raw);
408
409 $res->{description} = $comment;
410
411 if (my $crs = $res->{crs}) {
412 $res->{crs} = parse_property_string($crs_format, $crs);
413 }
414
415 if (my $migration = $res->{migration}) {
416 $res->{migration} = parse_property_string($migration_format, $migration);
417 }
418
419 if (my $next_id = $res->{'next-id'}) {
420 $res->{'next-id'} = parse_property_string($next_id_format, $next_id);
421 }
422
423 if (my $ha = $res->{ha}) {
424 $res->{ha} = parse_property_string($ha_format, $ha);
425 }
426 if (my $notify = $res->{notify}) {
427 $res->{notify} = parse_property_string($notification_format, $notify);
428 }
429
430 if (my $u2f = $res->{u2f}) {
431 $res->{u2f} = parse_property_string($u2f_format, $u2f);
432 }
433
434 if (my $webauthn = $res->{webauthn}) {
435 $res->{webauthn} = parse_property_string($webauthn_format, $webauthn);
436 }
437
438 if (my $tag_style = $res->{'tag-style'}) {
439 $res->{'tag-style'} = parse_property_string($tag_style_format, $tag_style);
440 }
441
442 if (my $user_tag_privs = $res->{'user-tag-access'}) {
443 $res->{'user-tag-access'} =
444 parse_property_string($user_tag_privs_format, $user_tag_privs);
445
446 if (my $user_tags = $res->{'user-tag-access'}->{'user-allow-list'}) {
447 $res->{'user-tag-access'}->{'user-allow-list'} = [split(';', $user_tags)];
448 }
449 }
450
451 if (my $admin_tags = $res->{'registered-tags'}) {
452 $res->{'registered-tags'} = [split(';', $admin_tags)];
453 }
454
455 # for backwards compatibility only, new migration property has precedence
456 if (defined($res->{migration_unsecure})) {
457 if (defined($res->{migration}->{type})) {
458 warn "deprecated setting 'migration_unsecure' and new 'migration: type' " .
459 "set at same time! Ignore 'migration_unsecure'\n";
460 } else {
461 $res->{migration}->{type} = ($res->{migration_unsecure}) ? 'insecure' : 'secure';
462 }
463 }
464
465 # for backwards compatibility only, applet maps to html5
466 if (defined($res->{console}) && $res->{console} eq 'applet') {
467 $res->{console} = 'html5';
468 }
469
470 return $res;
471 }
472
473 sub write_datacenter_config {
474 my ($filename, $cfg) = @_;
475
476 # map deprecated setting to new one
477 if (defined($cfg->{migration_unsecure}) && !defined($cfg->{migration})) {
478 my $migration_unsecure = delete $cfg->{migration_unsecure};
479 $cfg->{migration}->{type} = ($migration_unsecure) ? 'insecure' : 'secure';
480 }
481
482 # map deprecated applet setting to html5
483 if (defined($cfg->{console}) && $cfg->{console} eq 'applet') {
484 $cfg->{console} = 'html5';
485 }
486
487 if (ref(my $crs = $cfg->{crs})) {
488 $cfg->{crs} = PVE::JSONSchema::print_property_string($crs, $crs_format);
489 }
490
491 if (ref(my $migration = $cfg->{migration})) {
492 $cfg->{migration} = PVE::JSONSchema::print_property_string($migration, $migration_format);
493 }
494
495 if (defined(my $next_id = $cfg->{'next-id'})) {
496 $next_id = parse_property_string($next_id_format, $next_id) if !ref($next_id);
497
498 my $lower = int($next_id->{lower} // $next_id_format->{lower}->{default});
499 my $upper = int($next_id->{upper} // $next_id_format->{upper}->{default});
500
501 die "lower ($lower) <= upper ($upper) boundary rule broken\n" if $lower > $upper;
502
503 $cfg->{'next-id'} = PVE::JSONSchema::print_property_string($next_id, $next_id_format);
504 }
505
506 if (ref(my $ha = $cfg->{ha})) {
507 $cfg->{ha} = PVE::JSONSchema::print_property_string($ha, $ha_format);
508 }
509 if (ref(my $notify = $cfg->{notify})) {
510 $cfg->{notify} = PVE::JSONSchema::print_property_string($notify, $notification_format);
511 }
512
513 if (ref(my $u2f = $cfg->{u2f})) {
514 $cfg->{u2f} = PVE::JSONSchema::print_property_string($u2f, $u2f_format);
515 }
516
517 if (ref(my $webauthn = $cfg->{webauthn})) {
518 $cfg->{webauthn} = PVE::JSONSchema::print_property_string($webauthn, $webauthn_format);
519 }
520
521 if (ref(my $tag_style = $cfg->{'tag-style'})) {
522 $cfg->{'tag-style'} = PVE::JSONSchema::print_property_string($tag_style, $tag_style_format);
523 }
524
525 if (ref(my $user_tag_privs = $cfg->{'user-tag-access'})) {
526 if (my $user_tags = $user_tag_privs->{'user-allow-list'}) {
527 $user_tag_privs->{'user-allow-list'} = join(';', sort $user_tags->@*);
528 }
529 $cfg->{'user-tag-access'} =
530 PVE::JSONSchema::print_property_string($user_tag_privs, $user_tag_privs_format);
531 }
532
533 if (ref(my $admin_tags = $cfg->{'registered-tags'})) {
534 $cfg->{'registered-tags'} = join(';', sort $admin_tags->@*);
535 }
536
537 my $comment = '';
538 # add description as comment to top of file
539 my $description = $cfg->{description} || '';
540 foreach my $line (split(/\n/, $description)) {
541 $comment .= '#' . PVE::Tools::encode_text($line) . "\n";
542 }
543 delete $cfg->{description}; # add only as comment, no additional key-value pair
544 my $dump = PVE::JSONSchema::dump_config($datacenter_schema, $filename, $cfg);
545
546 return $comment . "\n" . $dump;
547 }
548
549 PVE::Cluster::cfs_register_file(
550 'datacenter.cfg',
551 \&parse_datacenter_config,
552 \&write_datacenter_config,
553 );
554
555 1;