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