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