]>
git.proxmox.com Git - aab.git/blob - PVE/AAB.pm
14 my @BASE_PACKAGES = qw(base openssh vi nano);
15 my @BASE_EXCLUDES = qw(
28 my $PKGDIR = "/var/cache/pacman/pkg";
30 my ($aablibdir, $fake_init);
32 sub setup_defaults
($) {
35 $fake_init = "$aablibdir/scripts/init.bash";
38 setup_defaults
('/usr/lib/aab');
41 my ($data, $file, $perm) = @_;
43 die "no filename" if !$file;
46 my $fh = IO
::File-
>new ($file, O_WRONLY
| O_CREAT
, $perm) ||
47 die "unable to open file '$file'";
55 copy
($a, $b) or die "failed to copy $a => $b: $!";
60 rename($a, $b) or die "failed to rename $a => $b: $!";
65 symlink($a, $b) or die "failed to symlink $a => $b: $!";
76 my $fd = $self->{logfd
};
85 my $fh = IO
::File-
>new ("<$filename") || return $res;
88 while (defined (my $line = <$fh>)) {
89 next if $line =~ m/^\#/;
90 next if $line =~ m/^\s*$/;
100 if ($rec =~ s/^Description:\s*([^\n]*)(\n\s+.*)*$//si) {
101 $res->{headline
} = $1;
102 chomp $res->{headline
};
105 $res->{description
} = $long;
106 chomp $res->{description
};
107 } elsif ($rec =~ s/^([^:]+):\s*(.*\S)\s*\n//) {
108 my ($key, $value) = (lc ($1), $2);
109 if ($key eq 'source' || $key eq 'mirror') {
110 push @{$res->{$key}}, $value;
112 die "duplicate key '$key'\n" if defined ($res->{$key});
113 $res->{$key} = $value;
116 die "unable to parse config file: $rec";
120 die "unable to parse config file" if $rec;
122 $res->{architecture
} = 'amd64' if $res->{architecture
} eq 'x86_64';
128 my ($class, $config) = @_;
130 $config = read_config
('aab.conf') if !$config;
131 my $version = $config->{version
};
132 die "no 'version' specified\n" if !$version;
133 die "no 'section' specified\n" if !$config->{section
};
134 die "no 'description' specified\n" if !$config->{headline
};
135 die "no 'maintainer' specified\n" if !$config->{maintainer
};
137 my $name = $config->{name
} || die "no 'name' specified\n";
138 $name =~ m/^[a-z][0-9a-z\-\*\.]+$/ ||
139 die "illegal characters in name '$name'\n";
142 if ($name =~ m/^archlinux/) {
143 $targetname = "${name}_${version}_$config->{architecture}";
145 $targetname = "archlinux-${name}_${version}_$config->{architecture}";
148 my $self = { logfile
=> 'logfile',
150 targetname
=> $targetname,
151 incl
=> [@BASE_PACKAGES],
152 excl
=> [@BASE_EXCLUDES],
155 $self->{logfd
} = IO
::File-
>new($self->{logfile
}, O_WRONLY
| O_APPEND
| O_CREAT
)
156 or die "unable to open log file";
160 $self->__allocate_ve();
165 sub __sample_config
{
168 my $arch = $self->{config
}->{architecture
};
172 lxc.include = /usr/share/lxc/config/archlinux.common.conf
173 lxc.uts.name = localhost
174 lxc.rootfs.path = $self->{rootfs}
175 lxc.mount.entry = $self->{pkgcache} $self->{pkgdir} none bind 0 0
183 if (my $fd = IO
::File-
>new(".veid")) {
190 $self->{working_dir
} = getcwd
;
191 $self->{veconffile
} = "$self->{working_dir}/config";
192 $self->{rootfs
} = "$self->{working_dir}/rootfs";
193 $self->{pkgdir
} = "$self->{working_dir}/rootfs/$PKGDIR";
194 $self->{pkgcache
} = "$self->{working_dir}/pkgcache";
195 $self->{'pacman.conf'} = "$self->{working_dir}/pacman.conf";
198 $self->{veid
} = $cid;
204 UUID
::generate
($uuid);
205 UUID
::unparse
($uuid, $uuid_str);
206 $self->{veid
} = $uuid_str;
208 my $fd = IO
::File-
>new (">.veid") ||
209 die "unable to write '.veid'\n";
210 print $fd "$self->{veid}\n";
212 $self->logmsg("allocated VE $self->{veid}\n");
218 my $config = $self->{config
};
220 $self->{logfd
} = IO
::File-
>new($self->{logfile
}, O_WRONLY
| O_TRUNC
| O_CREAT
)
221 or die "unable to open log file";
223 my $cdata = $self->__sample_config();
225 my $fh = IO
::File-
>new($self->{veconffile
}, O_WRONLY
|O_CREAT
|O_EXCL
) ||
226 die "unable to write lxc config file '$self->{veconffile}' - $!";
230 if (!$config->{source
} && !$config->{mirror
}) {
231 die "no sources/mirrors specified";
234 $config->{source
} //= [];
235 $config->{mirror
} //= [];
237 my $servers = "Server = "
238 . join("\nServer = ", @{$config->{source
}}, @{$config->{mirror
}})
241 $fh = IO
::File-
>new($self->{'pacman.conf'}, O_WRONLY
|O_CREAT
|O_EXCL
) ||
242 die "unable to write pacman config file $self->{'pacman.conf'} - $!";
243 my $arch = $config->{architecture
};
244 $arch = 'x86_64' if $arch eq 'amd64';
247 HoldPkg = pacman glibc
260 if ($config->{architecture
} eq 'x86_64') {
261 print $fh "[multilib]\n$servers\n";
264 $self->logmsg("configured VE $self->{veid}\n");
270 my $veid = $self->{veid
};
272 my $res = { running
=> 0 };
274 $res->{exist
} = 1 if -d
"$self->{rootfs}/usr";
276 my $filename = "/proc/net/unix";
278 # similar test is used by lcxcontainers.c: list_active_containers
279 my $fh = IO
::File-
>new ($filename, "r");
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+)$/) {
285 if ($path =~ m!^@/\S+/$veid/command$!) {
298 my $veid = $self->{veid
}; # fixme
300 my $vestat = $self->ve_status();
301 if ($vestat->{running
}) {
302 $self->stop_container();
305 rmtree
$self->{rootfs
};
306 unlink $self->{veconffile
};
313 my $veid = $self->{veid
};
314 my $conffile = $self->{veconffile
};
316 $self->logmsg ("initialize VE $veid\n");
318 my $vestat = $self->ve_status();
319 if ($vestat->{running
}) {
320 $self->run_command ("lxc-stop -n $veid --rcfile $conffile --kill");
323 rmtree
$self->{rootfs
};
324 mkpath
"$self->{rootfs}/dev";
328 my ($self, $cmd, $input) = @_;
330 my $veid = $self->{veid
};
331 my $conffile = $self->{veconffile
};
333 if (ref ($cmd) eq 'ARRAY') {
334 unshift @$cmd, 'lxc-attach', '-n', $veid, '--rcfile', $conffile,'--clear-env', '--';
335 $self->run_command ($cmd, $input);
337 $self->run_command ("lxc-attach -n $veid --rcfile $conffile --clear-env -- $cmd", $input);
342 my ($self, @cmd) = @_;
344 my $veid = $self->{veid
};
345 my $conffile = $self->{veconffile
};
348 my $pid = open2
($reader, "<&STDIN", 'lxc-attach', '-n', $veid, '--rcfile', $conffile, '--', @cmd)
349 or die "unable to exec command";
351 while (defined (my $line = <$reader>)) {
352 $self->logmsg ($line);
358 die "ve_exec failed - status $rc\n" if $rc != 0;
362 my ($self, $cmd, $input, $getoutput) = @_;
364 my $reader = IO
::File-
>new();
365 my $writer = IO
::File-
>new();
366 my $error = IO
::File-
>new();
370 my $cmdstr = ref ($cmd) eq 'ARRAY' ?
join (' ', @$cmd) : $cmd;
374 if (ref ($cmd) eq 'ARRAY') {
375 $pid = open3
($writer, $reader, $error, @$cmd) || die $!;
377 $pid = open3
($writer, $reader, $error, $cmdstr) || die $!;
384 if ($orig_pid != $$) {
385 $self->logmsg ("ERROR: command '$cmdstr' failed - fork failed\n");
392 print $writer $input if defined $input;
395 my $select = new IO
::Select
;
396 $select->add ($reader);
397 $select->add ($error);
400 my $logfd = $self->{logfd
};
402 while ($select->count) {
403 my @handles = $select->can_read ();
405 foreach my $h (@handles) {
407 my $count = sysread ($h, $buf, 4096);
408 if (!defined ($count)) {
410 die "command '$cmdstr' failed: $!";
412 $select->remove ($h) if !$count;
416 $res .= $buf if $getoutput;
423 die "command '$cmdstr' failed with exit code $ec\n" if $ec;
428 sub start_container
{
430 my $veid = $self->{veid
};
431 $self->run_command(['lxc-start', '-n', $veid, '-f', $self->{veconffile
}, '/usr/bin/aab_fake_init']);
436 my $veid = $self->{veid
};
437 my $conffile = $self->{veconffile
};
438 $self->run_command ("lxc-stop -n $veid --rcfile $conffile --kill");
443 my $root = $self->{rootfs
};
444 return ('/usr/bin/pacman',
446 '--config', $self->{'pacman.conf'},
447 '--cachedir', $self->{pkgcache
},
452 my ($self, $packages) = @_;
453 my $root = $self->{rootfs
};
455 my @pacman = $self->pacman_command();
456 $self->run_command([@pacman, '-Sw', '--', @$packages]);
459 sub mask_systemd_unit
{
460 my ($self, $unit) = @_;
461 my $root = $self->{rootfs
};
462 symln
'/dev/null', "$root/etc/systemd/system/$unit";
466 my ($self, $include, $exclude) = @_;
467 my $root = $self->{rootfs
};
469 my @pacman = $self->pacman_command();
471 print "Fetching package database...\n";
472 mkpath
$self->{pkgcache
};
473 mkpath
$self->{pkgdir
};
474 mkpath
"$root/var/lib/pacman";
475 $self->run_command([@pacman, '-Syy']);
477 print "Figuring out what to install...\n";
478 my $incl = { map { $_ => 1 } @{$self->{incl
}} };
479 my $excl = { map { $_ => 1 } @{$self->{excl
}} };
481 foreach my $addinc (@$include) {
482 $incl->{$addinc} = 1;
483 delete $excl->{$addinc};
485 foreach my $addexc (@$exclude) {
486 $excl->{$addexc} = 1;
487 delete $incl->{$addexc};
492 foreach my $inc (keys %$lst) {
494 eval { $group = $self->run_command([@pacman, '-Sqg', $inc], undef, 1); };
498 $lst->{$_} = 1 foreach split(/\s+/, $group);
506 my $packages = [ grep { !$excl->{$_} } keys %$incl ];
508 print "Setting up basic environment...\n";
510 mkpath
"$root/usr/bin";
512 my $data = "# UNCONFIGURED FSTAB FOR BASE SYSTEM\n";
513 write_file
($data, "$root/etc/fstab", 0644);
515 write_file
("", "$root/etc/resolv.conf", 0644);
516 write_file
("localhost\n", "$root/etc/hostname", 0644);
517 $self->run_command(['install', '-m0755', $fake_init, "$root/usr/bin/aab_fake_init"]);
519 unlink "$root/etc/localtime";
520 symln
'/usr/share/zoneinfo/UTC', "$root/etc/localtime";
522 print "Caching packages...\n";
523 $self->cache_packages($packages);
524 #$self->copy_packages();
526 print "Creating device nodes for package manager...\n";
529 print "Installing package manager and essentials...\n";
530 # inetutils for 'hostname' for our init
531 $self->run_command([@pacman, '-S', 'pacman', 'inetutils', 'archlinux-keyring']);
533 print "Setting up pacman for installation from cache...\n";
534 my $file = "$root/etc/pacman.d/mirrorlist";
535 my $backup = "${file}.aab_orig";
537 rename_file
($file, $backup);
538 write_file
("Server = file://$PKGDIR\n", $file);
541 print "Populating keyring...\n";
542 $self->populate_keyring();
544 print "Removing device nodes...\n";
545 $self->cleanup_dev();
547 print "Starting container...\n";
548 $self->start_container();
550 print "Installing packages...\n";
551 $self->ve_command(['pacman', '-S', '--needed', '--noconfirm', '--', @$packages]);
553 print "Masking problematic systemd units...\n";
554 for my $unit (qw(sys-kernel-config.mount sys-kernel-debug.mount)) {
555 $self->mask_systemd_unit($unit);
559 # devices needed for gnupg to function:
561 '/dev/null' => ['c', '1', '3'],
562 '/dev/random' => ['c', '1', '9'], # fake /dev/random (really urandom)
563 '/dev/urandom' => ['c', '1', '9'],
564 '/dev/tty' => ['c', '5', '0'],
569 my $root = $self->{rootfs
};
571 # remove temporary device files
572 unlink "${root}$_" foreach keys %$devs;
577 my $root = $self->{rootfs
};
579 local $SIG{INT
} = $SIG{TERM
} = sub { $self->cleanup_dev; };
581 # we want to replace /dev/random, so delete devices first
582 $self->cleanup_dev();
584 foreach my $dev (keys %$devs) {
585 my ($type, $major, $minor) = @{$devs->{$dev}};
586 system('mknod', "${root}${dev}", $type, $major, $minor);
590 sub populate_keyring
{
592 my $root = $self->{rootfs
};
594 # generate weak master key and populate the keyring
595 system('unshare', '--fork', '--pid', 'chroot', "$root", 'pacman-key', '--init') == 0
596 or die "failed to initialize keyring: $?";
597 system('unshare', '--fork', '--pid', 'chroot', "$root", 'pacman-key', '--populate') == 0
598 or die "failed to populate keyring: $?";
603 my ($self, $pkglist) = @_;
605 $self->cache_packages($pkglist);
606 $self->ve_command(['pacman', '-S', '--needed', '--noconfirm', '--', @$pkglist]);
610 my ($self, $filename, $size) = @_;
612 my $config = $self->{config
};
616 $data .= "Name: $config->{name}\n";
617 $data .= "Version: $config->{version}\n";
618 $data .= "Type: lxc\n";
619 $data .= "OS: archlinux\n";
620 $data .= "Section: $config->{section}\n";
621 $data .= "Maintainer: $config->{maintainer}\n";
622 $data .= "Architecture: $config->{architecture}\n";
623 $data .= "Infopage: https://www.archlinux.org\n";
624 $data .= "Installed-Size: $size\n";
627 $data .= "Infopage: $config->{infopage}\n" if $config->{infopage
};
628 $data .= "ManageUrl: $config->{manageurl}\n" if $config->{manageurl
};
629 $data .= "Certified: $config->{certified}\n" if $config->{certified
};
632 $data .= "Description: $config->{headline}\n";
633 $data .= "$config->{description}\n" if $config->{description
};
635 write_file
($data, $filename, 0644);
639 my ($self, $compressor) = @_;
642 if (defined($compressor)) {
643 if ($compressor =~ /^\s*--zstd?\s*$/) {
645 } elsif ($compressor =~ /^\s*--(?:gz|gzip)\s*$/) {
646 $use_zstd = 0; # just boolean for now..
648 die "unkown compressor '$compressor'!\n";
652 my $rootdir = $self->{rootfs
};
654 print "Stopping container...\n";
655 $self->stop_container();
657 print "Rolling back mirrorlist changes...\n";
658 my $file = "$rootdir/etc/pacman.d/mirrorlist";
660 rename_file
($file.'.aab_orig', $file);
662 print "Removing weak temporary pacman keyring...\n";
663 rmtree
("$rootdir/etc/pacman.d/gnupg");
665 my $sizestr = $self->run_command("du -sm $rootdir", undef, 1);
667 if ($sizestr =~ m/^(\d+)\s+\Q$rootdir\E$/) {
670 die "unable to detect size\n";
672 $self->logmsg ("uncompressed size: $size MB\n");
674 $self->write_config ("$rootdir/etc/appliance.info", $size);
676 $self->logmsg ("creating final appliance archive\n");
678 my $compressor_ext = $use_zstd ?
'zst' : 'gz';
680 my $target = "$self->{targetname}.tar";
682 unlink "$target.$compressor_ext";
684 $self->run_command ("tar cpf $target --numeric-owner -C '$rootdir' ./etc/appliance.info");
685 $self->run_command ("tar rpf $target --numeric-owner -C '$rootdir' --exclude ./etc/appliance.info .");
687 $self->logmsg ("compressing archive ($compressor_ext)\n");
689 $self->run_command ("zstd -19 --rm $target");
691 $self->run_command ("gzip -9 $target");
694 my $target_size = int(-s
"$target.$compressor_ext") >> 20;
695 $self->logmsg ("created '$target.$compressor_ext' with size: $target_size MB\n");
700 my $veid = $self->{veid
};
701 my $conffile = $self->{veconffile
};
703 my $vestat = $self->ve_status();
704 if (!$vestat->{exist
}) {
705 $self->logmsg ("Please create the appliance first (bootstrap)");
709 if (!$vestat->{running
}) {
710 $self->start_container();
713 system ("lxc-attach -n $veid --rcfile $conffile --clear-env");
717 my ($self, $all) = @_;
719 unlink $self->{logfile
};
720 unlink $self->{'pacman.conf'};
723 unlink $self->{veconffile
};
725 rmtree
$self->{pkgcache
} if $all;