]> git.proxmox.com Git - aab.git/blame - PVE/AAB.pm
import missing open2
[aab.git] / PVE / AAB.pm
CommitLineData
7b25f331
WB
1package PVE::AAB;
2
3use strict;
4use warnings;
5
6use File::Path;
7use File::Copy;
8use IO::File;
9use IO::Select;
7c20fd82 10use IPC::Open2;
7b25f331
WB
11use IPC::Open3;
12use UUID;
13use Cwd;
14
15my @BASE_PACKAGES = qw(base openssh);
16my @BASE_EXCLUDES = qw(e2fsprogs
17 jfsutils
18 linux
19 lvm2
20 mdadm
21 netctl
22 pcmciautils
23 reiserfsprogs
24 xfsprogs);
25
26my $PKGDIR = "/var/cache/pacman/pkg";
27
28my ($aablibdir, $fake_init);
29
30sub setup_defaults($) {
31 my ($dir) = @_;
32 $aablibdir = $dir;
33 $fake_init = "$aablibdir/scripts/init.bash";
34}
35
36setup_defaults('/usr/lib/aab');
37
38sub write_file {
39 my ($data, $file, $perm) = @_;
40
41 die "no filename" if !$file;
42 unlink $file;
43
44 my $fh = IO::File->new ($file, O_WRONLY | O_CREAT, $perm) ||
45 die "unable to open file '$file'";
46
47 print $fh $data;
48 $fh->close;
49}
50
51sub copy_file {
52 my ($a, $b) = @_;
53 copy($a, $b) or die "failed to copy $a => $b: $!";
54}
55
56sub rename_file {
57 my ($a, $b) = @_;
58 rename($a, $b) or die "failed to rename $a => $b: $!";
59}
60
61sub symln {
62 my ($a, $b) = @_;
63 symlink($a, $b) or die "failed to symlink $a => $b: $!";
64}
65
66sub logmsg {
67 my $self = shift;
68 print STDERR @_;
69 $self->writelog (@_);
70}
71
72sub writelog {
73 my $self = shift;
74 my $fd = $self->{logfd};
75 print $fd @_;
76}
77
78sub read_config {
79 my ($filename) = @_;
80
81 my $res = {};
82
83 my $fh = IO::File->new ("<$filename") || return $res;
84 my $rec = '';
85
86 while (defined (my $line = <$fh>)) {
87 next if $line =~ m/^\#/;
88 next if $line =~ m/^\s*$/;
89 $rec .= $line;
90 };
91
92 close ($fh);
93
94 chomp $rec;
95 $rec .= "\n";
96
97 while ($rec) {
98 if ($rec =~ s/^Description:\s*([^\n]*)(\n\s+.*)*$//si) {
99 $res->{headline} = $1;
100 chomp $res->{headline};
101 my $long = $2;
102 $long =~ s/^\s+/ /;
103 $res->{description} = $long;
104 chomp $res->{description};
105 } elsif ($rec =~ s/^([^:]+):\s*(.*\S)\s*\n//) {
106 my ($key, $value) = (lc ($1), $2);
107 if ($key eq 'source' || $key eq 'mirror') {
108 push @{$res->{$key}}, $value;
109 } else {
110 die "duplicate key '$key'\n" if defined ($res->{$key});
111 $res->{$key} = $value;
112 }
113 } else {
114 die "unable to parse config file: $rec";
115 }
116 }
117
118 die "unable to parse config file" if $rec;
119
120 return $res;
121}
122
123sub new {
124 my ($class, $config) = @_;
125
126 $config = read_config ('aab.conf') if !$config;
127 my $version = $config->{version};
128 die "no 'version' specified\n" if !$version;
129 die "no 'section' specified\n" if !$config->{section};
130 die "no 'description' specified\n" if !$config->{headline};
131 die "no 'maintainer' specified\n" if !$config->{maintainer};
132
133 my $name = $config->{name} || die "no 'name' specified\n";
134 $name =~ m/^[a-z][0-9a-z\-\*\.]+$/ ||
135 die "illegal characters in name '$name'\n";
136
137 my $targetname;
138 if ($name =~ m/^archlinux/) {
139 $targetname = "${name}_${version}_$config->{architecture}";
140 } else {
141 $targetname = "archlinux-${name}_${version}_$config->{architecture}";
142 }
143
144 my $self = { logfile => 'logfile',
145 config => $config,
146 targetname => $targetname,
147 incl => [@BASE_PACKAGES],
148 excl => [@BASE_EXCLUDES],
149 };
150
151 $self->{logfd} = IO::File->new($self->{logfile}, O_WRONLY | O_APPEND | O_CREAT)
152 or die "unable to open log file";
153
154 bless $self, $class;
155
156 $self->__allocate_ve();
157
158 return $self;
159}
160
161sub __sample_config {
162 my ($self) = @_;
163
164 my $arch = $self->{config}->{architecture};
165
166 return <<"CFG";
167lxc.arch = $arch
168lxc.include = /usr/share/lxc/config/archlinux.common.conf
169lxc.utsname = localhost
170lxc.rootfs = $self->{rootfs}
171lxc.mount.entry = $self->{pkgcache} $self->{pkgdir} none bind 0 0
172CFG
173}
174
175sub __allocate_ve {
176 my ($self) = @_;
177
178 my $cid;
179 if (my $fd = IO::File->new(".veid")) {
180 $cid = <$fd>;
181 chomp $cid;
182 close ($fd);
183 }
184
185
186 $self->{working_dir} = getcwd;
187 $self->{veconffile} = "$self->{working_dir}/config";
188 $self->{rootfs} = "$self->{working_dir}/rootfs";
189 $self->{pkgdir} = "$self->{working_dir}/rootfs/$PKGDIR";
190 $self->{pkgcache} = "$self->{working_dir}/pkgcache";
191 $self->{'pacman.conf'} = "$self->{working_dir}/pacman.conf";
192
193 if ($cid) {
194 $self->{veid} = $cid;
195 return $cid;
196 }
197
198 my $uuid;
199 my $uuid_str;
200 UUID::generate($uuid);
201 UUID::unparse($uuid, $uuid_str);
202 $self->{veid} = $uuid_str;
203
204 my $fd = IO::File->new (">.veid") ||
205 die "unable to write '.veid'\n";
206 print $fd "$self->{veid}\n";
207 close ($fd);
208 $self->logmsg("allocated VE $self->{veid}\n");
209}
210
211sub initialize {
212 my ($self) = @_;
213
214 my $config = $self->{config};
215
216 $self->{logfd} = IO::File->new($self->{logfile}, O_WRONLY | O_TRUNC | O_CREAT)
217 or die "unable to open log file";
218
219 my $cdata = $self->__sample_config();
220
221 my $fh = IO::File->new($self->{veconffile}, O_WRONLY|O_CREAT|O_EXCL) ||
222 die "unable to write lxc config file '$self->{veconffile}' - $!";
223 print $fh $cdata;
224 close ($fh);
225
226 if (!$config->{source} && !$config->{mirror}) {
227 die "no sources/mirrors specified";
228 }
229
230 $config->{source} //= [];
231 $config->{mirror} //= [];
232
233 my $servers = "Server = "
234 . join("\nServer = ", @{$config->{source}}, @{$config->{mirror}})
235 . "\n";
236
237 $fh = IO::File->new($self->{'pacman.conf'}, O_WRONLY|O_CREAT|O_EXCL) ||
238 die "unable to write pacman config file $self->{'pacman.conf'} - $!";
239 print $fh <<"EOF";
240[options]
241HoldPkg = pacman glibc
242Architecture = $config->{architecture}
243CheckSpace
244SigLevel = Never
245
246[core]
247$servers
248[extra]
249$servers
250[community]
251$servers
7b25f331
WB
252EOF
253
4eaaed91
WB
254 if ($config->{architecture} eq 'x86_64') {
255 print $fh "[multilib]\n$servers\n";
256 }
257
7b25f331
WB
258 mkdir $self->{rootfs} || die "unable to create rootfs - $!";
259
260 $self->logmsg("configured VE $self->{veid}\n");
261}
262
263sub ve_status {
264 my ($self) = @_;
265
266 my $veid = $self->{veid};
267
268 my $res = { running => 0 };
269
270 $res->{exist} = 1 if -d "$self->{rootfs}/usr";
271
272 my $filename = "/proc/net/unix";
273
274 # similar test is used by lcxcontainers.c: list_active_containers
275 my $fh = IO::File->new ($filename, "r");
276 return $res if !$fh;
277
278 while (defined(my $line = <$fh>)) {
279 if ($line =~ m/^[a-f0-9]+:\s\S+\s\S+\s\S+\s\S+\s\S+\s\d+\s(\S+)$/) {
280 my $path = $1;
281 if ($path =~ m!^@/\S+/$veid/command$!) {
282 $res->{running} = 1;
283 }
284 }
285 }
286 close($fh);
287
288 return $res;
289}
290
291sub ve_destroy {
292 my ($self) = @_;
293
294 my $veid = $self->{veid}; # fixme
295
296 my $vestat = $self->ve_status();
297 if ($vestat->{running}) {
298 $self->stop_container();
299 }
300
301 rmtree $self->{rootfs};
302 unlink $self->{veconffile};
303}
304
305sub ve_init {
306 my ($self) = @_;
307
308
309 my $veid = $self->{veid};
d43d058d 310 my $conffile = $self->{veconffile};
7b25f331
WB
311
312 $self->logmsg ("initialize VE $veid\n");
313
314 my $vestat = $self->ve_status();
315 if ($vestat->{running}) {
d43d058d 316 $self->run_command ("lxc-stop -n $veid --rcfile $conffile --kill");
7b25f331
WB
317 }
318
319 rmtree $self->{rootfs};
320 mkpath $self->{rootfs};
321}
322
323sub ve_command {
324 my ($self, $cmd, $input) = @_;
325
326 my $veid = $self->{veid};
d43d058d 327 my $conffile = $self->{veconffile};
7b25f331
WB
328
329 if (ref ($cmd) eq 'ARRAY') {
d43d058d 330 unshift @$cmd, 'lxc-attach', '-n', $veid, '--rcfile', $conffile,'--clear-env', '--';
7b25f331
WB
331 $self->run_command ($cmd, $input);
332 } else {
d43d058d 333 $self->run_command ("lxc-attach -n $veid --rcfile $conffile --clear-env -- $cmd", $input);
7b25f331
WB
334 }
335}
336
337sub ve_exec {
338 my ($self, @cmd) = @_;
339
340 my $veid = $self->{veid};
d43d058d 341 my $conffile = $self->{veconffile};
7b25f331
WB
342
343 my $reader;
d43d058d 344 my $pid = open2($reader, "<&STDIN", 'lxc-attach', '-n', $veid, '--rcfile', $conffile, '--', @cmd)
7b25f331
WB
345 or die "unable to exec command";
346
347 while (defined (my $line = <$reader>)) {
348 $self->logmsg ($line);
349 }
350
351 waitpid ($pid, 0);
352 my $rc = $? >> 8;
353
354 die "ve_exec failed - status $rc\n" if $rc != 0;
355}
356
357sub run_command {
358 my ($self, $cmd, $input, $getoutput) = @_;
359
360 my $reader = IO::File->new();
361 my $writer = IO::File->new();
362 my $error = IO::File->new();
363
364 my $orig_pid = $$;
365
366 my $cmdstr = ref ($cmd) eq 'ARRAY' ? join (' ', @$cmd) : $cmd;
367
368 my $pid;
369 eval {
370 if (ref ($cmd) eq 'ARRAY') {
371 $pid = open3 ($writer, $reader, $error, @$cmd) || die $!;
372 } else {
373 $pid = open3 ($writer, $reader, $error, $cmdstr) || die $!;
374 }
375 };
376
377 my $err = $@;
378
379 # catch exec errors
380 if ($orig_pid != $$) {
381 $self->logmsg ("ERROR: command '$cmdstr' failed - fork failed\n");
382 POSIX::_exit (1);
383 kill ('KILL', $$);
384 }
385
386 die $err if $err;
387
388 print $writer $input if defined $input;
389 close $writer;
390
391 my $select = new IO::Select;
392 $select->add ($reader);
393 $select->add ($error);
394
395 my $res = '';
396 my $logfd = $self->{logfd};
397
398 while ($select->count) {
399 my @handles = $select->can_read ();
400
401 foreach my $h (@handles) {
402 my $buf = '';
403 my $count = sysread ($h, $buf, 4096);
404 if (!defined ($count)) {
405 waitpid ($pid, 0);
406 die "command '$cmdstr' failed: $!";
407 }
408 $select->remove ($h) if !$count;
409
410 print $logfd $buf;
411
412 $res .= $buf if $getoutput;
413 }
414 }
415
416 waitpid ($pid, 0);
417 my $ec = ($? >> 8);
418
419 die "command '$cmdstr' failed with exit code $ec\n" if $ec;
420
421 return $res;
422}
423
424sub start_container {
425 my ($self) = @_;
426 my $veid = $self->{veid};
427 $self->run_command(['lxc-start', '-n', $veid, '-f', $self->{veconffile}, '/usr/bin/aab_fake_init']);
428}
429
430sub stop_container {
431 my ($self) = @_;
432 my $veid = $self->{veid};
d43d058d
WB
433 my $conffile = $self->{veconffile};
434 $self->run_command ("lxc-stop -n $veid --rcfile $conffile --kill");
7b25f331
WB
435}
436
437sub pacman_command {
438 my ($self) = @_;
439 my $root = $self->{rootfs};
440 return ('/usr/bin/pacman',
441 '--root', $root,
5f96733f 442 '--config', $self->{'pacman.conf'},
7b25f331
WB
443 '--cachedir', $self->{pkgcache},
444 '--noconfirm');
445}
446
447sub cache_packages {
448 my ($self, $packages) = @_;
449 my $root = $self->{rootfs};
450
451 my @pacman = $self->pacman_command();
452 $self->run_command([@pacman, '-Sw', '--', @$packages]);
453}
454
455sub bootstrap {
456 my ($self, $include, $exclude) = @_;
457 my $root = $self->{rootfs};
458
459 my @pacman = $self->pacman_command();
460
461 print "Fetching package database...\n";
462 mkpath $self->{pkgcache};
463 mkpath $self->{pkgdir};
464 mkpath "$root/var/lib/pacman";
465 $self->run_command([@pacman, '-Sy']);
466
467 print "Figuring out what to install...\n";
468 my $incl = { map { $_ => 1 } @{$self->{incl}} };
469 my $excl = { map { $_ => 1 } @{$self->{excl}} };
470
471 foreach my $addinc (@$include) {
472 $incl->{$addinc} = 1;
473 delete $excl->{$addinc};
474 }
475 foreach my $addexc (@$exclude) {
476 $excl->{$addexc} = 1;
477 delete $incl->{$addexc};
478 }
479
480 my $expand = sub {
481 my ($lst) = @_;
482 foreach my $inc (keys %$lst) {
483 my $group;
484 eval { $group = $self->run_command([@pacman, '-Sqg', $inc], undef, 1); };
485 if ($group && !$@) {
486 # add the group
487 delete $lst->{$inc};
488 $lst->{$_} = 1 foreach split(/\s+/, $group);
489 }
490 }
491 };
492
493 $expand->($incl);
494 $expand->($excl);
495
496 my $packages = [ grep { !$excl->{$_} } keys %$incl ];
497
498 print "Setting up basic environment...\n";
499 mkpath "$root/etc";
500 mkpath "$root/usr/bin";
501
502 my $data = "# UNCONFIGURED FSTAB FOR BASE SYSTEM\n";
503 write_file ($data, "$root/etc/fstab", 0644);
504
505 write_file ("", "$root/etc/resolv.conf", 0644);
506 write_file("localhost\n", "$root/etc/hostname", 0644);
507 $self->run_command(['install', '-m0755', $fake_init, "$root/usr/bin/aab_fake_init"]);
508
509 unlink "$root/etc/localtime";
510 symln '/usr/share/zoneinfo/UTC', "$root/etc/localtime";
511
512 print "Caching packages...\n";
513 $self->cache_packages($packages);
514 #$self->copy_packages();
515
516 print "Installing package manager and essentials...\n";
517 # inetutils for 'hostname' for our init
518 $self->run_command([@pacman, '-S', 'pacman', 'inetutils', 'archlinux-keyring']);
519
520 print "Setting up pacman for installation from cache...\n";
521 my $file = "$root/etc/pacman.d/mirrorlist";
522 my $backup = "${file}.aab_orig";
523 if (!-f $backup) {
524 rename_file($file, $backup);
525 write_file("Server = file://$PKGDIR\n", $file);
526 }
527
528 print "Populating keyring...\n";
766f0fa3 529 $self->populate_keyring();
7b25f331
WB
530
531 print "Starting container...\n";
532 $self->start_container();
533
534 print "Installing packages...\n";
535 $self->ve_command(['pacman', '-S', '--needed', '--noconfirm', '--', @$packages]);
536}
537
766f0fa3
WB
538sub populate_keyring {
539 my ($self) = @_;
540 my $root = $self->{rootfs};
541
542 # devices needed for gnupg to function:
543 my $devs = {
544 '/dev/null' => ['c', '1', '3'],
545 '/dev/random' => ['c', '1', '9'], # fake /dev/random (really urandom)
546 '/dev/urandom' => ['c', '1', '9'],
547 '/dev/tty' => ['c', '5', '0'],
548 };
549
550 my $cleanup_dev = sub {
551 # remove temporary device files
552 unlink "${root}$_" foreach keys %$devs;
553 };
554 local $SIG{INT} = $SIG{TERM} = $cleanup_dev;
555
556 # at least /dev/null exists as regular file after installing the filesystem package,
557 # and we want to replace /dev/random, so delete devices first
558 &$cleanup_dev();
559
560 foreach my $dev (keys %$devs) {
561 my ($type, $major, $minor) = @{$devs->{$dev}};
562 system('mknod', "${root}${dev}", $type, $major, $minor);
563 }
564
565 # generate weak master key and populate the keyring
566 system('unshare', '--fork', '--pid', 'chroot', "$root", 'pacman-key', '--init') == 0
567 or die "failed to initialize keyring: $?";
568 system('unshare', '--fork', '--pid', 'chroot', "$root", 'pacman-key', '--populate') == 0
569 or die "failed to populate keyring: $?";
570
571 &$cleanup_dev();
572 # reset to original state
573 system('touch', "$root/dev/null");
574}
575
7b25f331
WB
576sub install {
577 my ($self, $pkglist) = @_;
578
579 $self->cache_packages($pkglist);
580 $self->ve_command(['pacman', '-S', '--needed', '--noconfirm', '--', @$pkglist]);
581}
582
583sub write_config {
584 my ($self, $filename, $size) = @_;
585
586 my $config = $self->{config};
587
588 my $data = '';
589
590 $data .= "Name: $config->{name}\n";
591 $data .= "Version: $config->{version}\n";
592 $data .= "Type: lxc\n";
593 $data .= "OS: archlinux\n";
594 $data .= "Section: $config->{section}\n";
595 $data .= "Maintainer: $config->{maintainer}\n";
596 $data .= "Architecture: $config->{architecture}\n";
b65bfe8c 597 $data .= "Infopage: https://www.archlinux.org\n";
7b25f331
WB
598 $data .= "Installed-Size: $size\n";
599
600 # optional
601 $data .= "Infopage: $config->{infopage}\n" if $config->{infopage};
602 $data .= "ManageUrl: $config->{manageurl}\n" if $config->{manageurl};
603 $data .= "Certified: $config->{certified}\n" if $config->{certified};
604
605 # description
606 $data .= "Description: $config->{headline}\n";
607 $data .= "$config->{description}\n" if $config->{description};
608
609 write_file ($data, $filename, 0644);
610}
611
612sub finalize {
613 my ($self) = @_;
614 my $rootdir = $self->{rootfs};
615
616 print "Stopping container...\n";
617 $self->stop_container();
618
619 print "Rolling back mirrorlist changes...\n";
620 my $file = "$rootdir/etc/pacman.d/mirrorlist";
621 unlink $file;
622 rename_file($file.'.aab_orig', $file);
623
e61d6533
WB
624 print "Removing weak temporary pacman keyring...\n";
625 rmtree("$rootdir/etc/pacman.d/gnupg");
626
7b25f331
WB
627 my $sizestr = $self->run_command("du -sm $rootdir", undef, 1);
628 my $size;
629 if ($sizestr =~ m/^(\d+)\s+\Q$rootdir\E$/) {
630 $size = $1;
631 } else {
632 die "unable to detect size\n";
633 }
634 $self->logmsg ("$size MB\n");
635
636 $self->write_config ("$rootdir/etc/appliance.info", $size);
637
638 $self->logmsg ("creating final appliance archive\n");
639
640 my $target = "$self->{targetname}.tar";
641 unlink $target;
642 unlink "$target.gz";
643
644 $self->run_command ("tar cpf $target --numeric-owner -C '$rootdir' ./etc/appliance.info");
645 $self->run_command ("tar rpf $target --numeric-owner -C '$rootdir' --exclude ./etc/appliance.info .");
646 $self->run_command ("gzip $target");
647}
648
649sub enter {
650 my ($self) = @_;
651 my $veid = $self->{veid};
d43d058d 652 my $conffile = $self->{veconffile};
7b25f331
WB
653
654 my $vestat = $self->ve_status();
655 if (!$vestat->{exist}) {
656 $self->logmsg ("Please create the appliance first (bootstrap)");
657 return;
658 }
659
660 if (!$vestat->{running}) {
661 $self->start_container();
662 }
663
d43d058d 664 system ("lxc-attach -n $veid --rcfile $conffile --clear-env");
7b25f331
WB
665}
666
667sub clean {
668 my ($self, $all) = @_;
669
670 unlink $self->{logfile};
671 unlink $self->{'pacman.conf'};
672 $self->ve_destroy();
673 unlink '.veid';
674 rmtree $self->{pkgcache} if $all;
675}
676
6771;