2
0
mirror of https://github.com/xcat2/xcat-core.git synced 2026-09-30 14:55:16 +00:00

Merge remote-tracking branch 'upstream/master' into HEAD

# Conflicts:
#	docs/source/overview/support_matrix.rst
This commit is contained in:
Daniel Hilst
2026-09-11 21:08:30 -03:00
96 changed files with 3999 additions and 825 deletions
+1 -1
View File
@@ -7,7 +7,7 @@ jobs:
steps:
- uses: actions/checkout@v6
- name: Install dependencies
run: sudo env DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends --no-install-suggests build-essential fakeroot reprepro devscripts debhelper libcapture-tiny-perl libfile-slurper-perl libjson-perl libparallel-forkmanager-perl libsoap-lite-perl libdbi-perl libcgi-pm-perl quilt openssh-server dpkg looptools genometools software-properties-common
run: sudo env DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends --no-install-suggests bats build-essential fakeroot reprepro devscripts debhelper libcapture-tiny-perl libfile-slurper-perl libjson-perl libparallel-forkmanager-perl libsoap-lite-perl libdbi-perl libcgi-pm-perl quilt openssh-server dpkg looptools genometools software-properties-common
- name: Run tests
run: perl github_action_xcat_test.pl
+13 -7
View File
@@ -147,16 +147,22 @@ use constant XCAT_PROBE_HELPERS => qw(
ServiceNodeUtils.pm
);
# Packages whose .deb carries a real architecture. Everything else in xcat-core is
# Perl and ships as Architecture: all -- one binary serving every Ubuntu release and
# every arch, which is why this build never needs a per-codename chroot.
my %ARCH_PACKAGES = map { $_ => 1 } qw(xCAT xCATsn xCAT-genesis-scripts);
# Packages whose .deb carries a real architecture, and the architectures each is built
# for. Everything else in xcat-core is Perl and ships as Architecture: all -- one binary
# serving every Ubuntu release and every arch, which is why this build never needs a
# per-codename chroot. xCAT-genesis-scripts has no riscv64 control file: riscv64 Genesis
# ships as an OpenEmbedded package.
my %ARCH_PACKAGES = (
'xCAT' => [qw(amd64 ppc64el riscv64)],
'xCATsn' => [qw(amd64 ppc64el riscv64)],
'xCAT-genesis-scripts' => [qw(amd64 ppc64el)],
);
# Ubuntu releases predating ppc64el. Kept as data rather than an `if` in the caller so
# the repo-assembly and the package-selection paths cannot disagree about it.
my %NO_PPC64EL = map { $_ => 1 } qw(saucy);
my @DEB_ARCHES = qw(amd64 ppc64el);
my @DEB_ARCHES = qw(amd64 ppc64el riscv64);
# The Ubuntu releases the apt repository serves by default. Single source of truth:
# the builder, the repo assembly and the tests all read it here, so they cannot drift.
@@ -323,8 +329,8 @@ sub stage_probe_helpers {
# 'all' is a single arch-independent build; the three arch packages get one per arch.
sub deb_package_arches {
my ($package) = @_;
return @DEB_ARCHES if $ARCH_PACKAGES{$package // ''};
return ('all');
my $arches = $ARCH_PACKAGES{ $package // '' };
return $arches ? @{$arches} : ('all');
}
# dist_arches: the architectures a release's apt repo declares.
+5 -5
View File
@@ -317,11 +317,11 @@ sub write_repo_metadata {
. /etc/lsb-release
cd `dirname $0`
host_arch=`uname -m`
if [ "$host_arch" != "ppc64le" ];then
host_arch="amd64"
else
host_arch="ppc64el"
fi
case "$host_arch" in
ppc64le) host_arch="ppc64el" ;;
riscv64) host_arch="riscv64" ;;
*) host_arch="amd64" ;;
esac
echo deb [arch=$host_arch] file://"`pwd`" $DISTRIB_CODENAME main > /etc/apt/sources.list.d/xcat-core.list
SCRIPT
@@ -9,7 +9,7 @@ In a homogeneous cluster, the management node is the same hardware architecture
The issues arises in a heterogeneous cluster, where the management node is running a different level operating system *or* hardware architecture as the compute nodes in which to deploy the image. The ``genimage`` command that builds stateless images depends on various utilities provided by the base operating system and needs to be run on a node with the same hardware architecture and *major* Operating System release as the nodes that will be booted from the image.
When running xCAT >= 2.17 on EL >= 8 based management node with x86_64 architecture, qemu-user-static can be used to cross-build ppc64*, aarch64 and riscv64 osimages. Therefore, you don't need to build images on systems with the target architecture anymore.
When running xCAT >= 2.17 on EL >= 8 based management node with x86_64 architecture, qemu-user-static can be used to cross-build ppc64*, aarch64 and riscv64 osimages. Therefore, you don't need to build images on systems with the target architecture anymore. The same applies to an Ubuntu management node, where ``qemu-user-static`` and ``binfmt-support`` are packages of the distribution and register the handler on install.
Cross-build ppc64*/aarch64/riscv64 stateless/statelite image on x86_64 management node
--------------------------------------------------------------------------------------
@@ -1,43 +1,43 @@
Support Matrix
==============
+---------+---------+---------+---------+---------+---------+---------+---------+---------+---------+---------+---------+---------+
| | RHEL | SLES | RHEL | SLES | Ubuntu | RHEL | SLES | Ubuntu | RHEL | SLES | Ubuntu | RHEL |
| | ppc64 | ppc64 | x86_64 | x86_64 | x86_64 | ppc64le | ppc64le | ppc64el | aarch64 | aarch64 | aarch64 | riscv64 |
| | CN | CN | CN | CN | CN | CN | CN | CN | CN | CN | CN | CN |
+=========+=========+=========+=========+=========+=========+=========+=========+=========+=========+=========+=========+=========+
| RHEL | | | | | | | | | | | | |
| ppc64 | yes | yes | yes | yes | yes | yes | yes | yes | no | no | no | no |
| MN/SN | | | [1]_ | [1]_ | [1]_ | | | | | | | |
+---------+---------+---------+---------+---------+---------+---------+---------+---------+---------+---------+---------+---------+
| SLES | | | | | | | | | | | | |
| ppc64 | yes | yes | yes | yes | yes | yes | yes | yes | no | no | no | no |
| MN/SN | | | [1]_ | [1]_ | [1]_ | | | | | | | |
+---------+---------+---------+---------+---------+---------+---------+---------+---------+---------+---------+---------+---------+
| RHEL | | | | | | | | | | | | |
| x86_64 | yes | yes | yes | yes | yes | yes | yes | yes | yes | no | no | yes |
| MN/SN | [4]_ | [4]_ | | | | | | | | | | [6]_ |
+---------+---------+---------+---------+---------+---------+---------+---------+---------+---------+---------+---------+---------+
| SLES | | | | | | | | | | | | |
| x86_64 | yes | yes | yes | yes | yes | yes | yes | yes | yes | no | no | no |
| MN/SN | [4]_ | [4]_ | | | | | | | | | | |
+---------+---------+---------+---------+---------+---------+---------+---------+---------+---------+---------+---------+---------+
| Ubuntu | | | | | | | | | | | | |
| x86_64 | yes | yes | yes | yes | yes | yes | yes | yes | yes | no | no | no |
| MN/SN | [5]_ | [5]_ | | | | | | | | | | |
+---------+---------+---------+---------+---------+---------+---------+---------+---------+---------+---------+---------+---------+
| RHEL | | | | | | | | | | | | |
| ppc64le | yes | yes | yes | yes | yes | yes | yes | yes | no | no | no | no |
| MN/SN | [2]_ | [2]_ | | | | | | | | | | |
+---------+---------+---------+---------+---------+---------+---------+---------+---------+---------+---------+---------+---------+
| SLES | | | | | | | | | | | | |
| ppc64le | no | no | yes | yes | yes | yes | yes | yes | no | no | no | no |
| MN/SN | | | | | | | | | | | | |
+---------+---------+---------+---------+---------+---------+---------+---------+---------+---------+---------+---------+---------+
| Ubuntu | | | | | | | | | | | | |
| ppc64el | yes | yes | yes | yes | yes | yes | yes | yes | no | no | no | no |
| MN/SN | [3]_ | [3]_ | | | | | | | | | | |
+---------+---------+---------+---------+---------+---------+---------+---------+---------+---------+---------+---------+---------+
+---------+---------+---------+---------+---------+---------+---------+---------+---------+---------+---------+---------+---------+---------+
| | RHEL | SLES | RHEL | SLES | Ubuntu | RHEL | SLES | Ubuntu | RHEL | SLES | Ubuntu | RHEL | Ubuntu |
| | ppc64 | ppc64 | x86_64 | x86_64 | x86_64 | ppc64le | ppc64le | ppc64el | aarch64 | aarch64 | aarch64 | riscv64 | riscv64 |
| | CN | CN | CN | CN | CN | CN | CN | CN | CN | CN | CN | CN | CN |
+=========+=========+=========+=========+=========+=========+=========+=========+=========+=========+=========+=========+=========+=========+
| RHEL | | | | | | | | | | | | | |
| ppc64 | yes | yes | yes | yes | yes | yes | yes | yes | no | no | no | no | no |
| MN/SN | | | [1]_ | [1]_ | [1]_ | | | | | | | | |
+---------+---------+---------+---------+---------+---------+---------+---------+---------+---------+---------+---------+---------+---------+
| SLES | | | | | | | | | | | | | |
| ppc64 | yes | yes | yes | yes | yes | yes | yes | yes | no | no | no | no | no |
| MN/SN | | | [1]_ | [1]_ | [1]_ | | | | | | | | |
+---------+---------+---------+---------+---------+---------+---------+---------+---------+---------+---------+---------+---------+---------+
| RHEL | | | | | | | | | | | | | |
| x86_64 | yes | yes | yes | yes | yes | yes | yes | yes | yes | no | no | yes | no |
| MN/SN | [4]_ | [4]_ | | | | | | | | | | [6]_ | |
+---------+---------+---------+---------+---------+---------+---------+---------+---------+---------+---------+---------+---------+---------+
| SLES | | | | | | | | | | | | | |
| x86_64 | yes | yes | yes | yes | yes | yes | yes | yes | yes | no | no | no | no |
| MN/SN | [4]_ | [4]_ | | | | | | | | | | | |
+---------+---------+---------+---------+---------+---------+---------+---------+---------+---------+---------+---------+---------+---------+
| Ubuntu | | | | | | | | | | | | | |
| x86_64 | yes | yes | yes | yes | yes | yes | yes | yes | yes | no | no | no | yes |
| MN/SN | [5]_ | [5]_ | | | | | | | | | | | [7]_ |
+---------+---------+---------+---------+---------+---------+---------+---------+---------+---------+---------+---------+---------+---------+
| RHEL | | | | | | | | | | | | | |
| ppc64le | yes | yes | yes | yes | yes | yes | yes | yes | no | no | no | no | no |
| MN/SN | [2]_ | [2]_ | | | | | | | | | | | |
+---------+---------+---------+---------+---------+---------+---------+---------+---------+---------+---------+---------+---------+---------+
| SLES | | | | | | | | | | | | | |
| ppc64le | no | no | yes | yes | yes | yes | yes | yes | no | no | no | no | no |
| MN/SN | | | | | | | | | | | | | |
+---------+---------+---------+---------+---------+---------+---------+---------+---------+---------+---------+---------+---------+---------+
| Ubuntu | | | | | | | | | | | | | |
| ppc64el | yes | yes | yes | yes | yes | yes | yes | yes | no | no | no | no | no |
| MN/SN | [3]_ | [3]_ | | | | | | | | | | | |
+---------+---------+---------+---------+---------+---------+---------+---------+---------+---------+---------+---------+---------+---------+
Notes:
@@ -54,3 +54,4 @@ Notes:
.. [4] If the compute nodes are DFM managed systems, will need the ppc64le DFM and ppc64le hardware server on the management node.
.. [5] Does not support DFM managed compute nodes, hardware control does not work.
.. [6] riscv64 compute nodes boot through UEFI firmware and grub2 only. The management node needs the riscv64 Genesis image (``xCAT-genesis-openembedded-riscv64``) and ``/tftpboot/boot/grub2/grub2.riscv64`` (see :doc:`/guides/install-guides/yum/grub2`). EL10 compute nodes are supported, validated from an EL10 x86_64 management node; a riscv64 management node is documented in :doc:`/guides/admin-guides/manage_clusters/riscv64/index`.
.. [7] Ubuntu 24.04 and 26.04 riscv64 compute nodes, stateful and stateless, validated from an Ubuntu x86_64 management node. The riscv64 packages that carry a binary (``goconserver``, ``ipmitool-xcat``, ``conserver-xcat``) come from a riscv64 xcat-dep apt repository; see :doc:`/guides/admin-guides/manage_clusters/riscv64/index`.
@@ -60,7 +60,10 @@ the repository declares. Only ``xCAT``, ``xCATsn`` and ``xCAT-genesis-scripts``
carry an architecture, and there the difference is packaging metadata rather than
compiled output. That is why this build needs no ``sbuild`` and no per-codename
chroot -- unlike xcat-deps, whose packages are compiled and genuinely differ per
release.
release. ``xCAT`` and ``xCATsn`` are built for riscv64 as well as amd64 and
ppc64el, and every release the repository serves declares the architecture;
``xCAT-genesis-scripts`` keeps the two architectures it has control files for,
because riscv64 Genesis ships as an OpenEmbedded package instead.
Helpers shared by both builders live in ``build-utils/lib/XCAT/BuildUtils.pm``.
@@ -45,7 +45,7 @@ Key Attributes
+--------------------------+----------------------+-----------------------------------+
| aarch64 | >=el8 | grub2 |
+--------------------------+----------------------+-----------------------------------+
| riscv64 | >=el10 | grub2,grub2-http,grub2-tftp |
| riscv64 | >=el10, >=ubuntu24.04| grub2,grub2-http,grub2-tftp |
+--------------------------+----------------------+-----------------------------------+
* postscripts:
@@ -6,6 +6,8 @@ The name of the packages that will be installed on the node are stored in the pa
* The package list file contains the names of the packages that comes from the os distro. They are stored in .pkglist file.
* The other package list file contains the names of the packages that do NOT come from the os distro. They are stored in .otherpkgs.pkglist file.
On Ubuntu releases that install with Subiquity, the packages in the .pkglist file are installed during the autoinstall from the configured apt mirror and the pkgdir mirrors, without recommended packages as ``ospkgs`` installs them, and ``ospkgs`` applies the whole list again after the first boot. A version pin, a target release or an architecture qualifier in the list, a list that carries a ``#ENV:`` setting, and the list of an osimage with ``environvar`` are installed by ``ospkgs`` only. The apt sources the installer uses for the pkgdir mirrors and the otherpkgs repository do not remain on the node: ``ospkgs`` and ``otherpkgs`` write their own after the first boot, as before. ``ospkgs`` writes http mirrors only, so an https mirror or a local directory in pkgdir serves the autoinstall and is not an apt source after the first boot.
The path to the package lists will be read from the osimage definition. Which osimage a node is using is specified by the provmethod attribute. To display this value for a node: ::
lsdef node1 -i provmethod
@@ -1,9 +1,10 @@
RISC-V 64-bit (riscv64)
=======================
xCAT manages RISC-V 64-bit (``riscv64``) compute nodes running EL10. Rocky
Linux 10 is the reference distribution; the RHEL 10 RISC-V developer preview
uses the same media layout. The general cluster management documentation under
xCAT manages RISC-V 64-bit (``riscv64``) compute nodes running EL10 or Ubuntu.
Rocky Linux 10 is the reference EL distribution and the RHEL 10 RISC-V developer
preview uses the same media layout; on the Ubuntu side, 24.04 and 26.04 are
supported from the live-server media. The general cluster management documentation under
:doc:`/guides/admin-guides/manage_clusters/index` applies; this page
only covers what is specific to the architecture.
@@ -21,16 +22,31 @@ What riscv64 nodes need
``grub2-http`` is recommended for installers, whose initrd is large.
* ``nodetype.arch`` and ``osimage.osarch`` are ``riscv64``. No alias is
needed: ``uname -m``, rpm and dpkg all use the same token.
* ``/tftpboot/boot/grub2/grub2.riscv64``: the EL grub2 UEFI image for riscv64
(the ``EFI/BOOT/grubriscv64.efi`` of the EL10 riscv64 BaseOS tree).
``copycds`` publishes it from the installation media when the management node
does not have it yet, the ``grub2-xcat`` package installs the same image, and
:doc:`/guides/install-guides/yum/grub2` describes copying it by hand. An image
that is already there is never replaced.
* ``/tftpboot/boot/grub2/grub2.riscv64``: the grub2 UEFI image the firmware
loads. On EL media it is the ``EFI/BOOT/grubriscv64.efi`` of the riscv64
BaseOS tree, which the ``grub2-xcat`` package also installs and
:doc:`/guides/install-guides/yum/grub2` describes copying by hand; ``copycds``
publishes it when the management node does not have it yet and keeps an image
that is already there.
Ubuntu media are different: the loader they carry boots only from the media,
because it holds a built-in configuration that searches for the live
filesystem and never reads the configuration ``nodeset`` writes. ``copycds``
therefore builds a netboot image from the ``grub-efi-riscv64-bin`` package on
the media, with the network modules and the ``/boot/grub2`` prefix compiled
in, and installs it under that name. An image already there is kept when it
carries that prefix and those modules, which is what the loader this path
builds looks like; anything else is replaced, because the image the media
carry cannot reach the configuration. When the media cannot produce a
replacement, whatever is there is left untouched and ``copycds`` says so:
check that the nodes still boot, since the file was not built for this path.
Building the loader needs ``grub-mkimage``, which ``grub-common`` provides and
``xcat-server`` requires.
* The riscv64 Genesis image (``xCAT-genesis-openembedded-riscv64``) for
discovery, BMC setup and flashing. Its kernel is loaded by grub2 through the
EFI stub. ``go-xcat`` installs the package; on a management node built another
way, install it explicitly (``dnf install xCAT-genesis-openembedded-riscv64``),
way, install it explicitly (``dnf install xCAT-genesis-openembedded-riscv64``,
or ``apt install xcat-genesis-openembedded-riscv64`` on Ubuntu),
the same way the images of other architectures are installed for a mixed
cluster. ``xcatconfig`` runs ``mknb riscv64`` for every installed image.
The management node itself is x86_64 (the validated combination, see
@@ -65,6 +81,9 @@ for the other architectures.
Stateful (diskful) installation
--------------------------------
EL10
~~~~
Import the Rocky Linux 10 riscv64 DVD with ``copycds``; it creates the
``rocky10.x-riscv64-install-compute`` osimage. The installer kernel and initrd
come from ``images/pxeboot`` on the media, like x86_64 and aarch64.
@@ -84,6 +103,28 @@ package lists add ``grub2-efi-riscv64`` and ``efibootmgr``, and the
these files. After ``nodeset <node> boot`` the firmware boots the installed
system because the per-node ``grub2-<node>`` loader link is removed.
Ubuntu
~~~~~~
Import the Ubuntu 24.04 or 26.04 riscv64 live-server ISO with ``copycds``; it
creates the ``ubuntu<version>-riscv64-install-compute`` osimage. The installer
kernel and initrd come from ``casper/vmlinux`` and ``casper/initrd``, where the
riscv64 media keep them.
The installer needs no riscv64 accommodation of the kind EL10 requires: Subiquity
installs ``grub-efi-riscv64`` itself, writes both ``\EFI\ubuntu\grubriscv64.efi``
and the removable-media fallback ``\EFI\BOOT\BOOTRISCV64.EFI``, and registers the
UEFI boot entry. The shared ``compute.subiquity.tmpl`` is used unchanged.
The autoinstall configuration is fetched from the management node over HTTP with
``ds=nocloud-net``. That argument holds a semicolon, which grub2 reads as a
command separator, so the boot loader configuration quotes it; a node whose
kernel command line ends before the seed URL is a sign of an unquoted separator.
Packages the media do not carry are taken from ``ports.ubuntu.com``, which is
where every architecture other than amd64 and i386 is published. Set
``site.ubuntu_apt_mirror`` to point at a local mirror instead.
Crash dumps
~~~~~~~~~~~
@@ -116,6 +157,15 @@ management node needs the riscv64 user-mode emulator registered with
systemd-binfmt, as described in
:doc:`/advanced/mixed_cluster/building_stateless_images`.
On Ubuntu, ``genimage`` builds the image with ``debootstrap`` from the
``compute.ubuntu24.04.riscv64`` and ``compute.ubuntu26.04.riscv64`` package
lists. It bootstraps from ``ports.ubuntu.com``, because ``archive.ubuntu.com``
publishes amd64 and i386 only; ``site.ubuntu_apt_mirror`` overrides that for a
local mirror serving every architecture. A management node of another
architecture needs the same ``qemu-user-static`` binfmt registration as EL, and
a release older than the one being built needs the target's ``debootstrap``
script, which is a symlink to ``gutsy`` for every modern Ubuntu.
Management node on riscv64
--------------------------
@@ -142,6 +192,15 @@ that disables weak dependencies (``install_weak_deps=False``) has to install
``perl-DB_File`` explicitly to keep the Confluent client working.
xCAT does not install anything from CPAN; every dependency is an rpm.
On Ubuntu the ``xcat`` and ``xcatsn`` packages are built for riscv64 and the
apt repository indexes the architecture, so ``apt install xcat`` brings up a
riscv64 management node. Everything else xCAT needs comes from the Ubuntu
riscv64 archive, except the xcat-dep packages that carry a binary:
``goconserver``, ``ipmitool-xcat`` and ``conserver-xcat`` must come from a
riscv64 xcat-dep apt repository. The remaining xcat-dep packages, including
``grub2-xcat`` and the x86-only boot loaders, are ``Architecture: all`` and
install anywhere.
Limitations
-----------
@@ -151,7 +210,9 @@ Limitations
HTTP boot firmware yet, and a node that ``nodeset`` has configured is offered
its per-node boot loader over TFTP, as on the other architectures, so keep PXE
boot enabled in the firmware.
* Ubuntu riscv64 is not supported yet.
* Ubuntu 26.04 riscv64 requires the RVA23 profile. A machine that implements
only the older profile stops with an illegal instruction early in userspace;
24.04 runs on the older profile.
* The serial console defaults to ``ttyS<site.defserialport>``; boards whose
firmware exposes the console on another device need
``linuximage.addkcmdline`` or the serial settings adjusted.
@@ -62,7 +62,7 @@ noderes Attributes:
ppc64le NonVirtualize ALL petitboot
ppc64le PowerKVM Guest ALL grub2,grub2-http,grub2-tftp
aarch64 >=el8 grub2
riscv64 >=el10 grub2,grub2-http,grub2-tftp
riscv64 >=el10, >=ubuntu24.04 grub2,grub2-http,grub2-tftp
@@ -547,7 +547,7 @@ group Attributes:
ppc64le NonVirtualize ALL petitboot
ppc64le PowerKVM Guest ALL grub2,grub2-http,grub2-tftp
aarch64 >=el8 grub2
riscv64 >=el10 grub2,grub2-http,grub2-tftp
riscv64 >=el10, >=ubuntu24.04 grub2,grub2-http,grub2-tftp
@@ -547,7 +547,7 @@ node Attributes:
ppc64le NonVirtualize ALL petitboot
ppc64le PowerKVM Guest ALL grub2,grub2-http,grub2-tftp
aarch64 >=el8 grub2
riscv64 >=el10 grub2,grub2-http,grub2-tftp
riscv64 >=el10, >=ubuntu24.04 grub2,grub2-http,grub2-tftp
+1 -1
View File
@@ -16,7 +16,7 @@ Differentiators
* Support Multiple Hardware
IBM Power, IBM Power LE, x86_64, aarch64 (alpha support), riscv64 (EL10, UEFI + grub2)
IBM Power, IBM Power LE, x86_64, aarch64 (alpha support), riscv64 (EL10 and Ubuntu, UEFI + grub2)
* Support Multiple Virtualization Infrastructures
+5
View File
@@ -24,3 +24,8 @@ Operating System & Hardware Support Matrix
.. note::
EL stands for Enterprise Linux, such as: Red Hat Enterprise Linux (RHEL), Rocky Linux, AlmaLinux, CentOS and Oracle Linux.
riscv64 support covers EL 10 (Rocky Linux 10 and the RHEL 10 RISC-V developer preview), on nodes
that boot through UEFI firmware and grub2. Both stateful and stateless nodes are supported, and
the management node itself can run on riscv64. See
:doc:`/guides/admin-guides/manage_clusters/riscv64/index` for details.
+47 -2
View File
@@ -311,8 +311,11 @@ sub preserve_source_tree{
return 1;
}
@output = runcmd("ls $unitsrc/xCAT-test/unit/*.t | wc -l");
print "[preserve_source_tree] preserved $srcdir in $unitsrc ($output[0] unit tests)\n";
@output = runcmd("find $unitsrc/xCAT-test/unit -name '*.t' | wc -l");
my $perl_count = $output[0];
@output = runcmd("find $unitsrc/xCAT-test/bats -name '*.bats' 2>/dev/null | wc -l");
my $bats_count = $output[0];
print "[preserve_source_tree] preserved $srcdir in $unitsrc ($perl_count Perl unit tests, $bats_count BATS tests)\n";
return 0;
}
@@ -466,6 +469,39 @@ sub run_unit_tests{
return 0;
}
#--------------------------------------------------------
# Fuction name: run_bats_tests
# Description: Run shell-script unit tests under xCAT-test/bats.
# Runs against the pre-build copy of the source tree taken by
# preserve_source_tree(), like the Perl unit tests.
# Attributes:
# Return code: 0 all tests passed, 1 otherwise
#--------------------------------------------------------
sub run_bats_tests{
my $testdir = "$unitsrc/xCAT-test/bats";
my @output = runcmd("find $testdir -name '*.bats' -print -quit 2>/dev/null");
if (!@output) {
print "[run_bats_tests] no BATS tests found under $testdir\n";
return 0;
}
my $cmd = "cd $unitsrc && bats -r xCAT-test/bats";
print "[run_bats_tests] running $cmd\n";
@output = runcmd("$cmd");
print Dumper \@output;
if($::RUNCMD_RC){
print RED "[run_bats_tests] $cmd ....[Failed]\n";
$check_result_str .= "> **BATS TESTS Failed** : Please click ``Details`` label in ``Merge pull request`` box for detailed information\n";
print $check_result_str;
return 1;
}
print "[run_bats_tests] $cmd ....[Pass]\n";
$check_result_str .= "> **BATS TESTS Successful**\n";
print $check_result_str;
return 0;
}
#--------------------------------------------------------
# Fuction name: check_syntax
# Description:
@@ -689,6 +725,15 @@ if($rst){
}
mark_time("run_unit_tests");
#Run shell-script unit tests.
print GREEN "\n------Running xCAT-test BATS tests ------\n";
$rst = run_bats_tests();
if($rst){
print RED "Run of xCAT-test BATS tests failed\n";
exit $rst;
}
mark_time("run_bats_tests");
#Check the syntax of changing code
print GREEN "\n------ Checking the syntax of changed code------\n";
$rst = check_syntax();
+1 -1
View File
@@ -8,7 +8,7 @@ Homepage: https://xcat.org/
Package: perl-xcat
Architecture: all
Depends: ${perl:Depends}, libhtml-form-perl
Depends: ${perl:Depends}, libhtml-form-perl, libxml-simple-perl, libxml-parser-perl, libio-socket-ssl-perl, libdbi-perl, libjson-perl, libwww-perl, libxml-libxml-perl, libexpect-perl, libsnmp-perl, libsocket6-perl, libio-socket-inet6-perl
Description: xCAT perl libraries
Provides perl xCAT libraries for core functionality. Required for all xCAT installations.
Includes xCAT::Table, xCAT::NodeRange, among others.
+1 -1
View File
@@ -676,7 +676,7 @@ passed as argument rather than by table value',
ppc64le NonVirtualize ALL petitboot
ppc64le PowerKVM Guest ALL grub2,grub2-http,grub2-tftp
aarch64 >=el8 grub2
riscv64 >=el10 grub2,grub2-http,grub2-tftp
riscv64 >=el10, >=ubuntu24.04 grub2,grub2-http,grub2-tftp
',
tftpserver => 'The TFTP server for this node (as known by this node). If not set, it defaults to networks.tftpserver.',
+2
View File
@@ -4876,6 +4876,7 @@ sub splitkcmdline {
# without network drivers instead of stopping at debootstrap.
my %DEBIAN_ARCH = (
'x86_64' => 'amd64',
'x86' => 'i386',
);
sub debian_arch {
@@ -4898,6 +4899,7 @@ my @XCAT_ARCH_FROM_DEBIAN = (
[ qr/^ppc64el$/ => 'ppc64el' ],
[ qr/ppc|powerpc/ => 'ppc64' ],
[ qr/^amd64$/ => 'x86_64' ],
[ qr/^riscv64$/ => 'riscv64' ],
);
sub xcat_arch_from_debian {
+2 -2
View File
@@ -7,8 +7,8 @@ Standards-Version: 3.9.4
Package: xcat-client
Architecture: all
Depends: ${perl:Depends}, perl-xcat (>= 2.13-snap000000000000)
Recommends: libsort-versions-perl, nmap
Depends: ${perl:Depends}, perl-xcat (>= 2.13-snap000000000000), libcapture-tiny-perl, nmap
Recommends: libsort-versions-perl
Description: Core executables and data of the xCAT management project
xCAT-client provides the fundamental xCAT commands (chtab, chnode, rpower,
etc) helpful in administrating systems at scale, with particular attention
@@ -9,14 +9,15 @@ depends() {
}
installkernel() {
local modules_dep modfile modname
local modules_dep modules_root modfile modname
if [[ -n "${kernel:-}" && -r "/lib/modules/$kernel/modules.dep" ]]; then
modules_dep="/lib/modules/$kernel/modules.dep"
elif [[ -n "${KERNELVERSION:-}" && -r "/lib/modules/$KERNELVERSION/modules.dep" ]]; then
modules_dep="/lib/modules/$KERNELVERSION/modules.dep"
modules_root="${DRACUT_MODULES_ROOT:-/lib/modules}"
if [[ -n "${kernel:-}" && -r "$modules_root/$kernel/modules.dep" ]]; then
modules_dep="$modules_root/$kernel/modules.dep"
elif [[ -n "${KERNELVERSION:-}" && -r "$modules_root/$KERNELVERSION/modules.dep" ]]; then
modules_dep="$modules_root/$KERNELVERSION/modules.dep"
else
modules_dep=$(ls -1 /lib/modules/*/modules.dep 2>/dev/null | head -n 1)
modules_dep=$(ls -1 "$modules_root"/*/modules.dep 2>/dev/null | head -n 1)
fi
[[ -r "$modules_dep" ]] || return 0
@@ -9,14 +9,15 @@ depends() {
}
installkernel() {
local modules_dep modfile modname
local modules_dep modules_root modfile modname
if [[ -n "${kernel:-}" && -r "/lib/modules/$kernel/modules.dep" ]]; then
modules_dep="/lib/modules/$kernel/modules.dep"
elif [[ -n "${KERNELVERSION:-}" && -r "/lib/modules/$KERNELVERSION/modules.dep" ]]; then
modules_dep="/lib/modules/$KERNELVERSION/modules.dep"
modules_root="${DRACUT_MODULES_ROOT:-/lib/modules}"
if [[ -n "${kernel:-}" && -r "$modules_root/$kernel/modules.dep" ]]; then
modules_dep="$modules_root/$kernel/modules.dep"
elif [[ -n "${KERNELVERSION:-}" && -r "$modules_root/$KERNELVERSION/modules.dep" ]]; then
modules_dep="$modules_root/$KERNELVERSION/modules.dep"
else
modules_dep=$(ls -1 /lib/modules/*/modules.dep 2>/dev/null | head -n 1)
modules_dep=$(ls -1 "$modules_root"/*/modules.dep 2>/dev/null | head -n 1)
fi
[[ -r "$modules_dep" ]] || return 0
+61 -20
View File
@@ -7,6 +7,8 @@ use lib "$::XCATROOT/probe/lib/perl";
use probe_utils;
use xCAT::CommandUtils;
use File::Basename;
use File::Temp qw(tempfile);
use POSIX qw(_exit sigprocmask WNOHANG SIG_BLOCK SIG_SETMASK SIGINT SIGTERM);
use IO::Socket::INET;
use Time::HiRes qw(gettimeofday sleep);
use Getopt::Long;
@@ -17,7 +19,6 @@ my $program_name = basename("$0");
my $output = "stdout";
my $duration = 10;
my $test = 0;
my $dumpfile = "/tmp/dhcpdumpfile.log";
my $nic;
$::USAGE = "Usage:
@@ -153,44 +154,57 @@ my $package = packdhcppkg($MAC);
probe_utils->send_msg("$output", "i", "Start to detect DHCP, please wait $duration seconds");
$msg = "fork a process to capture the packet by tcpdump";
my ($dumpfh, $dumpfile) = tempfile("detect_dhcpd.XXXXXX", TMPDIR => 1, UNLINK => 1);
close($dumpfh);
# INT and TERM stay blocked from the fork until the handler that stops the capture is in place.
my $stop_signals = POSIX::SigSet->new(SIGINT, SIGTERM);
my $signal_mask = POSIX::SigSet->new();
sigprocmask(SIG_BLOCK, $stop_signals, $signal_mask);
my $pid = fork;
if (!defined $pid) {
sigprocmask(SIG_SETMASK, $signal_mask);
probe_utils->send_msg("$output", "f", $msg);
exit 1;
} elsif ($pid == 0) {
# Child process
my $cmd = "$tcpdump -i $nic port 68 -n -vvvvvv > $dumpfile 2>/dev/null";
`$cmd`;
exit 0;
# Child process: tcpdump itself, so the parent owns exactly this pid.
sigprocmask(SIG_SETMASK, $signal_mask);
open(STDOUT, '>', $dumpfile) or _exit(1);
open(STDERR, '>', '/dev/null');
exec($tcpdump, '-i', $nic, 'port', '68', '-n', '-vvvvvv');
_exit(1);
}
$SIG{INT} = $SIG{TERM} = sub { kill_child(); exit 1; };
sigprocmask(SIG_SETMASK, $signal_mask);
probe_utils->send_msg("$output", "d", "The id of process which is used to capture the packet by tcpdump is $pid") if ($::VERBOSE);
my $start = Time::HiRes::gettimeofday();
$start =~ s/(\d.*)\.(\d.*)/$1/;
my $end = $start;
while ($end - $start <= $duration) {
$sock->send($package);
probe_utils->send_msg("$output", "d", "Send DHCP rquest result: $@") if ($::VERBOSE && $@);
unless ($sock->send($package)) {
probe_utils->send_msg("$output", "d", "Send DHCP discover error: $!") if ($::VERBOSE);
probe_utils->send_msg("$output", "f", "Send out DHCP discover");
kill_child();
exit 1;
}
sleep 2;
$end = Time::HiRes::gettimeofday();
$end =~ s/(\d.*)\.(\d.*)/$1/;
}
$msg = "Kill the process which is used to capture the packet by tcpdump";
kill_child();
waitpid($pid, 0);
sleep 1;
`ps aux|grep -v grep |grep $pid > /dev/null 2>&1`;
if (!$?) {
$msg = "Capture the packets by tcpdump";
my $capture_problem = kill_child();
if ($capture_problem) {
probe_utils->send_msg("$output", "d", "tcpdump $capture_problem") if ($::VERBOSE);
probe_utils->send_msg("$output", "f", $msg);
exit 1;
}
$msg = "Dump test result";
unless (open(FILE, "<$dumpfile")) {
probe_utils->send_msg("$output", "d", "Open dump file $dumpfile failed") if ($::VERBOSE);
probe_utils->send_msg("$output", "f", $msg);
`rm -f $dumpfile` if (-e "$dumpfile");
exit 1;
}
my %output;
@@ -273,7 +287,6 @@ if (scalar(@server)) {
}
}
`rm -f $dumpfile` if (-e "$dumpfile");
exit 0;
@@ -379,11 +392,39 @@ sub packdhcppkg {
return $package;
}
# Stop the capture and reap it, once: INT and TERM are blocked while the pid is taken and until
# tcpdump is reaped, so an interrupt in that window cannot orphan it or reach a recycled pid. The
# handlers stay installed, so a later signal still leaves through exit and the END cleanup.
# Returns '' when tcpdump ran until this TERM and stopped cleanly, otherwise what went wrong: it
# ended on its own, ignored TERM and was killed, or left on another signal or with a non-zero
# status.
sub kill_child {
kill 15, $pid;
my @pidoftcpdump = `ps -ef | grep -E "[0-9]+:[0-9]+:[0-9]+ $tcpdump -i $nic" | awk -F' ' '{print \$2}'`;
foreach my $cpid (@pidoftcpdump) {
kill 15, $cpid;
sigprocmask(SIG_BLOCK, $stop_signals);
my $child = $pid;
$pid = undef;
unless ($child) {
sigprocmask(SIG_SETMASK, $signal_mask);
return '';
}
probe_utils->send_msg("$output", "d", "Kill process $pid used to capture the packet by 'tcpdump'") if ($::VERBOSE);
my $reaped = waitpid($child, WNOHANG);
my $early = ($reaped == $child) ? 1 : 0;
if ($reaped == 0) {
kill 'TERM', $child;
foreach (1 .. 50) {
last if ($reaped = waitpid($child, WNOHANG)) != 0;
select(undef, undef, undef, 0.1);
}
if ($reaped == 0) {
kill 'KILL', $child;
$reaped = waitpid($child, 0);
}
}
sigprocmask(SIG_SETMASK, $signal_mask);
return "could not be reaped" if $reaped != $child;
my ($signal, $status) = ($? & 127, $? >> 8);
probe_utils->send_msg("$output", "d", "Kill process $child used to capture the packet by 'tcpdump'") if ($::VERBOSE);
my $how = $signal ? "on signal $signal" : "with status $status";
return "ended before the capture window did, $how" if $early;
return '' if $signal == 15 || (!$signal && !$status);
return "left the capture $how";
}
+1 -1
View File
@@ -8,7 +8,7 @@ Homepage: https://xcat.org/
Package: xcat-server
Architecture: all
Depends: ${perl:Depends}, grub2-xcat (>= 2.02-0.76.el7.1.snap201905160255), perl-xcat (>= 2.13-snap000000000000), xcat-client (>= 2.13-snap000000000000), libsys-syslog-perl, libio-socket-ssl-perl, libxml-simple-perl, make, ucf, libdbd-sqlite3-perl, libexpect-perl, libnet-dns-perl, libsoap-lite-perl, libxml-libxml-perl, libsnmp-perl, debootstrap, libdigest-sha-perl,libcrypt-rijndael-perl,libcrypt-cbc-perl,libjson-perl, libnet-https-nb-perl, libhttp-async-perl
Depends: ${perl:Depends}, grub-common, libcgi-pm-perl, grub2-xcat (>= 2.02-0.76.el7.1.snap201905160255), perl-xcat (>= 2.13-snap000000000000), xcat-client (>= 2.13-snap000000000000), libsys-syslog-perl, libio-socket-ssl-perl, libxml-simple-perl, make, ucf, libdbd-sqlite3-perl, libexpect-perl, libnet-dns-perl, libsoap-lite-perl, libxml-libxml-perl, libsnmp-perl, debootstrap, libdigest-sha-perl,libcrypt-rijndael-perl,libcrypt-cbc-perl,libjson-perl, libnet-https-nb-perl, libhttp-async-perl
Description: Server and configuration utilities of xCAT
xCAT-server provides the core server and configuration management components
of xCAT.
+62
View File
@@ -1889,6 +1889,68 @@ sub get_pkglist_tex
#----------------------------------------------------------------------------
=head3 get_pkglist_records
The records of one or more pkglist files, one per line kept whole and
includes followed. get_pkglist_tex joins records with commas, so it
cannot separate a record that itself contains a comma.
Arguments: comma-separated pkglist file names
Returns: list of records
=cut
#-----------------------------------------------------------------------------
sub get_pkglist_records
{
my $allfiles_pkglist = shift;
if ($allfiles_pkglist =~ "xCAT::") {
$allfiles_pkglist = shift;
}
my @records;
foreach my $pkglist (split(/,/, $allfiles_pkglist // ''))
{
next if $pkglist eq '';
push(@records, pkglist_file_records($pkglist, dirname($pkglist), 0));
}
return @records;
}
# pkglist_file_records: the records of one pkglist file, read as get_pkglist_tex reads them, with an
# #INCLUDE: record replaced by the records of the named file.
# A nested include resolves against the directory of the listed pkglist, as get_pkglist_tex resolves it.
sub pkglist_file_records
{
my ($file, $idir, $depth) = @_;
my @records;
open(my $fh, '<', $file) or return ("#INCLUDEBAD:cannot open pkglist file $file#");
while (my $line = <$fh>)
{
chomp($line);
$line =~ s/\s+$//;
$line =~ s/^\s*//;
next if $line eq '';
next
if ($line =~ /^#/
&& $line !~ /^#INCLUDE:[^#^\n]+#/
&& $line !~ /^#NEW_INSTALL_LIST#/
&& $line !~ /^#ENV:[^#^\n]+#/);
if ($line =~ /^#INCLUDE:([^#^\n]+)#(.*)$/ && $depth < 20)
{
my ($name, $note) = ($1, $2);
my $include = xCAT::Utils->varsubinline($name, \%ENV);
$include = "$idir/$include" unless $include =~ m{^/};
my @included = pkglist_file_records($include, $idir, $depth + 1);
$included[-1] .= $note if @included && $note ne '';
push(@records, @included);
next;
}
push(@records, $line);
}
close($fh);
return @records;
}
#----------------------------------------------------------------------------
=head3 includefile
handles #INCLUDE# in otherpkg.pkglist file
+236 -14
View File
@@ -173,6 +173,11 @@ sub subvars {
$inc =~ s/#INCLUDE_DEFAULT_RMPKGLIST_S#/#INCLUDE_RMPKGLIST:$pkglistfile#/g;
}
# osimage environvar reaches apt-get through ospkgs alone, so such an image installs its list there,
# and its pkgdir mirrors, which may need those variables too, stay out of the installer's sources
my $environvar_set = ( $namedargs{environvar} // '' ) =~ /\S/;
my $installer_pkgdirs = $environvar_set ? undef : $namedargs{pkgdirs};
my @autoinstall;
if (("ubuntu" eq $platform) || ("debian" eq $platform)) {
# since debian/ubuntu uses a preseed file instead of a kickstart file, pkglist
@@ -186,6 +191,7 @@ sub subvars {
if ($allpkglist =~ /#INCLUDEBAD:(.*)#/) {
return "$1";
}
@autoinstall = ubuntu_autoinstall_packages( xCAT::Postage->get_pkglist_records($pkglistfile) ) unless $environvar_set;
$allpkglist =~ s/,/ /g;
$inc =~ s/#INCLUDE_DEFAULT_PKGLIST_PRESEED#/$allpkglist/g;
@@ -364,7 +370,7 @@ sub subvars {
$inc =~ s/#INSTALL_SOURCES_IN_PRE#/$source_in_pre/g;
if (("ubuntu" eq $platform) || ("debian" eq $platform)) {
$inc =~ s/#INCLUDE_OSIMAGE_PKGDIR#/$pkgdirs[-1]/;
$inc =~ s/#UBUNTU_SUBIQUITY_APT_CONFIG#/ubuntu_subiquity_apt_config($media_dir)/eg;
$inc =~ s/#UBUNTU_SUBIQUITY_APT_CONFIG#/ubuntu_subiquity_apt_config($media_dir, $namedargs{osarch}, $installer_pkgdirs)/eg;
}
$inc =~ s/#WRITEREPO#/$writerepo/g;
}
@@ -377,7 +383,9 @@ sub subvars {
$inc =~ s/#INCLUDE_NOP:([^#^\n]+)#/includefile($1,1,0)/eg;
$inc =~ s/#XCATVAR:([^#]+)#/envvar($1)/eg;
$inc =~ s/#ENV:([^#]+)#/envvar($1)/eg;
$inc =~ s/#UBUNTU_SUBIQUITY_APT_CONFIG#/ubuntu_subiquity_apt_config($media_dir)/eg;
$inc =~ s/#UBUNTU_SUBIQUITY_APT_CONFIG#/ubuntu_subiquity_apt_config($media_dir, $namedargs{osarch}, $installer_pkgdirs)/eg;
# in the include pass, so a template that includes the stock Subiquity one gets its list as well
$inc =~ s/^((?:[ \t]*- [^\n]*\n)*)([ \t]*)- #INCLUDE_DEFAULT_PKGLIST_AUTOINSTALL#[ \t]*\n/$1 . ubuntu_autoinstall_items($2, $1, \@autoinstall)/meg;
$inc =~ s/#SUBIQUITYINSTALLNIC#/subiquity_install_nic()/eg;
$inc =~ s/#SUBIQUITYINSTALLMAC#/subiquity_install_mac()/eg;
$inc =~ s/#MACHINEPASSWORD#/machinepassword()/eg;
@@ -1764,26 +1772,103 @@ sub subiquity_install_mac {
return $macaddress;
}
# ubuntu_autoinstall_packages: the packages of the pkglist records (whole lines, as
# get_pkglist_records returns them) that a Subiquity autoinstall can install through its packages
# list. A record holds one or more space-separated packages, as the preseed path reads it, each a
# plain name or a task. A version pin or a target release stays with ospkgs, because the installer
# runs apt-get without --allow-downgrades and a pin can require one, and so does a name with an
# architecture qualifier, because a foreign architecture is enabled by a postscript that runs later. A preseed directive, told by its question type, a record that begins with a removal
# or a group, which ospkgs removes or installs whole, a removal written with a trailing hyphen as
# apt-get reads it, or a marker has
# no autoinstall form, a comment ends the packages of a record, and a record with a token that is
# none of these is left out whole. A list
# that carries a #ENV: setting, which only ospkgs can pass to apt-get, or an unreadable include is
# left to ospkgs whole; ospkgs still applies the whole list after the install.
my %PRESEED_TYPE = map { $_ => 1 } qw(string boolean select multiselect note password text seen title error);
sub ubuntu_autoinstall_packages
{
my @records = grep { defined } @_;
return () if grep { /#(?:ENV:|INCLUDEBAD:)/ } @records;
my (@packages, %seen);
RECORD: foreach my $record (@records) {
my @tokens = grep { length } split( /\s+/, $record );
next unless @tokens;
next if @tokens >= 3 && $PRESEED_TYPE{ $tokens[2] };
next if $tokens[0] =~ /^[-@]/; # ospkgs removes or installs the whole record
my @found;
foreach my $token (@tokens) {
last if $token =~ /^#/;
next if $token =~ /^[-@]/ || $token =~ /-$/;
next RECORD unless $token =~ m{^[a-z0-9][a-z0-9+.-]*(?::[a-z0-9-]+)?(?:[=/][^\s/=]+|\^)?$};
next if $token =~ m{[:=/]};
push @found, $token;
}
push @packages, grep { !$seen{$_}++ } @found;
}
return @packages;
}
# ubuntu_autoinstall_items: the list items for the pkglist packages at the token's indentation,
# leaving out packages the items above the token already name, each quoted so a name such as null
# or true stays a string. The time daemons exclude each
# other, so when the template names one, the pkglist's stay with ospkgs, as they did before.
my %UBUNTU_TIME_DAEMON = map { $_ => 1 } qw(chrony ntp ntpsec ntpdate ntpsec-ntpdate openntpd systemd-timesyncd);
sub ubuntu_autoinstall_items
{
my ($indent, $listed, $packages) = @_;
my %named = map { $_ => 1 } ( $listed =~ /^[ \t]*- (\S+)[ \t]*$/mg );
my $fixed_time_daemon = grep { $UBUNTU_TIME_DAEMON{$_} } keys %named;
my @items = grep { !$named{$_} } @$packages;
@items = grep { !$UBUNTU_TIME_DAEMON{ (split /[:=\/^]/, $_)[0] } } @items if $fixed_time_daemon;
return join( '', map { "$indent- \"$_\"\n" } @items );
}
sub ubuntu_subiquity_apt_mirror
{
my ($osarch) = @_;
# Apt mirror for Subiquity installs. site.ubuntu_apt_mirror overrides; otherwise default to the
# public archive. The minimal live-server install media is not a complete package source, so a
# real mirror is always required -- set site.ubuntu_apt_mirror to a local full mirror for
# airgapped clusters (or to a geo/ports mirror as needed).
my $default = 'http://archive.ubuntu.com/ubuntu';
#
# archive.ubuntu.com publishes amd64 and i386 only. Every other architecture, ppc64el and
# riscv64 included, is on the ports archive. The osimage's architecture decides it, because
# pkgdir is whatever path the administrator configured.
my $default = (!$osarch || $osarch =~ /^(?:amd64|i386|x86|x86_64)$/)
? 'http://archive.ubuntu.com/ubuntu'
: 'http://ports.ubuntu.com/ubuntu-ports';
my $site_tab = xCAT::Table->new('site');
return $default unless $site_tab;
my $ent = $site_tab->getAttribs({ key => 'ubuntu_apt_mirror' }, 'value');
return ($ent && defined($ent->{value}) && length($ent->{value})) ? $ent->{value} : $default;
}
# The key curtin names in the Deb822 source it writes for the primary apt mirror on 24.04 and later.
my $UBUNTU_ARCHIVE_KEYRING = '/usr/share/keyrings/ubuntu-archive-keyring.gpg';
sub ubuntu_subiquity_apt_config
{
my ($media_dir) = @_;
my ($media_dir, $osarch, $pkgdirs) = @_;
my $use_deb822 = ubuntu_subiquity_uses_deb822_sources($media_dir);
my @otherpkg_sources = ubuntu_subiquity_otherpkg_sources();
my $online_mirror = ubuntu_subiquity_apt_mirror($osarch);
my $mirror_key = $use_deb822 ? $UBUNTU_ARCHIVE_KEYRING : '';
my @otherpkg_sources = map { ubuntu_subiquity_otherpkg_source_spec( $_, $mirror_key, $online_mirror ) } ubuntu_subiquity_otherpkg_sources();
my @pkgdir_sources = ubuntu_subiquity_pkgdir_source_specs( $pkgdirs, $mirror_key, $online_mirror );
# apt rejects two sources for one repository whose options differ, so a pkgdir entry that repeats an
# otherpkgs repository adds its components to that source instead
my %otherpkg_by_key = map { ( my $uri = $_->{uri} ) =~ s{/+$}{}; ( "$uri $_->{suites}" => $_ ) } @otherpkg_sources;
@pkgdir_sources = grep {
( my $uri = $_->{uri} ) =~ s{/+$}{};
my $other = $otherpkg_by_key{"$uri $_->{suites}"};
ubuntu_subiquity_add_components( $other, $_->{components} ) if $other;
!$other;
} @pkgdir_sources;
my $online_mirror = ubuntu_subiquity_apt_mirror();
if ($online_mirror) {
# Online install: use the configured archive as the primary apt mirror so
# Subiquity/curtin can fetch whatever the minimal media lacks. No
@@ -1792,6 +1877,7 @@ sub ubuntu_subiquity_apt_config
' apt:',
' preserve_sources_list: false',
' geoip: false',
q( conf: 'APT::Install-Recommends "false";'),
' mirror-selection:',
' primary:',
" - uri: $online_mirror",
@@ -1809,12 +1895,16 @@ sub ubuntu_subiquity_apt_config
push @lines, ' xcat-ubuntu-updates.list:';
push @lines, qq( source: "deb $online_mirror \$RELEASE-updates main restricted universe multiverse");
}
if (@otherpkg_sources) {
if (@otherpkg_sources || @pkgdir_sources) {
push @lines, ' sources:' unless $need_sources_block;
my $index = 0;
foreach my $source (@otherpkg_sources) {
push @lines, " xcat-otherpkgs-$index.list:";
push @lines, qq( source: "deb [trusted=yes] $source ./");
push @lines, ubuntu_subiquity_source_lines( "xcat-otherpkgs-$index", $source, $use_deb822 );
$index++;
}
$index = 0;
foreach my $source (@pkgdir_sources) {
push @lines, ubuntu_subiquity_source_lines( "xcat-pkgdir-$index", $source, $use_deb822 );
$index++;
}
}
@@ -1826,6 +1916,7 @@ sub ubuntu_subiquity_apt_config
' preserve_sources_list: false',
' fallback: offline-install',
' geoip: false',
q( conf: 'APT::Install-Recommends "false";'),
' disable_suites:',
' - updates',
' - backports',
@@ -1855,22 +1946,36 @@ sub ubuntu_subiquity_apt_config
foreach my $source (@otherpkg_sources) {
push @lines, '';
push @lines, ' Types: deb';
push @lines, " URIs: $source";
push @lines, ' Suites: ./';
push @lines, ' Components:';
push @lines, " URIs: $source->{uri}";
push @lines, " Suites: $source->{suites}";
push @lines, ' Components:' . ( length $source->{components} ? " $source->{components}" : '' );
push @lines, ' Trusted: yes';
}
foreach my $source (@pkgdir_sources) {
push @lines, '';
push @lines, ' Types: deb';
push @lines, " URIs: $source->{uri}";
push @lines, " Suites: $source->{suites}";
push @lines, ' Components:' . ( length $source->{components} ? " $source->{components}" : '' );
push @lines, ' Trusted: yes' if $source->{trusted};
}
} else {
push @lines, ' mirror-selection:';
push @lines, ' primary:';
push @lines, ' - uri: file:/cdrom';
if (@otherpkg_sources) {
if (@otherpkg_sources || @pkgdir_sources) {
push @lines, ' sources:';
my $index = 0;
foreach my $source (@otherpkg_sources) {
push @lines, " xcat-otherpkgs-$index.list:";
push @lines, qq( source: "deb [trusted=yes] $source ./");
push @lines, qq( source: "$source->{line}");
$index++;
}
$index = 0;
foreach my $source (@pkgdir_sources) {
push @lines, " xcat-pkgdir-$index.list:";
push @lines, qq( source: "$source->{line}");
$index++;
}
}
@@ -1879,6 +1984,56 @@ sub ubuntu_subiquity_apt_config
return join( "\n", @lines );
}
# ubuntu_subiquity_pkgdir_source_specs: the apt sources of the entries after the install media in
# an osimage pkgdir value, which mkinstall hands over as pkgdirs and ospkgs receives as OSPKGDIR.
# An entry written as "URL suite components" is an apt source line, as ospkgs writes it, and a
# suite that is an exact path needs no component. A local directory that is a flat repository is
# served by the management node and trusted, as an otherpkgdir is. Anything else is no apt source
# for ospkgs either and is left out. An entry that names an Ubuntu archive mirror the installer
# already has a source for, the configured one or a default one, carries that source's signing
# key, the archive keyring on the Deb822 releases and none before them: apt rejects a second source
# for the same suite whose signing key differs.
sub ubuntu_subiquity_pkgdir_source_specs
{
my ( $pkgdirval, $mirror_key, @mirrors ) = @_;
$mirror_key //= '';
my %signed_uri = ubuntu_subiquity_signed_mirror_uris(@mirrors);
my @specs;
foreach my $entry ( split( /,/, $pkgdirval // '' ) ) {
$entry =~ s/^\s+|\s+$//g;
next if $entry eq '';
if ( $entry =~ m{^https?://} ) {
my ( $uri, $suite, @components ) = split( /\s+/, $entry );
next unless defined $suite && ( @components || $suite =~ m{/$} );
( my $bare = $uri ) =~ s{/+$}{};
my %spec = ( uri => $uri, suites => $suite, components => join( ' ', @components ), trusted => 0, signed_by => $signed_uri{$bare} ? $mirror_key : '' );
$spec{line} = ubuntu_subiquity_source_line( \%spec );
push @specs, \%spec;
}
elsif ( $entry !~ m{^[a-z]+://} && ubuntu_subiquity_local_apt_repo($entry) ) {
my $uri = ubuntu_subiquity_pkgdir_uri($entry);
my %spec = ( uri => $uri, suites => './', components => '', trusted => 1, signed_by => '' );
$spec{line} = ubuntu_subiquity_source_line( \%spec );
push @specs, \%spec;
}
}
# a directory and its own URL are one repository: one source, with the trust and components of both
my ( %kept, @unique );
foreach my $spec (@specs) {
( my $uri = $spec->{uri} ) =~ s{/+$}{};
if ( my $first = $kept{"$uri $spec->{suites}"} ) {
$first->{trusted} ||= $spec->{trusted};
$first->{signed_by} ||= $spec->{signed_by};
ubuntu_subiquity_add_components( $first, $spec->{components} );
next;
}
push @unique, $kept{"$uri $spec->{suites}"} = $spec;
}
return @unique;
}
sub ubuntu_subiquity_otherpkg_sources
{
my $nodetype_tab = xCAT::Table->new('nodetype');
@@ -1906,6 +2061,73 @@ sub ubuntu_subiquity_otherpkg_sources
return @sources;
}
# ubuntu_subiquity_add_components: the components of a repeated repository join the source kept for it.
sub ubuntu_subiquity_add_components
{
my ( $spec, $components ) = @_;
my %have = map { $_ => 1 } split( ' ', $spec->{components} );
$spec->{components} = join( ' ', split( ' ', $spec->{components} ), grep { !$have{$_}++ } split( ' ', $components // '' ) );
$spec->{line} = ubuntu_subiquity_source_line($spec);
return;
}
# ubuntu_subiquity_signed_mirror_uris: the apt mirror the installer already has a source for, the
# configured one or the architecture default, without a trailing slash.
sub ubuntu_subiquity_signed_mirror_uris
{
my (@mirrors) = @_;
return map { ( my $uri = $_ ) =~ s{/+$}{}; ( $uri => 1 ) } grep { defined && length } @mirrors;
}
# ubuntu_subiquity_source_line: the one-line form of a source, with the option its Deb822 form carries.
sub ubuntu_subiquity_source_line
{
my ($spec) = @_;
my @option = $spec->{trusted} ? ('[trusted=yes]') : $spec->{signed_by} ? ("[signed-by=$spec->{signed_by}]") : ();
return join( ' ', 'deb', @option, $spec->{uri}, $spec->{suites}, grep { length } $spec->{components} );
}
# ubuntu_subiquity_otherpkg_source_spec: the apt source of one otherpkgdir entry the installer gets.
# A bare URL or a local repository is a flat trusted repository, as otherpkgs treats it. An entry
# written as URL, suite and components is that source, trusted as well, unless the URL is an Ubuntu
# archive mirror the installer already has a source for: that one gets the same signing key and no
# trust, since apt rejects a second source for one suite whose options differ.
sub ubuntu_subiquity_otherpkg_source_spec
{
my ( $entry, $mirror_key, @mirrors ) = @_;
my ( $uri, $suite, @components ) = split( /\s+/, $entry );
my %spec = ( uri => $uri, suites => './', components => '', trusted => 1, signed_by => '' );
if ( defined $suite && length $suite ) {
my %signed = ubuntu_subiquity_signed_mirror_uris(@mirrors);
( my $bare = $uri ) =~ s{/+$}{};
@spec{qw(suites components)} = ( $suite, join( ' ', @components ) );
@spec{qw(trusted signed_by)} = ( 0, $mirror_key // '' ) if $signed{$bare};
}
$spec{line} = ubuntu_subiquity_source_line( \%spec );
return \%spec;
}
# ubuntu_subiquity_source_lines: one entry of the autoinstall sources mapping, a one-line source
# before Deb822 and a Deb822 stanza from 24.04 on: curtin converts a one-line source to Deb822
# there and keeps only its type, URI, suite and components, so a trusted repository would come out
# unsigned and be rejected.
sub ubuntu_subiquity_source_lines
{
my ( $name, $source, $use_deb822 ) = @_;
return ( " $name.list:", qq( source: "$source->{line}") ) unless $use_deb822;
my @lines = (
" $name.sources:",
' source: |',
' Types: deb',
" URIs: $source->{uri}",
" Suites: $source->{suites}",
' Components:' . ( length $source->{components} ? " $source->{components}" : '' ),
);
push @lines, " Signed-By: $source->{signed_by}" if $source->{signed_by};
push @lines, ' Trusted: yes' if $source->{trusted};
return @lines;
}
sub ubuntu_subiquity_uses_deb822_sources
{
my ($media_dir) = @_;
+188 -17
View File
@@ -192,6 +192,9 @@ my %INSTALL_BOOT_FILES = (
[ 'install/netboot/ubuntu-installer/{darch}/vmlinux', 'install/netboot/ubuntu-installer/{darch}/initrd.gz' ],
[ 'install/vmlinux', 'install/netboot/initrd.gz' ],
],
'riscv64' => [
[ 'casper/vmlinux', 'casper/initrd' ],
],
);
sub install_boot_files
@@ -201,9 +204,10 @@ sub install_boot_files
$darch = '' unless defined $darch;
my $family =
$arch =~ /x86/i ? 'x86'
: $arch =~ /ppc64/i ? 'ppc64'
: undef;
$arch =~ /x86/i ? 'x86'
: $arch =~ /ppc64/i ? 'ppc64'
: $arch =~ /riscv64/i ? 'riscv64'
: undef;
return unless $family;
foreach my $candidate (@{ $INSTALL_BOOT_FILES{$family} }) {
@@ -215,6 +219,166 @@ sub install_boot_files
return;
}
# The grub2 image on the media boots only from the media: it carries a built-in
# configuration that looks for the live filesystem and never reads the network
# configuration nodeset writes. A netboot image is built from the grub2 package
# the media ships instead.
my %MEDIA_GRUB2_BUILDS = (
'riscv64' => {
format => 'riscv64-efi',
package => 'grub-efi-riscv64-bin',
machine => 0x5064,
},
);
# Where the loader looks for the configuration nodeset writes, stored inside the image.
my $GRUB2_PREFIX = '/boot/grub2';
# The modules the network path needs before it can read a configuration file.
my @GRUB2_NETBOOT_MODULES = qw(
efinet tftp http net normal linux echo test configfile
search search_label search_fs_uuid search_fs_file
gzio part_gpt part_msdos ext2 fat all_video video font terminal reboot halt
);
sub install_media_grub2_loader {
my ($path, $arch, $callback) = @_;
my $build = $MEDIA_GRUB2_BUILDS{$arch};
return unless $build;
my $tftpdir = xCAT::TableUtils->getTftpDir();
unless ($tftpdir) {
_no_grub2_loader($arch, 'the TFTP directory is not known', $callback);
return;
}
my $target = "$tftpdir/boot/grub2/grub2.$arch";
return $target if _is_netboot_loader($target, $build);
# A file that failed the check is left where it is. The check cannot tell an image built for
# another boot path from one this plugin did not build: the loader on the media carries the
# same modules and differs only in the prefix. Moving a file aside on that evidence can take
# a working loader away from every node of the architecture, so it is replaced only once a
# working one exists, by the rename below.
my $kept = -e $target ? 1 : 0;
my ($package) = glob("$path/pool/main/g/grub2/$build->{package}_*_$arch.deb");
unless ($package && -r $package) {
_no_grub2_loader($arch, "the media carry no $build->{package} package", $callback, $kept);
return;
}
my $workdir = tempdir(CLEANUP => 1);
if (system('dpkg-deb', '-x', $package, $workdir) != 0) {
_no_grub2_loader($arch, "$package could not be unpacked", $callback, $kept);
return;
}
my $moduledir = "$workdir/usr/lib/grub/$build->{format}";
unless (-d $moduledir) {
_no_grub2_loader($arch, "$package carries no $build->{format} modules", $callback, $kept);
return;
}
mkpath("$tftpdir/boot/grub2");
# Built beside the target and renamed, so an interrupted run cannot leave a partial
# loader that nodeset would hand to every node of the architecture.
my $partial = "$target.$$";
my $rc = system('grub-mkimage', '-O', $build->{format}, '-d', $moduledir,
'-p', $GRUB2_PREFIX, '-o', $partial, @GRUB2_NETBOOT_MODULES);
unless ($rc == 0 and _is_netboot_loader($partial, $build)) {
unlink $partial;
_no_grub2_loader($arch, 'grub-mkimage could not build it', $callback, $kept);
return;
}
chmod 0644, $partial;
unless (rename($partial, $target)) {
unlink $partial;
_no_grub2_loader($arch, "it could not be renamed to $target: $!", $callback, $kept);
return;
}
$callback->({ data => "Installed $target from the media" }) if $callback;
return $target;
}
# UEFI loads the loader as a PE image for one machine, so anything else -- a truncated
# file, a stub carrying only headers, or the loader of another architecture -- cannot boot
# a node and is replaced. The fields below are the ones an image must have to be executed
# at all: sections to load, an entry point to jump to, and the subsystem UEFI runs.
sub _is_uefi_image {
my ($file, $machine) = @_;
my $size = -s $file;
return 0 unless ($machine and $size);
open(my $fh, '<', $file) or return 0;
binmode($fh);
my $ok = 0;
my ($dos, $coff, $optional);
if (read($fh, $dos, 64) == 64
and substr($dos, 0, 2) eq 'MZ'
and seek($fh, unpack('V', substr($dos, 60, 4)), 0)
and read($fh, $coff, 24) == 24
and substr($coff, 0, 4) eq "PE\0\0"
and unpack('v', substr($coff, 4, 2)) == $machine
and unpack('v', substr($coff, 6, 2)) > 0
and read($fh, $optional, 72) == 72
and unpack('v', substr($optional, 0, 2)) == 0x20b)
{
my $entry = unpack('V', substr($optional, 16, 4));
my $image = unpack('V', substr($optional, 56, 4));
my $headers = unpack('V', substr($optional, 60, 4));
my $system = unpack('v', substr($optional, 68, 2));
# 10 is the EFI application subsystem, the only one the firmware loads.
$ok = ($entry and $image and $headers and $system == 10
and $headers <= $size and $image <= $size);
}
close($fh);
return $ok ? 1 : 0;
}
# The modules a net boot cannot happen without. grub-mkimage records the name of every module
# it embeds, so their absence says the file is not a grub2 image built for this boot path,
# whatever its headers claim.
my @GRUB2_REQUIRED_MODULES = qw(efinet tftp http linux normal configfile search);
# grub-mkimage stores the prefix inside the image, so a loader built for this boot path
# carries the directory nodeset writes its configuration into. The image on the media has
# the same modules but no such prefix, which is why it cannot find that configuration: a
# file without it is replaced rather than trusted.
sub _is_netboot_loader {
my ($file, $build) = @_;
return 0 unless _is_uefi_image($file, $build->{machine});
open(my $fh, '<', $file) or return 0;
binmode($fh);
my $image = do { local $/; <$fh> };
close($fh);
return 0 unless defined $image and index($image, $GRUB2_PREFIX) >= 0;
for my $module (@GRUB2_REQUIRED_MODULES) {
return 0 if index($image, "\0$module\0") < 0;
}
return 1;
}
# Nothing else on an Ubuntu management node installs this loader, so a node of this
# architecture cannot boot until an administrator supplies one.
sub _no_grub2_loader {
my ($arch, $reason, $callback, $kept) = @_;
return unless $callback;
my $consequence = $kept
? "The grub2.$arch already in the boot/grub2 directory of the TFTP root was left alone. It "
. "was not built for this boot path, so check that nodes of this architecture still boot."
: "Nodes of this architecture will not boot until one is placed in the boot/grub2 "
. "directory of the TFTP root.";
$callback->({
warning => [ "No grub2.$arch boot loader was installed because $reason. $consequence" ] });
return;
}
sub is_ubuntu_live_media
{
my $media_path = shift;
@@ -517,6 +681,7 @@ sub copycd
}
$callback->({ data => "Media copy operation successful" });
install_media_grub2_loader($temppath, $arch, $callback);
unless ($noosimage) {
my @ret = xCAT::SvrUtils->update_tables_with_templates($distname, $arch, $temppath, $osdistroname, $legacyUB20);
if ($ret[0] != 0) {
@@ -645,6 +810,15 @@ sub subiquity_boot_params {
return (subiquity_kcmdline($base, $nfsip, $pkgdir, $instserver, $httpport, $node), undef);
}
# The Debian name of an install architecture, and whether xCAT installs Ubuntu
# on it. ppc64le is kept as is: the media paths key on both spellings.
my %INSTALL_ARCH = map { $_ => 1 } qw(x86_64 x86 ppc64le ppc64el riscv64);
sub install_darch {
my ($arch) = @_;
return ( xCAT::Utils::debian_arch($arch), $INSTALL_ARCH{ $arch // '' } ? 1 : 0 );
}
sub mkinstall {
xCAT::MsgUtils->message("S", "Doing debian mkinstall");
my $request = shift;
@@ -721,6 +895,7 @@ sub mkinstall {
my $partitionfile;
my $pkgdir;
my $pkgdirval;
my $environvar;
my @mirrors;
my $pkglistfile;
my $imagename; # set it if running of 'nodeset osimage=xxx'
@@ -743,12 +918,13 @@ sub mkinstall {
if (!$osimagetab) {
$osimagetab = xCAT::Table->new('osimage', -create => 1);
}
(my $ref) = $osimagetab->getAttribs({ imagename => $imagename }, 'osvers', 'osarch', 'profile', 'provmethod');
(my $ref) = $osimagetab->getAttribs({ imagename => $imagename }, 'osvers', 'osarch', 'profile', 'provmethod', 'environvar');
if ($ref) {
$img_hash{$imagename}->{osver} = $ref->{'osvers'};
$img_hash{$imagename}->{osarch} = $ref->{'osarch'};
$img_hash{$imagename}->{profile} = $ref->{'profile'};
$img_hash{$imagename}->{provmethod} = $ref->{'provmethod'};
$img_hash{$imagename}->{environvar} = $ref->{'environvar'};
if (!$linuximagetab) {
$linuximagetab = xCAT::Table->new('linuximage', -create => 1);
}
@@ -821,6 +997,7 @@ sub mkinstall {
$tmplfile = $ph->{template};
$pkgdirval = $ph->{pkgdir};
$environvar = $ph->{environvar};
my @pkgdirlist = split(/,/, $pkgdirval);
foreach (@pkgdirlist) {
if ($_ =~ /^http|ssh/) {
@@ -868,18 +1045,9 @@ sub mkinstall {
xCAT::MsgUtils->trace($verbose_on_off, "d", "debian->mkinstall: pkgdir=$pkgdir pkglistfile=$pkglistfile tmplfile=$tmplfile");
}
if ($arch eq "x86_64") {
$darch = "amd64";
}
elsif ($arch eq "x86") {
$darch = "i386";
}
else {
if ($arch ne "ppc64le" and $arch ne "ppc64el") {
xCAT::MsgUtils->message("S", "debian.pm: Unknown arch ($arch)");
}
$darch = $arch;
}
my $known;
($darch, $known) = install_darch($arch);
xCAT::MsgUtils->message("S", "debian.pm: Unknown arch ($arch)") unless $known;
my @missingparms;
unless ($os) {
@@ -957,7 +1125,10 @@ sub mkinstall {
$pkgdir,
$platform,
$partitionfile,
\%tmpl_hash
\%tmpl_hash,
osarch => $arch,
pkgdirs => $pkgdirval,
environvar => $environvar
);
}
+25 -1
View File
@@ -85,6 +85,29 @@ sub getstate {
}
}
# grub2 reads its configuration as a script, so an unquoted word carrying one of the
# characters below ends the linux command and the rest of the kernel command line is lost.
# The Ubuntu installer seed (ds=nocloud-net;s=<url>) is the usual casualty.
my $GRUB2_TERMINATOR = qr/[;{}|&<>()]/;
sub quote_kcmdline {
my $kcmdline = shift;
return $kcmdline unless (defined $kcmdline and $kcmdline =~ $GRUB2_TERMINATOR);
# Escaped in place rather than quoted as a whole: a value the caller quoted keeps the
# quoting it was given, which grub2 removes before the kernel sees the value.
my $escaped = '';
while (length $kcmdline) {
if ($kcmdline =~ s/^('[^']*'|"[^"]*")//) { $escaped .= $1; next; }
if ($kcmdline =~ s/^(\\.)//) { $escaped .= $1; next; }
$kcmdline =~ s/^(.)//s;
my $char = $1;
$escaped .= ($char =~ $GRUB2_TERMINATOR) ? "\\$char" : $char;
}
return $escaped;
}
sub setstate {
=pod
@@ -260,7 +283,8 @@ sub setstate {
}
if ($kern and $kern->{kcmdline}) {
print $pcfg " linux$efi $protocolrootdir/$kern->{kernel} $kern->{kcmdline} BOOTIF=\$net_default_mac\n";
my $kcmdline = quote_kcmdline($kern->{kcmdline});
print $pcfg " linux$efi $protocolrootdir/$kern->{kernel} $kcmdline BOOTIF=\$net_default_mac\n";
} else {
print $pcfg " linux$efi $protocolrootdir/$kern->{kernel} BOOTIF=\$net_default_mac\n";
}
+3 -3
View File
@@ -907,9 +907,9 @@ sub process_request {
}
}
if (exists $GRUB2_DISCOVERY_ARCHES{$arch} && !-e "$tftpdir/boot/grub2/grub2.$arch") {
# These configurations are only reachable through grub2.<arch>, which xCAT
# does not build.
$callback->({ data => ["Note: $tftpdir/boot/grub2/grub2.$arch is missing; $arch nodes need it to reach these configurations (it is installed by grub2-xcat, or copied from the EL $arch installation media)"] });
# These configurations are only reachable through grub2.<arch>. copycd builds it from
# Ubuntu media; on EL it is supplied by grub2-xcat or copied from the media.
$callback->({ data => ["Note: $tftpdir/boot/grub2/grub2.$arch is missing; $arch nodes need it to reach these configurations (copycd builds it from Ubuntu media; on EL it is installed by grub2-xcat or copied from the $arch installation media)"] });
}
if ($configfileonly && !$s390x_config_error) {
$callback->({ data => ["Write netboot config file done"] });
@@ -100,10 +100,7 @@ if [ ! -x /usr/bin/wget ]; then
sleep 36500d
fi
# These dispatcher scripts are not needed by the legacy post.xcat path. Newer
# wget parses HTML-looking regex strings inside downloaded scripts and fails the
# whole recursive download on bogus URLs.
wget -l inf -N -r --waitretry=10 --random-wait --retry-connrefused -e robots=off -nH --cut-dirs=2 --reject "index.html*,post.xcat.ng,post.xcat.rhels10" --no-parent -t 20 -T 60 http://${MASTER_IP}:${HTTPPORT}${INSTALLDIR}/postscripts/ -P /xcatpost 2> /tmp/wget.log
xcat_download_postscripts "${MASTER_IP}:${HTTPPORT}" "$INSTALLDIR" "/xcatpost" "/tmp/wget.log"
if [ "$?" != "0" ]; then
msgutil_r "$MASTER_IP" "error" "failed to download postscripts from http://$MASTER_IP$INSTALLDIR/postscripts/,check /tmp/wget.log on the node, halt ..." "/var/log/xcat/xcat.log" "$log_label"
/tmp/updateflag $MASTER $XCATIPORT "installstatus failed"
@@ -191,17 +191,6 @@ if [ -e "/tmp/xcat.install_disk" ]; then
fi
msgutil_r "$MASTER_IP" "info" "Found $instdisk, generate partition file..." "/var/log/xcat/xcat.log" "$log_label"
set_sles11_uefi_bootloader()
{
if grep -E 'install=.*sles11' /proc/cmdline >/dev/null 2>&1; then
# SLES 11 AutoYaST keeps the template's legacy MBR bootloader
# location unless the UEFI path explicitly selects elilo.
sed -i -e '/<lba_support /d' \
-e '/<linear /d' \
-e 's!<location>mbr</location>!<loader_type>elilo</loader_type>!' \
/tmp/profile/modified.xml
fi
}
if [ -d /sys/firmware/efi ]; then
sed -e 's!<device>XCATPARTITIONHOOK</device>!<device>'$instdisk'</device><partitions config:type="list"><partition><filesystem config:type="symbol">vfat</filesystem><mount>/boot/efi</mount><size>128mb</size></partition><partition><mount>swap</mount><size>auto</size></partition><partition><mount>/</mount><size>auto</size></partition></partitions>!' /tmp/profile/autoinst.xml > /tmp/profile/modified.xml
@@ -63,3 +63,27 @@ declare -F xcat_enable_active_nm_autoconnect &>/dev/null || function xcat_enable
nmcli con mod "$con_name" connection.autoconnect yes
done
}
declare -F xcat_download_postscripts &>/dev/null || function xcat_download_postscripts {
local server="$1"
local install_dir="${2:-/install}"
local postroot="${3:-/xcatpost}"
local log_file="${4:-/tmp/wget.log}"
export LANG=C
wget -l inf -N -r --waitretry=10 --random-wait --retry-connrefused -e robots=off -nH --cut-dirs=2 --reject "index.html*,post.xcat.ng,post.xcat.rhels10" --no-parent -t 20 -T 60 "http://${server}${install_dir}/postscripts/" -P "$postroot" 2> "$log_file"
}
declare -F set_sles11_uefi_bootloader &>/dev/null || function set_sles11_uefi_bootloader {
local cmdline="${1:-/proc/cmdline}"
local profile="${2:-/tmp/profile/modified.xml}"
if grep -E 'install=.*sles11' "$cmdline" >/dev/null 2>&1; then
# SLES 11 AutoYaST keeps the template's legacy MBR bootloader
# location unless the UEFI path explicitly selects elilo.
sed -i -e '/<lba_support /d' \
-e '/<linear /d' \
-e 's!<location>mbr</location>!<loader_type>elilo</loader_type>!' \
"$profile"
fi
}
@@ -43,6 +43,7 @@ autoinstall:
- bind9-dnsutils
- chrony
- gpg
- #INCLUDE_DEFAULT_PKGLIST_AUTOINSTALL#
early-commands:
- |
exec >/tmp/pre-install.log 2>&1
@@ -111,6 +112,11 @@ autoinstall:
cp ./#HOSTNAME#.post /target/root/post.script;
curtin in-target --target /target /root/post.script;
} >>/target/var/log/xcat/xcat.log 2>&1'
# The installer's sources for the otherpkgs repository and the pkgdir mirrors, and the apt
# configuration that kept recommended packages out, served the install; ospkgs and otherpkgs
# write their own after the first boot. A separate item, so the status of the post script above
# still decides whether the install goes on.
- rm -f /target/etc/apt/sources.list.d/xcat-otherpkgs-*.list /target/etc/apt/sources.list.d/xcat-otherpkgs-*.sources /target/etc/apt/sources.list.d/xcat-pkgdir-*.list /target/etc/apt/sources.list.d/xcat-pkgdir-*.sources /target/etc/apt/apt.conf.d/94curtin-config
# Flip the node to local-disk boot, or it PXE-loops back into the installer on reboot.
# xcatd's install monitor greets with "ready", then answers "next" with "done" and runs
# "nodeset <node> next". Require both tokens: another service on that port is not a flipped
@@ -0,0 +1,5 @@
openssh-server
chrony
gawk
nfs-common
snmpd
@@ -0,0 +1,5 @@
openssh-server
chrony
gawk
nfs-common
snmpd
@@ -0,0 +1,5 @@
openssh-server
chrony
gawk
nfs-common
snmpd
@@ -0,0 +1,15 @@
bash
nfs-common
openssl
isc-dhcp-client
libc-bin
openssh-server
openssh-client
wget
vim
rsync
busybox-static
gawk
bind9-dnsutils
chrony
gpg
@@ -278,6 +278,7 @@ sub default_net_drivers {
x86_64 => [qw(tg3 bnx2 bnx2x e1000 e1000e igb mlx_en mlx5_core virtio_net overlay)],
ppc64el => [qw(tg3 bnx2 bnx2x e1000 e1000e igb ibmveth ehea mlx_en mlx4_en mlx5_core virtio_net overlay)],
ppc64 => [qw(e1000 e1000e igb ibmveth ehea)],
riscv64 => [qw(e1000 e1000e igb ixgbe r8169 tg3 bnx2x mlx5_core virtio_net overlay)],
s390x => [qw(qdio ccwgroup)],
},
);
@@ -0,0 +1,22 @@
bash
nfs-common
openssl
isc-dhcp-client
libc-bin
linux-image-generic
openssh-server
openssh-client
wget
rsync
busybox-static
gawk
bind9-dnsutils
tar
gzip
xz-utils
cpio
chrony
util-linux-extra
iproute2
dracut
dracut-network
@@ -0,0 +1,18 @@
bash
nfs-common
openssl
isc-dhcp-client
libc-bin
linux-image-generic
openssh-server
openssh-client
wget
rsync
busybox-static
gawk
bind9-dnsutils
tar
gzip
xz-utils
cpio
chrony
+11 -1
View File
@@ -259,8 +259,13 @@ unless ($onlyinitrd) {
# site.ubuntu_apt_mirror overrides; otherwise default to the public archive. A live-server
# ISO is never a complete debootstrap source, so a real mirror is always required here.
my @aptmirror = xCAT::TableUtils->get_site_attribute("ubuntu_apt_mirror");
# archive.ubuntu.com publishes amd64 and i386 only. Every other architecture, ppc64el
# and riscv64 included, is on the ports archive.
my $default = ($uarch =~ /^(?:amd64|i386)$/)
? 'http://archive.ubuntu.com/ubuntu'
: 'http://ports.ubuntu.com/ubuntu-ports';
my $mirror = (defined $aptmirror[0] && length $aptmirror[0])
? $aptmirror[0] : 'http://archive.ubuntu.com/ubuntu';
? $aptmirror[0] : $default;
(my $codename = $osver) =~ s/^ubuntu//;
# Strip ONLY a trailing third component. A bare s/\.\d+$// also strips the minor from a
# two-part osvers (ubuntu26.04 -> "26"), which misses the %cn map below and reaches
@@ -1786,6 +1791,11 @@ EOMS
push @filestoadd, $_ if (-e "$rootimg_dir/$_");
}
} elsif ($arch =~ /riscv64/) {
foreach ("lib/riscv64-linux-gnu/libnss_files.so.2", "lib/riscv64-linux-gnu/libnss_dns.so.2") {
push @filestoadd, $_ if (-e "$rootimg_dir/$_");
}
} else {
push @filestoadd, "lib/libnss_dns.so.2" if (-e "$rootimg_dir/lib/libnss_dns.so.2");
}
+58 -23
View File
@@ -5,6 +5,8 @@ BEGIN {
use lib "$::XCATROOT/lib/perl";
use xCAT::CommandUtils;
use IO::Socket::INET;
use File::Temp qw(tempfile);
use POSIX qw(_exit sigprocmask WNOHANG SIG_BLOCK SIG_SETMASK SIGINT SIGTERM);
use Time::HiRes qw(gettimeofday sleep);
use Getopt::Long;
Getopt::Long::Configure("bundling");
@@ -92,16 +94,25 @@ if (-f "/etc/redhat-release") {
}
# fork a process to capture the packet by tcpdump
my ($dumpfh, $dumpfile) = tempfile("detect_dhcpd.XXXXXX", TMPDIR => 1, UNLINK => 1);
close($dumpfh);
# INT and TERM stay blocked from the fork until the handler that stops the capture is in place.
my $stop_signals = POSIX::SigSet->new(SIGINT, SIGTERM);
my $signal_mask = POSIX::SigSet->new();
sigprocmask(SIG_BLOCK, $stop_signals, $signal_mask);
my $pid = fork;
if (!defined $pid) { print "Fork failed.\n"; exit 1; }
my $dumpfile = "/tmp/dhcpdumpfile.log";
if (!defined $pid) { sigprocmask(SIG_SETMASK, $signal_mask); print "Fork failed.\n"; exit 1; }
if ($pid == 0) {
# Child process
my $cmd = "$tcpdump -i $IF port 68 -n -vvvvvv > $dumpfile 2>/dev/null";
`$cmd`;
exit 0;
# Child process: tcpdump itself, so the parent owns exactly this pid.
sigprocmask(SIG_SETMASK, $signal_mask);
open(STDOUT, '>', $dumpfile) or _exit(1);
open(STDERR, '>', '/dev/null');
exec($tcpdump, '-i', $nic, 'port', '68', '-n', '-vvvvvv');
_exit(1);
}
$SIG{INT} = $SIG{TERM} = sub { kill_child(); exit 1; };
sigprocmask(SIG_SETMASK, $signal_mask);
# generate the discover package
my $package = packdhcppkg($MAC);
@@ -141,25 +152,24 @@ if ($::TIMEOUT) {
my $end = Time::HiRes::gettimeofday();
$end =~ s/(\d.*)\.(\d.*)/$1/;
while ($end - $start <= $timeout) {
$sock->send($package) or die "Send discover error: $@\n";
unless ($sock->send($package)) {
print "Send discover error: $!\n";
kill_child();
exit 1;
}
sleep 2;
$end = Time::HiRes::gettimeofday();
$end =~ s/(\d.*)\.(\d.*)/$1/;
}
kill_child();
#kill the child process
kill 15, $pid;
my @pidoftcpdump = `ps -ef | grep -E "[0-9]+:[0-9]+:[0-9]+ $tcpdump -i $IF" | awk -F' ' '{print \$2}'`;
foreach my $cpid (@pidoftcpdump) {
kill 15, $cpid;
# print "try to kill $cpid\n";
my $capture_problem = kill_child();
if ($capture_problem) {
print "tcpdump $capture_problem.\n";
exit 1;
}
sleep 2;
open(FILE, "<$dumpfile") or die "Cannot open $dumpfile\n";
my %output;
my @snack = ();
@@ -239,7 +249,6 @@ if (scalar(@server)) {
}
}
#`rm -f $dumpfile`;
exit 0;
@@ -345,12 +354,38 @@ sub packdhcppkg {
return $package;
}
# Stop the capture and reap it, once: INT and TERM are blocked while the pid is taken and until
# tcpdump is reaped, so an interrupt in that window cannot orphan it or reach a recycled pid. The
# handlers stay installed, so a later signal still leaves through exit and the END cleanup.
# Returns '' when tcpdump ran until this TERM and stopped cleanly, otherwise what went wrong: it
# ended on its own, ignored TERM and was killed, or left on another signal or with a non-zero
# status.
sub kill_child {
kill 15, $pid;
my @pidoftcpdump = `ps -ef | grep -E "[0-9]+:[0-9]+:[0-9]+ $tcpdump -i $IF" | awk -F' ' '{print \$2}'`;
foreach my $cpid (@pidoftcpdump) {
kill 15, $cpid;
#print "try to kill $cpid\n";
sigprocmask(SIG_BLOCK, $stop_signals);
my $child = $pid;
$pid = undef;
unless ($child) {
sigprocmask(SIG_SETMASK, $signal_mask);
return '';
}
my $reaped = waitpid($child, WNOHANG);
my $early = ($reaped == $child) ? 1 : 0;
if ($reaped == 0) {
kill 'TERM', $child;
foreach (1 .. 50) {
last if ($reaped = waitpid($child, WNOHANG)) != 0;
select(undef, undef, undef, 0.1);
}
if ($reaped == 0) {
kill 'KILL', $child;
$reaped = waitpid($child, 0);
}
}
sigprocmask(SIG_SETMASK, $signal_mask);
return "could not be reaped" if $reaped != $child;
my ($signal, $status) = ($? & 127, $? >> 8);
my $how = $signal ? "on signal $signal" : "with status $status";
return "ended before the capture window did, $how" if $early;
return '' if $signal == 15 || (!$signal && !$status);
return "left the capture $how";
}
+33 -1
View File
@@ -2461,13 +2461,45 @@ case "${GO_XCAT_OS}" in
esac
case "${GO_XCAT_ARCH}" in
"ppc64"|"ppc64le"|"x86_64")
"ppc64"|"ppc64le"|"riscv64"|"x86_64")
;;
*)
exit_if_bad 1 "${GO_XCAT_ARCH}: unsupported instruction set architecture"
;;
esac
# A riscv64 management node has no legacy Genesis: its image ships as the OpenEmbedded package,
# which mknb installs. The legacy Genesis scripts and bases are therefore dropped and the
# OpenEmbedded package of the architecture is asked for instead.
#
# The x86 boot loaders stay: they are payload a management node SERVES to x86 nodes over TFTP,
# not host binaries, so a riscv64 management node needs them to boot a mixed cluster.
function riscv64_install_list()
{
local genesis_package="xCAT-genesis-openembedded-riscv64"
type dpkg >/dev/null 2>&1 && genesis_package="xcat-genesis-openembedded-riscv64"
local package
for package in "$@"; do
case "${package}" in
*genesis-scripts-*|*genesis-base-*)
;;
*)
printf '%s\n' "${package}"
;;
esac
done
printf '%s\n' "${genesis_package}"
}
if [ "${GO_XCAT_ARCH}" = "riscv64" ]; then
riscv64_list=()
while read -r package; do
riscv64_list+=("${package}")
done < <(riscv64_install_list "${GO_XCAT_INSTALL_LIST[@]}")
GO_XCAT_INSTALL_LIST=("${riscv64_list[@]}")
unset riscv64_list package
fi
GO_XCAT_LINUX_DISTRO="$(check_linux_distro)"
GO_XCAT_LINUX_VERSION="$(check_linux_version)"
+21
View File
@@ -0,0 +1,21 @@
# xCAT-test
Unit tests that run from the source checkout are split by implementation
language:
| Test type | Location | Runner |
| --------- | -------- | ------ |
| Perl unit tests | `xCAT-test/unit/*.t` | `prove -r xCAT-test/unit` |
| Shell unit tests | `xCAT-test/bats/*.bats` | `bats -r xCAT-test/bats` |
| CLI functional tests | `xCAT-test/autotest/testcase/` and `xCAT-test/autotest/bundle/` | `xcattest -f <cluster.conf> -t <case>` or `xcattest -f <cluster.conf> -b <bundle>` |
Use Perl `.t` tests for Perl modules, Perl scripts, templates, and repository
artifacts. Use BATS tests for shell-script behavior that can be exercised from
the checkout by sourcing a shell library or script and shadowing external
commands.
Shell behavior should not be tested by Perl tests that grep shell source. Put
those tests under `xCAT-test/bats` instead.
See `unit/README.md` and `bats/README.md` for the detailed rules for
each unit-test suite.
+21
View File
@@ -0,0 +1,21 @@
# xCAT-test/bats
Shell-script unit tests live here and run with:
```bash
bats -r xCAT-test/bats
```
The GitHub Actions `xcat_test` workflow runs this command after the Perl `.t`
unit tests. Use BATS for shell behavior that can be exercised from the source
tree without an installed xCAT, a live management node, or real services.
Prefer sourcing an existing shell library or sourceable script and calling the
function under test. Keep reusable install-template helpers in
`xCAT-server/share/xcat/install/scripts/scriptlib`, and reusable postscript
helpers in `xCAT/postscripts/xcatlib.sh`. Use scratch directories and shadowed
commands so tests cannot write to the host.
Extraction helpers in `helpers/shell_source.bash` are only for legacy code that
cannot safely be sourced yet. Do not add Perl `.t` tests that grep shell source
when the behavior can be tested with BATS.
+118
View File
@@ -0,0 +1,118 @@
#!/usr/bin/env bats
load 'helpers/shell_source'
setup()
{
GENESIS_SPEC="$(repo_path 'xCAT-genesis-builder/xCAT-genesis-base.spec')"
DRACUT_MODULE="$(repo_path 'xCAT-genesis-builder/dracut_105/el/module-setup.sh')"
DOXCAT="$(repo_path 'xCAT-genesis-scripts/usr/bin/doxcat')"
[ -r "$GENESIS_SPEC" ] || skip "$GENESIS_SPEC is required"
[ -r "$DRACUT_MODULE" ] || skip "$DRACUT_MODULE is required"
[ -r "$DOXCAT" ] || skip "$DOXCAT is required"
export GENESIS_SPEC DRACUT_MODULE DOXCAT
}
run_installkernel()
{
local modules_root="$1"
local instmods_log="$2"
kernel=5.14.0-test
DRACUT_MODULES_ROOT="$modules_root"
instmods()
{
printf '%s\n' "$1" >>"$instmods_log"
}
source "$DRACUT_MODULE"
installkernel
}
run_doxcat_modprobe_preamble()
{
local preamble="$1"
local modprobe_log="$2"
modprobe()
{
printf '%s\n' "$*" >>"$modprobe_log"
}
eval "$preamble"
}
run_doxcat_bootif_block()
{
local block="$1"
BOOTIF=01-aa-bb-cc-dd-ee-ff
bootnic=
log_label=test
gripeiter=2
logger() { :; }
sleep() { :; }
ip()
{
printf '%s\n' "$*" >>"$IP_LOG"
if [ "$*" = "link show" ]; then
cat <<'EOF'
2: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc mq state UP mode DEFAULT group default qlen 1000
link/ether 00:11:22:33:44:55 brd ff:ff:ff:ff:ff:ff
3: ib0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 65520 qdisc mq state UP mode DEFAULT group default qlen 256
link/infiniband 00:bb:cc:dd:ee:ff brd 00:ff:ff:ff:ff:ff
EOF
fi
}
eval "$block"
printf '%s\n' "$bootnic"
}
@test "genesis build requires kernel module packages" {
grep -Fxq 'BuildRequires: kernel-core' "$GENESIS_SPEC"
grep -Fxq 'BuildRequires: kernel-modules' "$GENESIS_SPEC"
grep -Fxq 'BuildRequires: kernel-modules-extra' "$GENESIS_SPEC"
}
@test "dracut genesis module installs every module from modules.dep" {
local modules_root="${BATS_TEST_TMPDIR}/modules"
local modules_dep="${modules_root}/5.14.0-test/modules.dep"
local instmods_log="${BATS_TEST_TMPDIR}/instmods.log"
mkdir -p "${modules_root}/5.14.0-test"
cat >"$modules_dep" <<'EOF'
kernel/drivers/infiniband/ulp/ipoib/ib_ipoib.ko.xz:
kernel/drivers/net/ethernet/intel/e1000e/e1000e.ko.xz:
EOF
run run_installkernel "$modules_root" "$instmods_log"
[ "$status" -eq 0 ]
grep -Fxq 'ib_ipoib' "$instmods_log"
grep -Fxq 'e1000e' "$instmods_log"
}
@test "doxcat loads IP over InfiniBand support during startup" {
local preamble
local modprobe_log="${BATS_TEST_TMPDIR}/modprobe.log"
preamble="$(extract_line_range "$DOXCAT" '^modprobe acpi_cpufreq' '^modprobe ib_ipoib$')" || return 1
run run_doxcat_modprobe_preamble "$preamble" "$modprobe_log"
[ "$status" -eq 0 ]
grep -Fxq 'ib_ipoib' "$modprobe_log"
}
@test "doxcat falls back to InfiniBand BOOTIF lookup after Ethernet lookup misses" {
local block
IP_LOG="${BATS_TEST_TMPDIR}/ip.log"
export IP_LOG
block="$(extract_shell_if_block "$DOXCAT" 'if [ ! -z "$BOOTIF" ]; then')" || return 1
run run_doxcat_bootif_block "$block"
[ "$status" -eq 0 ]
[ "$output" = "ib0" ]
[ "$(grep -c '^link show$' "$IP_LOG")" -eq 2 ]
}
@@ -0,0 +1,122 @@
#!/usr/bin/env bats
load 'helpers/go_xcat'
setup()
{
go_xcat_require_source
}
run_common_repository_case()
{
local case_dir="$1"
local common_present="$2"
shift 2
mkdir -p "$case_dir"
export ADD_LOG="${case_dir}/add.log"
export COMMON_PRESENT="$common_present"
export DOWNLOAD_LOG="${case_dir}/download.log"
export ID_LOG="${case_dir}/id.log"
export TEST_TMP="$case_dir"
go_xcat_load_functions \
add_xcat_dep_common_repo_yum_or_zypper \
xcat_dep_common_repo_configured \
refresh_xcat_dep_repository_ids
TMP_DIR="$TEST_TMP"
GO_XCAT_DEFAULT_BASE_URL=https://repo.example.invalid
GO_XCAT_DEP_REPOSITORY_IDS=(xcat-dep)
yum() { :; }
download_file()
{
printf '%s\n' "$1" >>"$DOWNLOAD_LOG"
[[ ${COMMON_PRESENT:-0} == 1 ]] || return 1
: >"$2"
}
add_repo_by_url_yum_or_zypper()
{
printf '%s %s\n' "$1" "$2" >>"$ADD_LOG"
}
xcat_dep_common_repo_configured()
{
[[ -s "$ADD_LOG" ]]
}
( add_xcat_dep_common_repo_yum_or_zypper "$@" )
refresh_xcat_dep_repository_ids
printf '%s\n' "${GO_XCAT_DEP_REPOSITORY_IDS[*]}" >"$ID_LOG"
}
run_template_generation()
{
local tmp_dir="$1"
local repo_log="$2"
mkdir -p "$tmp_dir"
export REPO_LOG="$repo_log"
export TEST_TMP="$tmp_dir"
go_xcat_load_functions add_repo_by_url_yum_or_zypper
TMP_DIR="$TEST_TMP"
GO_XCAT_DEFAULT_INSTALL_PATH=/install/xcat
yum() { :; }
add_repo_by_file() { cp "$1" "$REPO_LOG"; }
add_repo_by_url_yum_or_zypper \
https://repo.example.invalid/xcat-dep/common xcat-dep-common optional
}
@test "an available remote common repository is enabled" {
local case_dir="${BATS_TEST_TMPDIR}/remote-present"
run run_common_repository_case "$case_dir" 1 "" latest
[ "$status" -eq 0 ]
[ "$(read_file_or_empty "${case_dir}/download.log")" = "https://repo.example.invalid/yum/latest/xcat-dep/common/repodata/repomd.xml" ]
[ "$(read_file_or_empty "${case_dir}/add.log")" = "https://repo.example.invalid/yum/latest/xcat-dep/common xcat-dep-common" ]
[ "$(read_file_or_empty "${case_dir}/id.log")" = "xcat-dep xcat-dep-common" ]
}
@test "a release without the remote common repository remains usable" {
local case_dir="${BATS_TEST_TMPDIR}/remote-missing"
run run_common_repository_case "$case_dir" 0 "" 2.18
[ "$status" -eq 0 ]
[ "$(read_file_or_empty "${case_dir}/add.log")" = "" ]
[ "$(read_file_or_empty "${case_dir}/id.log")" = "xcat-dep" ]
}
@test "a custom repository file does not guess an unrelated common repository" {
local case_dir="${BATS_TEST_TMPDIR}/repo-file"
run run_common_repository_case "$case_dir" 1 https://repo.example.invalid/custom/xcat-dep.repo latest
[ "$status" -eq 0 ]
[ "$(read_file_or_empty "${case_dir}/download.log")" = "" ]
[ "$(read_file_or_empty "${case_dir}/add.log")" = "" ]
}
@test "a local common repository is enabled beside the distribution repository" {
local local_root="${BATS_TEST_TMPDIR}/local-repository"
local case_dir="${BATS_TEST_TMPDIR}/local-present"
mkdir -p "${local_root}/common/repodata"
: >"${local_root}/common/repodata/repomd.xml"
run run_common_repository_case "$case_dir" 0 "$local_root" latest
[ "$status" -eq 0 ]
[ "$(read_file_or_empty "${case_dir}/add.log")" = "${local_root}/common xcat-dep-common" ]
}
@test "the optional common repository template tolerates outages and verifies metadata" {
local template_log="${BATS_TEST_TMPDIR}/generated-common.repo"
run run_template_generation "$BATS_TEST_TMPDIR" "$template_log"
[ "$status" -eq 0 ]
grep -Fxq 'skip_if_unavailable=1' "$template_log"
grep -Fxq 'repo_gpgcheck=1' "$template_log"
}
+206
View File
@@ -0,0 +1,206 @@
#!/usr/bin/env bats
load 'helpers/go_xcat'
setup()
{
go_xcat_require_source
export CALLS="${BATS_TEST_TMPDIR}/calls"
export GO_XCAT_ARCH=x86_64
export GO_XCAT_LINUX_DISTRO=rocky
export GO_XCAT_LINUX_VERSION=10.2
export EPEL_HAS=1
export CRB_HAS=1
export QUERY_FAIL=0
export QUERY_WARNS=0
export SOURCE_ONLY=0
export ENTRY=check
}
run_el_repo_check()
{
rm -f "$CALLS"
go_xcat_load_functions \
repo_carries \
el_epel_and_crb_check \
install_packages_dnf \
install_packages_yum
EL_EPEL_TEST_RPM=perl-Crypt-CBC
EL_CRB_TEST_RPM=perl-IO-Tty
dnf()
{
echo "$*" >>"$CALLS"
if [[ ${QUERY_FAIL:-0} == 1 ]]; then
echo "Error: Failed to download metadata for repo 'epel'" >&2
return 1
fi
[[ ${QUERY_WARNS:-0} == 1 ]] && echo "Warning: repository 'extras' metadata is stale" >&2
local has=0
case "$*" in
*perl-Crypt-CBC*) has="$EPEL_HAS" ;;
*perl-IO-Tty*) has="$CRB_HAS" ;;
esac
[[ ${SOURCE_ONLY:-0} == 1 && "$*" != *"--arch"* ]] && has=1
case "$1" in
repoquery) [[ $has == 1 ]] && { echo "${@: -1}"; echo "${@: -1}"; }; return 0 ;;
list) [[ $has == 1 ]] && return 0; return 1 ;;
esac
return 0
}
yum()
{
dnf "$@"
}
case "${ENTRY:-check}" in
dnf) install_packages_dnf -y xCAT ;;
yum) install_packages_yum -y xCAT ;;
*) el_epel_and_crb_check dnf ;;
esac
}
@test "EL9 with EPEL and CRB passes after probing binary repositories" {
export GO_XCAT_LINUX_VERSION=9.5
run run_el_repo_check
[ "$status" -eq 0 ]
probes="$(joined_file_lines "$CALLS")"
[[ "$probes" =~ perl-Crypt-CBC.*\;.*perl-IO-Tty ]]
[[ "$probes" =~ ^repoquery\ ]]
[[ "$probes" =~ --arch\ x86_64,noarch ]]
}
@test "EL9 without EPEL stops and names the EL9 release package" {
export GO_XCAT_LINUX_VERSION=9.5
export EPEL_HAS=0
run run_el_repo_check
[ "$status" -eq 1 ]
[[ "$output" =~ epel-release-latest-9\.noarch ]]
}
@test "EL10 with EPEL and CRB passes after probing both repositories" {
run run_el_repo_check
[ "$status" -eq 0 ]
probes="$(joined_file_lines "$CALLS")"
[[ "$probes" =~ perl-Crypt-CBC.*\;.*perl-IO-Tty ]]
}
@test "EL10 without EPEL stops with the EL10 EPEL release package" {
export EPEL_HAS=0
run run_el_repo_check
[ "$status" -eq 1 ]
[[ "$output" =~ requires\ EPEL\ repository ]]
[[ "$output" =~ epel-release-latest-10\.noarch ]]
}
@test "EL10 without CRB stops with current CRB guidance" {
export GO_XCAT_LINUX_DISTRO=rhel
export CRB_HAS=0
run run_el_repo_check
[ "$status" -eq 1 ]
[[ "$output" =~ requires\ CRB\ repository ]]
[[ "$output" =~ "'dnf update epel-release' and then 'crb enable'" ]]
[[ ! "$output" =~ gpgcheck=0|centos-crb|subscription-manager ]]
}
@test "a source repository does not stand in for the binary one" {
export EPEL_HAS=0
export SOURCE_ONLY=1
run run_el_repo_check
[ "$status" -eq 1 ]
[[ "$output" =~ requires\ EPEL\ repository ]]
}
@test "Oracle Linux 10 without CRB names its CodeReady Builder command" {
export GO_XCAT_LINUX_DISTRO=ol
export GO_XCAT_LINUX_VERSION=10.1
export CRB_HAS=0
run run_el_repo_check
[ "$status" -eq 1 ]
[[ "$output" =~ dnf\ config-manager\ --enable\ ol10_codeready_builder ]]
}
@test "a failed repository query stops with the package manager error" {
export QUERY_FAIL=1
run run_el_repo_check
[ "$status" -eq 1 ]
[[ "$output" =~ Failed\ to\ download\ metadata ]]
[[ ! "$output" =~ requires\ EPEL\ repository ]]
}
@test "a warning on stderr does not stand in for a package" {
export EPEL_HAS=0
export QUERY_WARNS=1
run run_el_repo_check
[ "$status" -eq 1 ]
[[ "$output" =~ requires\ EPEL\ repository ]]
}
@test "a warning beside a real match does not fail the check" {
export QUERY_WARNS=1
run run_el_repo_check
[ "$status" -eq 0 ]
}
@test "CentOS Stream 10, which reports the major version alone, is checked" {
export GO_XCAT_LINUX_DISTRO=centos
export GO_XCAT_LINUX_VERSION=10
export EPEL_HAS=0
export CRB_HAS=0
run run_el_repo_check
[ "$status" -eq 1 ]
[[ "$output" =~ requires\ EPEL\ repository ]]
}
@test "EL8 and Fedora are not checked" {
export GO_XCAT_LINUX_VERSION=8.10
export EPEL_HAS=0
export CRB_HAS=0
run run_el_repo_check
[ "$status" -eq 0 ]
[ "$(joined_file_lines "$CALLS")" = "" ]
export GO_XCAT_LINUX_DISTRO=fedora
export GO_XCAT_LINUX_VERSION=42
run run_el_repo_check
[ "$status" -eq 0 ]
}
@test "dnf and yum installer paths run the check before installing" {
for entry in dnf yum; do
export ENTRY="$entry"
export EPEL_HAS=0
export CRB_HAS=1
run run_el_repo_check
[ "$status" -eq 1 ]
[[ "$output" =~ requires\ EPEL\ repository ]]
probes="$(joined_file_lines "$CALLS")"
[[ ! "$probes" =~ install ]]
export EPEL_HAS=1
export CRB_HAS=1
run run_el_repo_check
[ "$status" -eq 0 ]
probes="$(joined_file_lines "$CALLS")"
[[ "$probes" =~ perl-IO-Tty.*\;.*install\ initscripts.*\;.*install\ xCAT ]]
done
}
+56
View File
@@ -0,0 +1,56 @@
#!/usr/bin/env bash
go_xcat_default_source()
{
printf '%s\n' "${BATS_TEST_DIRNAME}/../../xCAT-server/share/xcat/tools/go-xcat"
}
go_xcat_require_source()
{
GO_XCAT_SOURCE="${XCAT_TEST_GO_XCAT:-$(go_xcat_default_source)}"
export GO_XCAT_SOURCE
[ -r "$GO_XCAT_SOURCE" ] || skip "$GO_XCAT_SOURCE is required"
}
go_xcat_extract_functions()
{
local function_name
for function_name in "$@"; do
awk -v name="$function_name" '
$0 == "function " name "()" { copy = 1 }
copy { print }
copy && /^}$/ { exit }
' "$GO_XCAT_SOURCE"
done
}
go_xcat_load_functions()
{
local function_body function_name
function_body="$(go_xcat_extract_functions "$@")" || return 1
eval "$function_body"
for function_name in "$@"; do
declare -F "$function_name" >/dev/null || {
printf 'missing %s\n' "$function_name" >&2
return 70
}
done
}
read_file_or_empty()
{
local path="$1"
[ -f "$path" ] || return 0
cat "$path"
}
joined_file_lines()
{
local path="$1"
local line separator=""
[ -f "$path" ] || return 0
while IFS= read -r line; do
printf '%s%s' "$separator" "$line"
separator=";"
done <"$path"
}
+132
View File
@@ -0,0 +1,132 @@
#!/usr/bin/env bash
repo_root()
{
printf '%s\n' "${BATS_TEST_DIRNAME}/../.."
}
repo_path()
{
printf '%s/%s\n' "$(repo_root)" "$1"
}
require_repo_file()
{
local path
path="$(repo_path "$1")"
[ -r "$path" ] || skip "$path is required"
printf '%s\n' "$path"
}
read_file_or_empty()
{
local path="$1"
[ -f "$path" ] || return 0
cat "$path"
}
extract_shell_function()
{
local file="$1"
local name="$2"
awk -v name="$name" '
BEGIN {
signature = "^[[:space:]]*(function[[:space:]]+)?" name "([[:space:]]*\\(\\))?[[:space:]]*$"
inline_signature = "^[[:space:]]*(function[[:space:]]+)?" name "([[:space:]]*\\(\\))?[[:space:]]*\\{"
}
$0 ~ signature || $0 ~ inline_signature {
copy = 1
}
copy {
print
opened += gsub(/\{/, "{")
closed += gsub(/\}/, "}")
if (opened > 0 && opened == closed) {
found = 1
exit
}
}
END {
if (!found) {
exit 1
}
}
' "$file"
}
extract_shell_if_block()
{
local file="$1"
local start="$2"
awk -v start="$start" '
index($0, start) {
copy = 1
}
copy {
print
if ($0 ~ /^[[:space:]]*if[[:space:]\[]/) {
depth++
}
line = $0
while (line ~ /(^|[;[:space:]])fi([;[:space:]]|$)/) {
depth--
sub(/(^|[;[:space:]])fi([;[:space:]]|$)/, " ", line)
}
if (depth == 0) {
found = 1
exit
}
}
END {
if (!found) {
exit 1
}
}
' "$file"
}
extract_line_range()
{
local file="$1"
local start="$2"
local end="$3"
awk -v start="$start" -v end="$end" '
$0 ~ start {
copy = 1
}
copy {
print
if ($0 ~ end) {
found = 1
exit
}
}
END {
if (!found) {
exit 1
}
}
' "$file"
}
extract_first_matching_line()
{
local file="$1"
local pattern="$2"
awk -v pattern="$pattern" '
$0 ~ pattern {
print
found = 1
exit
}
END {
if (!found) {
exit 1
}
}
' "$file"
}
@@ -0,0 +1,88 @@
#!/usr/bin/env bats
load 'helpers/shell_source'
setup()
{
SCRIPT_LIB="$(repo_path 'xCAT-server/share/xcat/install/scripts/scriptlib')"
XCATDSKLSPOST="$(repo_path 'xCAT/postscripts/xcatdsklspost')"
[ -r "$SCRIPT_LIB" ] || skip "$SCRIPT_LIB is required"
[ -r "$XCATDSKLSPOST" ] || skip "$XCATDSKLSPOST is required"
export SCRIPT_LIB XCATDSKLSPOST
}
capture_install_scriptlib_wget()
{
local wget_log="$1"
wget()
{
printf '%s\n' "$*" >"$wget_log"
return 0
}
source "$SCRIPT_LIB"
xcat_download_postscripts "192.0.2.10:80" "/install" "/xcatpost" "$wget_log"
}
capture_xcatdsklspost_wget()
{
local wget_log="$1"
local host_init_log="${BATS_TEST_TMPDIR}/host-init.log"
local download_log="${BATS_TEST_TMPDIR}/xcatdsklspost-wget-errors.log"
cat() { printf 'cat %s\n' "$*" >>"$host_init_log"; return 1; }
grep()
{
printf 'grep %s\n' "$*" >>"$host_init_log"
command grep "$@"
}
dirname() { printf 'dirname %s\n' "$*" >>"$host_init_log"; return 1; }
XCATDSKLSPOST_SOURCE_ONLY=1
XCAT_WGET_LOG="$download_log"
source "$XCATDSKLSPOST"
[ ! -e "$host_init_log" ] || return 1
[ "$XCAT_WGET_LOG" = "$download_log" ] || return 1
unset -f cat grep dirname
xcatpost="${BATS_TEST_TMPDIR}/xcatpost"
INSTALLDIR=/install
echolog() { :; }
sleep() { :; }
wget()
{
printf '%s\n' "$*" >"$wget_log"
printf '%s\n' 'mock wget stderr' >&2
return 0
}
download_postscripts 192.0.2.10:80
[ "$(read_file_or_empty "$download_log")" = "mock wget stderr" ]
}
assert_download_policy()
{
local args="$1"
[[ "$args" == *'--reject index.html*,post.xcat.ng,post.xcat.rhels10'* ]]
[[ "$args" == *'--no-parent'* ]]
[[ "$args" == *'postscripts/'* ]]
[[ "$args" != *'<a href='* ]]
}
@test "install scriptlib recursive download rejects dispatcher scripts" {
local wget_log="${BATS_TEST_TMPDIR}/post-xcat-wget.log"
run capture_install_scriptlib_wget "$wget_log"
[ "$status" -eq 0 ]
assert_download_policy "$(read_file_or_empty "$wget_log")"
}
@test "xcatdsklspost recursive download rejects dispatcher scripts" {
local wget_log="${BATS_TEST_TMPDIR}/xcatdsklspost-wget.log"
run capture_xcatdsklspost_wget "$wget_log"
[ "$status" -eq 0 ]
assert_download_policy "$(read_file_or_empty "$wget_log")"
}
+179
View File
@@ -0,0 +1,179 @@
#!/usr/bin/env bats
load 'helpers/shell_source'
setup()
{
PKGUTILS="$(repo_path 'xCAT/postscripts/xcatpkgutils.sh')"
OSPKGS="$(repo_path 'xCAT/postscripts/ospkgs')"
OTHERPKGS="$(repo_path 'xCAT/postscripts/otherpkgs')"
[ -r "$PKGUTILS" ] || skip "$PKGUTILS is required"
[ -r "$OSPKGS" ] || skip "$OSPKGS is required"
[ -r "$OTHERPKGS" ] || skip "$OTHERPKGS is required"
APT_LOG="${BATS_TEST_TMPDIR}/apt-get.log"
export PKGUTILS OSPKGS OTHERPKGS APT_LOG
}
# Every apt-get call is recorded as "<DEBIAN_FRONTEND>|<arguments>" and answers with APT_STATUS.
shadow_apt_get()
{
apt-get()
{
printf '%s|%s\n' "${DEBIAN_FRONTEND:-unset}" "$*" >>"$APT_LOG"
return "${APT_STATUS:-0}"
}
}
apt_call()
{
sed -n "${1}p" "$APT_LOG"
}
apt_calls()
{
wc -l <"$APT_LOG" | tr -d ' '
}
run_ospkgs_apt_block()
{
local block
block="$(extract_line_range "$OSPKGS" '# upgrade existing packages' '# remove packages')" || return 99
local ENVLIST="" groups="" pkgs=" foo bar" cudapkgs="${1:-}" RETURNVAL=0 ARCH=x86_64
eval "$block"
printf 'RETURNVAL=%s\n' "$RETURNVAL"
}
run_otherpkgs_apt_line()
{
local line
line="$(extract_first_matching_line "$OTHERPKGS" "$1")" || return 99
local envlist="" repo_pkgs="foo bar" result=""
eval "$line"
printf 'R=%s\n' "$?"
printf '%s\n' "$result"
}
@test "the postscripts and the package utilities ship executable" {
[ -x "$OSPKGS" ]
[ -x "$OTHERPKGS" ]
[ -x "$PKGUTILS" ]
}
@test "xcat_apt_get runs apt-get unattended and accepts the unsigned xCAT repositories" {
source "$PKGUTILS"
shadow_apt_get
run xcat_apt_get -q install --no-install-recommends foo bar
[ "$status" -eq 0 ]
[ "$(apt_call 1)" = "noninteractive|-y --allow-unauthenticated -q install --no-install-recommends foo bar" ]
[ "$(apt_calls)" -eq 1 ]
}
@test "xcat_apt_get returns the apt-get status" {
source "$PKGUTILS"
shadow_apt_get
APT_STATUS=100 run xcat_apt_get upgrade
[ "$status" -eq 100 ]
}
@test "a pkglist environment prefix reaches apt-get through the eval the postscripts use" {
source "$PKGUTILS"
apt-get()
{
printf '%s\n' "${ACCEPT_EULA:-unset}" >>"$APT_LOG"
}
local ENVLIST="ACCEPT_EULA=y"
run eval "$ENVLIST xcat_apt_get -q install foo"
[ "$status" -eq 0 ]
[ "$(apt_call 1)" = "y" ]
}
@test "ospkgs upgrades and installs through xcat_apt_get without --force-yes" {
source "$PKGUTILS"
shadow_apt_get
run run_ospkgs_apt_block
[ "$status" -eq 0 ]
[[ "$output" == *'RETURNVAL=0'* ]]
[ "$(apt_call 1 | cut -d'|' -f2)" = "-y update" ]
[ "$(apt_call 2)" = "noninteractive|-y --allow-unauthenticated -o Dpkg::Options::=--force-confold -o Dpkg::Options::=--force-confdef upgrade" ]
[ "$(apt_call 3)" = "noninteractive|-y --allow-unauthenticated -q install --no-install-recommends foo bar" ]
[ "$(apt_calls)" -eq 3 ]
! grep -q -- '--force-yes' "$APT_LOG"
}
@test "ospkgs keeps the apt-get failure status and still runs the later steps" {
source "$PKGUTILS"
shadow_apt_get
APT_STATUS=100 run run_ospkgs_apt_block
[ "$status" -eq 0 ]
[[ "$output" == *'RETURNVAL=100'* ]]
[ "$(apt_calls)" -eq 3 ]
}
@test "ospkgs reports a failed cuda package install" {
source "$PKGUTILS"
apt-get()
{
printf '%s|%s\n' "${DEBIAN_FRONTEND:-unset}" "$*" >>"$APT_LOG"
case "$*" in
*cuda*) return 100 ;;
esac
return 0
}
run run_ospkgs_apt_block " cuda-toolkit"
[ "$status" -eq 0 ]
[[ "$output" == *'RETURNVAL=100'* ]]
[ "$(apt_call 4)" = "noninteractive|-y --allow-unauthenticated -q install --no-install-recommends cuda-toolkit" ]
[ "$(apt_calls)" -eq 4 ]
}
run_ospkgs_rpm_cuda_block()
{
local block tail="${BATS_TEST_TMPDIR}/ospkgs-rpm-tail"
sed -n '/#install cuda package if any/,$p' "$OSPKGS" >"$tail"
block="$(extract_shell_if_block "$tail" 'if [ -n "$cudapkgs" ]; then')" || return 99
local ENVLIST="" yumcmd=fake_dnf cudapkgs=" cuda-toolkit" RETURNVAL=0 ARCH=x86_64 debug=0 log_label=ospkgs
logger() { :; }
fake_dnf()
{
printf '%s\n' "$*" >>"$APT_LOG"
return 100
}
eval "$block"
printf 'RETURNVAL=%s\n' "$RETURNVAL"
}
@test "ospkgs reports a failed cuda package install on yum and dnf nodes" {
run run_ospkgs_rpm_cuda_block
[ "$status" -eq 0 ]
[[ "$output" == *'RETURNVAL=100'* ]]
[ "$(apt_call 1)" = "-y install cuda-toolkit" ]
[ "$(apt_calls)" -eq 1 ]
}
@test "otherpkgs upgrades through xcat_apt_get without --force-yes" {
source "$PKGUTILS"
shadow_apt_get
run run_otherpkgs_apt_line 'result=`eval [$]envlist .*Dpkg::Options.* upgrade 2>&1`'
[ "$status" -eq 0 ]
[[ "$output" == *'R=0'* ]]
[ "$(apt_call 1)" = "noninteractive|-y --allow-unauthenticated -o Dpkg::Options::=--force-confold -o Dpkg::Options::=--force-confdef upgrade" ]
[ "$(apt_calls)" -eq 1 ]
}
@test "otherpkgs installs through xcat_apt_get without --force-yes" {
source "$PKGUTILS"
shadow_apt_get
run run_otherpkgs_apt_line 'result=`eval [$]envlist .*Dpkg::Options.* install [$]repo_pkgs 2>&1`'
[ "$status" -eq 0 ]
[[ "$output" == *'R=0'* ]]
[ "$(apt_call 1)" = "noninteractive|-y --allow-unauthenticated -q -o Dpkg::Options::=--force-confold -o Dpkg::Options::=--force-confdef install foo bar" ]
[ "$(apt_calls)" -eq 1 ]
}
+88
View File
@@ -0,0 +1,88 @@
#!/usr/bin/env bats
bats_require_minimum_version 1.5.0
load 'helpers/shell_source'
setup()
{
XCATLIB="$(repo_path 'xCAT/postscripts/xcatlib.sh')"
[ -r "$XCATLIB" ] || skip "$XCATLIB is required"
export XCATLIB
}
run_restart_fallback()
{
local poll_count=0
ps()
{
cat <<'EOF'
root 4321 0.0 0.0 15432 2048 ? Ss 10:00 0:00 /usr/sbin/sshd
EOF
}
kill()
{
if [ "$1" = "-9" ]; then
printf 'kill %s\n' "$*" >>"$EVENT_LOG"
return 0
fi
poll_count=$((poll_count + 1))
if [ "$poll_count" -eq 1 ]; then
printf 'poll %s alive\n' "$2" >>"$EVENT_LOG"
return 0
fi
printf 'poll %s gone\n' "$2" >>"$EVENT_LOG"
return 1
}
sleep() { :; }
sshd()
{
printf '%s\n' start >>"$EVENT_LOG"
}
source "$XCATLIB"
xcat_restart_sshd_after_failed_service_restart sshd
}
run_wait_for_processes()
{
local rc
source "$XCATLIB"
xcat_wait_for_processes_to_exit "$*" 3
rc=$?
printf '%s\n' "$rc"
return 0
}
@test "remoteshell restart fallback kills, waits, then starts sshd" {
EVENT_LOG="${BATS_TEST_TMPDIR}/events.log"
export EVENT_LOG
run run_restart_fallback
[ "$status" -eq 0 ]
[ "$(read_file_or_empty "$EVENT_LOG")" = $'kill -9 4321\npoll 4321 alive\npoll 4321 gone\nstart' ]
run -1 grep -Eq '^kill 9( |$)' "$EVENT_LOG"
}
@test "remoteshell wait loop reports a still-running process and gives up" {
local child
sleep 30 &
child=$!
run run_wait_for_processes "$child"
kill -KILL "$child" 2>/dev/null || true
wait "$child" 2>/dev/null || true
[ "$status" -eq 0 ]
[ "$output" = "1" ]
}
@test "remoteshell wait loop returns as soon as killed processes are gone" {
run run_wait_for_processes 999999
[ "$status" -eq 0 ]
[ "$output" = "0" ]
}
+47
View File
@@ -0,0 +1,47 @@
#!/usr/bin/env bats
bats_require_minimum_version 1.5.0
load 'helpers/shell_source'
setup()
{
SCRIPT_LIB="$(repo_path 'xCAT-server/share/xcat/install/scripts/scriptlib')"
[ -r "$SCRIPT_LIB" ] || skip "$SCRIPT_LIB is required"
export SCRIPT_LIB
}
@test "SLES 11 UEFI install changes the AutoYaST bootloader to elilo" {
local cmdline="${BATS_TEST_TMPDIR}/cmdline"
local profile="${BATS_TEST_TMPDIR}/modified.xml"
printf '%s\n' 'BOOT_IMAGE=/linux install=http://192.0.2.10/install/sles11/ppc64le' >"$cmdline"
cat >"$profile" <<'EOF'
<bootloader>
<lba_support config:type="boolean">true</lba_support>
<linear config:type="boolean">true</linear>
<location>mbr</location>
</bootloader>
EOF
source "$SCRIPT_LIB"
run set_sles11_uefi_bootloader "$cmdline" "$profile"
[ "$status" -eq 0 ]
grep -Fxq '<loader_type>elilo</loader_type>' "$profile"
run -1 grep -q '<location>mbr</location>' "$profile"
run -1 grep -q '<lba_support ' "$profile"
run -1 grep -q '<linear ' "$profile"
}
@test "non-SLES 11 install media keeps the legacy bootloader template value" {
local cmdline="${BATS_TEST_TMPDIR}/cmdline"
local profile="${BATS_TEST_TMPDIR}/modified.xml"
printf '%s\n' 'BOOT_IMAGE=/linux install=http://192.0.2.10/install/sles15/ppc64le' >"$cmdline"
printf '%s\n' '<location>mbr</location>' >"$profile"
source "$SCRIPT_LIB"
run set_sles11_uefi_bootloader "$cmdline" "$profile"
[ "$status" -eq 0 ]
grep -Fxq '<location>mbr</location>' "$profile"
}
+40
View File
@@ -0,0 +1,40 @@
#!/usr/bin/env bats
load 'helpers/shell_source'
setup()
{
ADD_SSH="$(repo_path 'xCAT-server/share/xcat/netboot/add-on/statelite/add_ssh')"
[ -r "$ADD_SSH" ] || skip "$ADD_SSH is required"
export ADD_SSH
}
run_sshd_config_block()
{
local root="$1"
local block
block="$(extract_shell_if_block "$ADD_SSH" 'if [ -r $ROOTDIR/etc/ssh/sshd_config ]')" || return 1
ROOTDIR="$root"
eval "$block"
}
@test "statelite add_ssh writes sshd settings below systemd's open-file limit" {
local root="${BATS_TEST_TMPDIR}/rootimg"
local sshd_config="${root}/etc/ssh/sshd_config"
mkdir -p "${root}/etc/ssh"
cat >"$sshd_config" <<'EOF'
X11Forwarding no
KeyRegenerationInterval 3600
MaxStartups 1024
EOF
run run_sshd_config_block "$root"
[ "$status" -eq 0 ]
grep -Fxq 'X11Forwarding yes' "$sshd_config"
grep -Fxq 'KeyRegenerationInterval 0' "$sshd_config"
grep -Fxq '#MaxStartups 1024' "$sshd_config"
grep -Fxq 'MaxStartups 100:30:200' "$sshd_config"
! grep -Fxq 'MaxStartups 1024' "$sshd_config"
}
+5
View File
@@ -46,6 +46,11 @@ so putting a test in `integration/` does not cost it CI coverage. What differs i
each suite is allowed to depend on, and that unit tests also run standalone from a
bare checkout with no xCAT at all.
Shell-script unit tests belong in [`../bats`](../bats/README.md)
and run with BATS. Do not add Perl `.t` tests that grep shell source when the
behavior can be exercised by sourcing a shell library or script and shadowing the
external commands it calls.
The distinction matters because a test that needs an absent environment does not fail
-- it calls `plan skip_all` and reports as skipped. A handful of those in a suite of
several hundred assertions is easy to stop reading. Keeping the two kinds in separate
+11 -7
View File
@@ -60,15 +60,19 @@ is_deeply( [deb_package_arches('perl-xCAT')], ['all'],
'a Perl package is built once, arch-independent' );
is_deeply( [deb_package_arches('xCAT-probe')], ['all'],
'xCAT-probe is arch-independent too' );
for my $pkg (qw(xCAT xCATsn xCAT-genesis-scripts)) {
is_deeply( [deb_package_arches($pkg)], ['amd64', 'ppc64el'],
"$pkg is built per architecture" );
for my $pkg (qw(xCAT xCATsn)) {
is_deeply( [deb_package_arches($pkg)], ['amd64', 'ppc64el', 'riscv64'],
"$pkg is built for every architecture a management node runs on" );
}
# xCAT-genesis-scripts has one control file per architecture and there is no riscv64 one,
# so asking for that build would stop the whole run.
is_deeply( [deb_package_arches('xCAT-genesis-scripts')], ['amd64', 'ppc64el'],
'xCAT-genesis-scripts is built only for the architectures it has a control file for' );
is_deeply( [deb_package_arches(undef)], ['all'],
'an undefined package name does not blow up the arch lookup' );
is_deeply( [dist_arches('noble')], ['amd64', 'ppc64el'],
'a current release serves both architectures' );
is_deeply( [dist_arches('noble')], ['amd64', 'ppc64el', 'riscv64'],
'a current release serves every architecture' );
is_deeply( [dist_arches('saucy')], ['amd64'],
'saucy predates ppc64el and serves only amd64' );
@@ -151,8 +155,8 @@ is( scalar( () = $rewritten =~ /^ -- xCAT Build /mg ), 1,
my $dists = reprepro_distributions([qw(focal noble)], 'DEADBEEF');
is( scalar(() = $dists =~ /^Codename:/mg), 2, 'one stanza per release' );
like( $dists, qr/^Codename: focal\nArchitectures: amd64 ppc64el$/m,
'a release declares both architectures, on the line after its codename' );
like( $dists, qr/^Codename: focal\nArchitectures: amd64 ppc64el riscv64$/m,
'a release declares every architecture, on the line after its codename' );
is( scalar(() = $dists =~ /^SignWith: DEADBEEF$/mg), 2,
'every stanza is signed when a key is given' );
+88
View File
@@ -0,0 +1,88 @@
#!/usr/bin/env perl
use strict;
use warnings;
use File::Spec;
use File::Temp qw(tempdir);
use FindBin;
use Test::More;
# A riscv64 management node needs an xcat and xcatsn deb built for the architecture and a
# mklocalrepo.sh that points the host at the matching repository instead of amd64.
#
# The generated script is extracted from builddebs.pl and run with a stub uname ahead of
# $PATH, so the mapping under test is the shipped code. Only the path it writes is
# redirected into the sandbox, because it writes an apt source list.
my $repo_root = File::Spec->rel2abs( File::Spec->catdir( $FindBin::Bin, '..', '..' ) );
my $builder = File::Spec->catfile( $repo_root, 'builddebs.pl' );
plan skip_all => "builddebs.pl not found" unless -f $builder;
my $src = do { local $/; open my $fh, '<', $builder or die $!; <$fh> };
# BAIL_OUT rather than skip: a rename that stops this matching must fail loudly instead of
# silently covering nothing.
my ($script) = $src =~ /write_script\("\$repodir\/mklocalrepo\.sh", <<'SCRIPT'\);\n(.*?)\nSCRIPT\n/ms;
BAIL_OUT('could not extract mklocalrepo.sh from builddebs.pl') unless defined $script;
my $dir = tempdir( CLEANUP => 1 );
my $run = 0;
# Run the generated script for one host architecture and return the apt source line it wrote.
sub sources_line_for {
my ($uname) = @_;
$run++;
my $root = File::Spec->catdir( $dir, "run$run" );
mkdir $root;
mkdir "$root/bin";
open( my $stub, '>', "$root/bin/uname" ) or die $!;
print {$stub} "#!/bin/bash\necho $uname\n";
close($stub);
chmod 0755, "$root/bin/uname";
my $release = File::Spec->catfile( $root, 'lsb-release' );
open( my $rel, '>', $release ) or die $!;
print {$rel} "DISTRIB_CODENAME=noble\n";
close($rel);
my $listed = File::Spec->catfile( $root, 'sources.list' );
( my $sandboxed = $script ) =~ s{/etc/lsb-release}{$release};
$sandboxed =~ s{/etc/apt/sources\.list\.d/\S+}{$listed};
my $harness = File::Spec->catfile( $root, 'harness.sh' );
open( my $fh, '>', $harness ) or die $!;
print {$fh} "#!/bin/bash\n$sandboxed\n";
close($fh);
local $ENV{PATH} = "$root/bin:$ENV{PATH}";
system( '/bin/bash', $harness );
open( my $out, '<', $listed ) or die $!;
my $line = do { local $/; <$out> };
close($out);
return $line;
}
for my $case (
[ 'riscv64', 'riscv64' ],
[ 'ppc64le', 'ppc64el' ],
[ 'x86_64', 'amd64' ],
)
{
my ( $uname, $want ) = @$case;
like( sources_line_for($uname), qr/^deb \[arch=\Q$want\E\] /,
"mklocalrepo.sh gives a $uname host the $want repository" );
}
# The debs themselves must exist for the architecture.
for my $case ( [ 'xCAT', 'xcat' ], [ 'xCATsn', 'xcatsn' ] ) {
my ( $component, $package ) = @$case;
my $control = File::Spec->catfile( $repo_root, $component, 'debian', 'control' );
my $text = do { local $/; open my $fh, '<', $control or die $!; <$fh> };
my ($arches) = $text =~ /^Package: \Q$package\E\nArchitecture: (.*)$/m;
ok( defined $arches, "$package declares an architecture" );
like( $arches || '', qr/\briscv64\b/, "$package is built for riscv64" );
like( $arches || '', qr/\bamd64\b.*\bppc64el\b/, "$package keeps amd64 and ppc64el" );
}
done_testing();
+9
View File
@@ -16,6 +16,11 @@ use xCAT::Utils;
# --- what debootstrap and the package lists are given ----------------------
is(xCAT::Utils->debian_arch('x86_64'), 'amd64',
'Debian calls x86_64 amd64');
# genimage passes this value to debootstrap --arch and reads it to pick the apt mirror, and
# debootstrap knows i386, not xCAT's x86.
is(xCAT::Utils->debian_arch('x86'), 'i386',
'debian_arch: the 32-bit x86 token becomes i386');
is(xCAT::Utils->debian_arch('ppc64el'), 'ppc64el',
'the Debian name for POWER LE is unchanged');
is(xCAT::Utils->debian_arch('ppc64le'), 'ppc64le',
@@ -38,6 +43,10 @@ is(xCAT::Utils->xcat_arch_from_debian('ppc64el'), 'ppc64el',
'POWER LE media keeps the Debian name xCAT uses for Ubuntu');
is(xCAT::Utils->xcat_arch_from_debian('powerpc'), 'ppc64',
'POWER BE media installs ppc64 nodes');
is(xCAT::Utils->xcat_arch_from_debian('riscv64'), 'riscv64',
'riscv64 media installs riscv64 nodes');
is(xCAT::Utils->debian_arch('riscv64'), 'riscv64',
'Debian and xCAT agree on the riscv64 name');
is(xCAT::Utils->xcat_arch_from_debian('nonesuch'), undef,
'media xCAT has no name for resolves to nothing');
is(xCAT::Utils->xcat_arch_from_debian(''), undef,
@@ -0,0 +1,73 @@
#!/usr/bin/env perl
use strict;
use warnings;
use File::Spec;
use FindBin;
use Test::More;
# dh_perl adds no module dependencies, unlike the rpm generator, so a deb declares only what its
# control file names. perl-xcat declared libhtml-form-perl alone while its files load nine other
# modules at top level, xcat-server shipped the REST API without CGI, xcat-client ships z/VM
# helpers that load Capture::Tiny, and nmap and ipmitool-xcat were recommendations where the rpm
# packages require them.
my $repo_root = File::Spec->rel2abs( File::Spec->catdir( $FindBin::Bin, '..', '..' ) );
# The relation fields of one binary package stanza as sets of package names, plus the version
# bound of each relation that has one.
sub relations_of {
my ( $source, $package ) = @_;
my $file = File::Spec->catfile( $repo_root, $source, 'debian', 'control' );
open( my $fh, '<', $file ) or die "Unable to read $file: $!";
my $control = do { local $/; <$fh> };
close($fh);
my ($stanza) = $control =~ /^Package:\s*\Q$package\E\s*\n(.*?)(?:\n\n|\z)/ms;
die "no stanza for $package in $file" unless defined $stanza;
my %rel;
foreach my $field (qw(Depends Recommends)) {
my ($line) = $stanza =~ /^$field:\s*(.*)$/m;
foreach my $entry ( split( /[,|]/, $line // '' ) ) {
my ( $name, $bound ) = $entry =~ /^\s*(\S+?)\s*(?:\(([^)]*)\))?\s*(?:\[[^\]]*\])?\s*$/;
next unless defined $name;
$rel{$field}{$name} = 1;
$rel{version}{$name} = $bound if defined $bound;
}
}
return \%rel;
}
sub depends_on {
my ( $rel, $name, $label ) = @_;
ok( $rel->{Depends}{$name}, "$label depends on $name" );
ok( !$rel->{Recommends}{$name}, "... and no longer only recommends it" ) if $rel->{Recommends}{$name};
}
my $perl_xcat = relations_of( 'perl-xCAT', 'perl-xcat' );
depends_on( $perl_xcat, $_, 'perl-xcat' ) for qw(
libhtml-form-perl libxml-simple-perl libxml-parser-perl libio-socket-ssl-perl libdbi-perl libjson-perl
libwww-perl libxml-libxml-perl libexpect-perl libsnmp-perl libsocket6-perl libio-socket-inet6-perl
);
my $client = relations_of( 'xCAT-client', 'xcat-client' );
depends_on( $client, 'libcapture-tiny-perl', 'xcat-client' );
my $server = relations_of( 'xCAT-server', 'xcat-server' );
depends_on( $server, 'libcgi-pm-perl', 'xcat-server' );
# nmap and ipmitool-xcat are hard requirements on EL and were only recommended here.
my $xcat = relations_of( 'xCAT', 'xcat' );
my $xcatsn = relations_of( 'xCATsn', 'xcatsn' );
foreach my $case ( [ $xcat, 'xcat' ], [ $xcatsn, 'xcatsn' ], [ $client, 'xcat-client' ] ) {
my ( $rel, $label ) = @$case;
ok( $rel->{Depends}{nmap}, "$label depends on nmap" );
ok( !$rel->{Recommends}{nmap}, "... and does not recommend it as well" );
}
foreach my $case ( [ $xcat, 'xcat' ], [ $xcatsn, 'xcatsn' ] ) {
my ( $rel, $label ) = @$case;
ok( $rel->{Depends}{'ipmitool-xcat'}, "$label depends on ipmitool-xcat" );
ok( !$rel->{Recommends}{'ipmitool-xcat'}, "... and does not recommend it as well" );
is( $rel->{version}{'ipmitool-xcat'}, '>= 1.8.18-4', "... at the floor xCAT.spec requires" );
}
done_testing();
+53
View File
@@ -0,0 +1,53 @@
#!/usr/bin/env perl
use strict;
use warnings;
use File::Path qw(make_path);
use File::Temp qw(tempdir);
use FindBin;
use Test::More;
# mkinstall logged "Unknown arch" for every architecture other than x86_64, x86, ppc64le and
# ppc64el, so each diskful riscv64 install produced a false message. install_darch names the
# architectures xCAT installs Ubuntu on and the Debian name each one maps to.
# xCAT modules put $::XCATROOT/lib/perl ahead of @INC as they compile, so on a host with xCAT
# installed the modules loaded after the first one would come from /opt/xcat. XCATROOT points
# at this checkout before any of them compiles.
BEGIN {
my $root = tempdir( CLEANUP => 1 );
make_path("$root/lib");
symlink( "$FindBin::Bin/../../perl-xCAT", "$root/lib/perl" ) or die "symlink: $!";
$ENV{XCATROOT} = $root;
}
BEGIN {
package xCAT::TableUtils;
our $tftpdir;
sub getTftpDir { return $tftpdir; }
$INC{'xCAT/TableUtils.pm'} = __FILE__;
}
use lib "$FindBin::Bin/../../perl-xCAT";
use lib "$FindBin::Bin/../../xCAT-server/lib/perl";
my $plugin = "$FindBin::Bin/../../xCAT-server/lib/xcat/plugins/debian.pm";
require $plugin;
my @cases = (
# arch Debian name installs Ubuntu
[ 'x86_64', 'amd64', 1 ],
[ 'x86', 'i386', 1 ],
[ 'ppc64le', 'ppc64le', 1 ],
[ 'ppc64el', 'ppc64el', 1 ],
[ 'riscv64', 'riscv64', 1 ],
[ 'aarch64', 'aarch64', 0 ],
[ 'sparc', 'sparc', 0 ],
);
foreach my $case (@cases) {
my ( $arch, $darch, $known ) = @$case;
my ( $got_darch, $got_known ) = xCAT_plugin::debian::install_darch($arch);
is( $got_darch, $darch, "$arch installs from the $darch tree" );
is( $got_known, $known, $known ? "... and is an architecture xCAT installs Ubuntu on" : "... and is reported as unknown" );
}
done_testing();
@@ -91,6 +91,18 @@ is(
'POWER does not accept the x86 live layout',
);
# --- riscv64 -------------------------------------------------------------
is(
resolved('riscv64', 'riscv64', media('casper/vmlinux', 'casper/initrd')),
'casper/vmlinux|casper/initrd',
'the riscv64 live image keeps its kernel under a different name',
);
is(
resolved('riscv64', 'riscv64', media('casper/vmlinuz', 'casper/initrd')),
undef,
'riscv64 does not accept the kernel name the other live images use',
);
# --- nothing to boot -------------------------------------------------------
is(resolved('x86_64', 'amd64', media('casper/vmlinuz')), undef,
'a kernel without its initrd is not a match');
+134 -2
View File
@@ -52,8 +52,8 @@ my $marker = File::Spec->catfile( tempdir( @fixture_dir, CLEANUP => 1 ), 'tcpdum
sub fixture_bin {
my (%with) = @_;
my $bindir = tempdir( @fixture_dir, CLEANUP => 1 );
write_script( "$bindir/tcpdump", qq{printf '%s\\n' "\$0" "\$*" > '$marker'\nexit 0\n} );
foreach my $tool (qw(awk head grep ps)) {
write_script( "$bindir/tcpdump", qq{printf '%s\\n' "\$0" "\$*" > '$marker'\ntrap 'exit 0' TERM\nwhile :; do sleep 1; done\n} );
foreach my $tool (qw(awk head grep ps sleep)) {
my $real = xCAT::CommandUtils::find_executable($tool) or next;
symlink( $real, "$bindir/$tool" ) or die "symlink $tool: $!";
}
@@ -142,4 +142,136 @@ SKIP: {
is( $args, "-i lo port 68 -n -vvvvvv", '... with the capture arguments' );
}
# ---- capture lifecycle: a private dump file, tcpdump run directly and stopped by pid ------------
# Both copies wrote /tmp/dhcpdumpfile.log, ran tcpdump behind a shell, killed every tcpdump on the
# interface by pattern, and reported zero servers when tcpdump failed to start. The fake tcpdump
# below records who started it and where its output goes, waits for the TERM the script owes it,
# and a second one fails at once. ps records any use, since the scripts no longer need it.
sub lifecycle_bin {
my (%with) = @_;
my $bindir = tempdir( @fixture_dir, CLEANUP => 1 );
my $record = "$with{record}";
if ( $with{fail} ) {
write_script( "$bindir/tcpdump", qq{printf 'started\\n' >> '$record'\nexit 1\n} );
} elsif ( $with{quit} ) {
write_script( "$bindir/tcpdump", qq{printf 'started\\n' >> '$record'\nexit 0\n} );
} elsif ( $with{killed} ) {
write_script( "$bindir/tcpdump", qq{printf 'started\\n' >> '$record'\nkill -9 \$\$\n} );
} elsif ( $with{stubborn} ) {
write_script( "$bindir/tcpdump", qq{trap '' TERM\nprintf 'started\\n' >> '$record'\nwhile :; do sleep 1; done\n} );
} else {
write_script( "$bindir/tcpdump",
qq{parent=\$(cat /proc/\$PPID/comm 2>/dev/null)\n}
. qq{out=\$(readlink /proc/\$\$/fd/1 2>/dev/null)\n}
. qq{printf 'pid=%s ppid=%s parent=%s out=%s\\n' "\$\$" "\$PPID" "\$parent" "\$out" >> '$record'\n}
. qq{trap 'printf "TERM\\n" >> "$record"; exit 0' TERM\n}
. qq{while :; do sleep 1; done\n} );
}
write_script( "$bindir/ps", qq{printf 'ps %s\\n' "\$*" >> '$record.ps'\nexit 0\n} );
foreach my $tool (qw(awk head grep cat readlink sleep)) {
my $real = xCAT::CommandUtils::find_executable($tool) or next;
symlink( $real, "$bindir/$tool" ) or die "symlink $tool: $!";
}
symlink( $with{real_ip}, "$bindir/ip" ) or die "symlink ip: $!";
return $bindir;
}
sub run_isolated_status {
my ( $ns, $bindir, $tmpdir, $script, @args ) = @_;
my $command = "env PATH='$bindir' XCATROOT='$root' TMPDIR='$tmpdir' " . script_command( $script, @args );
my $shell = "$ns->{unshare} $ns->{flags} sh -c \"$ns->{setup} && exec $command\" 2>&1";
my $out = `$shell`;
return ( $out, $? >> 8 );
}
sub slurp_lines {
my ($path) = @_;
open( my $fh, '<', $path ) or return;
chomp( my @lines = <$fh> );
close($fh);
return @lines;
}
SKIP: {
skip 'the private /tmp would hide this checkout or its fixtures', 44
if index( $repo, '/tmp/' ) == 0 || !@fixture_dir;
my $ns = isolation();
skip 'no private network namespace on this host', 44 unless $ns;
foreach my $case ( [ $tools, 'the tool', '-t' ], [ $probe, 'the probe', '-d' ] ) {
my ( $script, $label, $window ) = @$case;
my $tmpdir = tempdir( @fixture_dir, CLEANUP => 1 );
my $record = File::Spec->catfile( tempdir( @fixture_dir, CLEANUP => 1 ), 'tcpdump.record' );
my $bindir = lifecycle_bin( record => $record, real_ip => $ns->{ip} );
my ( $out, $status ) = run_isolated_status( $ns, $bindir, $tmpdir, $script, '-i', 'lo', '-m', $mac, $window, '1' );
is( $status, 0, "$label exits 0 after a capture window" );
my @rec = slurp_lines($record);
my ($start) = grep { /^pid=/ } @rec;
ok( defined $start, "$label started tcpdump" ) or diag($out);
like( $start // '', qr/ parent=perl /, '... directly from the script, with no shell in between' );
like( $start // '', qr{ out=\Q$tmpdir\E/detect_dhcpd\.\w+$}, '... writing a private capture file under TMPDIR' );
ok( ( grep { $_ eq 'TERM' } @rec ), '... and stopped it with TERM when the window ended' );
ok( !-e "$record.ps", '... without searching the process table' );
my @left = glob("$tmpdir/detect_dhcpd.*");
is( scalar(@left), 0, '... and removed the capture file on exit' );
my $failing = lifecycle_bin( record => $record, real_ip => $ns->{ip}, fail => 1 );
( $out, $status ) = run_isolated_status( $ns, $failing, $tmpdir, $script, '-i', 'lo', '-m', $mac, $window, '1' );
is( $status, 1, "$label exits 1 when tcpdump fails to start" ) or diag($out);
like( $out, qr/tcpdump ended before the capture window did, with status 1|Capture the packets by tcpdump/, '... and says the capture failed' );
unlike( $out, qr/0 servers repl/, '... instead of reporting zero servers' );
@left = glob("$tmpdir/detect_dhcpd.*");
is( scalar(@left), 0, '... and leaves no capture file behind' );
my $quitting = lifecycle_bin( record => $record, real_ip => $ns->{ip}, quit => 1 );
( $out, $status ) = run_isolated_status( $ns, $quitting, $tmpdir, $script, '-i', 'lo', '-m', $mac, $window, '1' );
is( $status, 1, "$label exits 1 when tcpdump quits early with status 0" ) or diag($out);
unlike( $out, qr/0 servers repl/, '... instead of reporting zero servers' );
my $dying = lifecycle_bin( record => $record, real_ip => $ns->{ip}, killed => 1 );
( $out, $status ) = run_isolated_status( $ns, $dying, $tmpdir, $script, '-i', 'lo', '-m', $mac, $window, '1' );
is( $status, 1, "$label exits 1 when tcpdump dies on another signal" ) or diag($out);
like( $out, qr/on signal 9|Capture the packets by tcpdump/, '... and names the signal or the failed step' );
my $stubborn = lifecycle_bin( record => $record, real_ip => $ns->{ip}, stubborn => 1 );
my $began = time;
( $out, $status ) = run_isolated_status( $ns, $stubborn, $tmpdir, $script, '-i', 'lo', '-m', $mac, $window, '1' );
is( $status, 1, "$label exits 1 when tcpdump ignores TERM" ) or diag($out);
cmp_ok( time - $began, '<', 20, '... after a bounded grace period' );
like( $out, qr/on signal 9|Capture the packets by tcpdump/, '... having killed it' );
# An interrupt while the capture runs: the script is signalled once the fake tcpdump has
# reported the script's pid, and must stop tcpdump and remove the file on its way out.
my $int_record = File::Spec->catfile( tempdir( @fixture_dir, CLEANUP => 1 ), 'tcpdump.record' );
my $int_bin = lifecycle_bin( record => $int_record, real_ip => $ns->{ip} );
my $runner = fork;
die "fork: $!" unless defined $runner;
if ( $runner == 0 ) {
my ( undef, $st ) = run_isolated_status( $ns, $int_bin, $tmpdir, $script, '-i', 'lo', '-m', $mac, $window, '30' );
exit $st;
}
my $started;
foreach ( 1 .. 150 ) {
($started) = grep { /^pid=/ } slurp_lines($int_record);
last if $started;
select( undef, undef, undef, 0.2 );
}
my ($tcpdump_pid) = ( $started // '' ) =~ /^pid=(\d+)/;
my ($script_pid) = ( $started // '' ) =~ / ppid=(\d+)/;
ok( $script_pid, "$label reports the pid to interrupt" ) or diag( $started // 'tcpdump never started' );
if ($script_pid) {
kill 'INT', $script_pid;
select( undef, undef, undef, 0.3 );
kill 'INT', $script_pid;
}
waitpid( $runner, 0 );
is( $? >> 8, 1, '... and exits 1 on a double interrupt during the capture' );
ok( ( grep { $_ eq 'TERM' } slurp_lines($int_record) ), '... after stopping tcpdump' );
ok( !( $tcpdump_pid && kill( 0, $tcpdump_pid ) ), '... which is gone' );
@left = glob("$tmpdir/detect_dhcpd.*");
is( scalar(@left), 0, '... and the capture file is removed' );
}
}
done_testing();
-40
View File
@@ -1,40 +0,0 @@
#!/usr/bin/env perl
use strict;
use warnings;
use File::Spec;
use FindBin;
use Test::More;
my $repo_root = File::Spec->rel2abs(File::Spec->catdir($FindBin::Bin, '..', '..'));
my $spec = read_file('xCAT-genesis-builder/xCAT-genesis-base.spec');
like($spec, qr/^BuildRequires:\s+kernel-core$/m, 'genesis build installs the kernel core');
like($spec, qr/^BuildRequires:\s+kernel-modules$/m, 'genesis build installs the standard kernel modules');
like($spec, qr/^BuildRequires:\s+kernel-modules-extra$/m, 'genesis build installs the extra kernel modules');
my $dracut_module = read_file('xCAT-genesis-builder/dracut_105/el/module-setup.sh');
like(
$dracut_module,
qr/installkernel\(\) \{.*?modules_dep=.*?while IFS= read -r modfile;.*?instmods "\$modname".*?done < "\$modules_dep"/s,
'genesis dracut module installs every module available to the build'
);
my $doxcat = read_file('xCAT-genesis-scripts/usr/bin/doxcat');
like($doxcat, qr/^modprobe ib_ipoib$/m, 'genesis loads the IP over InfiniBand module');
like(
$doxcat,
qr/if \[ -z "\$bootnic" \]; then\s+bootnic=`ip link show\|grep -B1 infiniband/s,
'genesis checks InfiniBand addresses only after the Ethernet lookup misses'
);
done_testing();
sub read_file {
my ($file) = @_;
my $path = File::Spec->catfile($repo_root, split m{/}, $file);
open(my $fh, '<', $path) or die "open $path: $!";
my $contents = do { local $/; <$fh> };
close($fh) or die "close $path: $!";
return $contents;
}
-165
View File
@@ -1,165 +0,0 @@
#!/usr/bin/env perl
use strict;
use warnings;
use File::Path qw(make_path);
use File::Temp qw(tempdir);
use FindBin;
use Test::More;
my $go_xcat = "$FindBin::Bin/../../xCAT-server/share/xcat/tools/go-xcat";
my $tmpdir = tempdir(CLEANUP => 1);
my $driver = "$tmpdir/driver.sh";
open(my $driver_fh, '>', $driver) or die "open $driver: $!";
print {$driver_fh} <<'DRIVER';
#!/bin/bash
set -euo pipefail
function_body=$(
for function_name in \
add_xcat_dep_common_repo_yum_or_zypper \
xcat_dep_common_repo_configured \
refresh_xcat_dep_repository_ids
do
awk -v name="$function_name" '
$0 == "function " name "()" { copy = 1 }
copy { print }
copy && /^}$/ { exit }
' "$GO_XCAT_SOURCE"
done
)
eval "$function_body"
TMP_DIR=$TEST_TMP
GO_XCAT_DEFAULT_BASE_URL=https://repo.example.invalid
GO_XCAT_DEP_REPOSITORY_IDS=(xcat-dep)
yum() { :; }
download_file() {
printf '%s\n' "$1" >>"$DOWNLOAD_LOG"
[[ ${COMMON_PRESENT:-0} == 1 ]] || return 1
: >"$2"
}
add_repo_by_url_yum_or_zypper() {
printf '%s %s\n' "$1" "$2" >>"$ADD_LOG"
}
xcat_dep_common_repo_configured() { [[ -s "$ADD_LOG" ]]; }
( add_xcat_dep_common_repo_yum_or_zypper "$@" )
refresh_xcat_dep_repository_ids
printf '%s\n' "${GO_XCAT_DEP_REPOSITORY_IDS[*]}" >"$ID_LOG"
DRIVER
close($driver_fh) or die "close $driver: $!";
chmod(0755, $driver) or die "chmod $driver: $!";
sub run_case {
my ($name, $present, @arguments) = @_;
my $case_dir = "$tmpdir/$name";
make_path($case_dir);
local %ENV = (
%ENV,
ADD_LOG => "$case_dir/add.log",
COMMON_PRESENT => $present,
DOWNLOAD_LOG => "$case_dir/download.log",
GO_XCAT_SOURCE => $go_xcat,
ID_LOG => "$case_dir/id.log",
TEST_TMP => $case_dir,
);
my $status = system('bash', $driver, @arguments);
return ($status >> 8, $case_dir);
}
sub read_file {
my ($path) = @_;
return '' unless -f $path;
open(my $fh, '<', $path) or die "open $path: $!";
my $content = do { local $/; <$fh> };
close($fh) or die "close $path: $!";
return $content;
}
my ($status, $case_dir) = run_case('remote-present', 1, '', 'latest');
is($status, 0, 'an available common repository is accepted');
is(
read_file("$case_dir/download.log"),
"https://repo.example.invalid/yum/latest/xcat-dep/common/repodata/repomd.xml\n",
'the default repository is probed before it is enabled',
);
is(
read_file("$case_dir/add.log"),
"https://repo.example.invalid/yum/latest/xcat-dep/common xcat-dep-common\n",
'the common repository uses its own repository ID',
);
is(read_file("$case_dir/id.log"), "xcat-dep xcat-dep-common\n",
'common packages are included in repository listings');
($status, $case_dir) = run_case('remote-missing', 0, '', '2.18');
is($status, 0, 'a release without the common repository remains usable');
is(read_file("$case_dir/add.log"), '', 'a missing common repository is not enabled');
is(read_file("$case_dir/id.log"), "xcat-dep\n",
'legacy package listings remain unchanged when common is absent');
($status, $case_dir) = run_case(
'repo-file', 1, 'https://repo.example.invalid/custom/xcat-dep.repo', 'latest'
);
is($status, 0, 'a custom repository file remains supported');
is(read_file("$case_dir/download.log"), '',
'the common URL is not guessed from a custom repository file');
is(read_file("$case_dir/add.log"), '',
'a custom repository file does not enable an unrelated repository');
my $local_root = "$tmpdir/local-repository";
make_path("$local_root/common/repodata");
open(my $repomd_fh, '>', "$local_root/common/repodata/repomd.xml") or die $!;
close($repomd_fh) or die $!;
($status, $case_dir) = run_case('local-present', 0, $local_root, 'latest');
is($status, 0, 'a local common repository is accepted');
is(
read_file("$case_dir/add.log"),
"$local_root/common xcat-dep-common\n",
'the local common repository is enabled beside the distribution repository',
);
my $template_driver = "$tmpdir/template-driver.sh";
open(my $template_fh, '>', $template_driver) or die "open $template_driver: $!";
print {$template_fh} <<'DRIVER';
#!/bin/bash
set -euo pipefail
function_body=$(
awk '
/^function add_repo_by_url_yum_or_zypper\(\)/ { copy = 1 }
copy { print }
copy && /^}$/ { exit }
' "$GO_XCAT_SOURCE"
)
eval "$function_body"
TMP_DIR=$TEST_TMP
GO_XCAT_DEFAULT_INSTALL_PATH=/install/xcat
yum() { :; }
add_repo_by_file() { cp "$1" "$REPO_LOG"; }
add_repo_by_url_yum_or_zypper \
https://repo.example.invalid/xcat-dep/common xcat-dep-common optional
DRIVER
close($template_fh) or die "close $template_driver: $!";
chmod(0755, $template_driver) or die "chmod $template_driver: $!";
my $template_log = "$tmpdir/generated-common.repo";
local %ENV = (
%ENV,
GO_XCAT_SOURCE => $go_xcat,
REPO_LOG => $template_log,
TEST_TMP => $tmpdir,
);
$status = system('bash', $template_driver);
is($status >> 8, 0, 'go-xcat can generate the optional common repository');
like(read_file($template_log), qr/^skip_if_unavailable=1$/m,
'the generated common repository stays optional during outages');
like(read_file($template_log), qr/^repo_gpgcheck=1$/m,
'the generated common repository verifies signed metadata');
done_testing();
-161
View File
@@ -1,161 +0,0 @@
#!/usr/bin/env perl
use strict;
use warnings;
use File::Temp qw(tempdir);
use FindBin;
use Test::More;
# go-xcat checked for the EPEL and CRB repositories on EL9 only, and only when the version
# carried a minor number, so CentOS Stream was never checked. On EL10 a management node without
# them failed inside dnf install with a dependency error instead of the message that names the
# missing repository, and the CRB message proposed a CentOS Stream repository file with
# signature checks disabled. The probe used dnf list, which an installed copy of the probe
# package satisfies, and which reports a failed query as a missing repository. The check
# functions are taken from the shipped script and run against a dnf stand-in that answers for
# each probe and records what was asked.
my $go_xcat = "$FindBin::Bin/../../xCAT-server/share/xcat/tools/go-xcat";
plan skip_all => 'go-xcat not found' unless -r $go_xcat;
my $tmpdir = tempdir( CLEANUP => 1 );
my $driver = "$tmpdir/driver.sh";
my $calls = "$tmpdir/calls";
open( my $fh, '>', $driver ) or die "open $driver: $!";
print {$fh} <<'DRIVER';
#!/bin/bash
set -uo pipefail
eval "$(awk '
/^function (repo_carries|el9?_epel_and_crb_check|install_packages_(dnf|yum))\(\)/ { copy = 1 }
copy { print }
copy && /^}$/ { copy = 0 }
' "$GO_XCAT_SOURCE")"
EL9_EPEL_TEST_RPM="perl-Crypt-CBC"; EL_EPEL_TEST_RPM="perl-Crypt-CBC"
EL9_CRB_TEST_RPM="perl-IO-Tty"; EL_CRB_TEST_RPM="perl-IO-Tty"
# repoquery prints the name when an enabled repository carries the package and nothing
# otherwise, and fails with a message when the repositories cannot be read. list -q exits 1
# when neither a repository nor the installed set has the package.
dnf() {
echo "$*" >> "$CALLS"
if [[ ${QUERY_FAIL:-0} == 1 ]]; then
echo "Error: Failed to download metadata for repo 'epel'" >&2
return 1
fi
[[ ${QUERY_WARNS:-0} == 1 ]] && echo "Warning: repository 'extras' metadata is stale" >&2
local has=0
case "$*" in
*perl-Crypt-CBC*) has="$EPEL_HAS" ;;
*perl-IO-Tty*) has="$CRB_HAS" ;;
esac
# A source repository answers for the name unless the query is limited to binary architectures.
[[ ${SOURCE_ONLY:-0} == 1 && "$*" != *"--arch"* ]] && has=1
case "$1" in
repoquery) [[ $has == 1 ]] && { echo "${@: -1}"; echo "${@: -1}"; }; return 0 ;;
list) [[ $has == 1 ]] && return 0; return 1 ;;
esac
return 0
}
yum() { dnf "$@"; }
# ENTRY=dnf or yum runs the installer function go-xcat dispatches to, which owns the check.
case "${ENTRY:-check}" in
dnf) install_packages_dnf -y xCAT ;;
yum) install_packages_yum -y xCAT ;;
*) if declare -F el_epel_and_crb_check >/dev/null; then el_epel_and_crb_check dnf; else el9_epel_and_crb_check dnf; fi ;;
esac
DRIVER
close($fh);
# Runs the check as go-xcat would on a host of this distro and version. Returns the exit status,
# the output, and the probes the dnf stand-in saw.
sub check {
my (%host) = @_;
unlink $calls;
local $ENV{GO_XCAT_SOURCE} = $go_xcat;
local $ENV{CALLS} = $calls;
local $ENV{GO_XCAT_LINUX_DISTRO} = $host{distro} || 'rocky';
local $ENV{GO_XCAT_LINUX_VERSION} = $host{version};
local $ENV{GO_XCAT_ARCH} = 'x86_64';
local $ENV{EPEL_HAS} = $host{epel} ? 1 : 0;
local $ENV{CRB_HAS} = $host{crb} ? 1 : 0;
local $ENV{QUERY_FAIL} = $host{query_fail} ? 1 : 0;
local $ENV{QUERY_WARNS} = $host{query_warns} ? 1 : 0;
local $ENV{SOURCE_ONLY} = $host{source_only} ? 1 : 0;
local $ENV{ENTRY} = $host{entry} || 'check';
my $out = `bash '$driver' 2>&1`;
my $rc = $? >> 8;
my @probes;
if ( open( my $cfh, '<', $calls ) ) { chomp( @probes = <$cfh> ); close($cfh); }
return ( $rc, $out, join( ';', @probes ) );
}
my ( $rc, $out, $probes );
( $rc, $out, $probes ) = check( version => '9.5', epel => 1, crb => 1 );
is( $rc, 0, 'EL9 with EPEL and CRB passes' );
like( $probes, qr/perl-Crypt-CBC.*;.*perl-IO-Tty/, '... after probing both repositories' );
like( $probes, qr/^repoquery /, '... through repoquery, which an installed copy does not satisfy' );
like( $probes, qr/--arch x86_64,noarch/, '... limited to the binary architectures' );
( $rc, $out, $probes ) = check( version => '9.5', epel => 0, crb => 1 );
is( $rc, 1, 'EL9 without EPEL stops' );
like( $out, qr/epel-release-latest-9\.noarch/, '... and names the EL9 EPEL release package' );
( $rc, $out, $probes ) = check( version => '10.2', epel => 1, crb => 1 );
is( $rc, 0, 'EL10 with EPEL and CRB passes' );
like( $probes, qr/perl-Crypt-CBC.*;.*perl-IO-Tty/, '... after probing both repositories' );
( $rc, $out, $probes ) = check( version => '10.2', epel => 0, crb => 1 );
is( $rc, 1, 'EL10 without EPEL stops' );
like( $out, qr/requires EPEL repository/, '... and names the missing repository' );
like( $out, qr/epel-release-latest-10\.noarch/, '... and the EL10 EPEL release package' );
( $rc, $out, $probes ) = check( distro => 'rhel', version => '10.2', epel => 1, crb => 0 );
is( $rc, 1, 'EL10 without CRB stops' );
like( $out, qr/requires CRB repository/, '... and names the missing repository' );
like( $out, qr/'dnf update epel-release' and then 'crb enable'/, '... with a current crb helper, which covers RHEL under RHSM and RHUI' );
unlike( $out, qr/gpgcheck=0|centos-crb|subscription-manager/, '... and no repository file or subscription-only command' );
( $rc, $out, $probes ) = check( version => '10.2', epel => 0, crb => 1, source_only => 1 );
is( $rc, 1, 'a source repository does not stand in for the binary one' );
like( $out, qr/requires EPEL repository/, '... so the missing repository is still reported' );
( $rc, $out, $probes ) = check( distro => 'ol', version => '10.1', epel => 1, crb => 0 );
is( $rc, 1, 'Oracle Linux 10 without CRB stops' );
like( $out, qr/dnf config-manager --enable ol10_codeready_builder/, '... with the command that enables its CodeReady Builder' );
( $rc, $out, $probes ) = check( version => '10.2', epel => 1, crb => 1, query_fail => 1 );
is( $rc, 1, 'a failed repository query stops' );
like( $out, qr/Failed to download metadata/, '... with the package manager error' );
unlike( $out, qr/requires EPEL repository/, '... and not as a missing repository' );
( $rc, $out, $probes ) = check( version => '10.2', epel => 0, crb => 1, query_warns => 1 );
is( $rc, 1, 'a warning on stderr does not stand in for a package' );
like( $out, qr/requires EPEL repository/, '... so the missing repository is still reported' );
( $rc, $out, $probes ) = check( version => '10.2', epel => 1, crb => 1, query_warns => 1 );
is( $rc, 0, 'a warning beside a real match does not fail the check' );
( $rc, $out, $probes ) = check( distro => 'centos', version => '10', epel => 0, crb => 0 );
is( $rc, 1, 'CentOS Stream 10, which reports the major version alone, is checked' );
like( $out, qr/requires EPEL repository/, '... and told about EPEL' );
( $rc, $out, $probes ) = check( version => '8.10', epel => 0, crb => 0 );
is( $rc, 0, 'EL8 is not checked' );
is( $probes, '', '... and nothing is probed' );
( $rc, $out, $probes ) = check( distro => 'fedora', version => '42', epel => 0, crb => 0 );
is( $rc, 0, 'Fedora is not checked' );
# The installer functions go-xcat dispatches to run the check before they install anything.
foreach my $entry (qw(dnf yum)) {
( $rc, $out, $probes ) = check( entry => $entry, version => '10.2', epel => 0, crb => 1 );
is( $rc, 1, "install_packages_$entry on EL10 without EPEL stops" );
like( $out, qr/requires EPEL repository/, '... with the EPEL message' );
unlike( $probes, qr/install/, '... before anything is installed' );
( $rc, $out, $probes ) = check( entry => $entry, version => '10.2', epel => 1, crb => 1 );
is( $rc, 0, "install_packages_$entry on EL10 with EPEL and CRB installs" );
like( $probes, qr/perl-IO-Tty.*;.*install initscripts.*;.*install xCAT/, '... after the check passed' );
}
done_testing();
+83
View File
@@ -0,0 +1,83 @@
#!/usr/bin/env perl
use strict;
use warnings;
use File::Temp qw(tempdir);
use FindBin;
use Test::More;
# go-xcat installs a fixed package list. On riscv64 that list asked for the amd64 and ppc64
# Genesis scripts and bases, so the supported install path put the x86 Genesis on a riscv64
# management node even though the xcat metapackage excludes it there.
#
# The x86 boot loaders are a different matter: they are payload the management node serves to x86
# nodes, so they belong on a riscv64 management node of a mixed cluster and must survive the filter.
#
# The function that builds the riscv64 list is taken from the shipped script and run, so the
# assertions read the package names go-xcat would hand to the package manager.
my $go_xcat = "$FindBin::Bin/../../xCAT-server/share/xcat/tools/go-xcat";
plan skip_all => 'go-xcat not found' unless -r $go_xcat;
my $tmpdir = tempdir( CLEANUP => 1 );
my $driver = "$tmpdir/driver.sh";
open( my $fh, '>', $driver ) or die "open $driver: $!";
print {$fh} <<'DRIVER';
#!/bin/bash
set -euo pipefail
eval "$(awk '
$0 == "function riscv64_install_list()" { copy = 1 }
copy { print }
copy && /^}$/ { exit }
' "$GO_XCAT_SOURCE")"
# The real list, as go-xcat defines it for each package manager.
if [[ ${WITH_DPKG:-0} == 1 ]]; then
dpkg() { :; }
list=(perl-xcat xcat-client xcat xcat-buildkit
xcat-genesis-scripts-amd64 xcat-genesis-scripts-ppc64 xcat-server
elilo-xcat grub2-xcat ipmitool-xcat syslinux-xcat
xcat-genesis-base-amd64 xcat-genesis-base-ppc64 xnba-undi)
else
type() { return 1; }
list=(perl-xCAT xCAT-client xCAT xCAT-buildkit
xCAT-genesis-scripts-ppc64 xCAT-genesis-scripts-x86_64 xCAT-server
elilo-xcat grub2-xcat ipmitool-xcat syslinux-xcat
xCAT-genesis-base-ppc64 xCAT-genesis-base-x86_64 xnba-undi)
fi
riscv64_install_list "${list[@]}"
DRIVER
close($fh);
chmod 0755, $driver;
sub riscv64_list {
my ($with_dpkg) = @_;
my $out = `GO_XCAT_SOURCE='$go_xcat' WITH_DPKG=$with_dpkg bash '$driver' 2>&1`;
is( $?, 0, "the riscv64 list builds (dpkg=$with_dpkg)" ) or diag($out);
return [ grep { length } split( /\n/, $out ) ];
}
foreach my $case ( [ 1, 'deb', 'xcat-genesis-openembedded-riscv64', 'xcat-server' ],
[ 0, 'rpm', 'xCAT-genesis-openembedded-riscv64', 'xCAT-server' ] )
{
my ( $with_dpkg, $name, $genesis, $server ) = @{$case};
my $list = riscv64_list($with_dpkg);
is_deeply( [ grep { /genesis-scripts-/i } @{$list} ], [],
"$name: no legacy Genesis scripts on riscv64" );
is_deeply( [ grep { /genesis-base-/i } @{$list} ], [],
"$name: no legacy Genesis base on riscv64" );
is_deeply( [ sort grep { /^(elilo-xcat|syslinux-xcat|xnba-undi)$/ } @{$list} ],
[ sort qw(elilo-xcat syslinux-xcat xnba-undi) ],
"$name: the x86 boot payload a mixed cluster needs is still installed" );
ok( scalar( grep { $_ eq $genesis } @{$list} ),
"$name: the riscv64 OpenEmbedded Genesis package is asked for" );
# The filter must take nothing else with it.
foreach my $keep ( $server, 'grub2-xcat', 'ipmitool-xcat' ) {
ok( scalar( grep { $_ eq $keep } @{$list} ), "$name: $keep is still installed" );
}
}
done_testing();
+63
View File
@@ -0,0 +1,63 @@
#!/usr/bin/env perl
use strict;
use warnings;
use FindBin;
use Test::More;
# grub2 reads its configuration as a script. A word carrying a command separator ends the
# linux command, so the kernel never sees the rest of the line. The Ubuntu installer seed
# (ds=nocloud-net;s=<url>) is written as one such word: the node booted without the seed URL
# and without BOOTIF, and the installer waited for someone to answer its questions.
use lib "$FindBin::Bin/../../perl-xCAT";
use lib "$FindBin::Bin/../../xCAT-server/lib/perl";
my $plugin = "$FindBin::Bin/../../xCAT-server/lib/xcat/plugins/grub2.pm";
plan skip_all => 'grub2.pm not found' unless -r $plugin;
eval { require $plugin; 1 } or plan skip_all => "could not load grub2.pm: $@";
my $quote = \&xCAT_plugin::grub2::quote_kcmdline;
my $seed = 'imgurl=http://mn/rootimg.cpio.gz ds=nocloud-net;s=http://mn/install/autoinst/n1/ quiet';
is( $quote->($seed),
'imgurl=http://mn/rootimg.cpio.gz ds=nocloud-net\;s=http://mn/install/autoinst/n1/ quiet',
'the installer seed keeps the arguments after it' );
my $plain = 'imgurl=http://mn/rootimg.cpio.gz XCAT=10.0.0.1:3001 console=ttyS0,115200 quiet';
is( $quote->($plain), $plain, 'a command line without a separator is unchanged' );
is( $quote->(undef), undef, 'an undefined command line stays undefined' );
is( $quote->(''), '', 'an empty command line stays empty' );
for my $char ( ';', '{', '}', '|', '&', '<', '>', '(', ')' ) {
is( $quote->("first opt=a${char}b last"), "first opt=a\\${char}b last",
"a separator $char is escaped" );
}
# grub2 removes the quoting before the kernel sees the value, so a value the caller quoted
# must keep the quotes it was given rather than gaining a second layer.
is( $quote->('first opt="a;b c" last'), 'first opt="a;b c" last',
'a double quoted value is passed through unchanged' );
is( $quote->(q{first 'ds=nocloud-net;s=http://mn/seed' last}),
q{first 'ds=nocloud-net;s=http://mn/seed' last},
'a single quoted value is passed through unchanged' );
# A quoted span and a bare separator in the same word: the span keeps its meaning and only
# the separator outside it is escaped.
is( $quote->(q{opt='a b';c}), q{opt='a b'\;c},
'a separator beside a quoted span is escaped without touching the span' );
is( $quote->('opt="a;b c" other=x;y'), 'opt="a;b c" other=x\;y',
'a bare separator beside a quoted word is escaped' );
# An escaped separator is already literal to grub2, so escaping it again would hand the
# kernel a backslash the caller never wrote.
is( $quote->('first ds=nocloud-net\;s=http://mn/seed last'),
'first ds=nocloud-net\;s=http://mn/seed last',
'a separator the caller escaped is passed through unchanged' );
is( $quote->('a\;b c;d'), 'a\;b c\;d',
'an escaped separator does not stop a real one being escaped' );
is( $quote->('BOOTIF=$net_default_mac x;y'), 'BOOTIF=$net_default_mac x\;y',
'a variable reference is left alone so BOOTIF still expands' );
done_testing();
+22 -5
View File
@@ -2,11 +2,23 @@
use strict;
use warnings;
use File::Path qw(make_path);
use File::Temp qw(tempdir);
use FindBin;
use lib "$FindBin::Bin/../lib";
use lib "$FindBin::Bin/../../perl-xCAT";
use Test::More;
# xCAT modules put $::XCATROOT/lib/perl ahead of @INC as they compile, so on a host with xCAT
# installed the schema, loaded after them, would come from /opt/xcat. XCATROOT points at this
# checkout before any of them compiles.
BEGIN {
my $root = tempdir( CLEANUP => 1 );
make_path("$root/lib");
symlink( "$FindBin::Bin/../../perl-xCAT", "$root/lib/perl" ) or die "symlink: $!";
$ENV{XCATROOT} = $root;
}
use XCAT::Test::File qw(repo_path);
use xCAT::ProfiledNodeUtils;
use xCAT::Utils;
@@ -56,9 +68,13 @@ is( xCAT::ProfiledNodeUtils::cal_netboot( $rule_table, [ 'aarch64', 'rhels', '9'
# ---------------------------------------------------------------------------
# Schema descriptions
# ---------------------------------------------------------------------------
SKIP: {
skip 'xCAT::Schema is not loadable here', 5
unless eval { require lib; lib->import( repo_path('perl-xCAT') ); require xCAT::Schema; 1 };
# The schema comes from this checkout, not from an installed xCAT, or the descriptions checked
# below would be those of whatever version the host carries.
{
require xCAT::Schema;
require Cwd;
like( Cwd::realpath( $INC{'xCAT/Schema.pm'} ), qr/^\Q@{[ Cwd::realpath( repo_path('perl-xCAT') ) ]}\E/,
'xCAT::Schema is loaded from the checkout' );
like( $xCAT::Schema::tabspec{nodetype}{descriptions}{arch}, qr/\briscv64\b/,
'nodetype.arch documents riscv64 as a valid value' );
@@ -68,8 +84,9 @@ SKIP: {
'osimage.osarch documents riscv64 as a valid value' );
like( $xCAT::Schema::tabspec{osimage}{descriptions}{osarch}, qr/\bs390x\b/,
'osimage.osarch documents s390x as a valid value' );
like( $xCAT::Schema::tabspec{noderes}{descriptions}{netboot}, qr/riscv64\s+>=el10\s+grub2,grub2-http,grub2-tftp/,
'noderes.netboot documents the riscv64 grub2 methods' );
like( $xCAT::Schema::tabspec{noderes}{descriptions}{netboot},
qr/riscv64\s+>=el10, >=ubuntu24\.04\s+grub2,grub2-http,grub2-tftp/,
'noderes.netboot documents the riscv64 grub2 methods for EL and Ubuntu' );
}
done_testing();
@@ -1,48 +0,0 @@
#!/usr/bin/env perl
use strict;
use warnings;
use FindBin;
use File::Spec;
use Test::More;
my $repo_root = File::Spec->catdir( $FindBin::Bin, '..', '..' );
my %scripts = (
'legacy install post.xcat' => File::Spec->catfile(
$repo_root,
'xCAT-server/share/xcat/install/scripts/post.xcat'
),
'legacy netboot xcatdsklspost' => File::Spec->catfile(
$repo_root,
'xCAT/postscripts/xcatdsklspost'
),
);
foreach my $path ( values %scripts ) {
plan skip_all => "$path not found" unless -r $path;
}
foreach my $name ( sort keys %scripts ) {
my $path = $scripts{$name};
open( my $fh, '<', $path ) or die "Unable to read $path: $!";
my $script = do { local $/; <$fh> };
close($fh);
like(
$script,
qr/--reject\s+"index\.html\*,post\.xcat\.ng,post\.xcat\.rhels10"/,
"$name recursive wget ignores dispatcher scripts that contain literal HTML-link regexes"
);
like(
$script,
qr/Newer\s+(?:#\s+)?wget\s+parses\s+HTML-looking\s+regex\s+strings/s,
"$name download policy documents why dispatcher scripts are excluded"
);
unlike(
$script,
qr/<a\s+href=/i,
"$name comments do not contain HTML links that wget can recurse into"
);
}
done_testing();
-26
View File
@@ -1,26 +0,0 @@
#!/usr/bin/env perl
use strict;
use warnings;
use File::Spec;
use FindBin;
use Test::More;
my $script = File::Spec->catfile( $FindBin::Bin, '..', '..',
'xCAT', 'postscripts', 'remoteshell' );
plan skip_all => 'remoteshell not found' unless -r $script;
open( my $fh, '<', $script ) or die "Unable to read $script: $!";
my $source = do { local $/; <$fh> };
close($fh);
# A kill without the hyphen reads the signal number as one more process id, so
# the target gets the default signal, which a process can catch, and the
# process with that id is signalled as well.
my @bare = ( $source =~ /^[^#\n]*\bkill\s+\d/gm );
is( scalar(@bare), 0, 'no kill gives the signal number without a hyphen' );
like( $source, qr/kill -9 \$PIDLIST/,
'the ssh daemon gets the signal that a process cannot catch' );
done_testing();
-62
View File
@@ -1,62 +0,0 @@
#!/usr/bin/env perl
use strict;
use warnings;
use File::Spec;
use FindBin;
use Test::More;
my $script = File::Spec->catfile( $FindBin::Bin, '..', '..',
'xCAT', 'postscripts', 'remoteshell' );
plan skip_all => 'remoteshell not found' unless -r $script;
plan skip_all => 'no bash' unless -x '/bin/bash';
open( my $fh, '<', $script ) or die "Unable to read $script: $!";
my $source = do { local $/; <$fh> };
close($fh);
my ($loop) = $source =~ /(waited=0\n.*?\n done)/s;
ok( defined($loop), 'the wait loop was found in the postscript' );
# Run the loop of the postscript against real processes, with a shorter bound
# so that the test does not spend the whole timeout of the postscript.
sub waits_for {
my (@pids) = @_;
( my $body = $loop ) =~ s/\$waited -lt 10/\$waited -lt 3/;
my $list = join( ' ', @pids );
my $out = `/bin/bash -c 'PIDLIST="$list"\n$body\necho \$alive' 2>/dev/null`;
chomp $out;
return $out;
}
# A process that is still running must be reported as alive, and the loop must
# give up rather than run for ever.
my $child = fork();
if ( !defined $child ) { plan skip_all => 'cannot fork' }
if ( $child == 0 ) { exec( 'sleep', '30' ); exit 1 }
my $start = time;
is( waits_for($child), '1', 'a process that is still running is reported alive' );
cmp_ok( time - $start, '<', 10, 'the loop gives up instead of waiting for ever' );
kill 'KILL', $child;
waitpid( $child, 0 );
# A process that has ended must be reported as gone, without waiting.
my $gone = fork();
if ( $gone == 0 ) { exit 0 }
waitpid( $gone, 0 );
$start = time;
is( waits_for($gone), '0', 'a process that has ended is reported gone' );
cmp_ok( time - $start, '<', 3, 'no time is spent once the process is gone' );
# The bound of the postscript itself, and the shape of the test.
like( $source, qr/while \[ \$waited -lt 10 \]/, 'the postscript waits at most ten seconds' );
like( $source, qr/kill -0 \$pid/, 'the check for a running process sends no signal' );
like( $source, qr/kill -9 \$PIDLIST/, 'the daemon still gets the signal it cannot catch' );
# The wait has to happen before the new daemon starts.
my ($block) = $source =~ /(PIDLIST=.*?\/usr\/sbin\/sshd)/s;
ok( defined($block), 'the fallback block was found' );
cmp_ok( index( $block, 'waited=0' ), '<', index( $block, '/usr/sbin/sshd' ),
'the wait comes before the new daemon starts' );
done_testing();
-20
View File
@@ -1,20 +0,0 @@
#!/usr/bin/env perl
use strict;
use warnings;
use Test::More;
my $pre_sles_path = defined $ENV{XCATROOT} ? "$ENV{XCATROOT}/share/xcat/install/scripts/pre.sles" : '';
$pre_sles_path = "xCAT-server/share/xcat/install/scripts/pre.sles"
unless -f $pre_sles_path;
plan skip_all => "pre.sles not found" unless -f $pre_sles_path;
my $src = do { local $/; open my $fh, '<', $pre_sles_path or die $!; <$fh> };
like($src, qr/sub set_sles11_uefi_bootloader\b|set_sles11_uefi_bootloader\(\)/, 'SLES UEFI bootloader helper exists');
like($src, qr/install=\.\*sles11/, 'SLES 11 UEFI bootloader change is scoped to SLES 11 install media');
like($src, qr/<loader_type>elilo<\/loader_type>/, 'SLES 11 UEFI install selects elilo');
like($src, qr/<location>mbr<\/location>/, 'legacy MBR template value is replaced at install time');
like($src, qr/if \[ -d \/sys\/firmware\/efi \]; then\s+sed .*?set_sles11_uefi_bootloader/s, 'UEFI default partitioning applies bootloader helper');
done_testing();
-17
View File
@@ -1,17 +0,0 @@
#!/usr/bin/env perl
use strict;
use warnings;
use Test::More;
my $script_path = defined $ENV{XCATROOT} ? "$ENV{XCATROOT}/share/xcat/netboot/add-on/statelite/add_ssh" : '';
$script_path = "xCAT-server/share/xcat/netboot/add-on/statelite/add_ssh"
unless -f $script_path;
plan skip_all => "add_ssh not found" unless -f $script_path;
my $script = do { local $/; open my $fh, '<', $script_path or die $!; <$fh> };
like($script, qr/MaxStartups 100:30:200/, 'add_ssh caps MaxStartups below systemd soft nofile limit');
unlike($script, qr/echo\s+"MaxStartups 1024"/, 'add_ssh does not force MaxStartups 1024');
done_testing();
+256
View File
@@ -0,0 +1,256 @@
#!/usr/bin/env perl
use strict;
use warnings;
use File::Path qw(make_path);
use File::Temp qw(tempdir);
use FindBin;
use Test::More;
# riscv64 nodes have no boot loader unless one reaches /tftpboot/boot/grub2. The image on
# the media cannot be used: it carries a built-in configuration that looks for the live
# filesystem, so a node that loads it drops to a grub prompt instead of reading the network
# configuration. copycd builds a netboot image from the grub2 package the media ship.
#
# dpkg-deb and grub-mkimage are shadowed by stubs ahead of $PATH, because a management node
# is the only place they exist. They record what copycd asked for, so the arguments that
# decide whether the image can boot over the network are what the assertions read.
BEGIN {
package xCAT::TableUtils;
our $tftpdir;
sub getTftpDir { return $tftpdir; }
$INC{'xCAT/TableUtils.pm'} = __FILE__;
}
use lib "$FindBin::Bin/../../perl-xCAT";
use lib "$FindBin::Bin/../../xCAT-server/lib/perl";
my $plugin = "$FindBin::Bin/../../xCAT-server/lib/xcat/plugins/debian.pm";
plan skip_all => 'debian.pm not found' unless -r $plugin;
eval { require $plugin; 1 } or plan skip_all => "could not load debian.pm: $@";
my $stubs = tempdir(CLEANUP => 1);
my $log = "$stubs/mkimage.args";
sub write_stub {
my ($name, $body) = @_;
open(my $fh, '>', "$stubs/$name") or die $!;
print {$fh} "#!/bin/bash\n$body";
close($fh);
chmod 0755, "$stubs/$name";
}
# dpkg-deb -x <package> <dir> lays down the module tree the package carries.
write_stub('dpkg-deb', <<'SH');
dir="${!#}"
mkdir -p "$dir/usr/lib/grub/riscv64-efi"
: > "$dir/usr/lib/grub/riscv64-efi/kernel.img"
SH
# grub-mkimage records its arguments and writes a riscv64 PE image to the -o path. Like the real
# one it embeds the prefix and the name of every module it was asked for, which is what tells a
# grub2 image apart from a file that merely carries the headers.
write_stub('grub-mkimage', <<'SH');
echo "$@" >> "$MKIMAGE_LOG"
[ -n "$MKIMAGE_FAIL" ] && exit 1
out=""; prefix=""; modules=()
while [ $# -gt 0 ]; do
case "$1" in
-o) out="$2"; shift 2 ;;
-p) prefix="$2"; shift 2 ;;
-O|-d) shift 2 ;;
*) modules+=("$1"); shift ;;
esac
done
perl -e 'my ($prefix, @modules) = @ARGV;
my $i = "MZ" . "\0" x 58 . pack("V", 64) . "PE\0\0" . pack("v", 0x5064)
. pack("v", 1) . "\0" x 16 . pack("v", 0x20b) . "\0" x 14 . pack("V", 0x1000)
. "\0" x 36 . pack("V", 4096) . pack("V", 4096) . "\0" x 4 . pack("v", 10) . "\0" x 2;
$i .= "$prefix\0";
$i .= "\0$_\0" for @modules;
print $i, "\0" x (4096 - length $i)' "$prefix" "${modules[@]}" > "$out"
SH
$ENV{PATH} = "$stubs:$ENV{PATH}";
$ENV{MKIMAGE_LOG} = $log;
sub media_with {
my (@files) = @_;
my $root = tempdir(CLEANUP => 1);
foreach my $file (@files) {
my $full = "$root/$file";
($full =~ m{^(.*)/[^/]+$}) and make_path($1);
open(my $fh, '>', $full) or die $!;
print {$fh} "content of $file";
close($fh);
}
return $root;
}
sub publish {
my ($arch, $media) = @_;
$xCAT::TableUtils::tftpdir = tempdir(CLEANUP => 1);
unlink $log;
my @said;
xCAT_plugin::debian::install_media_grub2_loader(
$media, $arch,
sub { push @said, ($_[0]->{data} // ()), @{ $_[0]->{warning} || [] } });
my $target = "$xCAT::TableUtils::tftpdir/boot/grub2/grub2.$arch";
return {
published => (-e $target ? 1 : 0),
built => (-r $log ? do { open my $fh, '<', $log; local $/; <$fh> } : ''),
said => join(' ', @said),
};
}
my $package = 'pool/main/g/grub2/grub-efi-riscv64-bin_2.12-1ubuntu7.3_riscv64.deb';
my $riscv = publish('riscv64', media_with($package, 'casper/vmlinux'));
is($riscv->{published}, 1, 'riscv64 media publish a grub2 loader');
like($riscv->{built}, qr/-O riscv64-efi/, 'the image is built for the riscv64 firmware');
like($riscv->{built}, qr{-p /boot/grub2},
'the image looks for its configuration where nodeset writes it');
like($riscv->{built}, qr{-d \S+/usr/lib/grub/riscv64-efi},
'the modules come from the package the media ship');
like($riscv->{built}, qr/\befinet\b.*\btftp\b/s, 'the image can reach the network');
like($riscv->{built}, qr/\bhttp\b/, 'the image can read a configuration over HTTP');
like($riscv->{said}, qr/Installed .*grub2\.riscv64 from the media/,
'copycd says where the loader came from');
my $x86 = publish('x86_64', media_with('EFI/boot/bootx64.efi', 'casper/vmlinuz'));
is($x86->{published}, 0, 'media of another architecture publish nothing');
my $none = publish('riscv64', media_with('casper/vmlinux'));
is($none->{published}, 0, 'riscv64 media without the grub2 package publish nothing');
like($none->{said}, qr/No grub2\.riscv64 boot loader was installed/,
'copycd says a riscv64 node will not boot without a loader');
# a build that fails must leave no loader behind, so nodeset reports the missing file
$ENV{MKIMAGE_FAIL} = 1;
my $failed = publish('riscv64', media_with($package));
delete $ENV{MKIMAGE_FAIL};
is($failed->{published}, 0, 'a failed build leaves no loader');
like($failed->{said}, qr/No grub2\.riscv64 boot loader was installed/,
'copycd reports a build that failed');
# The image is built beside the target and renamed, so an interrupted run cannot leave a
# partial loader behind for nodeset to hand out.
$xCAT::TableUtils::tftpdir = tempdir(CLEANUP => 1);
make_path("$xCAT::TableUtils::tftpdir/boot/grub2");
$ENV{MKIMAGE_FAIL} = 1;
xCAT_plugin::debian::install_media_grub2_loader(media_with($package), 'riscv64', undef);
delete $ENV{MKIMAGE_FAIL};
my @leftovers = glob("$xCAT::TableUtils::tftpdir/boot/grub2/grub2.riscv64*");
is(scalar @leftovers, 0, 'a failed build leaves nothing beside the target either');
# An empty target is what an interrupted older run left behind, so it must not be mistaken
# for an installed loader.
$xCAT::TableUtils::tftpdir = tempdir(CLEANUP => 1);
make_path("$xCAT::TableUtils::tftpdir/boot/grub2");
open(my $efh, '>', "$xCAT::TableUtils::tftpdir/boot/grub2/grub2.riscv64") or die $!;
close($efh);
unlink $log;
xCAT_plugin::debian::install_media_grub2_loader(media_with($package), 'riscv64', undef);
ok(-s "$xCAT::TableUtils::tftpdir/boot/grub2/grub2.riscv64",
'an empty loader left by an earlier run is replaced');
# UEFI loads the loader as a PE image for one machine. A real riscv64 one is kept, and
# anything else -- text, a header-only stub, another architecture's loader -- is replaced,
# because a node given one of those cannot boot.
sub uefi_image {
my (%f) = @_;
my $size = $f{size} // 4096;
my $image =
"MZ" . ( "\0" x 58 ) . pack( 'V', 64 )
. "PE\0\0"
. pack( 'v', $f{machine} // 0x5064 )
. pack( 'v', $f{sections} // 1 ) . ( "\0" x 16 )
. pack( 'v', 0x20b ) . ( "\0" x 14 )
. pack( 'V', $f{entry} // 0x1000 ) . ( "\0" x 36 )
. pack( 'V', $size ) . pack( 'V', $size ) . ( "\0" x 4 )
. pack( 'v', $f{subsystem} // 10 ) . ( "\0" x 2 );
my $prefix = exists $f{prefix} ? $f{prefix} : '/boot/grub2';
$image .= "$prefix\0" if length $prefix;
# grub-mkimage records the name of every embedded module, so a real loader carries them.
my @modules = exists $f{modules}
? @{ $f{modules} }
: qw(efinet tftp http linux normal configfile search);
$image .= "\0$_\0" for @modules;
return $image . ( "\0" x ( $size - length $image ) );
}
sub existing_loader_kept {
my ($bytes) = @_;
$xCAT::TableUtils::tftpdir = tempdir( CLEANUP => 1 );
make_path("$xCAT::TableUtils::tftpdir/boot/grub2");
my $target = "$xCAT::TableUtils::tftpdir/boot/grub2/grub2.riscv64";
open( my $fh, '>', $target ) or die $!;
binmode($fh);
print {$fh} $bytes;
close($fh);
xCAT_plugin::debian::install_media_grub2_loader( media_with($package), 'riscv64', undef );
open( my $rfh, '<', $target ) or die $!;
binmode($rfh);
my $now = do { local $/; <$rfh> };
close($rfh);
return $now eq $bytes;
}
ok( existing_loader_kept( uefi_image() ), 'a riscv64 loader already in place is kept' );
ok( !existing_loader_kept('not a loader at all'),
'a file that is not a UEFI image is replaced' );
ok( !existing_loader_kept('MZ and nothing else'),
'a file that only starts with the DOS signature is replaced' );
ok( !existing_loader_kept( substr( uefi_image(), 0, 200 ) ),
'a loader truncated to its headers is replaced' );
ok( !existing_loader_kept( uefi_image( machine => 0x8664 ) ),
"another architecture's loader is replaced" );
# The fields below are what makes a PE image executable at all. A file carrying the
# signatures but none of them cannot boot a node, so it must not be mistaken for a loader.
ok( !existing_loader_kept( uefi_image( sections => 0 ) ),
'an image with no sections to load is replaced' );
ok( !existing_loader_kept( uefi_image( entry => 0 ) ),
'an image with no entry point is replaced' );
ok( !existing_loader_kept( uefi_image( subsystem => 3 ) ),
'an image that is not an EFI application is replaced' );
# The image on the media carries the same modules but not the prefix this plugin builds with,
# so it looks for the live filesystem instead of the configuration nodeset writes.
ok( !existing_loader_kept( uefi_image( prefix => '' ) ),
'a valid EFI image built for another boot path is replaced' );
# Headers and a prefix are cheap to fabricate and say nothing about what the image can do.
# Without the modules a net boot goes through, the file cannot fetch a kernel over the network.
ok( !existing_loader_kept( uefi_image( modules => [] ) ),
'an image carrying no grub2 modules is replaced' );
ok( !existing_loader_kept( uefi_image( modules => [qw(linux normal configfile search)] ) ),
'an image with no network modules is replaced' );
ok( existing_loader_kept( uefi_image( modules => [qw(efinet tftp http linux normal configfile search gzio)] ) ),
'an image carrying more modules than the minimum is kept' );
# A rebuild that cannot run must leave the loader alone. The check cannot tell an image this
# plugin did not build from one built for another boot path -- the media loader carries the same
# modules -- so removing one on that evidence can take a working loader away from every node.
$xCAT::TableUtils::tftpdir = tempdir( CLEANUP => 1 );
make_path("$xCAT::TableUtils::tftpdir/boot/grub2");
my $rejected = "$xCAT::TableUtils::tftpdir/boot/grub2/grub2.riscv64";
open( my $bad, '>', $rejected ) or die $!;
print {$bad} 'not a loader at all';
close($bad);
my @told;
xCAT_plugin::debian::install_media_grub2_loader(
media_with('casper/vmlinux'), 'riscv64',
sub { push @told, ( $_[0]->{data} // () ), @{ $_[0]->{warning} || [] } } );
ok( -e $rejected, 'a rejected loader survives a media that cannot replace it' );
my $left = -e $rejected
? do { open my $fh, '<', $rejected or die $!; local $/; <$fh> }
: '(removed)';
is( $left, 'not a loader at all', '... byte for byte' );
like( join( ' ', @told ), qr/No grub2\.riscv64 boot loader was installed/,
'and copycd says no loader was installed' );
like( join( ' ', @told ), qr/was left alone/,
'... and that it did not touch what was there' );
done_testing();
@@ -0,0 +1,63 @@
#!/usr/bin/env perl
use strict;
use warnings;
use File::Temp qw(tempdir);
use FindBin;
use Test::More;
# The stubbed test proves the decisions copycd makes. This one proves the artifact: it runs the
# real grub-mkimage over the grub2 package of real Ubuntu media and checks that what comes out is
# what the management node will serve to a riscv64 node.
#
# It needs copied riscv64 media and the grub2 build tools, so it runs where those exist:
# XCAT_TEST_UBUNTU_RISCV64_MEDIA=/install/ubuntu24.04.4/riscv64 perl <this test>
# Without them it skips, which is why the stubbed test still covers the decisions.
BEGIN {
package xCAT::TableUtils;
our $tftpdir;
sub getTftpDir { return $tftpdir; }
$INC{'xCAT/TableUtils.pm'} = __FILE__;
}
my $media = $ENV{XCAT_TEST_UBUNTU_RISCV64_MEDIA};
plan skip_all => 'set XCAT_TEST_UBUNTU_RISCV64_MEDIA to copied riscv64 media' unless $media;
plan skip_all => "no media at $media" unless -d $media;
foreach my $tool (qw(dpkg-deb grub-mkimage)) {
my $found = grep { -x "$_/$tool" } split( /:/, $ENV{PATH} // '' );
plan skip_all => "$tool is not installed" unless $found;
}
use lib "$FindBin::Bin/../../perl-xCAT";
use lib "$FindBin::Bin/../../xCAT-server/lib/perl";
my $plugin = "$FindBin::Bin/../../xCAT-server/lib/xcat/plugins/debian.pm";
plan skip_all => 'debian.pm not found' unless -r $plugin;
eval { require $plugin; 1 } or plan skip_all => "could not load debian.pm: $@";
$xCAT::TableUtils::tftpdir = tempdir( CLEANUP => 1 );
my @told;
my $target = xCAT_plugin::debian::install_media_grub2_loader(
$media, 'riscv64',
sub { push @told, ( $_[0]->{data} // () ), @{ $_[0]->{warning} || [] } } );
ok( defined $target, 'the media produced a loader' )
or diag( join( "\n", @told ) ), done_testing(), exit;
is( $target, "$xCAT::TableUtils::tftpdir/boot/grub2/grub2.riscv64",
'it is where nodeset serves it from' );
ok( -s $target, '... and it is not empty' );
# The same checks nodeset depends on, against a real grub-mkimage image rather than a fabricated one.
my $build = { machine => 0x5064, format => 'riscv64-efi', package => 'grub-efi-riscv64-bin' };
ok( xCAT_plugin::debian::_is_uefi_image( $target, $build->{machine} ),
'the image is a riscv64 UEFI application' );
ok( xCAT_plugin::debian::_is_netboot_loader( $target, $build ),
'... carrying the prefix and the modules a net boot needs' );
# A second import must not rebuild it: the image already in place is the one the nodes booted.
my $before = ( stat($target) )[9];
my $again = xCAT_plugin::debian::install_media_grub2_loader( $media, 'riscv64', undef );
is( $again, $target, 'a second import returns the loader already in place' );
is( ( stat($target) )[9], $before, '... without rewriting it' );
done_testing();
@@ -0,0 +1,80 @@
#!/usr/bin/env perl
use strict;
use warnings;
use File::Spec;
use FindBin;
use Test::More;
# archive.ubuntu.com publishes amd64 and i386 only. A ppc64el or riscv64 netboot image built
# against it finds no package at all, and debootstrap fails before it copies anything, so the
# default mirror has to follow the image architecture.
#
# Driven by the real selection code: the two statements are extracted from genimage and
# evaluated here, so this test tracks the script rather than a copy of it.
my $repo_root = File::Spec->rel2abs( File::Spec->catdir( $FindBin::Bin, '..', '..' ) );
use lib "$FindBin::Bin/../../perl-xCAT";
require xCAT::Utils;
my $genimage_path = File::Spec->catfile(
$repo_root, 'xCAT-server', 'share', 'xcat', 'netboot', 'ubuntu', 'genimage'
);
plan skip_all => "genimage not found at $genimage_path" unless -f $genimage_path;
my $src = do { local $/; open my $fh, '<', $genimage_path or die $!; <$fh> };
my ($default) = $src =~ /^\s*(my \$default = \(\$uarch =~.*?;)\s*$/ms;
ok( defined $default, 'found the default mirror selection in genimage' )
or done_testing(), exit;
my ($pick) = $src =~ /^\s*(my \$mirror = \(defined \$aptmirror\[0\].*?;)\s*$/ms;
ok( defined $pick, 'found the mirror override in genimage' )
or done_testing(), exit;
# genimage does not read osimage.osarch directly: it converts it first, so the value the selection
# sees is whatever xCAT::Utils::debian_arch returns. Driving the xCAT token through that conversion
# is what catches a token the map does not know.
sub choose_osarch {
my ( $osarch, $site ) = @_;
return choose( xCAT::Utils->debian_arch($osarch), $site );
}
sub choose {
my ( $uarch, $site ) = @_;
my $set = defined $site ? "('$site')" : "()";
## no critic (BuiltinFunctions::ProhibitStringyEval)
my $mirror = eval "my \$uarch = '$uarch'; my \@aptmirror = $set; $default $pick \$mirror";
## use critic
die "failed to evaluate the genimage mirror selection: $@" if $@;
return $mirror;
}
is( choose('riscv64'), 'http://ports.ubuntu.com/ubuntu-ports',
'a riscv64 image takes the ports archive' );
is( choose('ppc64el'), 'http://ports.ubuntu.com/ubuntu-ports',
'a ppc64el image takes the ports archive' );
is( choose('amd64'), 'http://archive.ubuntu.com/ubuntu',
'an amd64 image keeps the main archive' );
is( choose('i386'), 'http://archive.ubuntu.com/ubuntu',
'an i386 image keeps the main archive' );
is( choose( 'riscv64', 'http://mirror.example.invalid/ubuntu' ),
'http://mirror.example.invalid/ubuntu',
'site.ubuntu_apt_mirror overrides the ports archive' );
is( choose( 'amd64', 'http://mirror.example.invalid/ubuntu' ),
'http://mirror.example.invalid/ubuntu',
'site.ubuntu_apt_mirror overrides the main archive' );
is( choose( 'riscv64', '' ), 'http://ports.ubuntu.com/ubuntu-ports',
'an empty site.ubuntu_apt_mirror does not blank the mirror' );
# The architecture reaches the selection as an xCAT osarch value, not as a Debian one.
is( choose_osarch('x86_64'), 'http://archive.ubuntu.com/ubuntu',
'osarch x86_64 reaches the archive' );
is( choose_osarch('x86'), 'http://archive.ubuntu.com/ubuntu',
'osarch x86 reaches the archive, which is where i386 lives' );
is( choose_osarch('riscv64'), 'http://ports.ubuntu.com/ubuntu-ports',
'osarch riscv64 reaches the ports archive' );
is( choose_osarch('ppc64el'), 'http://ports.ubuntu.com/ubuntu-ports',
'osarch ppc64el reaches the ports archive' );
done_testing();
@@ -0,0 +1,67 @@
#!/usr/bin/env perl
use strict;
use warnings;
use File::Path qw(make_path);
use File::Spec;
use File::Temp qw(tempdir);
use FindBin;
use Test::More;
# A netboot image resolves names with the libnss libraries of its own architecture, and
# Ubuntu keeps them in a per-architecture directory. Build a root filesystem for each
# architecture and run genimage's selection over it.
my $repo_root = File::Spec->rel2abs(File::Spec->catdir($FindBin::Bin, '..', '..'));
my $genimage = File::Spec->catfile(
$repo_root, 'xCAT-server', 'share', 'xcat', 'netboot', 'ubuntu', 'genimage');
plan skip_all => "genimage not found at $genimage" unless -f $genimage;
my $src = do { local $/; open my $fh, '<', $genimage or die $!; <$fh> };
my ($selection) =
$src =~ /^(\s*if \(\$arch =~ \/x86_64\/\) \{.*?\n\s*\} else \{.*?\n\s*\}\n)/ms;
ok(defined $selection, 'found the resolver library selection in genimage')
or do { done_testing(); exit };
sub selected {
my ($arch, @present) = @_;
my $rootimg_dir = tempdir(CLEANUP => 1);
foreach my $file (@present) {
my $full = "$rootimg_dir/$file";
($full =~ m{^(.*)/[^/]+$}) and make_path($1);
open(my $fh, '>', $full) or die $!;
close($fh);
}
my @filestoadd;
eval "$selection 1" or die $@; ## no critic (BuiltinFunctions::ProhibitStringyEval)
return join(',', sort @filestoadd);
}
is(
selected('riscv64', 'lib/riscv64-linux-gnu/libnss_files.so.2',
'lib/riscv64-linux-gnu/libnss_dns.so.2'),
'lib/riscv64-linux-gnu/libnss_dns.so.2,lib/riscv64-linux-gnu/libnss_files.so.2',
'a riscv64 image takes the riscv64 resolver libraries',
);
is(
selected('riscv64', 'lib/libnss_dns.so.2'),
'',
'riscv64 does not fall back to the path that holds no riscv64 library',
);
is(
selected('x86_64', 'lib/x86_64-linux-gnu/libnss_dns.so.2'),
'lib/x86_64-linux-gnu/libnss_dns.so.2',
'x86_64 selection is unchanged',
);
is(
selected('ppc64el', 'lib/powerpc64le-linux-gnu/libnss_files.so.2'),
'lib/powerpc64le-linux-gnu/libnss_files.so.2',
'ppc64el selection is unchanged',
);
is(
selected('s390x', 'lib/libnss_dns.so.2'),
'lib/libnss_dns.so.2',
'an architecture with no branch keeps the generic library',
);
done_testing();
@@ -0,0 +1,38 @@
#!/usr/bin/env perl
use strict;
use warnings;
use FindBin;
use Test::More;
# Without a driver list the Ubuntu netboot image ships no network module at all, and a node
# whose NIC is not built into the kernel cannot reach its root filesystem. QEMU hides this,
# because virtio-net is built into the Ubuntu riscv64 kernel; a machine with an r8169 or an
# e1000e does not boot.
use lib "$FindBin::Bin/../../perl-xCAT";
use lib "$FindBin::Bin/../../xCAT-server/lib/perl";
use lib "$FindBin::Bin/../../xCAT-server/share/xcat/netboot/imgutils";
require imgutils;
my @riscv = imgutils::default_net_drivers( 'ubuntu', 'riscv64' );
ok( scalar @riscv, 'a riscv64 Ubuntu image is given network drivers' );
for my $driver (qw(virtio_net e1000 e1000e igb r8169 tg3 mlx5_core)) {
ok( scalar( grep { $_ eq $driver } @riscv ), "the list carries $driver" );
}
# Every Ubuntu row carries overlay, because the netboot root is an overlay mount.
ok( scalar( grep { $_ eq 'overlay' } @riscv ), 'the list carries overlay' );
# The architectures that already worked must keep the drivers they had.
is_deeply(
[ imgutils::default_net_drivers( 'ubuntu', 'x86_64' ) ],
[qw(tg3 bnx2 bnx2x e1000 e1000e igb mlx_en mlx5_core virtio_net overlay)],
'x86_64 keeps its drivers' );
ok( scalar( imgutils::default_net_drivers( 'ubuntu', 'ppc64el' ) ),
'ppc64el keeps its drivers' );
is_deeply( [ imgutils::default_net_drivers( 'ubuntu', 'sparc' ) ], [],
'an architecture with no entry still gets an empty list' );
done_testing();
+46
View File
@@ -0,0 +1,46 @@
#!/usr/bin/env perl
use strict;
use warnings;
use File::Spec;
use FindBin;
use Test::More;
# A diskless image needs a kernel and the tools its boot scripts call. Without a package
# list for the architecture, genimage debootstraps whatever the generic list holds and the
# image cannot boot. Compare the riscv64 lists with the x86_64 ones they follow.
use lib "$FindBin::Bin/../lib";
use XCAT::Test::File qw(repo_path);
sub packages {
my ($path) = @_;
my $full = repo_path($path);
return unless -r $full;
open(my $fh, '<', $full) or die "cannot read $full: $!";
my @packages = grep { length && !/^#/ } map { my $l = $_; chomp $l; $l =~ s/\s+//g; $l } <$fh>;
close($fh);
return \@packages;
}
foreach my $release (qw(24.04 26.04)) {
my $netboot = "xCAT-server/share/xcat/netboot/ubuntu/compute.ubuntu$release.riscv64.pkglist";
my $x86 = "xCAT-server/share/xcat/netboot/ubuntu/compute.ubuntu$release.x86_64.pkglist";
my $riscv_packages = packages($netboot);
ok($riscv_packages, "$release has a riscv64 netboot package list");
is_deeply($riscv_packages, packages($x86),
"the $release riscv64 image installs what the x86_64 image installs");
ok(scalar(grep { $_ eq 'linux-image-generic' } @{$riscv_packages}),
"the $release riscv64 image installs a kernel");
ok(scalar(grep { $_ eq 'nfs-common' } @{$riscv_packages}),
"the $release riscv64 image can mount its root over NFS");
}
my $install = packages(
'xCAT-server/share/xcat/install/ubuntu/compute.ubuntu26.04.riscv64.pkglist');
ok($install, '26.04 has a riscv64 install package list');
is_deeply($install,
packages('xCAT-server/share/xcat/install/ubuntu/compute.ubuntu26.04.x86_64.pkglist'),
'the 26.04 riscv64 install takes what the x86_64 install takes');
done_testing();
+8 -1
View File
@@ -39,13 +39,20 @@ sub resolved {
}
# The shared lists keep ntp for the releases that still carry it. aarch64 has no list of its own.
foreach my $case ( [ $install, 'compute' ], [ $install, 'service' ], [ $install, 'kvm' ], [ $netboot, 'compute' ] ) {
# The install compute profile has a 24.04 list of its own, with chrony: the Subiquity template
# installs chrony and ntp cannot join it in one apt transaction (ubuntu_subiquity_pkglists.t).
foreach my $case ( [ $install, 'service' ], [ $install, 'kvm' ], [ $netboot, 'compute' ] ) {
my ( $dir, $profile ) = @$case;
my ( $file, $p ) = resolved( $dir, $profile, 'ubuntu24.04.4', 'aarch64' );
is( $file, "$profile.pkglist", "$profile on 24.04 without a list of its own resolves to the shared list" );
ok( $p->{ntp}, "... which keeps ntp" );
ok( !$p->{$_}, "... and no longer names $_" ) for qw(libodbc1 qemu-kvm libvirt-bin);
}
{
my ( $file, $p ) = resolved( $install, 'compute', 'ubuntu24.04.4', 'aarch64' );
is( $file, 'compute.ubuntu24.04.pkglist', 'the install compute profile on 24.04 resolves its own list' );
ok( $p->{chrony} && !$p->{ntp}, '... which names chrony and not ntp' );
}
ok( packages_in("$install/service.pkglist")->{unixodbc}, 'the shared service list names unixodbc' );
ok( packages_in("$install/service.pkglist")->{'libdbd-pg-perl'}, '... and the PostgreSQL driver beside the MySQL one' );
@@ -0,0 +1,61 @@
#!/usr/bin/env perl
use strict;
use warnings;
use FindBin;
use Test::More;
# archive.ubuntu.com publishes amd64 and i386 only, so a ppc64el or riscv64 stateful install
# is handed a mirror that carries no package for it. The architecture being installed is the
# last component of the media directory.
BEGIN {
package xCAT::Table;
our $value;
sub new { return bless {}, shift }
sub getAttribs { return defined $value ? { value => $value } : undef }
$INC{'xCAT/Table.pm'} = __FILE__;
}
use lib "$FindBin::Bin/../../perl-xCAT";
use lib "$FindBin::Bin/../../xCAT-server/lib/perl";
my $module = "$FindBin::Bin/../../xCAT-server/lib/perl/xCAT/Template.pm";
plan skip_all => 'Template.pm not found' unless -r $module;
eval { require $module; 1 } or plan skip_all => "could not load Template.pm: $@";
my $mirror = \&xCAT::Template::ubuntu_subiquity_apt_mirror;
# The osimage's architecture decides the mirror. pkgdir cannot: it is whatever path the
# administrator configured, so an amd64 image under /srv/custom-media would be read as a
# non-x86 architecture and sent to the ports archive.
is( $mirror->('riscv64'), 'http://ports.ubuntu.com/ubuntu-ports',
'a riscv64 image takes the ports archive' );
is( $mirror->('ppc64el'), 'http://ports.ubuntu.com/ubuntu-ports',
'a ppc64el image takes the ports archive' );
is( $mirror->('ppc64le'), 'http://ports.ubuntu.com/ubuntu-ports',
'the other spelling of ppc64 takes the ports archive' );
for my $x86 (qw(x86_64 amd64 x86 i386)) {
is( $mirror->($x86), 'http://archive.ubuntu.com/ubuntu',
"an $x86 image keeps the main archive" );
}
# With no architecture there is nothing to key on, so the previous default stands.
is( $mirror->(undef), 'http://archive.ubuntu.com/ubuntu',
'an unknown architecture keeps the previous default' );
# site.ubuntu_apt_mirror still wins, which is how an airgapped cluster points at its own.
{
local $xCAT::Table::value = 'http://mirror.example.invalid/ubuntu';
is( $mirror->('riscv64'), 'http://mirror.example.invalid/ubuntu',
'site.ubuntu_apt_mirror overrides the ports archive' );
is( $mirror->('x86_64'), 'http://mirror.example.invalid/ubuntu',
'site.ubuntu_apt_mirror overrides the main archive' );
}
{
local $xCAT::Table::value = '';
is( $mirror->('riscv64'), 'http://ports.ubuntu.com/ubuntu-ports',
'an empty site.ubuntu_apt_mirror does not blank the mirror' );
}
done_testing();
+22 -1
View File
@@ -21,11 +21,32 @@ sub apt_config_for {
no warnings 'redefine';
local *xCAT::Template::ubuntu_subiquity_apt_mirror = sub { $opt{mirror} };
local *xCAT::Template::ubuntu_subiquity_uses_deb822_sources = sub { $opt{deb822} };
local *xCAT::Template::ubuntu_subiquity_otherpkg_sources = sub { () };
local *xCAT::Template::ubuntu_subiquity_otherpkg_sources = sub { @{ $opt{others} || [] } };
local *xCAT::Template::ubuntu_subiquity_uses_generated_cdrom_source = sub { 0 };
return xCAT::Template::ubuntu_subiquity_apt_config('/some/media/dir');
}
# --- the otherpkgs repository: a one-line source before Deb822, a Deb822 stanza from 24.04 on, ---
# --- since curtin drops the options of a one-line source when it converts it there ---
my $others = [ 'http://192.0.2.10/install/post/otherpkgs/ubuntu24.04/x86_64' ];
my $classic_others = apt_config_for( mirror => $MIRROR, deb822 => 0, others => $others );
like( $classic_others, qr{^ xcat-otherpkgs-0\.list:\n source: "deb \[trusted=yes\] http://192\.0\.2\.10/install/post/otherpkgs/ubuntu24\.04/x86_64 \./"$}m,
'classic: the otherpkgs repository is a one-line trusted source' );
my $deb822_others = apt_config_for( mirror => $MIRROR, deb822 => 1, others => $others );
like( $deb822_others,
qr{^ xcat-otherpkgs-0\.sources:\n source: \|\n Types: deb\n URIs: http://192\.0\.2\.10/install/post/otherpkgs/ubuntu24\.04/x86_64\n Suites: \./\n Components:\n Trusted: yes(?:\n|\z)}m,
'Deb822: the otherpkgs repository is a Deb822 stanza carrying Trusted: yes' );
unlike( $deb822_others, qr/xcat-otherpkgs-0\.list|trusted=yes/, 'Deb822: and no one-line form remains' );
# an otherpkgdir written as URL, suite and components is that source, trusted, not a flat repository at a URL with spaces
my $mirror_others = [ 'http://mirror.example/ubuntu noble main universe' ];
like( apt_config_for( mirror => $MIRROR, deb822 => 0, others => $mirror_others ),
qr{^ xcat-otherpkgs-0\.list:\n source: "deb \[trusted=yes\] http://mirror\.example/ubuntu noble main universe"$}m,
'classic: an otherpkgdir mirror entry keeps its suite and components' );
like( apt_config_for( mirror => $MIRROR, deb822 => 1, others => $mirror_others ),
qr{^ xcat-otherpkgs-0\.sources:\n source: \|\n Types: deb\n URIs: http://mirror\.example/ubuntu\n Suites: noble\n Components: main universe\n Trusted: yes(?:\n|\z)}m,
'Deb822: and becomes a stanza with them as fields, so apt reads one URI' );
# --- online, classic sources (20.04 / 22.04): the archive must be added via sources: ---
my $classic = apt_config_for( mirror => $MIRROR, deb822 => 0 );
@@ -0,0 +1,138 @@
#!/usr/bin/env perl
use strict;
use warnings;
use File::Spec;
use File::Temp;
use FindBin;
use Test::More;
# ospkgs installs the pkglist from every pkgdir entry, the media first and the mirrors after it,
# and the Subiquity autoinstall now installs the pkglist too, so the installer must be given the
# same mirrors. The specs are derived from the pkgdir value alone, and the apt configuration is
# rendered for real with only the database readers stubbed.
my $repo = File::Spec->rel2abs( File::Spec->catdir( $FindBin::Bin, '..', '..' ) );
my @incs = ( "$repo/perl-xCAT", "$repo/xCAT-server/lib/perl" );
my $devnull = File::Spec->devnull();
my $probe = join( ' ', $^X, ( map { "-I$_" } @incs ), '-e', "'require xCAT::Template; 1'", ">$devnull", "2>&1" );
plan skip_all => 'xCAT::Template cannot be loaded here' if system($probe) != 0;
require lib;
lib->import(@incs);
require xCAT::Template;
{
no warnings qw(redefine once);
*xCAT::Template::ubuntu_subiquity_pkgdir_uri = sub { return "http://192.0.2.10$_[0]" };
}
my $flat = File::Temp->newdir();
open( my $pfh, '>', "$flat/Packages" ) or die $!; close $pfh;
open( my $rfh, '>', "$flat/Release" ) or die $!; close $rfh;
my @specs = xCAT::Template::ubuntu_subiquity_pkgdir_source_specs(
"/install/ubuntu24.04.4/x86_64, http://mirror.example/ubuntu noble main universe ,http://repo.example/extra,ssh://host/path,http://mirror.example/ubuntu jammy,http://flat.example/pool ./,$flat"
);
is( scalar(@specs), 3, 'the media path, the bare URL, the ssh entry and a suite without components are no apt sources; the rest are' );
is_deeply( $specs[0], { uri => 'http://mirror.example/ubuntu', suites => 'noble', components => 'main universe', trusted => 0, signed_by => '',
line => 'deb http://mirror.example/ubuntu noble main universe' },
'an entry written as URL suite components is used as written, and is not marked trusted' );
is_deeply( $specs[1], { uri => 'http://flat.example/pool', suites => './', components => '', trusted => 0, signed_by => '',
line => 'deb http://flat.example/pool ./' },
'an exact-path suite needs no component' );
is_deeply( $specs[2], { uri => "http://192.0.2.10$flat", suites => './', components => '', trusted => 1, signed_by => '',
line => "deb [trusted=yes] http://192.0.2.10$flat ./" },
'a local flat repository is served by the management node and trusted, as an otherpkgdir is' );
is_deeply( [ xCAT::Template::ubuntu_subiquity_pkgdir_source_specs(undef) ], [], 'no pkgdir gives no sources' );
my @alias = xCAT::Template::ubuntu_subiquity_pkgdir_source_specs("$flat,http://192.0.2.10$flat/ ./");
is( scalar(@alias), 1, 'a local repository and its own URL in pkgdir are one source' );
is( $alias[0]{trusted}, 1, '... trusted, as the directory entry is' );
my $keyring = '/usr/share/keyrings/ubuntu-archive-keyring.gpg';
is_deeply(
[ xCAT::Template::ubuntu_subiquity_pkgdir_source_specs( 'http://archive.example/ubuntu/ noble-proposed main,http://mirror.example/ubuntu noble main', $keyring, 'http://archive.example/ubuntu' ) ],
[ { uri => 'http://archive.example/ubuntu/', suites => 'noble-proposed', components => 'main', trusted => 0, signed_by => $keyring,
line => "deb [signed-by=$keyring] http://archive.example/ubuntu/ noble-proposed main" },
{ uri => 'http://mirror.example/ubuntu', suites => 'noble', components => 'main', trusted => 0, signed_by => '', line => 'deb http://mirror.example/ubuntu noble main' } ],
'an entry that names the apt mirror, trailing slash or not, carries the key given for that mirror; another mirror gets none' );
is_deeply(
[ xCAT::Template::ubuntu_subiquity_pkgdir_source_specs( 'http://archive.example/ubuntu jammy main', '', 'http://archive.example/ubuntu' ) ],
[ { uri => 'http://archive.example/ubuntu', suites => 'jammy', components => 'main', trusted => 0, signed_by => '', line => 'deb http://archive.example/ubuntu jammy main' } ],
'without a key for the mirror the entry is used as written' );
our ( $apt_mirror, @otherpkg_sources ) = ( '', () );
{
no warnings qw(redefine once);
*xCAT::Template::ubuntu_subiquity_apt_mirror = sub { return $main::apt_mirror };
*xCAT::Template::ubuntu_subiquity_otherpkg_sources = sub { return @main::otherpkg_sources };
}
# the value mkinstall hands over: the media first, then two mirrors
my $mirrors = '/install/ubuntu24.04.4/x86_64,http://mirror.example/ubuntu noble main,http://repo.example/extra';
sub apt_config_for {
my ( $media_dir, %args ) = @_;
local $apt_mirror = $args{mirror} // '';
local @otherpkg_sources = @{ $args{others} || [] };
return xCAT::Template::ubuntu_subiquity_apt_config( $media_dir, undef, $args{pkgdirs} );
}
my $online = apt_config_for( 'ubuntu24.04', mirror => 'http://archive.example/ubuntu', pkgdirs => $mirrors );
like( $online, qr/^ sources:$/m, 'online: the pkgdir mirrors open the sources mapping' );
like( $online, qr{^ xcat-pkgdir-0\.sources:\n source: \|\n Types: deb\n URIs: http://mirror\.example/ubuntu\n Suites: noble\n Components: main(?:\n|\z)}m,
'... as a Deb822 stanza on a Deb822 release, with the suite and components as written' );
unlike( $online, qr{xcat-pkgdir-1|repo\.example}, '... and the bare URL is not offered as a repository' );
my $online_both = apt_config_for( 'ubuntu22.04', mirror => 'http://archive.example/ubuntu', others => ['http://mn/otherpkgs'], pkgdirs => $mirrors );
is( scalar( () = $online_both =~ /^ sources:$/mg ), 1, 'online with otherpkgs and pkgdir mirrors: one sources mapping' );
like( $online_both, qr/xcat-otherpkgs-0\.list:.*xcat-pkgdir-0\.list:/s, '... otherpkgs first, then the mirror' );
my $offline_deb822 = apt_config_for( 'ubuntu24.04', pkgdirs => $mirrors );
like( $offline_deb822, qr{^ URIs: http://mirror\.example/ubuntu\n Suites: noble\n Components: main(?:\n|\z)}m, 'offline Deb822: a stanza per mirror' );
unlike( $offline_deb822, qr{repo\.example|Trusted: yes\n(?:.*\n)* URIs: http://mirror}, '... nothing trusted and no bare URL' );
my $offline_legacy = apt_config_for( 'ubuntu22.04', pkgdirs => $mirrors );
like( $offline_legacy, qr{^ sources:\n xcat-pkgdir-0\.list:\n source: "deb http://mirror\.example/ubuntu noble main"$}m, 'offline legacy: .list files under sources' );
my $same = apt_config_for( 'ubuntu24.04', mirror => 'http://mirror.example/ubuntu', pkgdirs => 'http://mirror.example/ubuntu noble-proposed main' );
like( $same, qr{^ xcat-pkgdir-0\.sources:\n source: \|\n Types: deb\n URIs: http://mirror\.example/ubuntu\n Suites: noble-proposed\n Components: main\n Signed-By: \Q$keyring\E(?:\n|\z)}m,
'another suite of the apt mirror is added with the signing key the installer gives that mirror' );
# before Deb822 the archive sources are rendered here without a key, so a repeated mirror entry must carry none either
foreach my $release ( [ 'ubuntu20.04', 'focal' ], [ 'ubuntu22.04', 'jammy' ] ) {
my ( $media, $suite ) = @$release;
my $legacy_same = apt_config_for( $media, mirror => 'http://mirror.example/ubuntu', pkgdirs => "http://mirror.example/ubuntu $suite main" );
like( $legacy_same, qr{^ xcat-pkgdir-0\.list:\n source: "deb http://mirror\.example/ubuntu \Q$suite\E main"$}m,
"$media: an entry that repeats the apt mirror carries no signing key, like the archive sources rendered next to it" );
like( $legacy_same, qr{^ xcat-ubuntu-archive\.list:\n source: "deb http://mirror\.example/ubuntu \$RELEASE main restricted universe multiverse"$}m,
"$media: which stay as they were" );
}
# a repository named by otherpkgdir and by pkgdir is offered once, trusted, on both source forms
foreach my $media ( 'ubuntu24.04', 'ubuntu22.04' ) {
my $overlap = apt_config_for( $media, mirror => 'http://archive.example/ubuntu', others => ['http://repo.example/ubuntu'],
pkgdirs => '/install/ubuntu24.04.4/x86_64,http://repo.example/ubuntu/ ./' );
like( $overlap, qr/xcat-otherpkgs-0\./, "$media: the repository the otherpkgdir names is offered as the trusted otherpkgs source" );
unlike( $overlap, qr/xcat-pkgdir/, "$media: and not again as a pkgdir source with other options" );
}
# an otherpkgdir entry that names the apt mirror itself gets the options of the source the installer already has for it
my $archive_others = ['http://mirror.example/ubuntu noble universe'];
like( apt_config_for( 'ubuntu22.04', mirror => 'http://mirror.example/ubuntu', others => $archive_others ),
qr{^ xcat-otherpkgs-0\.list:\n source: "deb http://mirror\.example/ubuntu noble universe"$}m,
'classic: an otherpkgdir entry for the apt mirror carries no option, like the archive sources next to it' );
like( apt_config_for( 'ubuntu24.04', mirror => 'http://mirror.example/ubuntu', others => $archive_others ),
qr{^ xcat-otherpkgs-0\.sources:\n source: \|\n Types: deb\n URIs: http://mirror\.example/ubuntu\n Suites: noble\n Components: universe\n Signed-By: \Q$keyring\E(?:\n|\z)}m,
'Deb822: and carries the archive keyring and no Trusted, as the primary source does' );
# the same repository and suite with other components: the pkgdir components join the otherpkgs source
my $union = apt_config_for( 'ubuntu24.04', mirror => 'http://archive.example/ubuntu', others => ['http://repo.example/team stable tools'],
pkgdirs => '/install/ubuntu24.04.4/x86_64,http://repo.example/team/ stable compute tools' );
like( $union, qr{^ xcat-otherpkgs-0\.sources:\n source: \|\n Types: deb\n URIs: http://repo\.example/team\n Suites: stable\n Components: tools compute\n Trusted: yes(?:\n|\z)}m,
'a pkgdir entry that repeats an otherpkgs repository adds its components to that source' );
unlike( $union, qr/xcat-pkgdir/, '... and is not a second source' );
my $union_legacy = apt_config_for( 'ubuntu22.04', mirror => 'http://archive.example/ubuntu', others => ['http://repo.example/team stable tools'],
pkgdirs => '/install/ubuntu24.04.4/x86_64,http://repo.example/team stable compute' );
like( $union_legacy, qr{^ xcat-otherpkgs-0\.list:\n source: "deb \[trusted=yes\] http://repo\.example/team stable tools compute"$}m,
'in the one-line form as well' );
my $none = apt_config_for( 'ubuntu24.04', mirror => 'http://archive.example/ubuntu' );
unlike( $none, qr/sources:/, 'without otherpkgs or mirrors no sources mapping is rendered on a Deb822 release' );
done_testing();
+200
View File
@@ -0,0 +1,200 @@
#!/usr/bin/env perl
use strict;
use warnings;
no warnings 'once';
use FindBin;
use lib "$FindBin::Bin/../lib";
use File::Spec;
use File::Temp;
use Test::More;
use XCAT::Test::File qw(repo_path);
sub read_text { my ($path) = @_; open( my $fh, '<', $path ) or die "$path: $!"; local $/; my $text = <$fh>; close($fh); return $text; }
sub write_text { my ( $path, $text ) = @_; open( my $fh, '>', $path ) or die "$path: $!"; print {$fh} $text; close($fh); return; }
# A Subiquity autoinstall installs the packages of its user-data packages list, and until now the
# template named a fixed set, so the osimage pkglist reached the node only through ospkgs after
# the first boot. The template can now carry #INCLUDE_DEFAULT_PKGLIST_AUTOINSTALL# on a list line,
# and Template.pm renders one list item per pkglist package in its place.
my $module = repo_path('xCAT-server/lib/perl/xCAT/Template.pm');
plan skip_all => 'Template.pm not found' unless -r $module;
my @incs = ( repo_path('perl-xCAT'), repo_path('xCAT-server/lib/perl') );
# Every xCAT module prepends $XCATROOT/lib/perl as it compiles, so on a host with xCAT installed
# the modules Template.pm loads afterwards would come from /opt/xcat. Point it at the checkout.
my $xcatroot = File::Temp->newdir();
mkdir "$xcatroot/lib" or die "$xcatroot/lib: $!";
symlink( repo_path('xCAT-server/lib/perl'), "$xcatroot/lib/perl" ) or die "symlink: $!";
$ENV{XCATROOT} = "$xcatroot";
my $devnull = File::Spec->devnull();
my $probe = join( ' ', $^X, ( map { "-I$_" } @incs ), '-e', "'require xCAT::Template; 1'", ">$devnull", "2>&1" );
plan skip_all => 'xCAT::Template cannot be loaded here' if system($probe) != 0;
require lib;
lib->import(@incs);
require xCAT::Template;
require xCAT::Postage; # the pkglist reader Template.pm calls, loaded by the plugin in production
# ---- the record filter: only what apt can be asked for in an autoinstall packages list --------
my @packages = xCAT::Template::ubuntu_autoinstall_packages(
'openssh-server', ' gawk', 'ntp', '-snmpd', '@core', '#NEW_INSTALL_LIST#', 'd-i pkgsel/include string foo',
'd-i tasksel/first multiselect standard,not-a-real-package', 'nfs-common=1:2.6.4-3ubuntu5', 'gawk', 'Bad_Name', '',
'libc6', 'libc6:i386', 'curl/noble', 'dns-server^', 'a/b/c', 'vim rsync -busybox-static gpg', '@Group With Space', 'wget Bad_Name', 'bc # a calculator',
'wget-', 'libc6:i386-', 'tree+', '-snmpd apache2', '@core bc'
);
is_deeply( \@packages, [qw(openssh-server gawk ntp libc6 dns-server^ vim rsync gpg bc tree+)],
'names and tasks are kept once each, a space-separated line gives each package, a comment ends it; pins, target releases, architecture qualifiers, removals in either hyphen form, a record that begins with a removal or a group, markers, directives and unknown syntax are not' );
is_deeply( [ xCAT::Template::ubuntu_autoinstall_packages( 'msodbcsql18', '#ENV:ACCEPT_EULA=Y#', 'gawk' ) ], [],
'a list with an apt environment setting stays with ospkgs whole' );
is_deeply( [ xCAT::Template::ubuntu_autoinstall_packages( 'gawk', 'msodbcsql18 #ENV:ACCEPT_EULA=Y#' ) ], [],
'so does a list with the setting after a package on the same line, where get_envlist reads it too' );
is_deeply( [ xCAT::Template::ubuntu_autoinstall_packages( 'gawk', '#INCLUDEBAD:cannot open pkglist file /absent.pkglist#' ) ], [],
'a list with an unreadable include stays with ospkgs whole' );
is_deeply( [ xCAT::Template::ubuntu_autoinstall_packages() ], [], 'no records give no packages' );
# ---- the record reader: lines kept whole, includes followed, the comma text unchanged ---------
{
my $d = File::Temp->newdir();
write_text( "$d/common.pkglist", "# shared\nnfs-common\n\@Group With Space\n" );
write_text( "$d/compute.pkglist", "openssh-server\n # a comment\nd-i tasksel/first multiselect standard,not-a-real-package\n#INCLUDE:$d/common.pkglist#\n#NEW_INSTALL_LIST#\nchrony\n" );
my @records = xCAT::Postage->get_pkglist_records("$d/compute.pkglist");
is_deeply( \@records,
[ 'openssh-server', 'd-i tasksel/first multiselect standard,not-a-real-package', 'nfs-common', '@Group With Space', '#NEW_INSTALL_LIST#', 'chrony' ],
'records are whole lines, comments dropped, the include expanded in place' );
is( xCAT::Postage->get_pkglist_tex("$d/compute.pkglist"),
'openssh-server,d-i tasksel/first multiselect standard,not-a-real-package,nfs-common,@Group With Space,#NEW_INSTALL_LIST#,chrony',
'the comma text ospkgs receives is unchanged, and cannot tell the directive comma apart' );
my @missing = xCAT::Postage->get_pkglist_records("$d/absent.pkglist");
like( $missing[0], qr/^#INCLUDEBAD:/, 'an unreadable file yields the INCLUDEBAD marker record' );
# top.pkglist includes sub/common.pkglist, which includes leaf.pkglist: the leaf next to top.pkglist is the one meant
mkdir "$d/sub" or die "$d/sub: $!";
write_text( "$d/top.pkglist", "#INCLUDE:sub/common.pkglist#\n" );
write_text( "$d/sub/common.pkglist", "#INCLUDE:leaf.pkglist#\n" );
write_text( "$d/leaf.pkglist", "nfs-common\n" );
write_text( "$d/sub/leaf.pkglist", "snmpd\n" );
is_deeply( [ xCAT::Postage->get_pkglist_records("$d/top.pkglist") ], ['nfs-common'],
'a nested include resolves against the directory of the listed pkglist' );
is_deeply( [ xCAT::Postage->get_pkglist_records("$d/top.pkglist") ], [ split /,/, xCAT::Postage->get_pkglist_tex("$d/top.pkglist") ],
'and reads the same files get_pkglist_tex reads' );
write_text( "$d/note.pkglist", "#INCLUDE:leaf.pkglist# # the shared leaf\nbc # a calculator\n" );
my @noted = xCAT::Postage->get_pkglist_records("$d/note.pkglist");
is_deeply( \@noted, [ split /,/, xCAT::Postage->get_pkglist_tex("$d/note.pkglist") ],
'an include followed by a note is expanded, the note staying on the last record as get_pkglist_tex leaves it' );
is_deeply( [ xCAT::Template::ubuntu_autoinstall_packages(@noted) ], [qw(nfs-common bc)], 'and the notes add no packages' );
}
is( xCAT::Template::ubuntu_autoinstall_items( " ", " - wget\n - gpg\n", [qw(gawk gpg chrony)] ),
" - \"gawk\"\n - \"chrony\"\n", 'items already listed above the token are not repeated, and every item is a quoted string' );
is( xCAT::Template::ubuntu_autoinstall_items( " ", "", [qw(null true 12)] ), " - \"null\"\n - \"true\"\n - \"12\"\n",
'names YAML would read as null, boolean or number stay strings' );
is( xCAT::Template::ubuntu_autoinstall_items( " ", " - wget\n - chrony\n", [qw(gawk ntp ntpdate snmpd)] ),
" - \"gawk\"\n - \"snmpd\"\n", 'a time daemon the template installs keeps the pkglist time daemons with ospkgs' );
is( xCAT::Template::ubuntu_autoinstall_items( " ", " - wget\n", [qw(gawk ntp)] ),
" - \"gawk\"\n - \"ntp\"\n", 'without a fixed time daemon the pkglist one is installed' );
# ---- the rendering: the token line becomes one item per package, at its own indentation -------
my %site;
no warnings 'redefine', 'once';
local *xCAT::TableUtils::get_site_attribute = sub {
my ( undef, $key ) = @_;
return defined $site{$key} ? ( $site{$key} ) : ();
};
local *xCAT::NetworkUtils::getipaddr = sub { return '192.0.2.10'; };
local *xCAT::Template::getPersistentKcmdline = sub { return ''; };
use warnings;
my $dir = File::Temp->newdir();
my $included = File::Spec->catfile( "$dir", 'common.pkglist' );
my $pkglist = File::Spec->catfile( "$dir", 'compute.pkglist' );
write_text( $included, "# shared\nnfs-common\nsnmpd\n" );
write_text( $pkglist, "openssh-server\n# a comment\nchrony rsync # time and files\nwget=1.21.2-2ubuntu1\n-ntp\nwget-\n\@standard\nd-i tasksel/first multiselect standard,not-a-real-package\n#INCLUDE:$included#\n" );
my $in = File::Spec->catfile( "$dir", 'in.tmpl' );
write_text( $in,
" packages:\n"
. " - openssh-server\n"
. " - wget\n"
. " - #INCLUDE_DEFAULT_PKGLIST_AUTOINSTALL#\n"
. " late-commands:\n"
. " - echo done\n" );
my $render = sub {
my ( $list, %extra ) = @_;
%site = ( installdir => '/install' );
my $out = File::Spec->catfile( "$dir", 'out.' . ( defined $list ? 'list' : 'none' ) );
xCAT::Template->subvars( $in, $out, 'testnode', $list, '/install/ubuntu24.04/x86_64', 'ubuntu', undef,
{ xcatmaster => '192.0.2.10' }, osarch => 'x86_64', %extra );
return read_text($out);
};
my $rendered = $render->($pkglist);
is( $rendered,
" packages:\n"
. " - openssh-server\n"
. " - wget\n"
. " - \"chrony\"\n"
. " - \"rsync\"\n"
. " - \"nfs-common\"\n"
. " - \"snmpd\"\n"
. " late-commands:\n"
. " - echo done\n",
'the pkglist packages, includes followed, become list items at the token indentation, without repeating the items above' );
unlike( $rendered, qr/not-a-real-package/, 'a package name inside a preseed directive with commas is not a package' );
unlike( $rendered, qr/wget=/, 'a version pin is not an item: it stays with ospkgs' );
unlike( $rendered, qr/wget-/, 'a trailing-hyphen removal is not an item either: the installer would remove the package the postscripts need' );
unlike( $rendered, qr/"(?:time|and|files)"/, 'an inline comment adds no items' );
# a site template that includes the stock one: the token arrives with the include and must be expanded too
my $wrapper = File::Spec->catfile( "$dir", 'wrapper.tmpl' );
write_text( $wrapper, "#INCLUDE:$in#\n" );
my $render_via = sub {
my ($list) = @_;
%site = ( installdir => '/install' );
my $out = File::Spec->catfile( "$dir", 'out.wrapper.' . ( defined $list ? 'list' : 'none' ) );
xCAT::Template->subvars( $wrapper, $out, 'testnode', $list, '/install/ubuntu24.04/x86_64', 'ubuntu', undef, { xcatmaster => '192.0.2.10' }, osarch => 'x86_64' );
return read_text($out);
};
is( $render_via->($pkglist), $rendered, 'a template that includes the stock one renders the same package list' );
my $without = $render->(undef);
is( $without,
" packages:\n - openssh-server\n - wget\n late-commands:\n - echo done\n",
'an osimage without a pkglist keeps the template packages and loses only the token line' );
is( $render_via->(undef), $without, 'and through an including template the token line goes as well, leaving no empty item' );
my $env_list = File::Spec->catfile( "$dir", 'env.pkglist' );
my $env_included = File::Spec->catfile( "$dir", 'env-common.pkglist' );
write_text( $env_included, "msodbcsql18 #ENV:ACCEPT_EULA=Y#\n" );
write_text( $env_list, "gawk\n#INCLUDE:$env_included#\n" );
is( $render->($env_list), $without, 'a pkglist whose include carries an apt environment setting is left to ospkgs whole, and the token line goes' );
is( $render->( $pkglist, environvar => 'ACCEPT_EULA=Y' ), $without,
'an osimage with environvar installs its pkglist through ospkgs alone, where the variables reach apt-get, and the token line goes' );
# the pkgdir mirrors may need those variables as well, so they stay out of the installer's apt sources too
{
no warnings 'redefine', 'once';
local *xCAT::Template::ubuntu_subiquity_otherpkg_sources = sub { () };
local *xCAT::Template::ubuntu_subiquity_apt_mirror = sub { 'http://archive.example/ubuntu' };
my $apt_in = File::Spec->catfile( "$dir", 'apt.tmpl' );
write_text( $apt_in, "#UBUNTU_SUBIQUITY_APT_CONFIG#\n" );
my $apt_render = sub {
my (%extra) = @_;
%site = ( installdir => '/install' );
my $out = File::Spec->catfile( "$dir", 'out.apt' );
xCAT::Template->subvars( $apt_in, $out, 'testnode', $pkglist, '/install/ubuntu24.04/x86_64', 'ubuntu', undef, { xcatmaster => '192.0.2.10' },
osarch => 'x86_64', pkgdirs => '/install/ubuntu24.04/x86_64,http://mirror.example/ubuntu noble main', %extra );
return read_text($out);
};
like( $apt_render->(), qr{URIs: http://mirror\.example/ubuntu}, 'the pkgdir mirror joins the installer sources' );
like( $apt_render->(), qr{^ conf: 'APT::Install-Recommends "false";'$}m, 'the installer installs without recommended packages, as ospkgs does' );
unlike( $apt_render->( environvar => 'http_proxy=http://proxy.example:3128' ), qr{mirror\.example|xcat-pkgdir},
'but not for an osimage with environvar, whose mirrors may need those variables' );
}
done_testing();
@@ -0,0 +1,62 @@
#!/usr/bin/env perl
use strict;
use warnings;
use File::Spec;
use FindBin;
use Test::More;
use lib "$FindBin::Bin/../../perl-xCAT";
use lib "$FindBin::Bin/../../xCAT-server/lib/perl";
use xCAT::Postage;
use xCAT::SvrUtils;
# The Subiquity autoinstall installs the template's fixed packages and the osimage pkglist in one
# apt transaction, and the template names chrony. The shared compute.pkglist names ntp for the
# releases before Subiquity, and on the Subiquity releases ntp pulls ntpsec, which conflicts with
# chrony, so those releases need their own default list. The lists are resolved the way
# mkinstall resolves them and their packages are read the way ospkgs reads them.
my $repo = File::Spec->rel2abs( File::Spec->catdir( $FindBin::Bin, '..', '..' ) );
my $install = "$repo/xCAT-server/share/xcat/install/ubuntu";
my $template = "$install/compute.subiquity.tmpl";
plan skip_all => 'the Ubuntu install directory is not here' unless -d $install && -f $template;
sub packages_in {
my ($path) = @_;
return { map { $_ => 1 } grep { length } split /,/, xCAT::Postage::get_pkglist_tex($path) };
}
sub resolved {
my ( $os, $arch ) = @_;
return xCAT::SvrUtils->get_pkglist_file_name( $install, 'compute', $os, $arch, $os =~ /^(ubuntu\d+\.\d+)/ ? $1 : $os );
}
open( my $tfh, '<', $template ) or die "$template: $!";
my $body = do { local $/; <$tfh> };
close($tfh);
my ($block) = $body =~ /^ packages:\n((?: - .*\n)+)/m;
my %fixed = map { $_ => 1 } ( $block =~ /^ - ([^#\s]+)$/mg );
ok( $fixed{chrony}, 'the Subiquity template names chrony among its fixed packages' );
my %time_daemon = map { $_ => 1 } qw(chrony ntp ntpsec);
foreach my $case ( [ 'ubuntu20.04.6', 'x86_64' ], [ 'ubuntu20.04.6', 'ppc64le' ],
[ 'ubuntu22.04.5', 'x86_64' ], [ 'ubuntu22.04.5', 'ppc64le' ],
[ 'ubuntu24.04.4', 'x86_64' ], [ 'ubuntu24.04.4', 'ppc64le' ], [ 'ubuntu24.04.4', 'riscv64' ],
[ 'ubuntu26.04.1', 'x86_64' ], [ 'ubuntu26.04.1', 'riscv64' ] ) {
my ( $os, $arch ) = @$case;
my $file = resolved( $os, $arch );
ok( $file, "$os $arch resolves a compute pkglist" ) or next;
my $packages = packages_in($file);
ok( $packages->{chrony}, "$os $arch: the list names chrony, the daemon the template installs" );
ok( !$packages->{ntp} && !$packages->{ntpdate}, "$os $arch: ... and not ntp, which would conflict with it" );
my @daemons = grep { $time_daemon{$_} } keys %{ { %fixed, %$packages } };
is( scalar(@daemons), 1, "$os $arch: the autoinstall transaction carries exactly one time daemon" );
}
# The releases before Subiquity keep the shared list and its ntp.
my $legacy = resolved( 'ubuntu16.04.7', 'x86_64' );
is( $legacy, "$install/compute.pkglist", '16.04 still resolves the shared list' );
ok( packages_in($legacy)->{ntp}, '... which keeps ntp for the releases where chrony was not the default' );
done_testing();
@@ -16,6 +16,9 @@ like($tmpl, qr/autoinstall:/, 'template has autoinstall: key');
like($tmpl, qr/version:\s*1/, 'template has version: 1');
like($tmpl, qr/^\s*identity:/m, 'template has an identity section so subiquity does not prompt');
like($tmpl, qr/^ - #INCLUDE_DEFAULT_PKGLIST_AUTOINSTALL#\n/m, 'the packages list carries the osimage pkglist through the autoinstall token');
like($tmpl, qr/^ - openssh-server\n/m, '... and keeps openssh-server, which xCAT needs on the node');
like($tmpl, qr/^ - wget\n/m, '... and wget, which the early and late commands use');
like($tmpl, qr/kernel:/, 'template has kernel section');
like($tmpl, qr/package:\s*linux-generic/, 'template specifies linux-generic kernel');
like($tmpl, qr/#UBUNTU_SUBIQUITY_APT_CONFIG#/, 'template renders apt section from osimage context');
@@ -78,5 +81,39 @@ unlike($tmpl, qr/if \[ -x \/tmp\/pre\.sh \]/, 'pre.sh not checked with -x');
# Subiquity behavior can be handled without cloning this template per release.
unlike($tmpl, qr/noble-|jammy-|focal-/, 'template avoids release-specific apt suite names');
like($tmpl, qr/#UBUNTU_SUBIQUITY_APT_CONFIG#/, 'template keeps dynamic apt renderer marker');
like($tmpl, qr{2>&1'\n(?:\s*#[^\n]*\n)*\s*- rm -f (?:/target/etc/apt/sources\.list\.d/xcat-(?:otherpkgs|pkgdir)-\*\.(?:list|sources)\s+){4}/target/etc/apt/apt\.conf\.d/94curtin-config$}m,
'the installer sources for the otherpkgs repository and the pkgdir mirrors, and the apt configuration curtin wrote, are removed from the target by a late-command of their own, after the post script block');
# the post script block: its status is the post script's, so a failed post script stops the install
{
my ($block) = $tmpl =~ /\n - '(\{\n.*?\n \} >>\/target\/var\/log\/xcat\/xcat\.log 2>&1)'\n/s;
ok( defined $block, 'the post script block is found' ) or last;
$block =~ s/''/'/g;
require File::Temp;
my $root = File::Temp->newdir();
mkdir "$root/bin" or die;
for my $tool ( 'curtin', 'wget' ) {
open( my $fh, '>', "$root/bin/$tool" ) or die;
print {$fh} $tool eq 'curtin' ? "#!/bin/sh\ncase \"\$*\" in *post.script*) exit 42;; esac\nexit 0\n" : "#!/bin/sh\nfor a; do case \"\$a\" in http*) touch \"\${a##*/}\";; esac; done\nexit 0\n";
close $fh; chmod 0755, "$root/bin/$tool";
}
( my $script = $block ) =~ s{/target}{$root/target}g;
$script =~ s{/tmp/pre-install\.log}{$root/pre-install.log}g;
for my $token ( [ '#SUBIQUITYINSTALLNIC#', '' ], [ '#SUBIQUITYINSTALLMAC#', '52:54:00:00:00:01' ], [ '#HOSTNAME#', 'cn1' ], [ '#XCATVAR:XCATMASTER#', '192.0.2.10' ],
[ '#COLONHTTPPORT#', '' ], [ '#TABLEBLANKOKAY:bootparams:$NODE:kcmdline#', '' ] ) {
$script =~ s/\Q$token->[0]\E/$token->[1]/g;
}
require File::Path;
File::Path::make_path( map { "$root/target/$_" } qw(etc/default root var/log/xcat) );
open( my $hosts, '>', "$root/target/etc/hosts" ) or die; print {$hosts} "127.0.0.1 localhost\n"; close $hosts;
open( my $pre, '>', "$root/pre-install.log" ) or die; close $pre;
my $cwd = File::Spec->rel2abs('.');
chdir $root or die;
local $ENV{PATH} = "$root/bin:$ENV{PATH}";
system( 'sh', '-c', $script );
my $status = $? >> 8;
chdir $cwd or die;
is( $status, 42, 'a failing post script fails the late-command block, so Subiquity stops the install instead of switching the node to disk boot' );
}
done_testing();
@@ -0,0 +1,90 @@
#!/usr/bin/env perl
use strict;
use warnings;
use File::Spec;
use FindBin;
use Test::More;
# A riscv64 management node has no legacy Genesis: the image ships as the OpenEmbedded package
# xcat-genesis-openembedded-riscv64, which mknb consumes. xCAT.spec already says so for the rpm
# side and gives riscv64 its own dependency block.
#
# The deb side named xcat-genesis-scripts-amd64 in a plain Depends, and that package is
# Architecture: all, so apt installed the x86 Genesis scripts (and, through them, the x86 Genesis
# base) on a riscv64 management node. Restrict the dependency to the architectures that have a
# legacy Genesis, and leave amd64 and ppc64el untouched.
my $repo_root = File::Spec->rel2abs(
File::Spec->catdir( $FindBin::Bin, '..', '..' )
);
sub read_file {
my ($filename) = @_;
open( my $fh, '<', $filename ) or die "Unable to read $filename: $!";
my $content = do { local $/; <$fh> };
close($fh);
return $content;
}
my $have_dpkg_deps = eval { require Dpkg::Deps; 1 } ? 1 : 0;
foreach my $pkg ( [ 'xCAT', 'xcat' ], [ 'xCATsn', 'xcatsn' ] ) {
my ( $dir, $name ) = @$pkg;
my $control = read_file( File::Spec->catfile( $repo_root, $dir, 'debian', 'control' ) );
my ($depends) = $control =~ /^Depends:\s*(.*)$/m;
ok( defined $depends, "$name debian/control has a Depends line" );
my ($recommends) = $control =~ /^Recommends:\s*(.*)$/m;
ok( defined $recommends, "$name debian/control has a Recommends line" );
my ($entry) = grep { /xcat-genesis-scripts/ } split( /\s*,\s*/, $depends );
ok( defined $entry, "$name depends on a legacy Genesis scripts package" );
like( $entry, qr/\[!riscv64\]/,
"$name excludes riscv64 from the legacy Genesis scripts dependency" );
SKIP: {
skip( "Dpkg::Deps is not available", 7 ) unless $have_dpkg_deps;
# Dpkg::Deps cannot parse a substvar, which dpkg-gencontrol expands before it gets here.
( my $parsable = $depends ) =~ s/\$\{[^}]*\}\s*,?\s*//g;
my %reduced = map {
my $d = Dpkg::Deps::deps_parse( $parsable, reduce_arch => 1, host_arch => $_ );
$_ => ( defined $d ? $d->output() : '' )
} qw(riscv64 amd64 ppc64el);
unlike( $reduced{riscv64}, qr/xcat-genesis-scripts/,
"$name on riscv64 does not pull the legacy Genesis scripts" );
like( $reduced{amd64}, qr/xcat-genesis-scripts-amd64/,
"$name on amd64 still pulls them" );
like( $reduced{ppc64el}, qr/xcat-genesis-scripts-amd64/,
"$name on ppc64el still pulls them" );
# The restriction must not take anything else with it: every other dependency of the
# amd64 package must survive on riscv64.
my @lost = grep { $reduced{riscv64} !~ /\Q$_\E/ }
grep { !/xcat-genesis-scripts/ }
map { my $d = $_; $d =~ s/\s*\(.*//; $d =~ s/\s*\[.*//; $d }
split( /\s*,\s*/, $reduced{amd64} );
is_deeply( \@lost, [],
"$name on riscv64 keeps every other dependency" )
or diag( "dropped: @lost" );
# A management node of any architecture can provision nodes of another, so what it SERVES
# to those nodes -- the x86 boot payload and the Genesis images of the other architectures
# -- stays recommended everywhere. Only the legacy Genesis of this node is architecture
# specific, and that one is a dependency, not a recommendation.
my %reduced_recommends = map {
my $d = Dpkg::Deps::deps_parse( $recommends, reduce_arch => 1, host_arch => $_ );
$_ => ( defined $d ? $d->output() : '' )
} qw(riscv64 amd64);
like( $reduced_recommends{riscv64}, qr/\bsyslinux-xcat\b/,
"$name on riscv64 still recommends the x86 boot payload it serves" );
like( $reduced_recommends{riscv64}, qr/xcat-genesis-openembedded-x86-64/,
"$name on riscv64 still recommends the Genesis image of the other architectures" );
like( $reduced_recommends{amd64}, qr/\bsyslinux-xcat\b/,
"$name on amd64 is unchanged" );
}
}
done_testing();
@@ -0,0 +1,39 @@
#!/usr/bin/env perl
use strict;
use warnings;
use File::Spec;
use FindBin;
use Test::More;
# copycd builds the riscv64 boot loader by running grub-mkimage, and the plugin that runs it ships
# in xcat-server. Nothing declared the package that provides that command, so a management or
# service node installed without it copies riscv64 media and then produces no loader at all, while
# DHCP keeps pointing every riscv64 node at the path where the loader should be.
#
# grub-common provides /usr/bin/grub-mkimage on every supported Ubuntu release. That the plugin
# runs grub-mkimage is shown by ubuntu_copycd_grub2_loader.t, which drives it through a stub.
my $repo_root = File::Spec->rel2abs( File::Spec->catdir( $FindBin::Bin, '..', '..' ) );
sub depends_of {
my ($package) = @_;
my $file = File::Spec->catfile( $repo_root, $package, 'debian', 'control' );
open( my $fh, '<', $file ) or die "Unable to read $file: $!";
my $control = do { local $/; <$fh> };
close($fh);
my ($depends) = $control =~ /^Depends:\s*(.*)$/m;
return [ split( /\s*,\s*/, $depends // '' ) ];
}
my $server = depends_of('xCAT-server');
ok( scalar( grep { /^grub-common\b/ } @{$server} ),
'xcat-server depends on the package that provides grub-mkimage' );
# The plugin runs there, so the metapackages inherit it and must not carry their own copy.
foreach my $package (qw(xCAT xCATsn)) {
is_deeply( [ grep { /^grub-common\b/ } @{ depends_of($package) } ], [],
"$package leaves the dependency with the package that runs the command" );
}
done_testing();
+3 -3
View File
@@ -8,9 +8,9 @@ Vcs-browser: https://github.com/xcat2/xcat-core.git
Homepage: https://xcat.org/
Package: xcat
Architecture: amd64 ppc64el
Depends: ${perl:Depends}, goconserver(>= 0.3.3-snap000000000000), xcat-server (>= 2.13-snap000000000000), xcat-client (>= 2.13-snap000000000000), libdbd-sqlite3-perl, isc-dhcp-server | kea, bind9, apache2, nfs-kernel-server, libxml-parser-perl, rsync, tftpd-hpa, libnet-telnet-perl, chrony | ntp, xcat-genesis-scripts-amd64 (>= 2.13-snap000000000000)
Recommends: net-tools, nmap, kea, tftp-hpa, ipmitool-xcat (>= 1.8.17-1), syslinux[any-amd64], libsys-virt-perl, syslinux-xcat, xnba-undi, elilo-xcat, util-linux-extra, xcat-buildkit (>= 2.13-snap000000000000), xcat-probe (>= 2.13-snap000000000000), xcat-genesis-openembedded-x86-64, xcat-genesis-openembedded-ppc64le, xcat-genesis-openembedded-riscv64, xcat-genesis-openembedded-s390x
Architecture: amd64 ppc64el riscv64
Depends: ${perl:Depends}, goconserver(>= 0.3.3-snap000000000000), xcat-server (>= 2.13-snap000000000000), xcat-client (>= 2.13-snap000000000000), libdbd-sqlite3-perl, isc-dhcp-server | kea, bind9, apache2, nfs-kernel-server, libxml-parser-perl, rsync, tftpd-hpa, libnet-telnet-perl, chrony | ntp, nmap, ipmitool-xcat (>= 1.8.18-4), xcat-genesis-scripts-amd64 (>= 2.13-snap000000000000) [!riscv64]
Recommends: net-tools, kea, tftp-hpa, syslinux[any-amd64], libsys-virt-perl, syslinux-xcat, xnba-undi, elilo-xcat, util-linux-extra, xcat-buildkit (>= 2.13-snap000000000000), xcat-probe (>= 2.13-snap000000000000), xcat-genesis-openembedded-x86-64, xcat-genesis-openembedded-ppc64le, xcat-genesis-openembedded-riscv64, xcat-genesis-openembedded-s390x
Suggests: yaboot-xcat
Description: Metapackage for a common, default xCAT setup
xCAT is Extreme Cluster/Cloud Administration Toolkit. xCAT offers complete
+6 -6
View File
@@ -829,7 +829,7 @@ elif ( pmatch "$OSVER" "ubuntu*" ); then
# upgrade existing packages
apt-get -y update
command="DEBIAN_FRONTEND=noninteractive apt-get -y --allow-unauthenticated --force-yes -o Dpkg::Options::='--force-confold' -o Dpkg::Options::='--force-confdef' upgrade "
command="xcat_apt_get -o Dpkg::Options::='--force-confold' -o Dpkg::Options::='--force-confdef' upgrade"
echo "=== $command"
eval $command
R=$?
@@ -841,7 +841,7 @@ elif ( pmatch "$OSVER" "ubuntu*" ); then
# but keeping this here doesn't really hurt. Anything that looks like a group will
# be installed as though it were a package.)
if [ -n "$groups" ]; then
command="$ENVLIST apt-get -q -y --force-yes install --no-install-recommends $groups"
command="$ENVLIST xcat_apt_get -q install --no-install-recommends $groups"
echo "=== $command"
eval $command
R=$?
@@ -855,7 +855,7 @@ elif ( pmatch "$OSVER" "ubuntu*" ); then
# install packages
if [ -n "$pkgs" ]; then
command="$ENVLIST apt-get -q -y --force-yes install --no-install-recommends $pkgs"
command="$ENVLIST xcat_apt_get -q install --no-install-recommends $pkgs"
echo "=== $command"
eval $command
R=$?
@@ -865,16 +865,16 @@ elif ( pmatch "$OSVER" "ubuntu*" ); then
fi
if [ -n "$cudapkgs" ]; then
command="$ENVLIST apt-get -q -y --force-yes install --no-install-recommends $cudapkgs"
command="$ENVLIST xcat_apt_get -q install --no-install-recommends $cudapkgs"
echo "=== $command"
# the nvidia-346 postinstall script will trigger the building of nvidia driver, it will use the ARCH environment parameter, unset the ARCH env variable will resolve this issue
original_arch=$ARCH
unset ARCH
eval $command
R=$?
# re declare the ARCH env after installing cuda command done
ARCH=$original_arch
export ARCH
R=$?
if [ $R -ne 0 ]; then
RETURNVAL=$R
fi
@@ -997,10 +997,10 @@ else
original_arch=$ARCH
unset ARCH
result=`eval $cmd 2>&1`
R=$?
# re declare the ARCH env after installing cuda command done
ARCH=$original_arch
export ARCH
R=$?
if [ $R -ne 0 ]; then
RETURNVAL=$R
logger -t $log_label -p local4.info "ospkgs: $cmd\n $result"
+4 -4
View File
@@ -905,9 +905,9 @@ EOF`
elif [ $hasapt -eq 1 ]; then
apt_get_update_if_repos_changed $REPOFILE
if [ $VERBOSE ]; then
echo "$envlist DEBIAN_FRONTEND=noninteractive apt-get -y --allow-unauthenticated --force-yes -o Dpkg::Options::='--force-confold' -o Dpkg::Options::='--force-confdef' upgrade"
echo "$envlist xcat_apt_get -o Dpkg::Options::='--force-confold' -o Dpkg::Options::='--force-confdef' upgrade"
fi
result=`eval $envlist DEBIAN_FRONTEND=noninteractive apt-get -y --allow-unauthenticated --force-yes -o Dpkg::Options::='--force-confold' -o Dpkg::Options::='--force-confdef' upgrade 2>&1`
result=`eval $envlist xcat_apt_get -o Dpkg::Options::='--force-confold' -o Dpkg::Options::='--force-confdef' upgrade 2>&1`
R=$?
if [ $R -ne 0 ]; then
RETURNVAL=$R
@@ -1022,9 +1022,9 @@ EOF`
elif [ $hasapt -eq 1 ]; then
apt_get_update_if_repos_changed $REPOFILE
if [ $VERBOSE ]; then
echo "$envlist DEBIAN_FRONTEND=noninteractive apt-get -q -y --force-yes -o Dpkg::Options::='--force-confold' -o Dpkg::Options::='--force-confdef' install $repo_pkgs"
echo "$envlist xcat_apt_get -q -o Dpkg::Options::='--force-confold' -o Dpkg::Options::='--force-confdef' install $repo_pkgs"
fi
result=`eval $envlist DEBIAN_FRONTEND=noninteractive apt-get -q -y --force-yes -o Dpkg::Options::='--force-confold' -o Dpkg::Options::='--force-confdef' install $repo_pkgs 2>&1`
result=`eval $envlist xcat_apt_get -q -o Dpkg::Options::='--force-confold' -o Dpkg::Options::='--force-confdef' install $repo_pkgs 2>&1`
R=$?
if [ $R -ne 0 ]; then
RETURNVAL=$R
+1 -15
View File
@@ -631,20 +631,6 @@ fi
#if the service restart with "service/systemctl" failed
#try to kill the process and start
if [ "$?" != "0" ];then
PIDLIST=`ps aux | grep -v grep | grep "/usr/sbin/sshd"|awk -F" " '{print $2}'|xargs`
if [ -n "$PIDLIST" ]; then
kill -9 $PIDLIST
waited=0
while [ $waited -lt 10 ]; do
alive=0
for pid in $PIDLIST; do
if kill -0 $pid 2>/dev/null; then alive=1; fi
done
if [ $alive -eq 0 ]; then break; fi
sleep 1
waited=`expr $waited + 1`
done
fi
/usr/sbin/sshd
xcat_restart_sshd_after_failed_service_restart
fi
kill -9 $CREDPID
+69 -57
View File
@@ -19,26 +19,34 @@
#
#####################################################
[ -f "/xcatpost/xcatlib.sh" ] && . /xcatpost/xcatlib.sh
if [ "$XCATDSKLSPOST_SOURCE_ONLY" != "1" ]; then
if [ -f "/xcatpost/xcatlib.sh" ]; then
. /xcatpost/xcatlib.sh
else
str_dir_name=`dirname $0`
[ -f "$str_dir_name/xcatlib.sh" ] && . "$str_dir_name/xcatlib.sh"
fi
if [ -f /xcatpost/mypostscript.post ]; then
XCATDEBUGMODE=`grep 'XCATDEBUGMODE=' /xcatpost/mypostscript.post | cut -d= -f2 | tr -d \'\" | tr A-Z a-z`
MASTER_IP=`grep '^MASTER_IP=' /xcatpost/mypostscript.post |cut -d= -f2|sed s/\'//g`
NODE=`grep '^NODE=' /xcatpost/mypostscript.post |cut -d= -f2|sed s/\'//g`
else
for param in `cat /proc/cmdline`; do
key=`echo $param|awk -F= '{print $1}'`
if [ "$key" = "xcatdebugmode" ]; then
XCATDEBUGMODE=`echo $param|awk -F= '{print $2}'| tr -d \'\" | tr A-Z a-z`
fi
if [ -f /xcatpost/mypostscript.post ]; then
XCATDEBUGMODE=`grep 'XCATDEBUGMODE=' /xcatpost/mypostscript.post | cut -d= -f2 | tr -d \'\" | tr A-Z a-z`
MASTER_IP=`grep '^MASTER_IP=' /xcatpost/mypostscript.post |cut -d= -f2|sed s/\'//g`
NODE=`grep '^NODE=' /xcatpost/mypostscript.post |cut -d= -f2|sed s/\'//g`
else
for param in `cat /proc/cmdline`; do
key=`echo $param|awk -F= '{print $1}'`
if [ "$key" = "xcatdebugmode" ]; then
XCATDEBUGMODE=`echo $param|awk -F= '{print $2}'| tr -d \'\" | tr A-Z a-z`
fi
if [ "$key" = "LOGSERVER" ]; then
MASTER_IP=`echo $param|awk -F= '{print $2}'`
fi
done
if [ "$key" = "LOGSERVER" ]; then
MASTER_IP=`echo $param|awk -F= '{print $2}'`
fi
done
fi
fi
XCATINFOFILE=/opt/xcat/xcatinfo
XCAT_WGET_LOG=${XCAT_WGET_LOG:-/tmp/wget.log}
#echolog: process message log and echo in xcatdsklspost
#arguments:
@@ -83,6 +91,45 @@ echolog()
}
download_mypostscript()
{
server=$1
node=$2
max_retries=$3
TFTPDIR=$4
if [ -z $server ]; then
return 1;
fi
if [ -z "$TFTPDIR" ]; then
TFTPDIR="/tftpboot"
fi
retry=0
rc=1
echolog "debug" "trying to download http://$server$TFTPDIR/mypostscripts/mypostscript.$node..."
while [ 0 -eq 0 ]; do
wget -N --waitretry=10 --random-wait -T 60 http://$server$TFTPDIR/mypostscripts/mypostscript.$node -P /$xcatpost 2>> "$XCAT_WGET_LOG"
rc=$?
# if no error and the file was downloaded
if [ $rc -eq 0 ] && [ -f /$xcatpost/mypostscript.$node ]; then
mv /$xcatpost/mypostscript.$node /$xcatpost/mypostscript
echolog "debug" "mypostscript.$node is downloaded successfully."
return 0
fi
retry=$(($retry+1))
if [ $retry -eq $max_retries ]; then
echolog "debug" "http://$server$TFTPDIR/mypostscripts/mypostscript.$node is not available."
break
fi
done
return $rc
}
download_postscripts()
{
server=$1
@@ -111,16 +158,16 @@ download_postscripts()
# These dispatcher scripts are not needed by the legacy netboot post
# path. Newer wget parses HTML-looking regex strings inside downloaded
# scripts and fails the whole recursive download on bogus URLs.
export LANG=C; wget -l inf -nH -N -r --waitretry=10 --random-wait -e robots=off -T 60 -nH --cut-dirs=2 --reject "index.html*,post.xcat.ng,post.xcat.rhels10" --no-parent http://$server$INSTALLDIR/postscripts/ -P /$xcatpost 2> /tmp/wget.log
export LANG=C; wget -l inf -nH -N -r --waitretry=10 --random-wait -e robots=off -T 60 -nH --cut-dirs=2 --reject "index.html*,post.xcat.ng,post.xcat.rhels10" --no-parent http://$server$INSTALLDIR/postscripts/ -P /$xcatpost 2> "$XCAT_WGET_LOG"
rc=$?
if [ $rc -eq 0 ]; then
# return from wget was 0 but some OS do not return errors, so we
# have additional checks for
# failed: Connection httpd not running
# 404: Not Found - if directory does not exist
grep -i -E "... failed: Connection refused.$" /tmp/wget.log
grep -i -E "... failed: Connection refused.$" "$XCAT_WGET_LOG"
rc1=$?
grep -i -E "ERROR 404: Not Found.$" /tmp/wget.log
grep -i -E "ERROR 404: Not Found.$" "$XCAT_WGET_LOG"
rc2=$?
# check to see no errors at all, grep returns 1
if [ $rc1 -eq 1 ] && [ $rc2 -eq 1 ]; then
@@ -143,45 +190,6 @@ download_postscripts()
}
download_mypostscript()
{
server=$1
node=$2
max_retries=$3
TFTPDIR=$4
if [ -z $server ]; then
return 1;
fi
if [ -z "$TFTPDIR" ]; then
TFTPDIR="/tftpboot"
fi
retry=0
rc=1
echolog "debug" "trying to download http://$server$TFTPDIR/mypostscripts/mypostscript.$node..."
while [ 0 -eq 0 ]; do
wget -N --waitretry=10 --random-wait -T 60 http://$server$TFTPDIR/mypostscripts/mypostscript.$node -P /$xcatpost 2>> /tmp/wget.log
rc=$?
# if no error and the file was downloaded
if [ $rc -eq 0 ] && [ -f /$xcatpost/mypostscript.$node ]; then
mv /$xcatpost/mypostscript.$node /$xcatpost/mypostscript
echolog "debug" "mypostscript.$node is downloaded successfully."
return 0
fi
retry=$(($retry+1))
if [ $retry -eq $max_retries ]; then
echolog "debug" "http://$server$TFTPDIR/mypostscripts/mypostscript.$node is not available."
break
fi
done
return $rc
}
# pmatch determines if 1st argument string is matched by 2nd argument pattern
pmatch ()
@@ -216,6 +224,10 @@ parsehttpserver ()
fi
}
if [ "$XCATDSKLSPOST_SOURCE_ONLY" = "1" ]; then
return 0 2>/dev/null || exit 0
fi
# Main
# parse the arguments
log_label="xcat.updatenode"
+36
View File
@@ -833,6 +833,42 @@ function msgutil {
msgutil_r "" "$@"
}
function xcat_wait_for_processes_to_exit {
local pidlist="$1"
local max_wait="${2:-10}"
local waited=0
local alive
local pid
while [ $waited -lt $max_wait ]; do
alive=0
for pid in $pidlist; do
if kill -0 $pid 2>/dev/null; then
alive=1
fi
done
if [ $alive -eq 0 ]; then
return 0
fi
sleep 1
waited=`expr $waited + 1`
done
return 1
}
function xcat_restart_sshd_after_failed_service_restart {
local sshd_cmd="${1:-/usr/sbin/sshd}"
local PIDLIST
PIDLIST=`ps aux | grep -v grep | grep "/usr/sbin/sshd"|awk -F" " '{print $2}'|xargs`
if [ -n "$PIDLIST" ]; then
kill -9 $PIDLIST
xcat_wait_for_processes_to_exit "$PIDLIST"
fi
$sshd_cmd
}
function fetch_mypostscript {
local postroot
postroot=$1
+6
View File
@@ -14,6 +14,12 @@ xcat_find_rpm_package_manager()
fi
}
# The xCAT repositories are unsigned, and an unattended node cannot answer a debconf prompt.
xcat_apt_get()
{
DEBIAN_FRONTEND=noninteractive apt-get -y --allow-unauthenticated "$@"
}
# Keep the marker assignment last so callers know the whole library loaded.
xcat_is_el_modular_pkgdir()
+3 -3
View File
@@ -7,9 +7,9 @@ Standards-Version: 3.9.4
Homepage: https://xcat.org/
Package: xcatsn
Architecture: amd64 ppc64el
Depends: ${perl:Depends}, goconserver (>=0.3.3-snap000000000000), xcat-server (>= 2.13-snap000000000000), xcat-client (>= 2.13-snap000000000000), libdbd-sqlite3-perl, libxml-parser-perl, tftpd-hpa, libnet-telnet-perl, isc-dhcp-server | kea, bind9, apache2, nfs-kernel-server, xcat-genesis-scripts-amd64 (>= 2.13-snap000000000000)
Recommends: net-tools, nmap, kea, tftp-hpa, ipmitool-xcat (>= 1.8.17-1), syslinux[any-amd64], libsys-virt-perl, syslinux-xcat, xnba-undi, elilo-xcat, xcat-buildkit (>= 2.13-snap000000000000), xcat-probe (>= 2.13-snap000000000000), xcat-genesis-openembedded-x86-64, xcat-genesis-openembedded-ppc64le, xcat-genesis-openembedded-riscv64, xcat-genesis-openembedded-s390x
Architecture: amd64 ppc64el riscv64
Depends: ${perl:Depends}, goconserver (>=0.3.3-snap000000000000), xcat-server (>= 2.13-snap000000000000), xcat-client (>= 2.13-snap000000000000), libdbd-sqlite3-perl, libxml-parser-perl, tftpd-hpa, libnet-telnet-perl, isc-dhcp-server | kea, bind9, apache2, nfs-kernel-server, nmap, ipmitool-xcat (>= 1.8.18-4), xcat-genesis-scripts-amd64 (>= 2.13-snap000000000000) [!riscv64]
Recommends: net-tools, kea, tftp-hpa, syslinux[any-amd64], libsys-virt-perl, syslinux-xcat, xnba-undi, elilo-xcat, xcat-buildkit (>= 2.13-snap000000000000), xcat-probe (>= 2.13-snap000000000000), xcat-genesis-openembedded-x86-64, xcat-genesis-openembedded-ppc64le, xcat-genesis-openembedded-riscv64, xcat-genesis-openembedded-s390x
Suggests: yaboot-xcat
Description: Metapackage for a common, default xCAT service node setup
xCATsn is a service node management package intended for at-scale