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