]> git.proxmox.com Git - pve-container.git/blob - src/pve-update-lxc-config
fix #1618: do not double encode the description
[pve-container.git] / src / pve-update-lxc-config
1 #!/usr/bin/perl
2
3 # update old beta1 config
4 # todo: remove this script after 4.0 Release
5
6 use strict;
7 use warnings;
8
9 use PVE::Tools;
10 use PVE::JSONSchema qw(get_standard_option);
11 use PVE::Network;
12
13 sub parse_volume_id {
14 my ($volid, $noerr) = @_;
15
16 if ($volid =~ m/^([a-z][a-z0-9\-\_\.]*[a-z0-9]):(.+)$/i) {
17 return wantarray ? ($1, $2) : $1;
18 }
19 return undef if $noerr;
20 die "unable to parse volume ID '$volid'\n";
21 }
22
23 sub parse_lxc_size {
24 my ($name, $value) = @_;
25
26 if ($value =~ m/^(\d+)(b|k|m|g)?$/i) {
27 my ($res, $unit) = ($1, lc($2 || 'b'));
28
29 return $res if $unit eq 'b';
30 return $res*1024 if $unit eq 'k';
31 return $res*1024*1024 if $unit eq 'm';
32 return $res*1024*1024*1024 if $unit eq 'g';
33 }
34
35 return undef;
36 }
37
38 sub verify_searchdomain_list {
39 my ($searchdomain_list) = @_;
40
41 my @list = ();
42 foreach my $server (PVE::Tools::split_list($searchdomain_list)) {
43 # todo: should we add checks for valid dns domains?
44 push @list, $server;
45 }
46
47 return join(' ', @list);
48 }
49
50 sub verify_nameserver_list {
51 my ($nameserver_list) = @_;
52
53 my @list = ();
54 foreach my $server (PVE::Tools::split_list($nameserver_list)) {
55 PVE::JSONSchema::pve_verify_ip($server);
56 push @list, $server;
57 }
58
59 return join(' ', @list);
60 }
61
62 my $valid_lxc_keys = {
63 'lxc.arch' => 'i386|x86|i686|x86_64|amd64',
64 'lxc.include' => 1,
65 'lxc.rootfs' => 1,
66 'lxc.mount' => 1,
67 'lxc.utsname' => 1,
68
69 'lxc.id_map' => 1,
70
71 'lxc.cgroup.memory.limit_in_bytes' => \&parse_lxc_size,
72 'lxc.cgroup.memory.memsw.limit_in_bytes' => \&parse_lxc_size,
73 'lxc.cgroup.cpu.cfs_period_us' => '\d+',
74 'lxc.cgroup.cpu.cfs_quota_us' => '\d+',
75 'lxc.cgroup.cpu.shares' => '\d+',
76
77 # mount related
78 'lxc.mount' => 1,
79 'lxc.mount.entry' => 1,
80 'lxc.mount.auto' => 1,
81
82 # security related
83 'lxc.seccomp' => 1,
84
85 # not used by pve
86 'lxc.tty' => '\d+',
87 'lxc.pts' => 1,
88 'lxc.haltsignal' => 1,
89 'lxc.rebootsignal' => 1,
90 'lxc.stopsignal' => 1,
91 'lxc.init_cmd' => 1,
92 'lxc.console' => 1,
93 'lxc.console.logfile' => 1,
94 'lxc.devttydir' => 1,
95 'lxc.autodev' => 1,
96 'lxc.kmsg' => 1,
97 'lxc.cap.drop' => 1,
98 'lxc.cap.keep' => 1,
99 'lxc.aa_profile' => 1,
100 'lxc.aa_allow_incomplete' => 1,
101 'lxc.se_context' => 1,
102 'lxc.loglevel' => 1,
103 'lxc.logfile' => 1,
104 'lxc.environment' => 1,
105 'lxc.cgroup.devices.deny' => 1,
106
107 # autostart
108 'lxc.start.auto' => 1,
109 'lxc.start.delay' => 1,
110 'lxc.start.order' => 1,
111 'lxc.group' => 1,
112
113 # hooks
114 'lxc.hook.pre-start' => 1,
115 'lxc.hook.pre-mount' => 1,
116 'lxc.hook.mount' => 1,
117 'lxc.hook.autodev' => 1,
118 'lxc.hook.start' => 1,
119 'lxc.hook.post-stop' => 1,
120 'lxc.hook.clone' => 1,
121
122 # pve related keys
123 'pve.nameserver' => sub {
124 my ($name, $value) = @_;
125 return verify_nameserver_list($value);
126 },
127 'pve.searchdomain' => sub {
128 my ($name, $value) = @_;
129 return verify_searchdomain_list($value);
130 },
131 'pve.onboot' => '(0|1)',
132 'pve.startup' => sub {
133 my ($name, $value) = @_;
134 return PVE::JSONSchema::pve_verify_startup_order($value);
135 },
136 'pve.comment' => 1,
137 'pve.disksize' => '\d+(\.\d+)?',
138 'pve.volid' => sub {
139 my ($name, $value) = @_;
140 parse_volume_id($value);
141 return $value;
142 },
143
144 #pve snapshot
145 'pve.lock' => 1,
146 'pve.snaptime' => 1,
147 'pve.snapcomment' => 1,
148 'pve.parent' => 1,
149 'pve.snapstate' => 1,
150 'pve.snapname' => 1,
151 };
152
153 my $valid_lxc_network_keys = {
154 type => 1,
155 mtu => 1,
156 name => 1, # ifname inside container
157 'veth.pair' => 1, # ifname at host (eth${vmid}.X)
158 hwaddr => 1,
159 };
160
161 my $valid_pve_network_keys = {
162 bridge => 1,
163 tag => 1,
164 firewall => 1,
165 ip => 1,
166 gw => 1,
167 ip6 => 1,
168 gw6 => 1,
169 };
170
171 my $lxc_array_configs = {
172 'lxc.network' => 1,
173 'lxc.mount' => 1,
174 'lxc.include' => 1,
175 'lxc.id_map' => 1,
176 'lxc.cgroup.devices.deny' => 1,
177 };
178
179 sub parse_lxc_option {
180 my ($name, $value) = @_;
181
182 my $parser = $valid_lxc_keys->{$name};
183
184 die "invalid key '$name'\n" if !defined($parser);
185
186 if ($parser eq '1') {
187 return $value;
188 } elsif (ref($parser)) {
189 my $res = &$parser($name, $value);
190 return $res if defined($res);
191 } else {
192 # assume regex
193 return $value if $value =~ m/^$parser$/;
194 }
195
196 die "unable to parse value '$value' for option '$name'\n";
197 }
198
199 sub parse_lxc_config {
200 my ($filename, $raw) = @_;
201
202 return undef if !defined($raw);
203
204 my $data = {
205 #digest => Digest::SHA::sha1_hex($raw),
206 };
207
208 $filename =~ m|/lxc/(\d+)/config$|
209 || die "got strange filename '$filename'";
210
211 # Note: restore pass filename "lxc/0/config"
212 my $vmid = $1;
213
214 my $check_net_vmid = sub {
215 my ($netvmid) = @_;
216 $vmid ||= $netvmid;
217 die "wrong vmid for network interface pair\n" if $vmid != $netvmid;
218 };
219
220 my $network_counter = 0;
221 my $network_list = [];
222 my $host_ifnames = {};
223 my $snapname;
224 my $network;
225
226 my $push_network = sub {
227 my ($netconf) = @_;
228 return if !$netconf;
229 push @{$network_list}, $netconf;
230 $network_counter++;
231 if (my $netname = $netconf->{'veth.pair'}) {
232 if ($netname =~ m/^veth(\d+).(\d)$/) {
233 &$check_net_vmid($1);
234 $host_ifnames->{$netname} = 1;
235 } else {
236 die "wrong network interface pair\n";
237 }
238 }
239 };
240
241 my $finalize_section = sub {
242 &$push_network($network); # flush
243
244 foreach my $net (@{$network_list}) {
245 next if $net->{type} eq 'empty'; # skip
246 if (!$net->{hwaddr}) {
247 my $dc = PVE::Cluster::cfs_read_file('datacenter.cfg');
248 $net->{hwaddr} = PVE::Tools::random_ether_addr($dc->{mac_prefix});
249 }
250 die "unsupported network type '$net->{type}'\n" if $net->{type} ne 'veth';
251 die "undefined veth network pair'\n" if !$net->{'veth.pair'};
252
253 if ($net->{'veth.pair'} =~ m/^veth\d+.(\d+)$/) {
254 if ($snapname) {
255 $data->{snapshots}->{$snapname}->{"net$1"} = $net;
256 } else {
257 $data->{"net$1"} = $net;
258 }
259 }
260 }
261
262 # reset helper vars
263 $network_counter = 0;
264 $network_list = [];
265 $host_ifnames = {};
266 $network = undef;
267 };
268
269 while ($raw && $raw =~ s/^(.*)?(\n|$)//) {
270 my $line = $1;
271 next if $line =~ m/^\s*$/; # skip empty lines
272 next if $line =~ m/^#/; # skip comments
273
274 # snap.pve.snapname starts new sections
275 if ($line =~ m/^(snap\.)?pve\.snapname\s*=\s*(\w*)\s*$/) {
276 my $value = $2;
277
278 &$finalize_section();
279
280 $snapname = $value;
281 $data->{snapshots}->{$snapname}->{'pve.snapname'} = $snapname;
282
283 } elsif ($line =~ m/^(snap\.)?lxc\.network\.(\S+)\s*=\s*(\S+)\s*$/) {
284 my ($subkey, $value) = ($2, $3);
285 if ($subkey eq 'type') {
286 &$push_network($network);
287 $network = { type => $value };
288 } elsif ($valid_lxc_network_keys->{$subkey}) {
289 $network->{$subkey} = $value;
290 } else {
291 die "unable to parse config line: $line\n";
292 }
293 } elsif ($line =~ m/^(snap\.)?pve\.network\.(\S+)\s*=\s*(\S+)\s*$/) {
294 my ($subkey, $value) = ($2, $3);
295 if ($valid_pve_network_keys->{$subkey}) {
296 $network->{$subkey} = $value;
297 } else {
298 die "unable to parse config line: $line\n";
299 }
300 } elsif ($line =~ m/^(snap\.)?((?:pve|lxc)\.\S+)\s*=\s*(\S.*)\s*$/) {
301 my ($name, $value) = ($2, $3);
302
303 if ($lxc_array_configs->{$name}) {
304 $data->{$name} = [] if !defined($data->{$name});
305 if ($snapname) {
306 push @{$data->{snapshots}->{$snapname}->{$name}}, parse_lxc_option($name, $value);
307 } else {
308 push @{$data->{$name}}, parse_lxc_option($name, $value);
309 }
310 } else {
311 if ($snapname) {
312 die "multiple definitions for $name\n" if defined($data->{snapshots}->{$snapname}->{$name});
313 $data->{snapshots}->{$snapname}->{$name} = parse_lxc_option($name, $value);
314 } else {
315 die "multiple definitions for $name\n" if defined($data->{$name});
316 $data->{$name} = parse_lxc_option($name, $value);
317 }
318 }
319 } else {
320 die "unable to parse config line: $line\n";
321 }
322 }
323
324 &$finalize_section();
325
326 return $data;
327 }
328
329 # Same confdesc entries are unchanged
330 # Deleted ones have `deprecated => 1`
331 # New ones `new => 1`
332 # New required ones `required => 1`
333 my $confdesc = {
334 onboot => {
335 optional => 1,
336 type => 'boolean',
337 description => "Specifies whether a VM will be started during system bootup.",
338 default => 0,
339 },
340 startup => get_standard_option('pve-startup-order'),
341 arch => {
342 optional => 1,
343 type => 'string',
344 enum => ['amd64', 'i386'],
345 description => "OS architecture type.",
346 default => 'amd64',
347 new => 1,
348 required => 1,
349 },
350 ostype => {
351 optional => 1,
352 type => 'string',
353 enum => ['debian', 'ubuntu', 'centos'],
354 description => "OS type. Corresponds to lxc setup scripts in /usr/share/lxc/config/<ostype>.common.conf.",
355 new => 1,
356 required => 1,
357 },
358 tty => {
359 optional => 1,
360 type => 'integer',
361 description => "Specify the number of tty available to the container",
362 minimum => 0,
363 maximum => 6,
364 default => 4,
365 new => 1,
366 },
367 cpulimit => {
368 optional => 1,
369 type => 'number',
370 description => "Limit of CPU usage. Note if the computer has 2 CPUs, it has total of '2' CPU time. Value '0' indicates no CPU limit.",
371 minimum => 0,
372 maximum => 128,
373 default => 0,
374 },
375 cpuunits => {
376 optional => 1,
377 type => 'integer',
378 description => "CPU weight for a VM. Argument is used in the kernel fair scheduler. The larger the number is, the more CPU time this VM gets. Number is relative to weights of all the other running VMs.\n\nNOTE: You can disable fair-scheduler configuration by setting this to 0.",
379 minimum => 0,
380 maximum => 500000,
381 default => 1000,
382 },
383 memory => {
384 optional => 1,
385 type => 'integer',
386 description => "Amount of RAM for the VM in MB.",
387 minimum => 16,
388 default => 512,
389 },
390 swap => {
391 optional => 1,
392 type => 'integer',
393 description => "Amount of SWAP for the VM in MB.",
394 minimum => 0,
395 default => 512,
396 },
397 disk => {
398 optional => 1,
399 type => 'number',
400 description => "Amount of disk space for the VM in GB. A zero indicates no limits.",
401 minimum => 0,
402 default => 4,
403 deprecated => 1,
404 },
405 hostname => {
406 optional => 1,
407 description => "Set a host name for the container.",
408 type => 'string',
409 maxLength => 255,
410 },
411 description => {
412 optional => 1,
413 type => 'string',
414 description => "Container description. Only used on the configuration web interface.",
415 },
416 searchdomain => {
417 optional => 1,
418 type => 'string',
419 description => "Sets DNS search domains for a container. Create will automatically use the setting from the host if you neither set searchdomain or nameserver.",
420 },
421 nameserver => {
422 optional => 1,
423 type => 'string',
424 description => "Sets DNS server IP address for a container. Create will automatically use the setting from the host if you neither set searchdomain or nameserver.",
425 },
426 rootfs => { #get_standard_option('pve-ct-rootfs'),
427 type => 'string', format => 'pve-ct-mountpoint',
428 typetext => '[volume=]volume,] [,backup=yes|no] [,size=\d+]',
429 description => "Use volume as container root.",
430 optional => 1,
431 new => 1,
432 required => 1,
433 },
434 # parent => {
435 # optional => 1,
436 # type => 'string', format => 'pve-configid',
437 # maxLength => 40,
438 # description => "Parent snapshot name. This is used internally, and should not be modified.",
439 # new => 1,
440 # },
441 # snaptime => {
442 # optional => 1,
443 # description => "Timestamp for snapshots.",
444 # type => 'integer',
445 # minimum => 0,
446 # new => 1,
447 # },
448 };
449
450 my $MAX_LXC_NETWORKS = 10;
451 for (my $i = 0; $i < $MAX_LXC_NETWORKS; $i++) {
452 $confdesc->{"net$i"} = {
453 optional => 1,
454 type => 'string', format => 'pve-lxc-network',
455 description => "Specifies network interfaces for the container.\n\n".
456 "The string should have the follow format:\n\n".
457 "-net<[0-9]> bridge=<vmbr<Nummber>>[,hwaddr=<MAC>]\n".
458 "[,mtu=<Number>][,name=<String>][,ip=<IPv4Format/CIDR>]\n".
459 ",ip6=<IPv6Format/CIDR>][,gw=<GatwayIPv4>]\n".
460 ",gw6=<GatwayIPv6>][,firewall=<[1|0]>][,tag=<VlanNo>]",
461 };
462 }
463
464 sub json_config_properties {
465 my $prop = shift;
466
467 foreach my $opt (keys %$confdesc) {
468 $prop->{$opt} = $confdesc->{$opt};
469 }
470
471 return $prop;
472 }
473
474 sub print_lxc_network {
475 my $net = shift;
476
477 die "no network name defined\n" if !$net->{name};
478
479 my $res = "name=$net->{name}";
480
481 foreach my $k (qw(hwaddr mtu bridge ip gw ip6 gw6 firewall tag)) {
482 next if !defined($net->{$k});
483 $res .= ",$k=$net->{$k}";
484 }
485
486 return $res;
487 }
488
489 sub lxc_conf_to_pve {
490 my ($vmid, $lxc_conf) = @_;
491
492 my $properties = json_config_properties();
493
494 my $conf = {}; # digest => $lxc_conf->{digest} };
495
496 foreach my $k (keys %$properties) {
497
498 if ($k eq 'description') {
499 if (my $raw = $lxc_conf->{'pve.comment'}) {
500 $conf->{$k} = PVE::Tools::decode_text($raw);
501 }
502 } elsif ($k eq 'onboot') {
503 $conf->{$k} = $lxc_conf->{'pve.onboot'} if $lxc_conf->{'pve.onboot'};
504 } elsif ($k eq 'startup') {
505 $conf->{$k} = $lxc_conf->{'pve.startup'} if $lxc_conf->{'pve.startup'};
506 } elsif ($k eq 'arch') {
507 $conf->{$k} = $lxc_conf->{'lxc.arch'} if $lxc_conf->{'lxc.arch'};
508 } elsif ($k eq 'ostype') {
509 foreach (@{$lxc_conf->{'lxc.include'}}) {
510 if (m@^\s*/usr/share/lxc/config/(.*)\.common\.conf\s*$@) {
511 $conf->{$k} = $1;
512 }
513 }
514 } elsif ($k eq 'tty') {
515 $conf->{$k} = $lxc_conf->{'lxc.tty'} if $lxc_conf->{'lxc.tty'};
516 } elsif ($k eq 'rootfs') {
517 $conf->{$k} = $lxc_conf->{'pve.volid'} if $lxc_conf->{'pve.volid'};
518 } elsif ($k eq 'hostname') {
519 $conf->{$k} = $lxc_conf->{'lxc.utsname'} if $lxc_conf->{'lxc.utsname'};
520 } elsif ($k eq 'nameserver') {
521 $conf->{$k} = $lxc_conf->{'pve.nameserver'} if $lxc_conf->{'pve.nameserver'};
522 } elsif ($k eq 'searchdomain') {
523 $conf->{$k} = $lxc_conf->{'pve.searchdomain'} if $lxc_conf->{'pve.searchdomain'};
524 } elsif ($k eq 'memory') {
525 if (my $value = $lxc_conf->{'lxc.cgroup.memory.limit_in_bytes'}) {
526 $conf->{$k} = int($value / (1024*1024));
527 }
528 } elsif ($k eq 'swap') {
529 if (my $value = $lxc_conf->{'lxc.cgroup.memory.memsw.limit_in_bytes'}) {
530 my $mem = $lxc_conf->{'lxc.cgroup.memory.limit_in_bytes'} || 0;
531 $conf->{$k} = int(($value - $mem) / (1024*1024));
532 }
533 } elsif ($k eq 'cpulimit') {
534 my $cfs_period_us = $lxc_conf->{'lxc.cgroup.cpu.cfs_period_us'};
535 my $cfs_quota_us = $lxc_conf->{'lxc.cgroup.cpu.cfs_quota_us'};
536
537 if ($cfs_period_us && $cfs_quota_us) {
538 $conf->{$k} = $cfs_quota_us/$cfs_period_us;
539 } else {
540 $conf->{$k} = 0;
541 }
542 } elsif ($k eq 'cpuunits') {
543 $conf->{$k} = $lxc_conf->{'lxc.cgroup.cpu.shares'} || 1024;
544 } elsif ($k eq 'disk') {
545 $conf->{$k} = defined($lxc_conf->{'pve.disksize'}) ?
546 $lxc_conf->{'pve.disksize'} : 0;
547 } elsif ($k =~ m/^net\d$/) {
548 my $net = $lxc_conf->{$k};
549 next if !$net;
550 $conf->{$k} = print_lxc_network($net);
551 }
552 }
553
554 if (my $parent = $lxc_conf->{'pve.parent'}) {
555 $conf->{parent} = $lxc_conf->{'pve.parent'};
556 }
557
558 if (my $parent = $lxc_conf->{'pve.snapcomment'}) {
559 $conf->{description} = $lxc_conf->{'pve.snapcomment'};
560 }
561
562 if (my $parent = $lxc_conf->{'pve.snaptime'}) {
563 $conf->{snaptime} = $lxc_conf->{'pve.snaptime'};
564 }
565
566 return $conf;
567 }
568
569 sub convert($) {
570 my ($vmid) = @_;
571 my $out = "/etc/pve/lxc/${vmid}.conf";
572 my $old = "/etc/pve/lxc/${vmid}/config";
573
574 return if -f $out;
575
576 print "converting $old to $out\n";
577
578 open my $fh, '<', $old or die "failed to open config $old: $!";
579 my $raw = do { local $/; <$fh> };
580 close $fh;
581
582 my $data = parse_lxc_config($old, $raw);
583 my $new = lxc_conf_to_pve($vmid, $data);
584
585 delete $new->{$_} for grep { $confdesc->{$_}->{deprecated} } keys %$new;
586
587 foreach my $required (grep { $confdesc->{$_}->{required} } keys %$confdesc) {
588 if (!$new->{$required}) {
589 die "failed to create required config key '$required'";
590 }
591 }
592
593 open $fh, '>', $out or die "failed to open output file $out: $!";
594 print {$fh} "$_: $new->{$_}\n" for sort keys %$new;
595 close $fh;
596 }
597
598 chdir '/etc/pve/lxc' or die "failed to change directory to /etc/pve/lxc: $!";
599 for (<*>) {
600 next if !/^\d+$/ || ! -d $_;
601 eval { convert($_); };
602 warn $@ if $@;
603 }