diff --git a/.github/workflows/xcat_test.yml b/.github/workflows/xcat_test.yml index 0fe2cd911..04d50aff0 100644 --- a/.github/workflows/xcat_test.yml +++ b/.github/workflows/xcat_test.yml @@ -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 diff --git a/build-utils/lib/XCAT/BuildUtils.pm b/build-utils/lib/XCAT/BuildUtils.pm index e0deac209..54df11487 100644 --- a/build-utils/lib/XCAT/BuildUtils.pm +++ b/build-utils/lib/XCAT/BuildUtils.pm @@ -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. diff --git a/builddebs.pl b/builddebs.pl index 7e148223e..873936b44 100755 --- a/builddebs.pl +++ b/builddebs.pl @@ -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 diff --git a/docs/source/advanced/mixed_cluster/building_stateless_images.rst b/docs/source/advanced/mixed_cluster/building_stateless_images.rst index c3a1ffa5a..590829abd 100644 --- a/docs/source/advanced/mixed_cluster/building_stateless_images.rst +++ b/docs/source/advanced/mixed_cluster/building_stateless_images.rst @@ -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 -------------------------------------------------------------------------------------- diff --git a/docs/source/advanced/mixed_cluster/support_matrix.rst b/docs/source/advanced/mixed_cluster/support_matrix.rst index 8bf5e69e7..50a6d9ad4 100644 --- a/docs/source/advanced/mixed_cluster/support_matrix.rst +++ b/docs/source/advanced/mixed_cluster/support_matrix.rst @@ -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`. diff --git a/docs/source/developers/guides/code/builds.rst b/docs/source/developers/guides/code/builds.rst index 607075ce5..1146e4225 100644 --- a/docs/source/developers/guides/code/builds.rst +++ b/docs/source/developers/guides/code/builds.rst @@ -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``. diff --git a/docs/source/guides/admin-guides/basic_concepts/xcat_object/node.rst b/docs/source/guides/admin-guides/basic_concepts/xcat_object/node.rst index c72a78d42..debe934e0 100644 --- a/docs/source/guides/admin-guides/basic_concepts/xcat_object/node.rst +++ b/docs/source/guides/admin-guides/basic_concepts/xcat_object/node.rst @@ -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: diff --git a/docs/source/guides/admin-guides/manage_clusters/common/deployment/additionalpkg/additional_pkg_overview.rst b/docs/source/guides/admin-guides/manage_clusters/common/deployment/additionalpkg/additional_pkg_overview.rst index f867e901a..27ce4d7a3 100644 --- a/docs/source/guides/admin-guides/manage_clusters/common/deployment/additionalpkg/additional_pkg_overview.rst +++ b/docs/source/guides/admin-guides/manage_clusters/common/deployment/additionalpkg/additional_pkg_overview.rst @@ -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 diff --git a/docs/source/guides/admin-guides/manage_clusters/riscv64/index.rst b/docs/source/guides/admin-guides/manage_clusters/riscv64/index.rst index faa4ce191..1605ae0ae 100644 --- a/docs/source/guides/admin-guides/manage_clusters/riscv64/index.rst +++ b/docs/source/guides/admin-guides/manage_clusters/riscv64/index.rst @@ -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 boot`` the firmware boots the installed system because the per-node ``grub2-`` loader link is removed. +Ubuntu +~~~~~~ + +Import the Ubuntu 24.04 or 26.04 riscv64 live-server ISO with ``copycds``; it +creates the ``ubuntu-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``; boards whose firmware exposes the console on another device need ``linuximage.addkcmdline`` or the serial settings adjusted. diff --git a/docs/source/guides/admin-guides/references/man5/noderes.5.rst b/docs/source/guides/admin-guides/references/man5/noderes.5.rst index 733a5afab..d10f8fd4b 100644 --- a/docs/source/guides/admin-guides/references/man5/noderes.5.rst +++ b/docs/source/guides/admin-guides/references/man5/noderes.5.rst @@ -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 diff --git a/docs/source/guides/admin-guides/references/man7/group.7.rst b/docs/source/guides/admin-guides/references/man7/group.7.rst index 477bda943..da1ae65dd 100644 --- a/docs/source/guides/admin-guides/references/man7/group.7.rst +++ b/docs/source/guides/admin-guides/references/man7/group.7.rst @@ -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 diff --git a/docs/source/guides/admin-guides/references/man7/node.7.rst b/docs/source/guides/admin-guides/references/man7/node.7.rst index 6c290d1d7..34067aaa5 100644 --- a/docs/source/guides/admin-guides/references/man7/node.7.rst +++ b/docs/source/guides/admin-guides/references/man7/node.7.rst @@ -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 diff --git a/docs/source/overview/differentiators.rst b/docs/source/overview/differentiators.rst index 7e9e68206..d8c335351 100644 --- a/docs/source/overview/differentiators.rst +++ b/docs/source/overview/differentiators.rst @@ -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 diff --git a/docs/source/overview/support_matrix.rst b/docs/source/overview/support_matrix.rst index 02e847bf2..d64c18a92 100644 --- a/docs/source/overview/support_matrix.rst +++ b/docs/source/overview/support_matrix.rst @@ -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. diff --git a/github_action_xcat_test.pl b/github_action_xcat_test.pl index f4480771b..4a47d4c7d 100644 --- a/github_action_xcat_test.pl +++ b/github_action_xcat_test.pl @@ -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(); diff --git a/perl-xCAT/debian/control b/perl-xCAT/debian/control index 0726e8de6..8e1714b83 100644 --- a/perl-xCAT/debian/control +++ b/perl-xCAT/debian/control @@ -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. diff --git a/perl-xCAT/xCAT/Schema.pm b/perl-xCAT/xCAT/Schema.pm index 50dde765c..4fdafac6b 100644 --- a/perl-xCAT/xCAT/Schema.pm +++ b/perl-xCAT/xCAT/Schema.pm @@ -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.', diff --git a/perl-xCAT/xCAT/Utils.pm b/perl-xCAT/xCAT/Utils.pm index 838e40bbb..7a4405330 100644 --- a/perl-xCAT/xCAT/Utils.pm +++ b/perl-xCAT/xCAT/Utils.pm @@ -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 { diff --git a/xCAT-client/debian/control b/xCAT-client/debian/control index db9b62fe4..689bbb216 100644 --- a/xCAT-client/debian/control +++ b/xCAT-client/debian/control @@ -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 diff --git a/xCAT-genesis-builder/dracut_105/el/module-setup.sh b/xCAT-genesis-builder/dracut_105/el/module-setup.sh index e2bb98250..302c4758f 100755 --- a/xCAT-genesis-builder/dracut_105/el/module-setup.sh +++ b/xCAT-genesis-builder/dracut_105/el/module-setup.sh @@ -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 diff --git a/xCAT-genesis-builder/dracut_105/ubuntu/module-setup.sh b/xCAT-genesis-builder/dracut_105/ubuntu/module-setup.sh index e4b8b0e3d..ba5325247 100755 --- a/xCAT-genesis-builder/dracut_105/ubuntu/module-setup.sh +++ b/xCAT-genesis-builder/dracut_105/ubuntu/module-setup.sh @@ -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 diff --git a/xCAT-probe/subcmds/detect_dhcpd b/xCAT-probe/subcmds/detect_dhcpd index c8e073d88..9cb01f23b 100755 --- a/xCAT-probe/subcmds/detect_dhcpd +++ b/xCAT-probe/subcmds/detect_dhcpd @@ -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"; } diff --git a/xCAT-server/debian/control b/xCAT-server/debian/control index 4560b5462..9e0fbe6e6 100644 --- a/xCAT-server/debian/control +++ b/xCAT-server/debian/control @@ -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. diff --git a/xCAT-server/lib/perl/xCAT/Postage.pm b/xCAT-server/lib/perl/xCAT/Postage.pm index e9d40ba8c..0eccbb26c 100644 --- a/xCAT-server/lib/perl/xCAT/Postage.pm +++ b/xCAT-server/lib/perl/xCAT/Postage.pm @@ -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 diff --git a/xCAT-server/lib/perl/xCAT/Template.pm b/xCAT-server/lib/perl/xCAT/Template.pm index 177cf48ec..79b26101f 100644 --- a/xCAT-server/lib/perl/xCAT/Template.pm +++ b/xCAT-server/lib/perl/xCAT/Template.pm @@ -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) = @_; diff --git a/xCAT-server/lib/xcat/plugins/debian.pm b/xCAT-server/lib/xcat/plugins/debian.pm index 7f98fdf7e..e085efc28 100644 --- a/xCAT-server/lib/xcat/plugins/debian.pm +++ b/xCAT-server/lib/xcat/plugins/debian.pm @@ -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 ); } diff --git a/xCAT-server/lib/xcat/plugins/grub2.pm b/xCAT-server/lib/xcat/plugins/grub2.pm index 88a693069..17e48e3b4 100644 --- a/xCAT-server/lib/xcat/plugins/grub2.pm +++ b/xCAT-server/lib/xcat/plugins/grub2.pm @@ -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=) 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"; } diff --git a/xCAT-server/lib/xcat/plugins/mknb.pm b/xCAT-server/lib/xcat/plugins/mknb.pm index c7a6ced38..3c75d0634 100644 --- a/xCAT-server/lib/xcat/plugins/mknb.pm +++ b/xCAT-server/lib/xcat/plugins/mknb.pm @@ -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., 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.. 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"] }); diff --git a/xCAT-server/share/xcat/install/scripts/post.xcat b/xCAT-server/share/xcat/install/scripts/post.xcat index d6ac63ff0..bb40d0f22 100755 --- a/xCAT-server/share/xcat/install/scripts/post.xcat +++ b/xCAT-server/share/xcat/install/scripts/post.xcat @@ -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" diff --git a/xCAT-server/share/xcat/install/scripts/pre.sles b/xCAT-server/share/xcat/install/scripts/pre.sles index 292b4c6b7..1eaa4c7c1 100644 --- a/xCAT-server/share/xcat/install/scripts/pre.sles +++ b/xCAT-server/share/xcat/install/scripts/pre.sles @@ -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 '/mbr!elilo!' \ - /tmp/profile/modified.xml - fi -} if [ -d /sys/firmware/efi ]; then sed -e 's!XCATPARTITIONHOOK!'$instdisk'vfat/boot/efi128mbswapauto/auto!' /tmp/profile/autoinst.xml > /tmp/profile/modified.xml diff --git a/xCAT-server/share/xcat/install/scripts/scriptlib b/xCAT-server/share/xcat/install/scripts/scriptlib index ab74bddb6..de427d3e2 100644 --- a/xCAT-server/share/xcat/install/scripts/scriptlib +++ b/xCAT-server/share/xcat/install/scripts/scriptlib @@ -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 '/mbr!elilo!' \ + "$profile" + fi +} diff --git a/xCAT-server/share/xcat/install/ubuntu/compute.subiquity.tmpl b/xCAT-server/share/xcat/install/ubuntu/compute.subiquity.tmpl index 5918a61e3..328ccd258 100644 --- a/xCAT-server/share/xcat/install/ubuntu/compute.subiquity.tmpl +++ b/xCAT-server/share/xcat/install/ubuntu/compute.subiquity.tmpl @@ -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 next". Require both tokens: another service on that port is not a flipped diff --git a/xCAT-server/share/xcat/install/ubuntu/compute.ubuntu20.04.pkglist b/xCAT-server/share/xcat/install/ubuntu/compute.ubuntu20.04.pkglist new file mode 100644 index 000000000..aaec41835 --- /dev/null +++ b/xCAT-server/share/xcat/install/ubuntu/compute.ubuntu20.04.pkglist @@ -0,0 +1,5 @@ +openssh-server +chrony +gawk +nfs-common +snmpd diff --git a/xCAT-server/share/xcat/install/ubuntu/compute.ubuntu22.04.pkglist b/xCAT-server/share/xcat/install/ubuntu/compute.ubuntu22.04.pkglist new file mode 100644 index 000000000..aaec41835 --- /dev/null +++ b/xCAT-server/share/xcat/install/ubuntu/compute.ubuntu22.04.pkglist @@ -0,0 +1,5 @@ +openssh-server +chrony +gawk +nfs-common +snmpd diff --git a/xCAT-server/share/xcat/install/ubuntu/compute.ubuntu24.04.pkglist b/xCAT-server/share/xcat/install/ubuntu/compute.ubuntu24.04.pkglist new file mode 100644 index 000000000..aaec41835 --- /dev/null +++ b/xCAT-server/share/xcat/install/ubuntu/compute.ubuntu24.04.pkglist @@ -0,0 +1,5 @@ +openssh-server +chrony +gawk +nfs-common +snmpd diff --git a/xCAT-server/share/xcat/install/ubuntu/compute.ubuntu26.04.riscv64.pkglist b/xCAT-server/share/xcat/install/ubuntu/compute.ubuntu26.04.riscv64.pkglist new file mode 100644 index 000000000..813cb2bc2 --- /dev/null +++ b/xCAT-server/share/xcat/install/ubuntu/compute.ubuntu26.04.riscv64.pkglist @@ -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 diff --git a/xCAT-server/share/xcat/netboot/imgutils/imgutils.pm b/xCAT-server/share/xcat/netboot/imgutils/imgutils.pm index 644a6e671..dbea8cd28 100644 --- a/xCAT-server/share/xcat/netboot/imgutils/imgutils.pm +++ b/xCAT-server/share/xcat/netboot/imgutils/imgutils.pm @@ -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)], }, ); diff --git a/xCAT-server/share/xcat/netboot/ubuntu/compute.ubuntu24.04.riscv64.pkglist b/xCAT-server/share/xcat/netboot/ubuntu/compute.ubuntu24.04.riscv64.pkglist new file mode 100644 index 000000000..4c6b2b3c4 --- /dev/null +++ b/xCAT-server/share/xcat/netboot/ubuntu/compute.ubuntu24.04.riscv64.pkglist @@ -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 diff --git a/xCAT-server/share/xcat/netboot/ubuntu/compute.ubuntu26.04.riscv64.pkglist b/xCAT-server/share/xcat/netboot/ubuntu/compute.ubuntu26.04.riscv64.pkglist new file mode 100644 index 000000000..ef2f26fe1 --- /dev/null +++ b/xCAT-server/share/xcat/netboot/ubuntu/compute.ubuntu26.04.riscv64.pkglist @@ -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 diff --git a/xCAT-server/share/xcat/netboot/ubuntu/genimage b/xCAT-server/share/xcat/netboot/ubuntu/genimage index 475d9ae00..17005cdb5 100755 --- a/xCAT-server/share/xcat/netboot/ubuntu/genimage +++ b/xCAT-server/share/xcat/netboot/ubuntu/genimage @@ -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"); } diff --git a/xCAT-server/share/xcat/tools/detect_dhcpd b/xCAT-server/share/xcat/tools/detect_dhcpd index 8d9288301..7671219bb 100755 --- a/xCAT-server/share/xcat/tools/detect_dhcpd +++ b/xCAT-server/share/xcat/tools/detect_dhcpd @@ -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"; } diff --git a/xCAT-server/share/xcat/tools/go-xcat b/xCAT-server/share/xcat/tools/go-xcat index a076797b9..f1c452c7c 100755 --- a/xCAT-server/share/xcat/tools/go-xcat +++ b/xCAT-server/share/xcat/tools/go-xcat @@ -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)" diff --git a/xCAT-test/README.md b/xCAT-test/README.md new file mode 100644 index 000000000..01fbe54d2 --- /dev/null +++ b/xCAT-test/README.md @@ -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 -t ` or `xcattest -f -b ` | + +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. diff --git a/xCAT-test/bats/README.md b/xCAT-test/bats/README.md new file mode 100644 index 000000000..ac65379ab --- /dev/null +++ b/xCAT-test/bats/README.md @@ -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. diff --git a/xCAT-test/bats/genesis_ib_modules.bats b/xCAT-test/bats/genesis_ib_modules.bats new file mode 100644 index 000000000..ed132043e --- /dev/null +++ b/xCAT-test/bats/genesis_ib_modules.bats @@ -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: 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: 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 ] +} diff --git a/xCAT-test/bats/go_xcat_common_repository.bats b/xCAT-test/bats/go_xcat_common_repository.bats new file mode 100644 index 000000000..b49f4fce0 --- /dev/null +++ b/xCAT-test/bats/go_xcat_common_repository.bats @@ -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" +} diff --git a/xCAT-test/bats/go_xcat_el_repo_check.bats b/xCAT-test/bats/go_xcat_el_repo_check.bats new file mode 100644 index 000000000..0064f8673 --- /dev/null +++ b/xCAT-test/bats/go_xcat_el_repo_check.bats @@ -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 +} diff --git a/xCAT-test/bats/helpers/go_xcat.bash b/xCAT-test/bats/helpers/go_xcat.bash new file mode 100644 index 000000000..ee4ec3c44 --- /dev/null +++ b/xCAT-test/bats/helpers/go_xcat.bash @@ -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" +} diff --git a/xCAT-test/bats/helpers/shell_source.bash b/xCAT-test/bats/helpers/shell_source.bash new file mode 100644 index 000000000..61a0bea8d --- /dev/null +++ b/xCAT-test/bats/helpers/shell_source.bash @@ -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" +} diff --git a/xCAT-test/bats/post_xcat_download_policy.bats b/xCAT-test/bats/post_xcat_download_policy.bats new file mode 100644 index 000000000..41810bb97 --- /dev/null +++ b/xCAT-test/bats/post_xcat_download_policy.bats @@ -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" != *'|" 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 ] +} diff --git a/xCAT-test/bats/remoteshell_restart.bats b/xCAT-test/bats/remoteshell_restart.bats new file mode 100644 index 000000000..b4727d7fc --- /dev/null +++ b/xCAT-test/bats/remoteshell_restart.bats @@ -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" ] +} diff --git a/xCAT-test/bats/sles_pre_script.bats b/xCAT-test/bats/sles_pre_script.bats new file mode 100644 index 000000000..bd8387ab0 --- /dev/null +++ b/xCAT-test/bats/sles_pre_script.bats @@ -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' + +true +true +mbr + +EOF + + source "$SCRIPT_LIB" + run set_sles11_uefi_bootloader "$cmdline" "$profile" + [ "$status" -eq 0 ] + grep -Fxq 'elilo' "$profile" + run -1 grep -q 'mbr' "$profile" + run -1 grep -q '"$cmdline" + printf '%s\n' 'mbr' >"$profile" + + source "$SCRIPT_LIB" + run set_sles11_uefi_bootloader "$cmdline" "$profile" + [ "$status" -eq 0 ] + grep -Fxq 'mbr' "$profile" +} diff --git a/xCAT-test/bats/statelite_add_ssh.bats b/xCAT-test/bats/statelite_add_ssh.bats new file mode 100644 index 000000000..b633666f9 --- /dev/null +++ b/xCAT-test/bats/statelite_add_ssh.bats @@ -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" +} diff --git a/xCAT-test/unit/README.md b/xCAT-test/unit/README.md index 79096bcbf..daff2691c 100644 --- a/xCAT-test/unit/README.md +++ b/xCAT-test/unit/README.md @@ -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 diff --git a/xCAT-test/unit/build_utils.t b/xCAT-test/unit/build_utils.t index 8a44fbe39..eaa3a43ff 100644 --- a/xCAT-test/unit/build_utils.t +++ b/xCAT-test/unit/build_utils.t @@ -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' ); diff --git a/xCAT-test/unit/builddebs_riscv64.t b/xCAT-test/unit/builddebs_riscv64.t new file mode 100644 index 000000000..5dc2fc813 --- /dev/null +++ b/xCAT-test/unit/builddebs_riscv64.t @@ -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(); diff --git a/xCAT-test/unit/debian_arch_map.t b/xCAT-test/unit/debian_arch_map.t index 617004a43..a98df8224 100644 --- a/xCAT-test/unit/debian_arch_map.t +++ b/xCAT-test/unit/debian_arch_map.t @@ -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, diff --git a/xCAT-test/unit/debian_control_declarations.t b/xCAT-test/unit/debian_control_declarations.t new file mode 100644 index 000000000..e97feccbb --- /dev/null +++ b/xCAT-test/unit/debian_control_declarations.t @@ -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(); diff --git a/xCAT-test/unit/debian_install_arch.t b/xCAT-test/unit/debian_install_arch.t new file mode 100644 index 000000000..481718290 --- /dev/null +++ b/xCAT-test/unit/debian_install_arch.t @@ -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(); diff --git a/xCAT-test/unit/debian_install_boot_files.t b/xCAT-test/unit/debian_install_boot_files.t index b5783bc94..dc664cbb0 100644 --- a/xCAT-test/unit/debian_install_boot_files.t +++ b/xCAT-test/unit/debian_install_boot_files.t @@ -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'); diff --git a/xCAT-test/unit/detect_dhcpd_tcpdump_lookup.t b/xCAT-test/unit/detect_dhcpd_tcpdump_lookup.t index 8122b1d67..4f410e77d 100644 --- a/xCAT-test/unit/detect_dhcpd_tcpdump_lookup.t +++ b/xCAT-test/unit/detect_dhcpd_tcpdump_lookup.t @@ -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(); diff --git a/xCAT-test/unit/genesis_ib_modules.t b/xCAT-test/unit/genesis_ib_modules.t deleted file mode 100644 index ef2c3f36e..000000000 --- a/xCAT-test/unit/genesis_ib_modules.t +++ /dev/null @@ -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; -} diff --git a/xCAT-test/unit/go_xcat_common_repository.t b/xCAT-test/unit/go_xcat_common_repository.t deleted file mode 100644 index 21931d248..000000000 --- a/xCAT-test/unit/go_xcat_common_repository.t +++ /dev/null @@ -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(); diff --git a/xCAT-test/unit/go_xcat_el_repo_check.t b/xCAT-test/unit/go_xcat_el_repo_check.t deleted file mode 100644 index f8413b165..000000000 --- a/xCAT-test/unit/go_xcat_el_repo_check.t +++ /dev/null @@ -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(); diff --git a/xCAT-test/unit/go_xcat_riscv64_packages.t b/xCAT-test/unit/go_xcat_riscv64_packages.t new file mode 100644 index 000000000..0067209b3 --- /dev/null +++ b/xCAT-test/unit/go_xcat_riscv64_packages.t @@ -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(); diff --git a/xCAT-test/unit/grub2_kcmdline_quoting.t b/xCAT-test/unit/grub2_kcmdline_quoting.t new file mode 100644 index 000000000..1e2066c3d --- /dev/null +++ b/xCAT-test/unit/grub2_kcmdline_quoting.t @@ -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=) 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(); diff --git a/xCAT-test/unit/lookup_netboot_arch.t b/xCAT-test/unit/lookup_netboot_arch.t index d0491991b..2b5801bfb 100644 --- a/xCAT-test/unit/lookup_netboot_arch.t +++ b/xCAT-test/unit/lookup_netboot_arch.t @@ -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(); diff --git a/xCAT-test/unit/post_xcat_download_policy.t b/xCAT-test/unit/post_xcat_download_policy.t deleted file mode 100644 index 7cbf70492..000000000 --- a/xCAT-test/unit/post_xcat_download_policy.t +++ /dev/null @@ -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/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(); diff --git a/xCAT-test/unit/remoteshell_kill_wait.t b/xCAT-test/unit/remoteshell_kill_wait.t deleted file mode 100644 index 505fd2418..000000000 --- a/xCAT-test/unit/remoteshell_kill_wait.t +++ /dev/null @@ -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(); diff --git a/xCAT-test/unit/sles_pre_script.t b/xCAT-test/unit/sles_pre_script.t deleted file mode 100644 index 1ffa01eea..000000000 --- a/xCAT-test/unit/sles_pre_script.t +++ /dev/null @@ -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/elilo<\/loader_type>/, 'SLES 11 UEFI install selects elilo'); -like($src, qr/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(); diff --git a/xCAT-test/unit/statelite_add_ssh.t b/xCAT-test/unit/statelite_add_ssh.t deleted file mode 100644 index b3dc6ddbe..000000000 --- a/xCAT-test/unit/statelite_add_ssh.t +++ /dev/null @@ -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(); diff --git a/xCAT-test/unit/ubuntu_copycd_grub2_loader.t b/xCAT-test/unit/ubuntu_copycd_grub2_loader.t new file mode 100644 index 000000000..aab00ca4b --- /dev/null +++ b/xCAT-test/unit/ubuntu_copycd_grub2_loader.t @@ -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 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(); diff --git a/xCAT-test/unit/ubuntu_copycd_grub2_loader_media.t b/xCAT-test/unit/ubuntu_copycd_grub2_loader_media.t new file mode 100644 index 000000000..7ec01706c --- /dev/null +++ b/xCAT-test/unit/ubuntu_copycd_grub2_loader_media.t @@ -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 +# 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(); diff --git a/xCAT-test/unit/ubuntu_genimage_apt_mirror.t b/xCAT-test/unit/ubuntu_genimage_apt_mirror.t new file mode 100644 index 000000000..c3ad36a43 --- /dev/null +++ b/xCAT-test/unit/ubuntu_genimage_apt_mirror.t @@ -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(); diff --git a/xCAT-test/unit/ubuntu_genimage_resolver_libs.t b/xCAT-test/unit/ubuntu_genimage_resolver_libs.t new file mode 100644 index 000000000..d14460fa1 --- /dev/null +++ b/xCAT-test/unit/ubuntu_genimage_resolver_libs.t @@ -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(); diff --git a/xCAT-test/unit/ubuntu_riscv64_net_drivers.t b/xCAT-test/unit/ubuntu_riscv64_net_drivers.t new file mode 100644 index 000000000..6e98c33a2 --- /dev/null +++ b/xCAT-test/unit/ubuntu_riscv64_net_drivers.t @@ -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(); diff --git a/xCAT-test/unit/ubuntu_riscv64_pkglists.t b/xCAT-test/unit/ubuntu_riscv64_pkglists.t new file mode 100644 index 000000000..2d7097c3e --- /dev/null +++ b/xCAT-test/unit/ubuntu_riscv64_pkglists.t @@ -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(); diff --git a/xCAT-test/unit/ubuntu_shared_pkglists.t b/xCAT-test/unit/ubuntu_shared_pkglists.t index 0fa526fdc..3046c3985 100644 --- a/xCAT-test/unit/ubuntu_shared_pkglists.t +++ b/xCAT-test/unit/ubuntu_shared_pkglists.t @@ -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' ); diff --git a/xCAT-test/unit/ubuntu_subiquity_apt_mirror.t b/xCAT-test/unit/ubuntu_subiquity_apt_mirror.t new file mode 100644 index 000000000..59e397746 --- /dev/null +++ b/xCAT-test/unit/ubuntu_subiquity_apt_mirror.t @@ -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(); diff --git a/xCAT-test/unit/ubuntu_subiquity_apt_sources.t b/xCAT-test/unit/ubuntu_subiquity_apt_sources.t index 0c55d0e8c..d1e53b9d2 100644 --- a/xCAT-test/unit/ubuntu_subiquity_apt_sources.t +++ b/xCAT-test/unit/ubuntu_subiquity_apt_sources.t @@ -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 ); diff --git a/xCAT-test/unit/ubuntu_subiquity_pkgdir_sources.t b/xCAT-test/unit/ubuntu_subiquity_pkgdir_sources.t new file mode 100644 index 000000000..2dc89dfb9 --- /dev/null +++ b/xCAT-test/unit/ubuntu_subiquity_pkgdir_sources.t @@ -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(); diff --git a/xCAT-test/unit/ubuntu_subiquity_pkglist.t b/xCAT-test/unit/ubuntu_subiquity_pkglist.t new file mode 100644 index 000000000..05d15c361 --- /dev/null +++ b/xCAT-test/unit/ubuntu_subiquity_pkglist.t @@ -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(); diff --git a/xCAT-test/unit/ubuntu_subiquity_pkglists.t b/xCAT-test/unit/ubuntu_subiquity_pkglists.t new file mode 100644 index 000000000..393dee965 --- /dev/null +++ b/xCAT-test/unit/ubuntu_subiquity_pkglists.t @@ -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(); diff --git a/xCAT-test/unit/ubuntu_subiquity_template.t b/xCAT-test/unit/ubuntu_subiquity_template.t index 140dfaf39..ecff4688e 100644 --- a/xCAT-test/unit/ubuntu_subiquity_template.t +++ b/xCAT-test/unit/ubuntu_subiquity_template.t @@ -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(); diff --git a/xCAT-test/unit/xcat_riscv64_genesis_dependency.t b/xCAT-test/unit/xcat_riscv64_genesis_dependency.t new file mode 100644 index 000000000..4a1930c4a --- /dev/null +++ b/xCAT-test/unit/xcat_riscv64_genesis_dependency.t @@ -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(); diff --git a/xCAT-test/unit/xcat_server_grub_mkimage_dependency.t b/xCAT-test/unit/xcat_server_grub_mkimage_dependency.t new file mode 100644 index 000000000..a130a0f3a --- /dev/null +++ b/xCAT-test/unit/xcat_server_grub_mkimage_dependency.t @@ -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(); diff --git a/xCAT/debian/control b/xCAT/debian/control index 57a82ffdf..0037da394 100644 --- a/xCAT/debian/control +++ b/xCAT/debian/control @@ -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 diff --git a/xCAT/postscripts/ospkgs b/xCAT/postscripts/ospkgs index 712be9738..2900a92c0 100755 --- a/xCAT/postscripts/ospkgs +++ b/xCAT/postscripts/ospkgs @@ -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" diff --git a/xCAT/postscripts/otherpkgs b/xCAT/postscripts/otherpkgs index f252fc393..c89256e86 100755 --- a/xCAT/postscripts/otherpkgs +++ b/xCAT/postscripts/otherpkgs @@ -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 diff --git a/xCAT/postscripts/remoteshell b/xCAT/postscripts/remoteshell index 3850585f9..04fc145b4 100755 --- a/xCAT/postscripts/remoteshell +++ b/xCAT/postscripts/remoteshell @@ -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 diff --git a/xCAT/postscripts/xcatdsklspost b/xCAT/postscripts/xcatdsklspost index 13110575d..6f7dcc63a 100755 --- a/xCAT/postscripts/xcatdsklspost +++ b/xCAT/postscripts/xcatdsklspost @@ -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" diff --git a/xCAT/postscripts/xcatlib.sh b/xCAT/postscripts/xcatlib.sh index b06838589..6dd27ded5 100755 --- a/xCAT/postscripts/xcatlib.sh +++ b/xCAT/postscripts/xcatlib.sh @@ -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 diff --git a/xCAT/postscripts/xcatpkgutils.sh b/xCAT/postscripts/xcatpkgutils.sh index 766f98ac7..7848cd755 100755 --- a/xCAT/postscripts/xcatpkgutils.sh +++ b/xCAT/postscripts/xcatpkgutils.sh @@ -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() diff --git a/xCATsn/debian/control b/xCATsn/debian/control index 063bf4716..460081543 100644 --- a/xCATsn/debian/control +++ b/xCATsn/debian/control @@ -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