2
0
mirror of https://github.com/xcat2/xcat-core.git synced 2026-10-21 23:28:19 +00:00

Merge pull request #7962 from VersatusHPC/feat/uftp-rootimg-transport

feat(uftp): distribute stateless root images over UFTP multicast
This commit is contained in:
Daniel Hilst
2026-10-21 15:52:02 -03:00
committed by GitHub
48 changed files with 4611 additions and 31 deletions
+3
View File
@@ -64,6 +64,9 @@ jobs:
- name: Exercise the liteimg ramdisk archive
if: ${{ !cancelled() }}
run: prove xCAT-test/native/liteimg_ramdisk_archive.t
- name: Exercise the UFTP listener
if: ${{ !cancelled() }}
run: prove xCAT-test/native/uftp_listener.t
- name: Run tests
if: ${{ !cancelled() }}
run: perl github_action_xcat_test.pl
@@ -0,0 +1,141 @@
Distribute the root image with UFTP multicast
=============================================
A stateless node downloads its root image over HTTP while it boots. When many nodes boot at the same time, the image server sends the same image once per node. UFTP multicast sends the image once to all nodes that ask for it during a short batch window. HTTP stays available as the fallback.
UFTP distribution is optional. It is off unless the ``rootimgtransport`` attribute of the osimage, or ``site.rootimgtransport``, is ``uftp``. Images that use ``http``, the default, boot as before, and the UFTP software is not needed.
How it works
------------
#. ``genimage`` adds the UFTP receiver ``uftpd`` and a fetch helper to the stateless initial ramdisk when the image contains the ``uftp`` package of xcat-dep built for the image architecture. It checks that the ramdisk it built holds them, and records that the ramdisk can receive the root image over UFTP. Another ``uftp`` build or architecture in the image gets a warning and is left out.
#. ``packimage`` publishes the archive with its SHA-256 checksum file, as for every image.
#. For an image whose transport is ``uftp``, ``nodeset`` adds ``xcat.uftp=<listener port>`` and ``xcat.uftpfp=<server key fingerprint>`` to the kernel command line, next to ``imgurl=`` and ``rootimgchecksum=yes``.
#. The booting node downloads the checksum file of ``imgurl``. It asks ``xcat-uftp-listener`` on the image server for the archive that the checksum file lists, then starts its receiver in a private directory.
#. The listener collects the requests for the same archive for ``batch_delay`` seconds, then sends it to all of those nodes in one encrypted UFTP session. It drops a node that misses nearly all the data of a part of the image, so that node cannot delay the others.
#. The node checks the received archive against the checksum file, as it checks an HTTP download.
#. If any step fails, the node stops its receiver, removes its partial files, and downloads ``imgurl`` over HTTP. The node never extracts an archive that does not match the checksum file.
Supported configurations
------------------------
* Stateless images built by ``genimage`` for:
* the Red Hat family (RHEL, AlmaLinux, Rocky Linux, Oracle Linux, CentOS) with dracut 033 or later,
* openEuler 20.03, 22.03 and 24.03 LTS,
* SLES 15 and openSUSE Leap 15 with dracut 033 or later,
* Ubuntu with dracut.
* All ``packimage`` formats: ``cpio`` and ``tar`` with ``gzip``, ``pigz`` or ``xz``, and ``squashfs``.
* An IPv4 provisioning network that carries multicast between the image server and the nodes.
* ``site.httpmethod=http``.
* The ``uftp`` package of xcat-dep on the image server and in the image. It provides UFTP 5.0.4 with fixes to the sender and the receiver for Enterprise Linux, openEuler, SLES 15 SP6 or later, openSUSE Leap 15.6, and Ubuntu. Its SLES build needs glibc 2.34, which SLES 15 SP5 and older do not have.
``nodeset`` refuses a node of a ``uftp`` image, and keeps its current boot entry, in these cases:
* The root image checksum is turned off by ``rootimgchecksum``.
* ``site.httpmethod`` is ``https``. The node never sends a request meant for HTTPS over plain HTTP.
* The image was packed with ``packimage --tracker`` (BitTorrent).
* The initial ramdisk that nodes boot is not the one that ``genimage`` built with the UFTP receiver.
* The ``rootimgdir`` of the image is outside the ``image_roots`` of the listener, or has characters other than letters, digits and ``._+-`` in its path.
* ``linuximage.addkcmdline`` or ``bootparams.addkcmdline`` sets ``xcat.uftp`` or ``xcat.uftpfp``. ``nodeset`` generates them.
StateLite images ignore ``rootimgtransport``. Other UFTP builds, UFTP 4.x included, are not supported. ``genimage`` leaves their receiver out of the initial ramdisk, and the listener does not start with their sender. The ``uftp`` packages that Ubuntu ships are UFTP 4.10.
Security
--------
The kernel command line is the trust anchor. It reaches the node through the same TFTP or HTTP path as the kernel and the initial ramdisk. Anyone who can change those files can change the image that a node boots, with or without UFTP.
* The listener sends only an archive that ``packimage`` published with its checksum file in a configured image root. A request names the image path and a digest that the checksum file must list. Paths are checked after symbolic links are resolved. The image directory, the archive and its checksum file must be owned by root and not writable by other users. The listener opens the archive once and keeps it open until the transfer, so a later ``packimage`` cannot change the bytes of a queued transfer.
* No value from a request reaches the ``uftp`` command line. The listener runs ``uftp`` with an argument vector built from its own configuration.
* UFTP sessions are always encrypted. The listener signs them with ``/etc/xcat/uftp/server.key``. Nodes accept only encrypted sessions from a server with the fingerprint in ``xcat.uftpfp``.
* The nodes of a transfer share its group key, and the server key signs only the messages that set up the session. A node of the batch can therefore send the other nodes file names and file data as if it were the server. The receiver of xcat-dep keeps those files below its staging directory, and the node checks the archive against the checksum file. Such a node can make the other nodes download the image over HTTP.
* The listener refuses a ``uftp`` sender without the build mark of xcat-dep. Stock UFTP 5.0.4 can write past its buffers when a node sends a malformed status message.
* The checksum file protects the integrity of the archive against corruption and against changes on the network, for UFTP and HTTP alike. It does not give confidentiality, and it comes from the same image server as the archive.
* The HTTP fallback has the same confidentiality as an image download without UFTP: none. The image contains the encrypted root password and the files that you added to it.
* The listener accepts requests from any address that can reach ``listen_address``. Set ``allowed_networks`` to limit it to the provisioning networks.
Install UFTP
------------
#. Install the ``uftp`` package from xcat-dep on the management node and on every service node that serves images, for example: ::
dnf install uftp
#. Add ``uftp`` from xcat-dep to the image, for example as an other package of the osimage. The package must be built for the image architecture. See :doc:`Install Additional Other Packages</guides/admin-guides/manage_clusters/ppc64le/diskless/customize_image/additional_pkg>`.
Configure the listener
----------------------
The listener is installed with ``xCAT-server`` but is not enabled. Removing ``xCAT-server`` stops and disables it. After an upgrade of ``xCAT-server``, restart a running listener to load the new version.
#. Create the configuration from the sample, and set at least ``listen_address`` to the address of the image server on the provisioning network: ::
mkdir -p /etc/xcat/uftp
cp /opt/xcat/share/xcat/conf/uftp-listener.conf.sample /etc/xcat/uftp/listener.conf
Set ``image_roots`` if an image is outside ``/install/netboot``. Review ``rate_kbps``, ``block_size``, ``batch_delay`` and ``transfer_timeout`` for your network. See `Transfer rate and jumbo frames`_.
#. Create the server key and its fingerprint: ::
/opt/xcat/sbin/xcat-uftp-listener --init-key
``nodeset`` reads the listener port from ``/etc/xcat/uftp/listener.conf``, and the fingerprint from ``/etc/xcat/uftp/server.fingerprint``.
#. Check the configuration, then start the service: ::
/opt/xcat/sbin/xcat-uftp-listener --check
systemctl enable --now xcat-uftp-listener
#. Allow the traffic in the firewall of the image server: TCP ``listen_port`` (default 1045) from the nodes, outgoing UDP to ``public_multicast`` and ``private_multicast`` on ``udp_port`` (default 1044), and incoming UDP from the nodes. The nodes answer with unicast UDP to the random port that ``uftp`` sends from, and a stateful firewall does not relate those answers to the multicast traffic.
#. Make sure that the switches forward the multicast groups. With IGMP snooping enabled, the network needs an IGMP querier.
Each image server runs its own listener, which sends only the images in its own image roots. On a service node that serves images, copy ``/etc/xcat/uftp/listener.conf``, ``server.key`` and ``server.fingerprint`` from the management node. Then set ``listen_address``, and ``multicast_interface`` if it is set, to the address of the service node on its provisioning network, and start the service. ``nodeset`` can run on the management node for those nodes, so every image server must use the same listener port and key.
The listener writes its messages to the system journal (``journalctl -u xcat-uftp-listener``), and one log file for each transfer to ``/var/log/xcat-uftp``. The service can write only to that directory. To set another ``log_dir``, add it with ``ReadWritePaths=`` in a systemd drop-in for ``xcat-uftp-listener.service``.
Transfer rate and jumbo frames
------------------------------
Set ``rate_kbps`` to no more than the link speed of the slowest node. A node on a slower link loses the packets that do not fit, and the transfer repeats them for all nodes. The default of 100000 (100 Mb/s) works on any network, but it needs about 52 seconds for a 650 MB image.
One ``uftp`` process sends each transfer on one CPU core, and it handles every packet separately. With the default ``block_size`` of 1300 bytes, a 2.1 GHz Intel Xeon E5 v4 core sends about 1.3 Gb/s, whatever the link speed. A ``block_size`` of 8800 bytes lets the same core send about 3.5 Gb/s. To use it:
#. Set an MTU of 9000 or more on the listener interface and on the switch ports of the provisioning network.
#. Set the MTU of the provisioning network, so that DHCP gives it to the initial ramdisk of the nodes: ::
chdef -t network <netname> mtu=9000
makedhcp -n
#. Set ``block_size = 8800`` in ``/etc/xcat/uftp/listener.conf`` and restart ``xcat-uftp-listener``.
A node that boots with a smaller MTU receives no data. It leaves the transfer and downloads the image over HTTP.
Build, pack and boot
--------------------
#. Generate the image. ``genimage`` reports ``The initial ramdisk ... can receive the root image over UFTP.`` when the receiver is in the initial ramdisk: ::
genimage <osimage>
An existing image that gets the ``uftp`` package later needs ``genimage`` again to rebuild its initial ramdisk.
#. Pack the image, and set its transport: ::
packimage <osimage>
chdef -t osimage <osimage> rootimgtransport=uftp
To use UFTP for every stateless image that does not set the attribute, set ``site.rootimgtransport`` instead: ::
chdef -t site rootimgtransport=uftp
#. Write the boot settings, then boot the nodes: ::
nodeset <noderange> osimage=<osimage>
rpower <noderange> boot
After a ``packimage``, the nodes receive the new archive at their next boot without a new ``nodeset``. Run ``nodeset`` after every ``genimage``. When the initial ramdisk of a ``uftp`` image lacks the receiver, ``nodeset`` refuses its nodes. Install the ``uftp`` package in the image and run ``genimage`` again, or set ``rootimgtransport=http`` for the image.
To stop using UFTP for an image, set its ``rootimgtransport`` to ``http`` and run ``nodeset`` for its nodes.
@@ -14,5 +14,6 @@ Optional means all the subitems in this page are not necessary to finish an OS d
enable_kdump.rst
install_new_kernel.rst
acc_initrd_rootimg_gen_ppc64le.rst
uftp_rootimg.rst
trim_diskless_rootimg.rst
localdisk.rst
@@ -0,0 +1 @@
.. include:: ../../../common/deployment/uftp_rootimg.rst
@@ -19,7 +19,7 @@ SYNOPSIS
********
\ **linuximage Attributes:**\ \ *imagename*\ , \ *template*\ , \ *boottarget*\ , \ *addkcmdline*\ , \ *pkglist*\ , \ *pkgdir*\ , \ *otherpkglist*\ , \ *otherpkgdir*\ , \ *exlist*\ , \ *postinstall*\ , \ *rootimgdir*\ , \ *kerneldir*\ , \ *nodebootif*\ , \ *otherifce*\ , \ *netdrivers*\ , \ *kernelver*\ , \ *krpmver*\ , \ *permission*\ , \ *dump*\ , \ *crashkernelsize*\ , \ *partitionfile*\ , \ *driverupdatesrc*\ , \ *gpgcheck*\ , \ *rootimgchecksum*\ , \ *comments*\ , \ *disable*\
\ **linuximage Attributes:**\ \ *imagename*\ , \ *template*\ , \ *boottarget*\ , \ *addkcmdline*\ , \ *pkglist*\ , \ *pkgdir*\ , \ *otherpkglist*\ , \ *otherpkgdir*\ , \ *exlist*\ , \ *postinstall*\ , \ *rootimgdir*\ , \ *kerneldir*\ , \ *nodebootif*\ , \ *otherifce*\ , \ *netdrivers*\ , \ *kernelver*\ , \ *krpmver*\ , \ *permission*\ , \ *dump*\ , \ *crashkernelsize*\ , \ *partitionfile*\ , \ *driverupdatesrc*\ , \ *gpgcheck*\ , \ *rootimgchecksum*\ , \ *rootimgtransport*\ , \ *comments*\ , \ *disable*\
***********
@@ -201,6 +201,12 @@ linuximage Attributes:
\ **rootimgtransport**\
Stateless images only. How nodes get the compressed root image at boot. 'http' downloads it from the image server. 'uftp' receives it over UFTP multicast from xcat-uftp-listener, and downloads it over HTTP when that fails. If not set, the default is the value of site.rootimgtransport, which defaults to 'http'. 'uftp' needs the root image checksum (rootimgchecksum), an initial ramdisk that genimage built with the UFTP receiver, and site.httpmethod 'http'.
\ **comments**\
Any user-written notes.
@@ -336,6 +336,12 @@ site Attributes:
The first upgrade to a release with the check sets that
attribute to no for the images that exist then.
rootimgtransport: (http or uftp). Default is http. How the nodes of a stateless
image get the compressed root image at boot. uftp receives it
over UFTP multicast from xcat-uftp-listener, with HTTP as the
fallback. The rootimgtransport attribute of an osimage
overrides this value.
runbootscripts: If set to 'yes' the scripts listed in the postbootscripts
attribute in the osimage and postscripts tables will be run during
each reboot of stateful (diskful) nodes. This attribute has no
@@ -19,7 +19,7 @@ SYNOPSIS
********
\ **osimage Attributes:**\ \ *addkcmdline*\ , \ *boottarget*\ , \ *bosinst_data*\ , \ *cfmdir*\ , \ *configdump*\ , \ *crashkernelsize*\ , \ *description*\ , \ *driverupdatesrc*\ , \ *dump*\ , \ *environvar*\ , \ *exlist*\ , \ *fb_script*\ , \ *gpgcheck*\ , \ *groups*\ , \ *home*\ , \ *image_data*\ , \ *imagename*\ , \ *imagetype*\ , \ *installp_bundle*\ , \ *installto*\ , \ *isdeletable*\ , \ *kerneldir*\ , \ *kernelver*\ , \ *kitcomponents*\ , \ *krpmver*\ , \ *lpp_source*\ , \ *mksysb*\ , \ *netdrivers*\ , \ *nfsrootopts*\ , \ *nimmethod*\ , \ *nimtype*\ , \ *nodebootif*\ , \ *osarch*\ , \ *osdistroname*\ , \ *osname*\ , \ *osupdatename*\ , \ *osvers*\ , \ *otherifce*\ , \ *otherpkgdir*\ , \ *otherpkglist*\ , \ *otherpkgs*\ , \ *paging*\ , \ *partitionfile*\ , \ *permission*\ , \ *pkgdir*\ , \ *pkglist*\ , \ *postbootscripts*\ , \ *postinstall*\ , \ *postscripts*\ , \ *profile*\ , \ *provmethod*\ , \ *resolv_conf*\ , \ *root*\ , \ *rootfstype*\ , \ *rootimgchecksum*\ , \ *rootimgdir*\ , \ *script*\ , \ *serverrole*\ , \ *shared_home*\ , \ *shared_root*\ , \ *spot*\ , \ *synclists*\ , \ *template*\ , \ *tmp*\ , \ *usercomment*\ , \ *winpepath*
\ **osimage Attributes:**\ \ *addkcmdline*\ , \ *boottarget*\ , \ *bosinst_data*\ , \ *cfmdir*\ , \ *configdump*\ , \ *crashkernelsize*\ , \ *description*\ , \ *driverupdatesrc*\ , \ *dump*\ , \ *environvar*\ , \ *exlist*\ , \ *fb_script*\ , \ *gpgcheck*\ , \ *groups*\ , \ *home*\ , \ *image_data*\ , \ *imagename*\ , \ *imagetype*\ , \ *installp_bundle*\ , \ *installto*\ , \ *isdeletable*\ , \ *kerneldir*\ , \ *kernelver*\ , \ *kitcomponents*\ , \ *krpmver*\ , \ *lpp_source*\ , \ *mksysb*\ , \ *netdrivers*\ , \ *nfsrootopts*\ , \ *nimmethod*\ , \ *nimtype*\ , \ *nodebootif*\ , \ *osarch*\ , \ *osdistroname*\ , \ *osname*\ , \ *osupdatename*\ , \ *osvers*\ , \ *otherifce*\ , \ *otherpkgdir*\ , \ *otherpkglist*\ , \ *otherpkgs*\ , \ *paging*\ , \ *partitionfile*\ , \ *permission*\ , \ *pkgdir*\ , \ *pkglist*\ , \ *postbootscripts*\ , \ *postinstall*\ , \ *postscripts*\ , \ *profile*\ , \ *provmethod*\ , \ *resolv_conf*\ , \ *root*\ , \ *rootfstype*\ , \ *rootimgchecksum*\ , \ *rootimgdir*\ , \ *rootimgtransport*\ , \ *script*\ , \ *serverrole*\ , \ *shared_home*\ , \ *shared_root*\ , \ *spot*\ , \ *synclists*\ , \ *template*\ , \ *tmp*\ , \ *usercomment*\ , \ *winpepath*
***********
@@ -403,6 +403,12 @@ osimage Attributes:
\ **rootimgtransport**\ (linuximage.rootimgtransport)
Stateless images only. How nodes get the compressed root image at boot. 'http' downloads it from the image server. 'uftp' receives it over UFTP multicast from xcat-uftp-listener, and downloads it over HTTP when that fails. If not set, the default is the value of site.rootimgtransport, which defaults to 'http'. 'uftp' needs the root image checksum (rootimgchecksum), an initial ramdisk that genimage built with the UFTP receiver, and site.httpmethod 'http'.
\ **script**\ (nimimage.script)
The name of a NIM script resource.
+12 -1
View File
@@ -808,7 +808,7 @@ passed as argument rather than by table value',
},
},
linuximage => {
cols => [qw(imagename template boottarget addkcmdline pkglist pkgdir otherpkglist otherpkgdir exlist postinstall rootimgdir kerneldir nodebootif otherifce netdrivers kernelver krpmver permission dump crashkernelsize partitionfile driverupdatesrc gpgcheck rootimgchecksum comments disable)],
cols => [qw(imagename template boottarget addkcmdline pkglist pkgdir otherpkglist otherpkgdir exlist postinstall rootimgdir kerneldir nodebootif otherifce netdrivers kernelver krpmver permission dump crashkernelsize partitionfile driverupdatesrc gpgcheck rootimgchecksum rootimgtransport comments disable)],
keys => [qw(imagename)],
tablespace => 'XCATTBS32K',
table_desc => 'Information about a Linux operating system image that can be used to deploy cluster nodes.',
@@ -855,6 +855,7 @@ passed as argument rather than by table value',
driverupdatesrc => 'The source of the drivers which need to be loaded during the boot. Two types of driver update source are supported: Driver update disk and Driver rpm package. The value for this attribute should be comma separated sources. Each source should be the format tab:full_path_of_source_file. The tab keyword can be: dud (for Driver update disk) and rpm (for driver rpm). If missing the tab, the rpm format is the default. e.g. dud:/install/dud/dd.img,rpm:/install/rpm/d.rpm',
gpgcheck => "Diskless Red Hat family images only. Set to 'yes' or '1' to have genimage verify the signature of every package it installs from the pkgdir, kerneldir and otherpkgdir repositories, as it always does for openEuler images. The trusted keys are the RPM signing keys imported on the node that runs genimage (rpm -q gpg-pubkey), so import the key of every repository first, for example rpm --import /etc/pki/rpm-gpg/RPM-GPG-KEY-EPEL-9. The default is no verification.",
rootimgchecksum => "Stateless images and statelite ramdisk images only. Set to 'yes' or '1' to have nodeset tell nodes to boot the compressed root image only after it matches its SHA-256 checksum. Set to 'no' or '0' to have nodes boot it unchecked. packimage, liteimg and imgimport always write the checksum. If not set, the default is the value of site.rootimgchecksum, which defaults to 'yes'. The first upgrade to a release with the check sets this attribute to 'no' for the images that exist then.",
rootimgtransport => "Stateless images only. How nodes get the compressed root image at boot. 'http' downloads it from the image server. 'uftp' receives it over UFTP multicast from xcat-uftp-listener, and downloads it over HTTP when that fails. If not set, the default is the value of site.rootimgtransport, which defaults to 'http'. 'uftp' needs the root image checksum (rootimgchecksum), an initial ramdisk that genimage built with the UFTP receiver, and site.httpmethod 'http'.",
comments => 'Any user-written notes.',
disable => "Set to 'yes' or '1' to comment out this row.",
},
@@ -1235,6 +1236,11 @@ passed as argument rather than by table value',
" rootimgchecksum attribute of an osimage overrides this value.\n" .
" The first upgrade to a release with the check sets that\n" .
" attribute to no for the images that exist then.\n\n" .
" rootimgtransport: (http or uftp). Default is http. How the nodes of a stateless\n" .
" image get the compressed root image at boot. uftp receives it\n" .
" over UFTP multicast from xcat-uftp-listener, with HTTP as the\n" .
" fallback. The rootimgtransport attribute of an osimage\n" .
" overrides this value.\n\n" .
" runbootscripts: If set to 'yes' the scripts listed in the postbootscripts\n" .
" attribute in the osimage and postscripts tables will be run during\n" .
" each reboot of stateful (diskful) nodes. This attribute has no\n" .
@@ -3333,6 +3339,11 @@ push(@{ $defspec{node}->{'attrs'} }, @nodeattrs);
tabentry => 'linuximage.rootimgchecksum',
access_tabentry => 'linuximage.imagename=attr:imagename',
},
{ attr_name => 'rootimgtransport',
only_if => 'imagetype=linux',
tabentry => 'linuximage.rootimgtransport',
access_tabentry => 'linuximage.imagename=attr:imagename',
},
{ attr_name => 'exlist',
only_if => 'imagetype=linux',
tabentry => 'linuximage.exlist',
+1
View File
@@ -32,6 +32,7 @@ etc/init.d/xcatd opt/xcat/share/xcat/scripts
debian/xcatd.md5sum opt/xcat/share/xcat/scripts
debian/xcatd-init-state opt/xcat/share/xcat/scripts
etc/init.d/xcatd.service lib/systemd/system/
etc/init.d/xcat-uftp-listener.service lib/systemd/system/
xCAT-wsapi/* opt/xcat/ws/
LICENSE.html opt/xcat/share/doc/packages/xCAT-server
+1
View File
@@ -24,6 +24,7 @@ case "$1" in
if [ -f "/proc/cmdline" ]; then # prevent running it during install into chroot image
if "$xcatd_init_compat" can-use-systemctl; then
systemctl stop xcatd.service
systemctl disable --now xcat-uftp-listener.service >/dev/null 2>&1 || true
elif ! "$xcatd_init_compat" uses-systemd --explicit-target && [ -x /etc/init.d/xcatd ]; then
/etc/init.d/xcatd stop
fi
@@ -0,0 +1,28 @@
[Unit]
Description=xCAT UFTP multicast image listener
After=network-online.target
Wants=network-online.target
ConditionPathExists=/etc/xcat/uftp/listener.conf
[Service]
Type=simple
ExecStartPre=/opt/xcat/sbin/xcat-uftp-listener --check
ExecStart=/opt/xcat/sbin/xcat-uftp-listener
Restart=on-failure
RestartSec=10s
KillMode=mixed
TimeoutStopSec=15
LimitNOFILE=4096
CapabilityBoundingSet=
NoNewPrivileges=yes
PrivateTmp=yes
ProtectHome=yes
ProtectSystem=strict
LogsDirectory=xcat-uftp
ProtectKernelTunables=yes
ProtectKernelModules=yes
ProtectControlGroups=yes
RestrictAddressFamilies=AF_UNIX AF_INET AF_INET6 AF_NETLINK
[Install]
WantedBy=multi-user.target
+22
View File
@@ -2690,6 +2690,28 @@ sub rootimg_checksum_enabled {
return !(defined($value) && $value =~ /^\s*(?:no|0)\s*$/ix);
}
#--------------------------------------------------------------------------------------------------------
#rootimg_transport:
#description: tell how the nodes of an image get the compressed rootimage at boot
#argument:
# $value: the rootimgtransport attribute of the image, or undef
#return:
# 'http' or 'uftp', from the image or, if the image does not set it, from site.rootimgtransport,
# or undef and the error message for another value. Call it in list context.
#--------------------------------------------------------------------------------------------------------
sub rootimg_transport {
my (undef, $value) = @_;
my $source = 'The rootimgtransport attribute of the image';
if (!defined($value) || $value !~ /\S/x) {
($value) = xCAT::TableUtils->get_site_attribute('rootimgtransport');
$source = 'site.rootimgtransport';
}
return 'http' if !defined($value) || $value !~ /\S/x;
my $transport = lc($value =~ s/^\s+|\s+$//grx);
return $transport if $transport eq 'http' || $transport eq 'uftp';
return (undef, "$source is '$value', but only http and uftp are supported.");
}
#--------------------------------------------------------------------------------------------------------
#upgrade_rootimg_checksum:
#description: on an upgrade from a release without the root image check, keep the stateless and statelite
+890
View File
@@ -0,0 +1,890 @@
package xCAT::UFTP;
=head1 NAME
xCAT::UFTP - validation and request helpers for UFTP root image distribution
=head1 DESCRIPTION
genimage records which initial ramdisk carries the UFTP receiver. nodeset
turns that record and the listener configuration into kernel arguments.
xcat-uftp-listener sends a root image that packimage published with its
SHA-256 checksum, and keeps its transfer jobs with the subs in this module.
All of them share the checks in this module.
=cut
use strict;
use warnings;
use Cwd ();
use Digest::SHA ();
use Errno qw(ENOENT);
use Fcntl qw(:mode O_RDONLY O_NOFOLLOW);
use File::Basename ();
use File::Path ();
use IO::Handle ();
use Socket ();
our $CONFIG_FILE = '/etc/xcat/uftp/listener.conf';
our $HELPER = 'xcat-rootimg-fetch.sh';
our $TARGET_PREFIX = '/xcat-uftp/v1';
our @ARTIFACTS = qw(rootimg.cpio.gz rootimg.cpio.xz rootimg.tar.gz rootimg.tar.xz rootimg.sfs);
our @RECEIVERS = qw(/usr/sbin/uftpd /usr/bin/uftpd);
our @BOOT_KEYS = qw(xcat.uftp xcat.uftpfp);
our $KILL_GRACE = 5;
my $MAX_PATH = 1024;
my $MAX_SEGMENTS = 64;
my $MAX_REQUEST = 8192;
my $MAX_FINISHED = 1024;
my $SEGMENT = qr/[A-Za-z0-9_+-][A-Za-z0-9._+-]{0,254}/;
my $SHA256 = qr/[0-9a-f]{64}/;
my $FINGERPRINT = qr/(?:[0-9A-F]{2}:){19}[0-9A-F]{2}/;
my %ELF_MACHINE = (
x86 => [ 1, 3 ],
x86_64 => [ 2, 62 ],
ppc64 => [ 2, 21, 2 ],
ppc64le => [ 2, 21, 1 ],
ppc64el => [ 2, 21, 1 ],
aarch64 => [ 2, 183 ],
riscv64 => [ 2, 243 ],
s390x => [ 2, 22 ],
);
my %CONFIG = (
listen_address => { type => 'ipv4', required => 1 },
listen_port => { type => 'int', default => 1045, min => 1, max => 65535 },
allowed_networks => { type => 'networks', default => '' },
multicast_interface => { type => 'ipv4' },
image_roots => { type => 'dirs', default => '/install/netboot' },
udp_port => { type => 'int', default => 1044, min => 1, max => 65535 },
public_multicast => { type => 'mcast', default => '230.4.4.1' },
private_multicast => { type => 'mcast_pattern', default => '230.5.5.x' },
ttl => { type => 'int', default => 1, min => 1, max => 255 },
rate_kbps => { type => 'int', default => 100000, min => 1, max => 100000000 },
# uftp accepts block sizes from 512 to 8800 bytes.
block_size => { type => 'int', default => 1300, min => 512, max => 8800 },
batch_delay => { type => 'int', default => 30, min => 0, max => 3600 },
transfer_timeout => { type => 'int', default => 1800, min => 1, max => 86400 },
max_transfers => { type => 'int', default => 4, min => 1, max => 64 },
max_queued_jobs => { type => 'int', default => 32, min => 1, max => 1024 },
max_clients_per_job => { type => 'int', default => 1000, min => 1, max => 100000 },
max_connections => { type => 'int', default => 64, min => 1, max => 1024 },
request_timeout => { type => 'int', default => 10, min => 1, max => 300 },
job_retention => { type => 'int', default => 600, min => 0, max => 86400 },
encryption => { type => 'enum', default => 'aes256-gcm',
values => [qw(aes128-gcm aes256-gcm aes128-ccm aes256-ccm)] },
key_file => { type => 'abspath', default => '/etc/xcat/uftp/server.key' },
fingerprint_file => { type => 'abspath', default => '/etc/xcat/uftp/server.fingerprint' },
uftp => { type => 'abspath', default => '/usr/bin/uftp' },
uftp_keymgt => { type => 'abspath', default => '/usr/bin/uftp_keymgt' },
log_dir => { type => 'abspath', default => '/var/log/xcat-uftp' },
);
sub artifact_name_ok {
my ($name) = @_;
return defined($name) && grep { $_ eq $name } @ARTIFACTS;
}
sub sha256_fh {
my ($fh) = @_;
my $sha = Digest::SHA->new(256);
my $size = 0;
while (1) {
my $count = sysread($fh, my $buffer, 1048576);
return (undef, undef, "read failed: $!") unless defined($count);
last if $count == 0;
$size += $count;
$sha->add($buffer);
}
return ($sha->hexdigest, $size, undef);
}
sub sha256_file {
my ($path) = @_;
sysopen(my $fh, $path, O_RDONLY | O_NOFOLLOW)
or return (undef, undef, "Cannot open $path: $!");
binmode($fh);
my ($digest, $size, $error) = sha256_fh($fh);
unless (close($fh)) {
$error ||= "close failed: $!";
}
return (undef, undef, "Cannot read $path: $error") if $error;
return ($digest, $size, undef);
}
sub _read_text {
my ($path, $follow) = @_;
sysopen(my $fh, $path, $follow ? O_RDONLY : O_RDONLY | O_NOFOLLOW) or return (undef, "$!");
my $text = '';
while (1) {
my $count = sysread($fh, my $buffer, 65536);
return (undef, "$!") unless defined($count);
last if $count == 0;
$text .= $buffer;
return (undef, 'file too large') if length($text) > 65536;
}
close($fh) or return (undef, "$!");
return ($text, undef);
}
sub _write_atomic {
my ($path, $content, $mode) = @_;
my $temp = File::Basename::dirname($path) . '/.' . File::Basename::basename($path) . ".tmp$$";
unlink($temp);
sysopen(my $fh, $temp, Fcntl::O_WRONLY() | Fcntl::O_CREAT() | Fcntl::O_EXCL(), 0600)
or return "Cannot create $temp: $!";
my $fail = sub {
my ($message) = @_;
close($fh);
unlink($temp);
return $message;
};
my $written = syswrite($fh, $content);
return $fail->("Cannot write $temp: $!") unless defined($written) && $written == length($content);
return $fail->("Cannot flush $temp: $!") unless $fh->sync;
unless (close($fh)) {
my $message = "Cannot close $temp: $!";
unlink($temp);
return $message;
}
unless (chmod($mode, $temp) && rename($temp, $path)) {
my $message = "Cannot publish $path: $!";
unlink($temp);
return $message;
}
return undef;
}
sub write_file_atomic {
my ($path, $content, $mode) = @_;
return _write_atomic($path, $content, $mode);
}
sub normalize_image_path {
my ($path) = @_;
return undef unless defined($path) && length($path) <= $MAX_PATH;
return undef unless $path =~ m{^/};
$path =~ s{/+}{/}g;
$path =~ s{(?<=.)/$}{};
my @segments = split m{/}, substr($path, 1);
return undef unless @segments && @segments <= $MAX_SEGMENTS;
for my $segment (@segments) {
return undef unless $segment =~ /^$SEGMENT$/;
}
return '/' . join('/', @segments);
}
sub parse_transfer_target {
my ($target) = @_;
my $prefix = quotemeta("$TARGET_PREFIX/transfer/");
return (undef, undef, 'unknown request target')
unless defined($target) && $target =~ m{^$prefix($SHA256)(/.*)$};
my ($digest, $path) = ($1, $2);
my $normalized = normalize_image_path($path);
return (undef, undef, 'malformed image path')
unless defined($normalized) && $normalized eq $path;
return (undef, undef, 'unsupported image artifact')
unless artifact_name_ok(File::Basename::basename($path));
return ($digest, $path, undef);
}
sub parse_job_target {
my ($target) = @_;
my $prefix = quotemeta("$TARGET_PREFIX/job/");
return (undef, 'unknown request target')
unless defined($target) && $target =~ m{^$prefix([0-9a-f]{16})$};
return ($1, undef);
}
sub _trusted_entry {
my ($path, $want_dir) = @_;
my @st = lstat($path);
return "$path does not exist" unless @st;
return "$path is a symbolic link" if S_ISLNK($st[2]);
return "$path is not a directory" if $want_dir && !S_ISDIR($st[2]);
return "$path is not a regular file" if !$want_dir && !S_ISREG($st[2]);
return "$path is not owned by root" unless $st[4] == 0 || $st[4] == $>;
return "$path is writable by other users" if $st[2] & 022;
return undef;
}
sub _contained {
my ($path, $root) = @_;
return 1 if $root eq '/';
return $path eq $root || index($path, "$root/") == 0;
}
# Returns the lines of a checksum file in sha256sum format as pairs of digest and file name.
sub checksum_lines {
my ($text) = @_;
return map { /^($SHA256) [ *](\S+)$/ ? [ $1, $2 ] : () } split /\n/, $text // '';
}
sub resolve_artifact {
my ($roots, $image_path, $digest) = @_;
my $name = File::Basename::basename($image_path);
my $dir = File::Basename::dirname($image_path);
my $real = Cwd::realpath($dir);
return (undef, 'image directory does not exist') unless defined($real) && -d $real;
return (undef, 'image directory is outside the configured image roots')
unless grep { _contained($real, $_) } @$roots;
my $archive = "$real/$name";
for my $check ([$real, 1], [$archive, 0], ["$archive.sha256", 0]) {
my $error = _trusted_entry(@$check);
return (undef, "image is not published: $error") if $error;
}
# packimage publishes the archive with its checksum file, which lists the previous archive too while it replaces it.
my ($sums, $error) = _read_text("$archive.sha256");
return (undef, "cannot read the checksum of the image: $error") unless defined($sums);
return (undef, 'requested image generation is not the published one')
unless grep { $_->[0] eq $digest && $_->[1] eq $name } checksum_lines($sums);
my @link = lstat($archive);
sysopen(my $fh, $archive, O_RDONLY | O_NOFOLLOW)
or return (undef, "cannot open published image: $!");
binmode($fh);
my @st = stat($fh);
unless (@st && @link && S_ISREG($st[2]) && $st[0] == $link[0] && $st[1] == $link[1]) {
close($fh);
return (undef, 'published image changed during validation');
}
unless ($st[7] > 0) {
close($fh);
return (undef, 'published image is empty');
}
return ({
fh => $fh,
dev => $st[0],
ino => $st[1],
size => $st[7],
sha256 => $digest,
name => $name,
path => $archive,
}, undef);
}
my %REASONS = (
200 => 'OK',
400 => 'Bad Request',
403 => 'Forbidden',
404 => 'Not Found',
405 => 'Method Not Allowed',
408 => 'Request Timeout',
413 => 'Content Too Large',
500 => 'Internal Server Error',
503 => 'Service Unavailable',
);
sub parse_http_request {
my ($buffer) = @_;
my ($end) = grep { $_ >= 0 } sort { $a <=> $b } (index($buffer, "\r\n\r\n"), index($buffer, "\n\n"));
return { incomplete => 1 } unless defined($end);
my @lines = split /\r?\n/, substr($buffer, 0, $end);
my $line = shift(@lines) // '';
return { status => 400, reason => 'malformed request line' }
unless $line =~ m{^([A-Z]{1,16}) (\S{1,2048}) HTTP/1\.[01]$};
my ($method, $target) = ($1, $2);
return { status => 400, reason => 'too many header lines' } if @lines > 64;
for my $header (@lines) {
return { status => 400, reason => 'malformed header line' }
unless $header =~ /^[!#\$%&'*+.^_`|~0-9A-Za-z-]{1,64}:[\t\x20-\x7e]*$/;
}
return { status => 405, reason => 'only GET is supported' } unless $method eq 'GET';
return { status => 400, reason => 'malformed request target' }
unless $target =~ m{^/[A-Za-z0-9._+/-]*$};
return { method => $method, target => $target };
}
sub http_response {
my ($status, $body) = @_;
$body = '' unless defined($body);
my $reason = $REASONS{$status} || 'Error';
return "HTTP/1.0 $status $reason\r\nContent-Type: text/plain\r\n"
. 'Content-Length: ' . length($body) . "\r\nConnection: close\r\n\r\n$body";
}
sub format_record {
my (@pairs) = @_;
my $text = '';
while (@pairs) {
my ($key, $value) = splice(@pairs, 0, 2);
$text .= "$key=$value\n";
}
return $text;
}
sub ipv4_ok {
my ($address) = @_;
return 0 unless defined($address) && $address =~ /^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/;
for my $octet ($1, $2, $3, $4) {
return 0 if $octet > 255 || $octet =~ /^0\d/;
}
return 1;
}
sub ipv4_number {
my ($address) = @_;
return unpack('N', Socket::inet_aton($address));
}
sub address_allowed {
my ($networks, $address) = @_;
return 1 unless $networks && @$networks;
my $number = ipv4_number($address);
for my $network (@$networks) {
return 1 if ($number & $network->[1]) == $network->[0];
}
return 0;
}
sub _config_value {
my ($key, $spec, $value) = @_;
my $type = $spec->{type};
if ($type eq 'int') {
return (undef, "$key must be an integer") unless $value =~ /^(?:0|[1-9][0-9]{0,8})$/;
return (undef, "$key must be between $spec->{min} and $spec->{max}")
if $value < $spec->{min} || $value > $spec->{max};
return ($value + 0, undef);
}
if ($type eq 'ipv4') {
return (undef, "$key must be an IPv4 address") unless ipv4_ok($value);
return ($value, undef);
}
if ($type eq 'mcast') {
return (undef, "$key must be an IPv4 multicast address")
unless ipv4_ok($value) && $value =~ /^(\d+)\./ && $1 >= 224 && $1 <= 239;
return ($value, undef);
}
if ($type eq 'mcast_pattern') {
(my $probe = $value) =~ s/\bx\b/1/g;
return (undef, "$key must be an IPv4 multicast address pattern")
unless $value =~ /^\d/ && ipv4_ok($probe) && $probe =~ /^(\d+)\./ && $1 >= 224 && $1 <= 239;
return ($value, undef);
}
if ($type eq 'enum') {
return (undef, "$key must be one of " . join(', ', @{ $spec->{values} }))
unless grep { $_ eq $value } @{ $spec->{values} };
return ($value, undef);
}
if ($type eq 'abspath') {
return (undef, "$key must be an absolute path") unless $value =~ m{^/[^\0\s]*$};
return ($value, undef);
}
if ($type eq 'dirs') {
my @dirs;
for my $dir (split /\s*,\s*/, $value) {
return (undef, "$key entries must be absolute paths") unless $dir =~ m{^/};
my $real = Cwd::realpath($dir);
return (undef, "$key entry $dir is not a directory") unless defined($real) && -d $real;
push @dirs, $real;
}
return (undef, "$key must list at least one directory") unless @dirs;
return (\@dirs, undef);
}
if ($type eq 'networks') {
my @networks;
for my $entry (split /\s*,\s*/, $value) {
return (undef, "$key entry $entry must be address/prefix")
unless $entry =~ m{^([^/]+)/(\d{1,2})$} && ipv4_ok($1) && $2 <= 32;
my $mask = $2 ? (0xffffffff << (32 - $2)) & 0xffffffff : 0;
push @networks, [ ipv4_number($1) & $mask, $mask ];
}
return (\@networks, undef);
}
return (undef, "$key has an unknown type");
}
sub load_listener_config {
my ($path) = @_;
$path ||= $CONFIG_FILE;
my ($text, $error) = _read_text($path, 1);
return (undef, "Cannot read $path: $error") unless defined($text);
my %raw;
my $number = 0;
for my $line (split /\n/, $text) {
$number++;
next if $line =~ /^\s*(?:#|$)/;
return (undef, "$path line $number: expected key = value")
unless $line =~ /^\s*([a-z_]+)\s*=\s*(.*?)\s*$/;
my ($key, $value) = ($1, $2);
return (undef, "$path line $number: unknown key '$key'") unless $CONFIG{$key};
return (undef, "$path line $number: duplicate key '$key'") if exists($raw{$key});
$raw{$key} = $value;
}
my %config;
for my $key (sort keys %CONFIG) {
my $spec = $CONFIG{$key};
my $value = ($raw{$key} // '') ne '' ? $raw{$key} : $spec->{default};
if (!defined($value) || $value eq '' && $spec->{type} ne 'networks') {
return (undef, "$path: $key is required") if $spec->{required};
next;
}
my ($parsed, $value_error) = _config_value($key, $spec, $value);
return (undef, "$path: $value_error") if $value_error;
$config{$key} = $parsed;
}
$config{multicast_interface} ||= $config{listen_address};
$config{server_id} = sprintf('0x%08X', ipv4_number($config{multicast_interface}));
return (\%config, undef);
}
sub parse_fingerprint {
my ($text) = @_;
return undef unless defined($text);
return $1 if $text =~ /(?:^|fingerprint:\s*)($FINGERPRINT)\s*$/m;
return undef;
}
sub read_fingerprint {
my ($path) = @_;
my ($text, $error) = _read_text($path, 1);
return (undef, "Cannot read UFTP server fingerprint $path: $error") unless defined($text);
my $fingerprint = parse_fingerprint($text);
return (undef, "$path does not hold a UFTP key fingerprint") unless $fingerprint;
return ($fingerprint, undef);
}
sub uftp_command {
my ($config, $name, $hostlist) = @_;
return (
$config->{uftp},
'-I', $config->{multicast_interface},
'-U', $config->{server_id},
'-p', $config->{udp_port},
'-M', $config->{public_multicast},
'-P', $config->{private_multicast},
'-t', $config->{ttl},
'-R', $config->{rate_kbps},
'-b', $config->{block_size},
# Abort a receiver that misses nearly every block of a section, so it cannot hold the group open.
# uftp leaves up to 7 NAKs of each section uncounted, so a limit of 99 never aborts one.
'-N', 98,
'-Y', $config->{encryption},
'-h', 'sha256',
'-k', $config->{key_file},
'-H', "\@$hostlist",
'-x', 2,
'-l',
'-D', $name,
'/dev/fd/3',
);
}
# Returns the fingerprint of the server key, or undef and why the listener cannot send.
# $key_fingerprint returns the fingerprint that uftp_keymgt reports for the key, or undef and an error.
sub check_listener_runtime {
my ($config, $key_fingerprint) = @_;
return (undef, "UFTP sender $config->{uftp} is not executable; install the uftp package")
unless -f $config->{uftp} && -x _;
if (my $version_error = uftp_version_error($config->{uftp})) {
return (undef, $version_error);
}
my @st = stat($config->{key_file});
return (undef, "UFTP server key $config->{key_file} is missing; run xcat-uftp-listener --init-key")
unless @st;
return (undef, "UFTP server key $config->{key_file} must not be accessible by other users")
if $st[2] & 077;
my ($fingerprint, $error) = $key_fingerprint->();
return (undef, $error) if $error;
my ($published, $read_error) = read_fingerprint($config->{fingerprint_file});
return (undef, "$read_error; run xcat-uftp-listener --init-key") if $read_error;
return (undef, "$config->{fingerprint_file} does not match $config->{key_file}; run xcat-uftp-listener --init-key")
unless $published eq $fingerprint;
unless (-d $config->{log_dir}) {
eval { File::Path::make_path($config->{log_dir}, { mode => 0750 }) };
return (undef, "Cannot create $config->{log_dir}") unless -d $config->{log_dir};
}
# The file test operators ignore read-only mounts, such as those of the service sandbox.
my $probe = "$config->{log_dir}/.write-check.$$";
unlink($probe);
sysopen(my $fh, $probe, Fcntl::O_WRONLY() | Fcntl::O_CREAT() | Fcntl::O_EXCL(), 0600)
or return (undef, "Cannot write to $config->{log_dir}: $!");
close($fh);
unlink($probe);
return ($fingerprint, undef);
}
# The listener keeps its transfer jobs in this state. spawn starts a sender for a job and returns its PID,
# or undef and an error. signal sends a signal to a sender.
sub listener_state {
my ($config, %args) = @_;
return {
config => $config,
workdir => $args{workdir},
log => $args{log} || sub { },
spawn => $args{spawn},
signal => $args{signal} || sub { kill($_[0], $_[1]) },
jobs => {},
pending => {},
senders => {},
};
}
sub _job_id {
my ($state) = @_;
my $bytes = '';
if (open(my $random, '<:raw', '/dev/urandom')) {
read($random, $bytes, 8);
close($random);
}
$bytes = pack('N2', int(rand(2**32)), int(rand(2**32))) unless length($bytes) == 8;
my $id = unpack('H16', $bytes);
return exists($state->{jobs}{$id}) ? _job_id($state) : $id;
}
sub _jobs_in {
my ($state, @states) = @_;
my %want = map { $_ => 1 } @states;
return grep { $want{ $_->{state} } } values %{ $state->{jobs} };
}
sub _request_transfer {
my ($state, $peer, $target, $now) = @_;
my ($config, $log) = @$state{qw(config log)};
my ($digest, $path, $error) = parse_transfer_target($target);
if ($error) {
$log->('warning', "$peer: rejected request: $error");
return (400, "error=$error\n");
}
my ($image, $resolve_error) = resolve_artifact($config->{image_roots}, $path, $digest);
if ($resolve_error) {
$log->('warning', "$peer: rejected $path: $resolve_error");
return (403, "error=image is not published\n");
}
my $key = join(':', $image->{dev}, $image->{ino}, $digest, $config->{udp_port});
my $job = $state->{pending}{$key};
my $repeated;
if ($job) {
close($image->{fh});
$repeated = $job->{clients}{$peer};
unless ($repeated) {
if (keys(%{ $job->{clients} }) >= $config->{max_clients_per_job}) {
$log->('warning', "$peer: job $job->{id} is full");
return (503, "error=transfer is full\n");
}
$job->{clients}{$peer} = 1;
}
} else {
if (_jobs_in($state, 'pending', 'running') >= $config->{max_queued_jobs}) {
close($image->{fh});
$log->('warning', "$peer: too many queued transfers");
return (503, "error=too many queued transfers\n");
}
$job = {
%$image,
id => _job_id($state),
key => $key,
clients => { $peer => 1 },
created => $now,
start_at => $now + $config->{batch_delay},
state => 'pending',
};
$state->{pending}{$key} = $job;
$state->{jobs}{ $job->{id} } = $job;
$log->('info', "job $job->{id}: queued $path ($digest) for $config->{batch_delay}s");
}
$log->('info', $repeated ? "job $job->{id}: $peer asked again" : "job $job->{id}: added $peer");
return (200, format_record(
status => 'queued',
job => $job->{id},
file => $job->{name},
size => $job->{size},
port => $config->{udp_port},
mcast => $config->{public_multicast},
server_id => $config->{server_id},
server_ip => $config->{multicast_interface},
client_ip => $peer,
start_in => $job->{start_at} > $now ? $job->{start_at} - $now : 0,
timeout => $config->{transfer_timeout},
));
}
sub _job_status {
my ($state, $peer, $target, $now) = @_;
my ($id, $error) = parse_job_target($target);
return (400, "error=$error\n") if $error;
my $job = $state->{jobs}{$id};
return (404, "error=unknown job\n") unless $job && $job->{clients}{$peer};
my @fields = (state => $job->{state});
push @fields, (start_in => $job->{start_at} > $now ? $job->{start_at} - $now : 0)
if $job->{state} eq 'pending';
push @fields, (timeout => $state->{config}{transfer_timeout});
push @fields, (reason => $job->{reason}) if $job->{reason};
return (200, format_record(@fields));
}
# Returns the status and body of the reply to the request that $peer sent in $buffer, or an empty list
# while the request is incomplete.
sub listener_reply {
my ($state, $peer, $buffer, $now) = @_;
return (413, "error=request too large\n") if length($buffer) > $MAX_REQUEST;
my $request = parse_http_request($buffer);
return () if $request->{incomplete};
return ($request->{status}, "error=$request->{reason}\n") if $request->{status};
my $target = $request->{target};
return _request_transfer($state, $peer, $target, $now) if index($target, "$TARGET_PREFIX/transfer/") == 0;
return _job_status($state, $peer, $target, $now) if index($target, "$TARGET_PREFIX/job/") == 0;
return (404, "error=unknown request target\n");
}
# Returns the status and body that refuse a new connection from $peer while $open connections are open,
# or an empty list to accept it.
sub connection_refusal {
my ($state, $open, $peer) = @_;
my $config = $state->{config};
return (503, "error=too many connections\n") if $open >= $config->{max_connections};
unless (address_allowed($config->{allowed_networks}, $peer)) {
$state->{log}->('warning', "$peer: address is not in allowed_networks");
return (403, "error=client address is not allowed\n");
}
return ();
}
sub _finish_job {
my ($state, $job, $result, $reason, $now) = @_;
close($job->{fh}) if $job->{fh};
delete $job->{fh};
my $pending = $state->{pending};
delete $pending->{ $job->{key} } if $pending->{ $job->{key} } && $pending->{ $job->{key} } == $job;
unlink($job->{hostlist}) if $job->{hostlist};
$job->{state} = $result;
$job->{reason} = $reason if $reason;
$job->{finished} = $now;
$state->{log}->($result eq 'done' ? 'info' : 'error', "job $job->{id}: $result" . ($reason ? " ($reason)" : ''));
}
sub _start_job {
my ($state, $job, $now) = @_;
my $config = $state->{config};
delete $state->{pending}{ $job->{key} };
my $hostlist = "$state->{workdir}/$job->{id}.hosts";
my $hosts = join('', map { "$_\n" } sort keys %{ $job->{clients} });
if (my $error = _write_atomic($hostlist, $hosts, 0600)) {
return _finish_job($state, $job, 'failed', $error, $now);
}
$job->{hostlist} = $hostlist;
my $log = "$config->{log_dir}/job-$job->{id}.log";
my ($pid, $error) = $state->{spawn}->($job, $log, uftp_command($config, $job->{name}, $hostlist));
return _finish_job($state, $job, 'failed', $error, $now) unless $pid;
close($job->{fh});
delete $job->{fh};
$state->{senders}{$pid} = $job;
$job->{pid} = $pid;
$job->{state} = 'running';
$job->{started} = $now;
$job->{deadline} = $now + $config->{transfer_timeout};
$state->{log}->('info', "job $job->{id}: sending $job->{name} to "
. scalar(keys %{ $job->{clients} }) . " client(s), pid $pid, log $log");
}
# Starts each pending job whose batch delay has passed while fewer than max_transfers senders run,
# stops senders that exceed transfer_timeout, and forgets finished jobs after job_retention.
sub run_jobs {
my ($state, $now) = @_;
my $config = $state->{config};
my $running = _jobs_in($state, 'running');
for my $job (sort { $a->{start_at} <=> $b->{start_at} || $a->{created} <=> $b->{created} }
_jobs_in($state, 'pending')) {
if ($now > $job->{start_at} + $config->{transfer_timeout}) {
_finish_job($state, $job, 'failed', 'no transfer slot became free', $now);
next;
}
next if $job->{start_at} > $now || $running >= $config->{max_transfers};
_start_job($state, $job, $now);
$running++ if $job->{state} eq 'running';
}
for my $job (_jobs_in($state, 'running')) {
if (!$job->{timed_out} && $now >= $job->{deadline}) {
$job->{timed_out} = $now;
$state->{signal}->('TERM', $job->{pid});
} elsif ($job->{timed_out} && $now >= $job->{timed_out} + $KILL_GRACE) {
$state->{signal}->('KILL', $job->{pid});
}
}
my @finished = sort { $a->{finished} <=> $b->{finished} } _jobs_in($state, 'done', 'failed');
while (@finished && ($finished[0]{finished} + $config->{job_retention} <= $now
|| @finished > $MAX_FINISHED)) {
delete $state->{jobs}{ shift(@finished)->{id} };
}
}
# Finishes the job of the sender $pid, which exited with the wait status $status. $reason replaces the
# reason that the status gives.
sub sender_exited {
my ($state, $pid, $status, $now, $reason) = @_;
my $job = delete $state->{senders}{$pid} or return;
unless ($reason) {
if ($job->{timed_out}) {
$reason = "transfer exceeded $state->{config}{transfer_timeout}s";
} elsif ($status & 127) {
$reason = 'uftp killed by signal ' . ($status & 127);
} elsif ($status >> 8) {
$reason = 'uftp exited with status ' . ($status >> 8);
}
}
_finish_job($state, $job, $reason ? 'failed' : 'done', $reason, $now);
}
sub sender_pids {
my ($state) = @_;
return keys %{ $state->{senders} };
}
# Fails the jobs that still wait for a sender when the listener stops.
sub stop_jobs {
my ($state, $now) = @_;
_finish_job($state, $_, 'failed', 'listener stopped', $now) for _jobs_in($state, 'pending');
}
sub image_receiver {
my ($rootimg_dir) = @_;
for my $receiver (@RECEIVERS) {
return $receiver if -f "$rootimg_dir$receiver" && -x _;
}
return undef;
}
sub elf_arch_error {
my ($path, $arch) = @_;
my $want = $ELF_MACHINE{$arch};
return "UFTP receivers are not supported for architecture $arch" unless $want;
open(my $fh, '<:raw', $path) or return "Cannot read $path: $!";
my $count = read($fh, my $header, 20);
close($fh);
return "$path is not an ELF executable"
unless defined($count) && $count == 20 && substr($header, 0, 4) eq "\x7fELF";
my $class = ord(substr($header, 4, 1));
my $data = ord(substr($header, 5, 1));
my $machine = unpack($data == 2 ? 'n' : 'v', substr($header, 18, 2));
my ($want_class, $want_machine, $want_data) = @$want;
unless ($class == $want_class && $machine == $want_machine
&& (!defined($want_data) || $data == $want_data)) {
return "$path was built for another architecture than $arch (ELF class $class, machine $machine)";
}
return undef;
}
# The uftp package of xcat-dep marks its build, which carries fixes that stock UFTP 5 lacks.
sub uftp_version_error {
my ($path) = @_;
open(my $fh, '<:raw', $path) or return "Cannot read $path: $!";
my $content = do { local $/; <$fh> } // '';
close($fh);
my ($version, $mark) = $content =~ /UFTP version (\d+\.\d+(?:\.\d+)?)( \(xCAT build \d+\))?/;
return "Cannot find the UFTP version in $path" unless defined($version);
return "$path is UFTP $version; UFTP 5.x is required" unless $version =~ /^5\./;
return "$path is UFTP $version without the xCAT fixes; use the uftp package of xcat-dep" unless $mark;
return undef;
}
sub usable_receiver {
my ($rootimg_dir, $arch) = @_;
my $receiver = image_receiver($rootimg_dir);
return (undef, undef) unless $receiver;
my $error = elf_arch_error("$rootimg_dir$receiver", $arch)
|| uftp_version_error("$rootimg_dir$receiver");
return (undef, "$error; the initial ramdisk will not receive the root image over UFTP") if $error;
return ($receiver, undef);
}
sub initrd_missing_files {
my ($listing) = @_;
my @paths = map { (split /\s+/, $_)[-1] // '' } @$listing;
my %required = (
uftpd => qr{^(?:usr/)?s?bin/uftpd$},
sha256sum => qr{^(?:usr/)?bin/sha256sum$},
$HELPER => qr{^(?:usr/)?lib/\Q$HELPER\E$},
);
return grep { my $re = $required{$_}; !grep { $_ =~ $re } @paths } sort keys %required;
}
# The marker holds the SHA-256 of the ramdisk, so a replaced ramdisk does not match it.
sub write_initrd_marker {
my ($initrd) = @_;
my ($digest, undef, $error) = sha256_file($initrd);
return $error if $error;
return _write_atomic("$initrd.uftp", "sha256=$digest\n", 0644);
}
sub remove_initrd_marker {
my ($initrd) = @_;
return undef if unlink("$initrd.uftp") || $! == ENOENT;
return "Cannot remove $initrd.uftp: $!";
}
# $initrd can be the copy that nodes boot, and $marker the one genimage wrote next to the ramdisk it built.
sub verify_initrd_marker {
my ($initrd, $marker) = @_;
$marker //= "$initrd.uftp";
my $hint = 'install the uftp package in the image and run genimage again';
return "$initrd was not built with the UFTP receiver; $hint" unless -e $marker;
my ($text, $error) = _read_text($marker);
return "Cannot read $marker: $error" unless defined($text);
my ($recorded) = $text =~ /^sha256=($SHA256)\n?$/ or return "$marker is not a UFTP receiver marker";
my ($digest, undef, $hash_error) = sha256_file($initrd);
return $hash_error if $hash_error;
return "$initrd is not the initial ramdisk that genimage built with the UFTP receiver; $hint"
unless $digest eq $recorded;
return undef;
}
sub managed_boot_keys {
my ($kcmdline) = @_;
return () unless defined($kcmdline);
my %keys = map { $_ => 1 } @BOOT_KEYS;
my %found;
for my $token (split /\s+/, $kcmdline) {
my ($key) = split /=/, $token, 2;
$found{$key} = 1 if defined($key) && $keys{$key};
}
return sort keys %found;
}
# nodeset checks every node, so each version of an initial ramdisk and its marker is hashed once.
my %initrd_marker_errors;
# Returns the kernel arguments for receiving the root image over UFTP, or undef and why a node cannot.
sub boot_arguments {
my (%args) = @_;
my ($rootimgdir, $artifact) = @args{qw(rootimgdir artifact)};
return (undef, 'UFTP sends only the compressed root image of a stateless image') if $args{statelite};
return (undef, "UFTP needs the root image checksum, so set the rootimgchecksum attribute of the image to yes")
unless $args{checksum};
return (undef, "$rootimgdir has no root image that UFTP can send; run packimage") unless artifact_name_ok($artifact);
return (undef, "$rootimgdir/$artifact has a BitTorrent .metainfo file; UFTP cannot be combined with packimage --tracker")
if -e "$rootimgdir/$artifact.metainfo";
my $httpmethod = $args{httpmethod} || 'http';
return (undef, "UFTP image distribution requires site.httpmethod=http, not $httpmethod")
unless $httpmethod eq 'http';
for my $source ([ 'linuximage.addkcmdline', $args{addkcmdline} ], [ 'bootparams.addkcmdline', $args{node_addkcmdline} ]) {
my @managed = managed_boot_keys($source->[1]);
return (undef, "$source->[0] must not set " . join(', ', @managed) . '; nodeset generates them for UFTP images')
if @managed;
}
my $marker = $args{marker} // "$args{initrd}.uftp";
my $key = join(':', map { my @stat = stat($_); ($_, @stat ? @stat[ 0, 1, 7, 9 ] : '-') } $args{initrd}, $marker);
my $error = $initrd_marker_errors{$key} //= verify_initrd_marker($args{initrd}, $marker) // '';
return (undef, $error) if $error;
my ($config, $config_error) = load_listener_config($args{config});
return (undef, "UFTP listener configuration: $config_error") if $config_error;
return (undef, "$rootimgdir has a character that a UFTP request cannot carry; use only letters, digits and ._+- in its path")
unless normalize_image_path("$rootimgdir/$artifact");
my $real = Cwd::realpath($rootimgdir);
return (undef, "$rootimgdir is outside the image_roots of the UFTP listener in " . ($args{config} || $CONFIG_FILE))
unless defined($real) && grep { _contained($real, $_) } @{ $config->{image_roots} };
my ($fingerprint, $fingerprint_error) = read_fingerprint($config->{fingerprint_file});
return (undef, $fingerprint_error) if $fingerprint_error;
return ("xcat.uftp=$config->{listen_port} xcat.uftpfp=$fingerprint ", undef);
}
1;
+25 -2
View File
@@ -17,6 +17,7 @@ use xCAT::TableUtils;
use xCAT::NetworkUtils;
use xCAT::MsgUtils;
use xCAT::SvrUtils;
use xCAT::UFTP;
use xCAT::Yum;
use Getopt::Long;
@@ -404,6 +405,8 @@ sub mknetboot
my $tftpdir;
my $cfgpart;
my $rootimgchecksum;
my $rootimgtransport;
my $addkcmdline;
my $imagename; # set it if running of 'nodeset osimage=xxx'
if ($reshash->{$node}->[0] and $reshash->{$node}->[0]->{tftpdir}) {
@@ -435,11 +438,12 @@ sub mknetboot
if (!$linuximagetab) {
$linuximagetab = xCAT::Table->new('linuximage', -create => 1);
}
(my $ref1) = $linuximagetab->getAttribs({ imagename => $imagename }, 'rootimgdir', 'nodebootif', 'dump', 'crashkernelsize', 'partitionfile', 'rootimgchecksum');
(my $ref1) = $linuximagetab->getAttribs({ imagename => $imagename }, 'rootimgdir', 'nodebootif', 'dump', 'crashkernelsize', 'partitionfile', 'rootimgchecksum', 'rootimgtransport', 'addkcmdline');
if (($ref1) && ($ref1->{'rootimgdir'})) {
$img_hash{$imagename}->{rootimgdir} = $ref1->{'rootimgdir'};
}
$img_hash{$imagename}->{rootimgchecksum} = $ref1->{'rootimgchecksum'} if $ref1;
@{ $img_hash{$imagename} }{qw(rootimgtransport addkcmdline)} = @$ref1{qw(rootimgtransport addkcmdline)} if $ref1;
if (($ref1) && ($ref1->{'nodebootif'})) {
$img_hash{$imagename}->{nodebootif} = $ref1->{'nodebootif'};
}
@@ -497,6 +501,8 @@ sub mknetboot
$dump = $ph->{dump};
$cfgpart = $ph->{'cfgpart'};
$rootimgchecksum = $ph->{rootimgchecksum};
$rootimgtransport = $ph->{rootimgtransport};
$addkcmdline = $ph->{addkcmdline};
}
else {
my @ents = xCAT::TableUtils->get_site_attribute("disablenodesetwarning");
@@ -543,11 +549,12 @@ sub mknetboot
$linuximagetab = xCAT::Table->new('linuximage');
}
if ($linuximagetab) {
(my $ref1) = $linuximagetab->getAttribs({ imagename => $imgname }, 'dump', 'crashkernelsize', 'partitionfile', 'rootimgchecksum');
(my $ref1) = $linuximagetab->getAttribs({ imagename => $imgname }, 'dump', 'crashkernelsize', 'partitionfile', 'rootimgchecksum', 'rootimgtransport', 'addkcmdline');
if ($ref1 and $ref1->{'dump'}) {
$dump = $ref1->{'dump'};
}
$rootimgchecksum = $ref1->{'rootimgchecksum'} if $ref1;
($rootimgtransport, $addkcmdline) = @$ref1{qw(rootimgtransport addkcmdline)} if $ref1;
if ($ref1 and $ref1->{'crashkernelsize'}) {
$crashkernelsize = $ref1->{'crashkernelsize'};
}
@@ -668,6 +675,21 @@ sub mknetboot
next;
}
}
# Nodes boot the initial ramdisk of the image once the copy below runs, or the TFTP copy with --noupdateinitrd.
my $uftp_args = '';
unless ($statelite) {
my ($transport, $uftp_error) = xCAT::SvrUtils->rootimg_transport($rootimgtransport);
($uftp_args, $uftp_error) = xCAT::UFTP::boot_arguments(rootimgdir => $rootimgdir, artifact => $compressedrootimg,
checksum => xCAT::SvrUtils->rootimg_checksum_enabled($rootimgchecksum), httpmethod => $httpmethod,
addkcmdline => $addkcmdline, node_addkcmdline => $bootparams->{$node}->[0]->{addkcmdline},
initrd => ($noupdateinitrd ? $tftppath : $rootimgdir) . "/initrd-stateless.gz",
marker => "$rootimgdir/initrd-stateless.gz.uftp")
if $transport && $transport eq 'uftp';
if ($uftp_error) {
xCAT::MsgUtils->report_node_error($callback, $node, $uftp_error);
next;
}
}
my $copied = 1;
if ($docopy && !$noupdateinitrd) {
@@ -873,6 +895,7 @@ sub mknetboot
$kcmdline = "imgurl=$httpmethod://$imgsrvip:$httpport/$rootimgdir/$compressedrootimg ";
}
$kcmdline .= $checksum_arg;
$kcmdline .= $uftp_args;
$kcmdline .= "XCAT=$xcatmasterip:$xcatdport ";
$kcmdline .= "NODE=$node ";
+24 -2
View File
@@ -12,6 +12,7 @@ use File::Temp qw/tempdir/;
use xCAT::Table;
use xCAT::Utils;
use xCAT::SvrUtils;
use xCAT::UFTP;
use xCAT::MsgUtils;
use Data::Dumper;
use Getopt::Long;
@@ -1481,6 +1482,8 @@ sub mknetboot
my $nfsrootopts;
my $tftpdir;
my $rootimgchecksum;
my $rootimgtransport;
my $addkcmdline;
my $imagename; # set it if running of 'nodeset osimage=xxx'
if ($reshash->{$node}->[0] and $reshash->{$node}->[0]->{tftpdir}) {
@@ -1507,11 +1510,12 @@ sub mknetboot
if (!$linuximagetab) {
$linuximagetab = xCAT::Table->new('linuximage', -create => 1);
}
(my $ref1) = $linuximagetab->getAttribs({ imagename => $imagename }, 'rootimgdir', 'rootimgchecksum');
(my $ref1) = $linuximagetab->getAttribs({ imagename => $imagename }, 'rootimgdir', 'rootimgchecksum', 'rootimgtransport', 'addkcmdline');
if (($ref1) && ($ref1->{'rootimgdir'})) {
$img_hash{$imagename}->{rootimgdir} = $ref1->{'rootimgdir'};
}
$img_hash{$imagename}->{rootimgchecksum} = $ref1->{'rootimgchecksum'} if $ref1;
@{ $img_hash{$imagename} }{qw(rootimgtransport addkcmdline)} = @$ref1{qw(rootimgtransport addkcmdline)} if $ref1;
if (($ref1) && ($ref1->{'nodebootif'})) {
$img_hash{$imagename}->{nodebootif} = $ref1->{'nodebootif'};
}
@@ -1545,6 +1549,8 @@ sub mknetboot
$crashkernelsize = $ph->{crashkernelsize};
$dump = $ph->{dump};
$rootimgchecksum = $ph->{rootimgchecksum};
$rootimgtransport = $ph->{rootimgtransport};
$addkcmdline = $ph->{addkcmdline};
}
else {
# This is deprecated mode to define node's provmethod, not supported now.
@@ -1586,11 +1592,12 @@ sub mknetboot
$linuximagetab = xCAT::Table->new('linuximage');
}
if ($linuximagetab) {
(my $ref1) = $linuximagetab->getAttribs({ imagename => $imgname }, 'dump', 'crashkernelsize', 'rootimgchecksum');
(my $ref1) = $linuximagetab->getAttribs({ imagename => $imgname }, 'dump', 'crashkernelsize', 'rootimgchecksum', 'rootimgtransport', 'addkcmdline');
if ($ref1 and $ref1->{'dump'}) {
$dump = $ref1->{'dump'};
}
$rootimgchecksum = $ref1->{'rootimgchecksum'} if $ref1;
($rootimgtransport, $addkcmdline) = @$ref1{qw(rootimgtransport addkcmdline)} if $ref1;
if ($ref1 and $ref1->{'crashkernelsize'}) {
$crashkernelsize = $ref1->{'crashkernelsize'};
}
@@ -1692,6 +1699,20 @@ sub mknetboot
next;
}
}
# Nodes boot the initial ramdisk of the image once the copy below runs.
my $uftp_args = '';
unless ($statelite) {
my ($transport, $uftp_error) = xCAT::SvrUtils->rootimg_transport($rootimgtransport);
($uftp_args, $uftp_error) = xCAT::UFTP::boot_arguments(rootimgdir => $rootimgdir, artifact => $compressedrootimg,
checksum => xCAT::SvrUtils->rootimg_checksum_enabled($rootimgchecksum), httpmethod => 'http',
addkcmdline => $addkcmdline, node_addkcmdline => $bootparams->{$node}->[0]->{addkcmdline},
initrd => "$rootimgdir/initrd-stateless.gz", marker => "$rootimgdir/initrd-stateless.gz.uftp")
if $transport && $transport eq 'uftp';
if ($uftp_error) {
xCAT::MsgUtils->report_node_error($callback, $node, $uftp_error);
next;
}
}
my $copied = 1;
if ($docopy) {
@@ -1866,6 +1887,7 @@ sub mknetboot
$kcmdline =
"imgurl=http://$imgsrv:$httpport/$rootimgdir/$compressedrootimg ";
$kcmdline .= $checksum_arg;
$kcmdline .= $uftp_args;
$kcmdline .= "XCAT=$xcatmaster:$xcatdport ";
}
+7 -4
View File
@@ -641,6 +641,9 @@ sub get_files {
# nodeset reads this marker to tell whether the ramdisk checks the root image checksum.
$attrs->{ramdiskcheck} = "$ramdisk.rootimgcheck" if -r "$ramdisk.rootimgcheck";
# nodeset tells nodes to receive the root image over UFTP only when this marker matches the ramdisk.
$attrs->{ramdiskuftp} = "$ramdisk.uftp" if -r "$ramdisk.uftp";
}
unless ($kernel) {
@@ -931,7 +934,7 @@ sub make_bundle {
# these are the only files we copy in. (unless you have extras)
for my $a ("kernel", "ramdisk", "ramdiskcheck", "rootimg") {
for my $a ("kernel", "ramdisk", "ramdiskcheck", "ramdiskuftp", "rootimg") {
my $filenames = $attribs->{$a};
if ($filenames) {
my @file_array = split(',', $filenames);
@@ -1332,7 +1335,7 @@ sub change_profile {
$data->{linuximage}->{rootimgdir} = "$installdir/netboot/" . $data->{osimage}->{osvers} . "/" . $data->{osimage}->{osarch} . "/$new_profile";
}
for my $a ("kernel", "ramdisk", "ramdiskcheck", "rootimg", "rootimgtree", "litefile") {
for my $a ("kernel", "ramdisk", "ramdiskcheck", "ramdiskuftp", "rootimg", "rootimgtree", "litefile") {
if ($data->{$a}) {
my $fn = basename($data->{$a});
$data->{$a} = $data->{linuximage}->{rootimgdir} . "/$fn";
@@ -1958,7 +1961,7 @@ sub check_import {
$callback->({ error => ["Unable to open table '$table'"], errorcode => [1] });
return 0;
}
my @missing = grep { !-e "$imgdir/$_" && !-l "$imgdir/$_" } map { basename($_) } map { split(',', $data->{$_} // '') } qw(rootimg kernel ramdisk rootimgtree);
my @missing = grep { !-e "$imgdir/$_" && !-l "$imgdir/$_" } map { basename($_) } map { split(',', $data->{$_} // '') } qw(rootimg kernel ramdisk ramdiskuftp rootimgtree);
push @missing, grep { $data->{kit}{$_}{kitdir} && !-d "$imgdir/$_" } sort keys %{ $data->{kit} || {} };
# imgexport keeps an extra directory under its path and an extra file under its name.
@@ -2059,7 +2062,7 @@ sub place_boot_files {
my ($data, $imgdir, $callback, $placed, $held) = @_;
# The root image goes first, so a failed publication leaves the kernel and ramdisk that match the published image.
for my $a ("rootimg", "kernel", "ramdisk", "ramdiskcheck", "rootimgtree", "litefile") {
for my $a ("rootimg", "kernel", "ramdisk", "ramdiskcheck", "ramdiskuftp", "rootimgtree", "litefile") {
my $filenames = $data->{$a};
if ($filenames) {
my @file_array = split(',', $filenames);
+25 -2
View File
@@ -15,6 +15,7 @@ use xCAT::BootUtils qw(find_genesis_boot_files);
use xCAT::TableUtils;
use xCAT::NetworkUtils;
use xCAT::SvrUtils;
use xCAT::UFTP;
use xCAT::MsgUtils;
use Data::Dumper;
use Getopt::Long;
@@ -142,6 +143,8 @@ sub mknetboot
my $nfsrootopts;
my $cfgpart;
my $rootimgchecksum;
my $rootimgtransport;
my $addkcmdline;
my $imagename; # set it if running of 'nodeset osimage=xxx'
my $ent = $ntents->{$node}->[0];
@@ -162,11 +165,12 @@ sub mknetboot
if (!$linuximagetab) {
$linuximagetab = xCAT::Table->new('linuximage', -create => 1);
}
(my $ref1) = $linuximagetab->getAttribs({ imagename => $imagename }, 'rootimgdir', 'nodebootif', 'dump', 'crashkernelsize', 'partitionfile', 'rootimgchecksum');
(my $ref1) = $linuximagetab->getAttribs({ imagename => $imagename }, 'rootimgdir', 'nodebootif', 'dump', 'crashkernelsize', 'partitionfile', 'rootimgchecksum', 'rootimgtransport', 'addkcmdline');
if (($ref1) && ($ref1->{'rootimgdir'})) {
$img_hash{$imagename}->{rootimgdir} = $ref1->{'rootimgdir'};
}
$img_hash{$imagename}->{rootimgchecksum} = $ref1->{'rootimgchecksum'} if $ref1;
@{ $img_hash{$imagename} }{qw(rootimgtransport addkcmdline)} = @$ref1{qw(rootimgtransport addkcmdline)} if $ref1;
if (($ref1) && ($ref1->{'nodebootif'})) {
$img_hash{$imagename}->{nodebootif} = $ref1->{'nodebootif'};
}
@@ -217,6 +221,8 @@ sub mknetboot
$crashkernelsize = $ph->{crashkernelsize};
$cfgpart = $ph->{'cfgpart'};
$rootimgchecksum = $ph->{rootimgchecksum};
$rootimgtransport = $ph->{rootimgtransport};
$addkcmdline = $ph->{addkcmdline};
$rootimgdir = $ph->{rootimgdir};
unless ($rootimgdir) {
@@ -267,11 +273,12 @@ sub mknetboot
$linuximagetab = xCAT::Table->new('linuximage');
}
if ($linuximagetab) {
(my $ref1) = $linuximagetab->getAttribs({ imagename => $imgname }, 'dump', 'crashkernelsize', 'partitionfile', 'rootimgchecksum');
(my $ref1) = $linuximagetab->getAttribs({ imagename => $imgname }, 'dump', 'crashkernelsize', 'partitionfile', 'rootimgchecksum', 'rootimgtransport', 'addkcmdline');
if ($ref1 && $ref1->{'dump'}) {
$dump = $ref1->{'dump'};
}
$rootimgchecksum = $ref1->{'rootimgchecksum'} if $ref1;
($rootimgtransport, $addkcmdline) = @$ref1{qw(rootimgtransport addkcmdline)} if $ref1;
if ($ref1 and $ref1->{'crashkernelsize'}) {
$crashkernelsize = $ref1->{'crashkernelsize'};
}
@@ -416,6 +423,21 @@ sub mknetboot
next;
}
}
# Nodes boot the initial ramdisk of the image once the copy below runs, or the TFTP copy with --noupdateinitrd.
my $uftp_args = '';
unless ($statelite) {
my ($transport, $uftp_error) = xCAT::SvrUtils->rootimg_transport($rootimgtransport);
($uftp_args, $uftp_error) = xCAT::UFTP::boot_arguments(rootimgdir => $rootimgdir, artifact => $compressedrootimg,
checksum => xCAT::SvrUtils->rootimg_checksum_enabled($rootimgchecksum), httpmethod => $httpmethod,
addkcmdline => $addkcmdline, node_addkcmdline => $bootparams->{$node}->[0]->{addkcmdline},
initrd => ($noupdateinitrd ? $tftppath : $rootimgdir) . "/initrd-stateless.gz",
marker => "$rootimgdir/initrd-stateless.gz.uftp")
if $transport && $transport eq 'uftp';
if ($uftp_error) {
xCAT::MsgUtils->report_node_error($callback, $node, $uftp_error);
next;
}
}
my $copied = 1;
if ($docopy && !$noupdateinitrd) {
@@ -555,6 +577,7 @@ sub mknetboot
$kcmdline =
"imgurl=$httpmethod://$imgsrv:$httpport/$rootimgdir/$compressedrootimg ";
$kcmdline .= $checksum_arg;
$kcmdline .= $uftp_args;
}
$kcmdline .= "XCAT=$xcatmaster:$xcatdport ";
$kcmdline .= " XCATHTTPPORT=$httpport ";
+283
View File
@@ -0,0 +1,283 @@
#!/usr/bin/env perl
# Accept UFTP transfer requests from booting stateless nodes and multicast
# published root images to them with uftp(1).
BEGIN
{
$::XCATROOT = $ENV{'XCATROOT'} ? $ENV{'XCATROOT'} : '/opt/xcat';
}
use lib "$::XCATROOT/lib/perl";
use strict;
use warnings;
use Errno qw(EAGAIN EINTR EWOULDBLOCK);
use Fcntl qw(F_GETFD F_SETFD FD_CLOEXEC SEEK_SET);
use File::Basename qw(dirname);
use File::Path qw(make_path);
use File::Temp ();
use Getopt::Long qw(GetOptions);
use IO::Select;
use IO::Socket::INET;
use POSIX qw(WNOHANG);
use xCAT::UFTP;
my $config_path = $xCAT::UFTP::CONFIG_FILE;
my ($init_key, $check_only, $help);
GetOptions(
'config=s' => \$config_path,
'init-key' => \$init_key,
'check' => \$check_only,
'help|h' => \$help,
) or usage(1);
usage(0) if $help;
usage(1) if @ARGV;
sub usage {
my ($rc) = @_;
print STDERR "Usage: xcat-uftp-listener [--config FILE] [--init-key | --check]\n";
exit $rc;
}
sub logmsg {
my ($level, $message) = @_;
my @t = localtime();
printf STDERR "%04d-%02d-%02d %02d:%02d:%02d %s: %s\n",
$t[5] + 1900, $t[4] + 1, $t[3], $t[2], $t[1], $t[0], $level, $message;
}
sub fatal {
my ($message) = @_;
logmsg('error', $message);
exit 1;
}
my ($config, $config_error) = xCAT::UFTP::load_listener_config($config_path);
fatal($config_error) if $config_error;
sub key_fingerprint {
# uftp_keymgt prints the key description on standard error.
my $pid = open(my $pipe, '-|');
return (undef, "Cannot run $config->{uftp_keymgt}: $!") unless defined($pid);
if ($pid == 0) {
open(STDERR, '>&', \*STDOUT);
{ exec { $config->{uftp_keymgt} } $config->{uftp_keymgt}, $config->{key_file}; }
POSIX::_exit(127);
}
my $output = do { local $/; <$pipe> } // '';
close($pipe);
return (undef, "$config->{uftp_keymgt} failed for $config->{key_file}") if $?;
my $fingerprint = xCAT::UFTP::parse_fingerprint($output);
return (undef, "Cannot parse the fingerprint of $config->{key_file}") unless $fingerprint;
return ($fingerprint, undef);
}
sub init_key {
my $key = $config->{key_file};
fatal("$config->{uftp_keymgt} is not executable; install the uftp package")
unless -x $config->{uftp_keymgt};
make_path(dirname($key), { mode => 0755 }) unless -d dirname($key);
unless (-e $key) {
my $old = umask(077);
my $rc = system { $config->{uftp_keymgt} } $config->{uftp_keymgt}, '-g', 'ec:secp256r1', $key;
umask($old);
fatal("Cannot create the UFTP server key $key") if $rc;
}
fatal("Cannot restrict $key: $!") unless chmod(0600, $key);
my ($fingerprint, $error) = key_fingerprint();
fatal($error) if $error;
if ($error = xCAT::UFTP::write_file_atomic($config->{fingerprint_file}, "$fingerprint\n", 0644)) {
fatal($error);
}
print "UFTP server key: $key\nUFTP server key fingerprint: $fingerprint\n";
exit 0;
}
init_key() if $init_key;
my ($fingerprint, $runtime_error) = xCAT::UFTP::check_listener_runtime($config, \&key_fingerprint);
fatal($runtime_error) if $runtime_error;
if ($check_only) {
print "UFTP listener configuration is valid; server key fingerprint $fingerprint\n";
exit 0;
}
my $server = IO::Socket::INET->new(
LocalAddr => $config->{listen_address},
LocalPort => $config->{listen_port},
Proto => 'tcp',
Listen => 128,
ReuseAddr => 1,
) or fatal("Cannot listen on $config->{listen_address}:$config->{listen_port}: $!");
$server->blocking(0);
my $workdir = File::Temp::tempdir('xcat-uftp-XXXXXXXX', TMPDIR => 1, CLEANUP => 1);
chmod(0700, $workdir);
my %connections;
my $stopping = 0;
$SIG{TERM} = $SIG{INT} = sub { $stopping = 1 };
$SIG{PIPE} = 'IGNORE';
$SIG{HUP} = 'IGNORE';
my $state = xCAT::UFTP::listener_state($config, workdir => $workdir, log => \&logmsg, spawn => \&spawn_sender);
logmsg('info', "listening on $config->{listen_address}:$config->{listen_port}, "
. "multicast from $config->{multicast_interface} to $config->{public_multicast} port $config->{udp_port}, "
. 'image roots ' . join(',', @{ $config->{image_roots} }));
sub respond {
my ($conn, $status, $body) = @_;
$conn->{out} = xCAT::UFTP::http_response($status, $body);
$conn->{deadline} = time() + $config->{request_timeout};
}
sub close_connection {
my ($conn) = @_;
delete $connections{ fileno($conn->{sock}) };
close($conn->{sock});
}
sub refuse_connection {
my ($sock, $status, $body) = @_;
$sock->blocking(0);
syswrite($sock, xCAT::UFTP::http_response($status, $body));
close($sock);
}
sub accept_connections {
my ($now) = @_;
for (1 .. $config->{max_connections}) {
my $sock = $server->accept() or last;
my $peer = $sock->peerhost() // '';
if (my ($status, $body) = xCAT::UFTP::connection_refusal($state, scalar(keys %connections), $peer)) {
refuse_connection($sock, $status, $body);
next;
}
$sock->blocking(0);
$connections{ fileno($sock) } = {
sock => $sock,
peer => $peer,
buffer => '',
deadline => $now + $config->{request_timeout},
};
}
}
sub read_request {
my ($conn, $now) = @_;
my $count = sysread($conn->{sock}, my $data, 4096);
if (!defined($count)) {
return if $! == EAGAIN || $! == EWOULDBLOCK || $! == EINTR;
return close_connection($conn);
}
if ($count == 0) {
return close_connection($conn);
}
$conn->{buffer} .= $data;
my ($status, $body) = xCAT::UFTP::listener_reply($state, $conn->{peer}, $conn->{buffer}, $now);
respond($conn, $status, $body) if defined($status);
}
sub write_response {
my ($conn) = @_;
my $count = syswrite($conn->{sock}, $conn->{out});
if (!defined($count)) {
return if $! == EAGAIN || $! == EWOULDBLOCK || $! == EINTR;
return close_connection($conn);
}
substr($conn->{out}, 0, $count) = '';
close_connection($conn) unless length($conn->{out});
}
sub child_main {
my ($job, $log, @command) = @_;
$SIG{$_} = 'DEFAULT' for qw(TERM INT PIPE HUP);
open(STDIN, '<', '/dev/null');
open(STDOUT, '>>', $log) or POSIX::_exit(126);
open(STDERR, '>&', \*STDOUT) or POSIX::_exit(126);
my $fh = $job->{fh};
sysseek($fh, 0, SEEK_SET) or POSIX::_exit(91);
if (fileno($fh) != 3) {
POSIX::dup2(fileno($fh), 3) or POSIX::_exit(92);
}
open(my $image, '<&=', 3) or POSIX::_exit(92);
my $flags = fcntl($image, F_GETFD, 0);
fcntl($image, F_SETFD, $flags & ~FD_CLOEXEC) or POSIX::_exit(92);
print STDERR "starting: @command\n";
{ exec { $command[0] } @command; }
print STDERR "cannot execute $command[0]: $!\n";
POSIX::_exit(127);
}
sub spawn_sender {
my ($job, $log, @command) = @_;
my $pid = fork();
return (undef, "fork failed: $!") unless defined($pid);
child_main($job, $log, @command) if $pid == 0;
return ($pid, undef);
}
sub reap_senders {
my ($now) = @_;
while ((my $pid = waitpid(-1, WNOHANG)) > 0) {
xCAT::UFTP::sender_exited($state, $pid, $?, $now);
}
}
sub close_idle_connections {
my ($now) = @_;
for my $conn (values %connections) {
close_connection($conn) if $now >= $conn->{deadline};
}
}
sub shutdown_listener {
close($server);
close_connection($_) for values %connections;
my @pids = xCAT::UFTP::sender_pids($state);
kill('TERM', @pids) if @pids;
my $deadline = time() + $xCAT::UFTP::KILL_GRACE;
while (xCAT::UFTP::sender_pids($state) && time() < $deadline) {
reap_senders(time());
select(undef, undef, undef, 0.1);
}
@pids = xCAT::UFTP::sender_pids($state);
kill('KILL', @pids) if @pids;
while (xCAT::UFTP::sender_pids($state)) {
my $pid = waitpid(-1, 0);
last if $pid <= 0;
xCAT::UFTP::sender_exited($state, $pid, $?, time(), 'listener stopped');
}
xCAT::UFTP::stop_jobs($state, time());
logmsg('info', 'stopped');
}
while (!$stopping) {
my $now = time();
reap_senders($now);
xCAT::UFTP::run_jobs($state, $now);
close_idle_connections($now);
my $readers = IO::Select->new($server);
my $writers = IO::Select->new();
for my $conn (values %connections) {
if (defined($conn->{out})) {
$writers->add($conn->{sock});
} else {
$readers->add($conn->{sock});
}
}
my ($readable, $writable) = IO::Select->select($readers, $writers, undef, 1);
$now = time();
for my $sock (@{ $readable || [] }) {
if ($sock == $server) {
accept_connections($now);
} elsif (my $conn = $connections{ fileno($sock) }) {
read_request($conn, $now);
}
}
for my $sock (@{ $writable || [] }) {
my $conn = $connections{ fileno($sock) // -1 } or next;
write_response($conn) if defined($conn->{out});
}
}
shutdown_listener();
exit 0;
@@ -0,0 +1,72 @@
# Configuration for xcat-uftp-listener. Copy this file to
# /etc/xcat/uftp/listener.conf, set listen_address, and run
# "xcat-uftp-listener --init-key" once before starting the service.
# Every value is owned by the server; booting nodes cannot override them.
# IPv4 address of this server on the provisioning network. Nodes reach the
# listener on the same host as their imgurl. Required.
listen_address = 192.0.2.1
# TCP port for transfer requests. nodeset passes it to the nodes.
#listen_port = 1045
# Comma-separated IPv4 networks allowed to request transfers, for example
# 192.0.2.0/24. Empty allows any address that can reach listen_address.
#allowed_networks =
# Local IPv4 address that sends the multicast data. Defaults to
# listen_address. It is also the UFTP server ID that nodes accept.
#multicast_interface =
# Comma-separated directories that may hold published images. An image
# outside these directories is refused even if packimage published it.
#image_roots = /install/netboot
# UDP port, public announcement address, private data address pattern and
# TTL for the multicast session.
#udp_port = 1044
#public_multicast = 230.4.4.1
#private_multicast = 230.5.5.x
#ttl = 1
# Transmission rate in kilobits per second. Use no more than the link speed of
# the slowest node.
#rate_kbps = 100000
# Data bytes in each multicast packet, from 512 to 8800. The default fits an
# MTU of 1500. Use a larger value only when the listener, the switches and
# every node in the initramfs use an MTU of 9000 or more. A node with a
# smaller MTU receives no data, leaves the transfer and falls back to HTTP.
#block_size = 1300
# Seconds to collect requests for the same image before sending. Zero sends
# at once.
#batch_delay = 30
# Seconds a transfer may run before it is stopped. Nodes wait for the batch
# delay plus this value before they fall back to HTTP.
#transfer_timeout = 1800
# Limits on simultaneous senders, queued transfers, receivers in one
# transfer, open request connections, and seconds to receive a request.
#max_transfers = 4
#max_queued_jobs = 32
#max_clients_per_job = 1000
#max_connections = 64
#request_timeout = 10
# Seconds to keep the result of a finished transfer for status queries.
#job_retention = 600
# UFTP session encryption. Nodes accept only encrypted sessions signed by
# the key below.
#encryption = aes256-gcm
#key_file = /etc/xcat/uftp/server.key
#fingerprint_file = /etc/xcat/uftp/server.fingerprint
# UFTP programs and the directory for one log file per transfer. The
# xcat-uftp-listener service can write only to /var/log/xcat-uftp. For another
# log_dir, add it with ReadWritePaths= in a drop-in for the service.
#uftp = /usr/bin/uftp
#uftp_keymgt = /usr/bin/uftp_keymgt
#log_dir = /var/log/xcat-uftp
@@ -141,6 +141,52 @@ sub publish_boot_files {
return 1;
}
# Puts the UFTP fetch helper in the xCAT dracut module of an initial ramdisk when $wanted is true and the image has the
# UFTP 5 receiver of xcat-dep for its architecture, and removes an older copy otherwise. Returns the receiver or undef.
sub add_uftp_receiver {
my ($rootimg_dir, $arch, $moduledir, $wanted) = @_;
require xCAT::UFTP;
my $copy = "$moduledir/$xCAT::UFTP::HELPER";
if (-e $copy || -l $copy) {
unlink($copy) or die("Error: cannot remove $copy: $!\n");
}
return unless $wanted;
my ($receiver, $warning) = xCAT::UFTP::usable_receiver($rootimg_dir, $arch);
print "Warning: $warning\n" if $warning;
return unless $receiver;
my $helper = dirname(__FILE__) . "/$xCAT::UFTP::HELPER";
File::Copy::copy($helper, $copy) && chmod(0644, $copy) or die("Error: cannot copy $helper to $copy: $!\n");
print "Adding the UFTP receiver $receiver to the initial ramdisk.\n";
return $receiver;
}
# Returns the lsinitrd lines of the initial ramdisk $initrd, a path inside the image, or undef when the image cannot list it.
sub initrd_listing {
my ($rootimg_dir, $initrd) = @_;
my @listing = `chroot $rootimg_dir lsinitrd $initrd 2>&1`;
return $? ? undef : \@listing;
}
# Dies unless the initial ramdisk $initrd, a path inside the image, holds the UFTP receiver files.
sub check_uftp_initrd {
my ($rootimg_dir, $initrd, $mode) = @_;
require xCAT::UFTP;
my $listing = initrd_listing($rootimg_dir, $initrd);
my @missing = $listing ? xCAT::UFTP::initrd_missing_files($listing) : ('a readable file list');
die("Error: the initial ramdisk for $mode lacks the UFTP receiver files: @missing\n") if @missing;
return 1;
}
# Records next to a published initial ramdisk whether it can receive the root image over UFTP.
sub mark_uftp_receiver {
my ($initrd, $receiver) = @_;
require xCAT::UFTP;
my $error = $receiver ? xCAT::UFTP::write_initrd_marker($initrd) : xCAT::UFTP::remove_initrd_marker($initrd);
print "Warning: $error\n" if $error;
print "The initial ramdisk $initrd can receive the root image over UFTP.\n" if $receiver && !$error;
return !$error;
}
sub varsubinline{
my $line=shift;
my $refvardict=shift;
@@ -0,0 +1,460 @@
# Receive a stateless root image over UFTP multicast. xcatroot sources this
# file when nodeset passes xcat.uftp=, and calls xcat_uftp_rootimg. The file
# is not checked here: xcatroot checks it like an HTTP download.
: "${XCAT_UFTP_ATTEMPTS:=2}"
: "${XCAT_UFTP_GRACE:=30}"
: "${XCAT_UFTP_POLL:=5}"
: "${XCAT_UFTP_REQUEST_TIMEOUT:=15}"
: "${XCAT_UFTP_STOP_WAIT:=10}"
xcat_fetch_log() {
if command -v logger >/dev/null 2>&1; then
# SYSLOGHOST is either empty or "-n <server>" and must split.
logger $SYSLOGHOST -t "${log_label:-xcat.deployment}" -p "local4.$1" "$2"
fi
echo "$2" >&2
}
xcat_uint_ok() {
case "$1" in
0) return 0 ;;
''|0*|*[!0-9]*) return 1 ;;
esac
[ "${#1}" -le 9 ] && [ "$1" -le "$2" ]
}
xcat_port_ok() {
xcat_uint_ok "$1" 65535 && [ "$1" -gt 0 ]
}
xcat_ipv4_ok() {
case "$1" in
''|*[!0-9.]*|.*|*.|*..*) return 1 ;;
esac
_xf_ifs=$IFS
IFS=.
set -- $1
IFS=$_xf_ifs
[ "$#" -eq 4 ] || return 1
for _xf_octet do
xcat_uint_ok "$_xf_octet" 255 || return 1
done
}
xcat_sha256_ok() {
case "$1" in
*[!0-9a-f]*) return 1 ;;
esac
[ "${#1}" -eq 64 ]
}
xcat_fingerprint_ok() {
case "$1" in
??:??:??:??:??:??:??:??:??:??:??:??:??:??:??:??:??:??:??:??) ;;
*) return 1 ;;
esac
case "$1" in
*[!0-9A-F:]*) return 1 ;;
esac
}
# Print the first SHA-256 checksum that the sha256sum file $1 lists for the name $2.
xcat_listed_sha256() {
[ -r "$1" ] || return 1
while read -r _xf_sum _xf_name || [ -n "$_xf_sum" ]; do
if [ "${_xf_name#\*}" = "$2" ] && xcat_sha256_ok "$_xf_sum"; then
printf '%s\n' "$_xf_sum"
return 0
fi
done < "$1"
return 1
}
# Split an http(s) URL into XCAT_URL_SCHEME, XCAT_URL_HOST (IPv6 keeps its
# brackets), XCAT_URL_PORT (the scheme default when absent) and XCAT_URL_PATH.
xcat_url_parse() {
case "$1" in
http://*) XCAT_URL_SCHEME=http; _xf_rest=${1#http://} ;;
https://*) XCAT_URL_SCHEME=https; _xf_rest=${1#https://} ;;
*) return 1 ;;
esac
case "$_xf_rest" in
*/*) _xf_authority=${_xf_rest%%/*}; XCAT_URL_PATH=/${_xf_rest#*/} ;;
*) _xf_authority=$_xf_rest; XCAT_URL_PATH=/ ;;
esac
case "$_xf_authority" in
*@*) return 1 ;;
\[*\]:*) XCAT_URL_HOST="${_xf_authority%%]:*}]"; XCAT_URL_PORT=${_xf_authority##*]:} ;;
\[*\]) XCAT_URL_HOST=$_xf_authority; XCAT_URL_PORT= ;;
*\[*|*\]*|*:*:*) return 1 ;;
*:*) XCAT_URL_HOST=${_xf_authority%:*}; XCAT_URL_PORT=${_xf_authority##*:} ;;
*) XCAT_URL_HOST=$_xf_authority; XCAT_URL_PORT= ;;
esac
case "$XCAT_URL_HOST" in
''|\[\]) return 1 ;;
\[*\])
_xf_inner=${XCAT_URL_HOST#\[}
_xf_inner=${_xf_inner%\]}
case "$_xf_inner" in
*[!0-9A-Fa-f:.]*|*:*:*:*:*:*:*:*:*) return 1 ;;
*:*) ;;
*) return 1 ;;
esac
;;
*[!A-Za-z0-9.-]*|-*|.*) return 1 ;;
esac
if [ -z "$XCAT_URL_PORT" ]; then
if [ "$XCAT_URL_SCHEME" = https ]; then
XCAT_URL_PORT=443
else
XCAT_URL_PORT=80
fi
fi
xcat_port_ok "$XCAT_URL_PORT" || return 1
case "$XCAT_URL_PATH" in
*[?#%]*) return 1 ;;
esac
}
# Print the image path the listener expects: repeated slashes collapsed and
# every segment limited to the characters packimage accepts.
xcat_image_path() {
_xf_path=$1
while :; do
case "$_xf_path" in
*//*) _xf_path=${_xf_path%%//*}/${_xf_path#*//} ;;
*) break ;;
esac
done
case "$_xf_path" in
/|*/|[!/]*) return 1 ;;
esac
_xf_ifs=$IFS
IFS=/
set -f
set -- ${_xf_path#/}
set +f
IFS=$_xf_ifs
for _xf_segment do
case "$_xf_segment" in
''|.*|*[!A-Za-z0-9._+-]*) return 1 ;;
esac
done
printf '%s\n' "$_xf_path"
}
# Print the transfer request URL for an image URL: same scheme and host,
# the listener port, and the image path.
xcat_uftp_url() {
xcat_url_parse "$1" || return 1
_xf_image=$(xcat_image_path "$XCAT_URL_PATH") || return 1
printf '%s://%s:%s/xcat-uftp/v1/transfer/%s%s\n' \
"$XCAT_URL_SCHEME" "$XCAT_URL_HOST" "$2" "$3" "$_xf_image"
}
xcat_http_get() {
if command -v curl >/dev/null 2>&1; then
curl --fail --silent --show-error --max-time "$3" -o "$2" "$1"
elif command -v wget >/dev/null 2>&1; then
# wget -T limits idle time only, so a watchdog limits the whole request.
wget -q -T "$3" -t 1 -O "$2" "$1" &
_xf_get=$!
(
_xf_left=$3
while [ "$_xf_left" -gt 0 ]; do
sleep 1
kill -0 "$_xf_get" 2>/dev/null || exit 0
_xf_left=$((_xf_left - 1))
done
kill -KILL "$_xf_get" 2>/dev/null
) &
_xf_dog=$!
wait "$_xf_get"
_xf_rc=$?
kill "$_xf_dog" 2>/dev/null
wait "$_xf_dog" 2>/dev/null
return "$_xf_rc"
else
return 127
fi
}
xcat_uftp_read_ack() {
XCAT_ACK_STATUS='' XCAT_ACK_JOB='' XCAT_ACK_FILE='' XCAT_ACK_PORT='' XCAT_ACK_MCAST=''
XCAT_ACK_SERVER_ID='' XCAT_ACK_SERVER_IP='' XCAT_ACK_CLIENT_IP=''
XCAT_ACK_START_IN='' XCAT_ACK_TIMEOUT=''
while IFS= read -r _xf_line || [ -n "$_xf_line" ]; do
_xf_key=${_xf_line%%=*}
_xf_value=${_xf_line#*=}
[ "$_xf_key" != "$_xf_line" ] || return 1
case "$_xf_key" in
status) XCAT_ACK_STATUS=$_xf_value ;;
job) XCAT_ACK_JOB=$_xf_value ;;
file) XCAT_ACK_FILE=$_xf_value ;;
port) XCAT_ACK_PORT=$_xf_value ;;
mcast) XCAT_ACK_MCAST=$_xf_value ;;
server_id) XCAT_ACK_SERVER_ID=$_xf_value ;;
server_ip) XCAT_ACK_SERVER_IP=$_xf_value ;;
client_ip) XCAT_ACK_CLIENT_IP=$_xf_value ;;
start_in) XCAT_ACK_START_IN=$_xf_value ;;
timeout) XCAT_ACK_TIMEOUT=$_xf_value ;;
esac
done < "$1"
[ "$XCAT_ACK_STATUS" = queued ] || return 1
case "$XCAT_ACK_JOB" in
????????????????) ;;
*) return 1 ;;
esac
case "$XCAT_ACK_JOB" in
*[!0-9a-f]*) return 1 ;;
esac
[ "$XCAT_ACK_FILE" = "$2" ] || return 1
xcat_port_ok "$XCAT_ACK_PORT" || return 1
xcat_ipv4_ok "$XCAT_ACK_MCAST" || return 1
xcat_ipv4_ok "$XCAT_ACK_SERVER_IP" || return 1
xcat_ipv4_ok "$XCAT_ACK_CLIENT_IP" || return 1
case "$XCAT_ACK_SERVER_ID" in
0x????????) ;;
*) return 1 ;;
esac
case "${XCAT_ACK_SERVER_ID#0x}" in
*[!0-9A-F]*) return 1 ;;
esac
: "${XCAT_ACK_START_IN:=0}"
xcat_uint_ok "$XCAT_ACK_START_IN" 3600 || return 1
xcat_uint_ok "$XCAT_ACK_TIMEOUT" 86400 && [ "$XCAT_ACK_TIMEOUT" -gt 0 ]
}
# Print a monotonic time in whole seconds.
xcat_now() {
if [ -r /proc/uptime ]; then
read -r _xf_uptime _xf_idle < /proc/uptime
echo "${_xf_uptime%%.*}"
else
date +%s
fi
}
# Print the last three lines of the file $1. The initramfs has no tail.
xcat_last_lines() {
_xf_l1= _xf_l2= _xf_l3=
while IFS= read -r _xf_line || [ -n "$_xf_line" ]; do
_xf_l1=$_xf_l2 _xf_l2=$_xf_l3 _xf_l3=$_xf_line
done < "$1"
printf '%s %s %s' "$_xf_l1" "$_xf_l2" "$_xf_l3"
}
xcat_child_running() {
kill -0 "$1" 2>/dev/null || return 1
if [ -r "/proc/$1/stat" ]; then
read -r _xf_pid _xf_comm _xf_state _xf_rest < "/proc/$1/stat" || return 1
[ "$_xf_state" != Z ] || return 1
fi
return 0
}
xcat_uftp_start_receiver() {
printf '%s|%s|0|%s\n' "$XCAT_ACK_SERVER_ID" "$XCAT_ACK_SERVER_IP" "$XCAT_FETCH_FP" \
> "$1/servers" || return 1
"$XCAT_UFTPD" -d -q -E -x 2 -p "$XCAT_ACK_PORT" -M "$XCAT_ACK_MCAST" \
-I "$XCAT_ACK_CLIENT_IP" -S "$1/servers" -D "$1/dest" -T "$1/tmp" \
-F "$1/status" </dev/null >"$1/uftpd.log" 2>&1 &
XCAT_UFTP_PID=$!
}
xcat_uftp_stop_receiver() {
_xf_receiver=$XCAT_UFTP_PID
[ -n "$_xf_receiver" ] || return 0
XCAT_UFTP_PID=
kill -TERM "$_xf_receiver" 2>/dev/null
(
_xf_waited=0
while [ "$_xf_waited" -lt "$XCAT_UFTP_STOP_WAIT" ]; do
kill -0 "$_xf_receiver" 2>/dev/null || exit 0
sleep 1
_xf_waited=$((_xf_waited + 1))
done
kill -KILL "$_xf_receiver" 2>/dev/null
) &
_xf_watchdog=$!
wait "$_xf_receiver" 2>/dev/null
wait "$_xf_watchdog" 2>/dev/null
return 0
}
xcat_uftp_cleanup() {
xcat_uftp_stop_receiver
if [ -n "$XCAT_FETCH_STAGING" ]; then
rm -rf "$XCAT_FETCH_STAGING"
XCAT_FETCH_STAGING=
fi
}
# Succeed when this attempt's receiver reported the expected file as copied
# in a session it opened with the expected server, and the file was moved
# out of the temporary directory.
xcat_uftp_completed() {
[ -f "$1/dest/$XCAT_FETCH_FILE" ] && [ -r "$1/status" ] || return 1
_xf_sid=${XCAT_ACK_SERVER_ID#0x}
_xf_groups=
while IFS=';' read -r _xf_kind _xf_stamp _xf_server _xf_group _xf_name _xf_size _xf_result; do
[ "$_xf_server" = "$_xf_sid" ] || continue
case "$_xf_kind" in
CONNECT) _xf_groups="$_xf_groups $_xf_group " ;;
RESULT)
[ "$_xf_name" = "$XCAT_FETCH_FILE" ] && [ "$_xf_result" = copied ] || continue
case "$_xf_groups" in
*" $_xf_group "*) return 0 ;;
esac
;;
esac
done < "$1/status"
return 1
}
xcat_uftp_job_state() {
XCAT_JOB_STATE='' XCAT_JOB_START_IN='' XCAT_JOB_REASON=''
xcat_http_get "$XCAT_FETCH_JOB_URL" "$1/job" "$2" || return 1
while IFS= read -r _xf_line || [ -n "$_xf_line" ]; do
case "$_xf_line" in
state=*) XCAT_JOB_STATE=${_xf_line#state=} ;;
start_in=*) XCAT_JOB_START_IN=${_xf_line#start_in=} ;;
reason=*) XCAT_JOB_REASON=${_xf_line#reason=} ;;
esac
done < "$1/job"
case "$XCAT_JOB_STATE" in
pending|running|done|failed) ;;
*) return 1 ;;
esac
xcat_uint_ok "${XCAT_JOB_START_IN:-0}" 86400 || XCAT_JOB_START_IN=0
}
xcat_uftp_attempt() {
_xf_staging=$XCAT_FETCH_DIR.xcat-uftp.$$.$1
mkdir "$_xf_staging" || return 1
XCAT_FETCH_STAGING=$_xf_staging
mkdir "$XCAT_FETCH_STAGING/dest" "$XCAT_FETCH_STAGING/tmp" || return 1
if ! xcat_http_get "$XCAT_FETCH_LISTENER" "$XCAT_FETCH_STAGING/ack" "$XCAT_UFTP_REQUEST_TIMEOUT"; then
xcat_fetch_log warning "UFTP listener did not accept the request $XCAT_FETCH_LISTENER"
return 1
fi
if ! xcat_uftp_read_ack "$XCAT_FETCH_STAGING/ack" "$XCAT_FETCH_FILE"; then
xcat_fetch_log warning "UFTP listener sent an invalid reply"
return 1
fi
XCAT_FETCH_JOB_URL="$XCAT_URL_SCHEME://$XCAT_URL_HOST:$XCAT_FETCH_PORT/xcat-uftp/v1/job/$XCAT_ACK_JOB"
xcat_uftp_start_receiver "$XCAT_FETCH_STAGING" || return 1
xcat_fetch_log info "UFTP transfer $XCAT_ACK_JOB queued; sending starts in ${XCAT_ACK_START_IN}s"
_xf_start=$(xcat_now)
_xf_deadline=$((_xf_start + XCAT_ACK_START_IN + XCAT_ACK_TIMEOUT + XCAT_UFTP_GRACE))
_xf_cap=$((_xf_deadline + XCAT_ACK_TIMEOUT))
_xf_next_poll=$((_xf_start + XCAT_UFTP_POLL))
_xf_done_at=
while _xf_now=$(xcat_now) && [ "$_xf_now" -lt "$_xf_deadline" ]; do
if xcat_uftp_completed "$XCAT_FETCH_STAGING"; then
xcat_uftp_stop_receiver
# Check the type only after the receiver stops: a batch member can deliver a symbolic link.
_xf_file=$XCAT_FETCH_STAGING/dest/$XCAT_FETCH_FILE
if [ -f "$_xf_file" ] && [ ! -L "$_xf_file" ]; then
return 0
fi
xcat_fetch_log warning "$XCAT_FETCH_FILE from UFTP is not a regular file"
return 1
fi
if ! xcat_child_running "$XCAT_UFTP_PID"; then
xcat_fetch_log warning "UFTP receiver stopped: $(xcat_last_lines "$XCAT_FETCH_STAGING/uftpd.log" 2>/dev/null)"
return 1
fi
if [ "$_xf_now" -ge "$_xf_next_poll" ]; then
_xf_next_poll=$((_xf_now + XCAT_UFTP_POLL))
_xf_budget=$((_xf_deadline - _xf_now))
[ "$_xf_budget" -le "$XCAT_UFTP_REQUEST_TIMEOUT" ] || _xf_budget=$XCAT_UFTP_REQUEST_TIMEOUT
if xcat_uftp_job_state "$XCAT_FETCH_STAGING" "$_xf_budget"; then
case "$XCAT_JOB_STATE" in
failed)
xcat_fetch_log warning "UFTP transfer $XCAT_ACK_JOB failed on the server: $XCAT_JOB_REASON"
return 1
;;
done)
: "${_xf_done_at:=$_xf_now}"
if [ $((_xf_now - _xf_done_at)) -ge "$XCAT_UFTP_GRACE" ]; then
xcat_fetch_log warning "UFTP transfer $XCAT_ACK_JOB ended without this node"
return 1
fi
;;
pending)
_xf_wanted=$((_xf_now + XCAT_JOB_START_IN + XCAT_ACK_TIMEOUT + XCAT_UFTP_GRACE))
[ "$_xf_wanted" -le "$_xf_deadline" ] || _xf_deadline=$_xf_wanted
[ "$_xf_deadline" -le "$_xf_cap" ] || _xf_deadline=$_xf_cap
;;
esac
fi
fi
sleep 1
done
xcat_fetch_log warning "UFTP transfer $XCAT_ACK_JOB did not finish in $((_xf_deadline - _xf_start))s"
return 1
}
xcat_fetch_interrupted() {
xcat_uftp_cleanup
xcat_fetch_log err "Root image download interrupted"
exit 130
}
xcat_uftp_usable() {
if ! xcat_port_ok "$1" || ! xcat_fingerprint_ok "$2"; then
xcat_fetch_log warning "Ignoring malformed xcat.uftp or xcat.uftpfp kernel arguments"
return 1
fi
if ! xcat_url_parse "$XCAT_FETCH_URL" || [ "$XCAT_URL_SCHEME" != http ]; then
xcat_fetch_log warning "UFTP is used only with http image URLs"
return 1
fi
XCAT_UFTPD=$(command -v uftpd) || {
xcat_fetch_log warning "uftpd is not in the initramfs; run genimage with uftp installed in the image"
return 1
}
XCAT_FETCH_LISTENER=$(xcat_uftp_url "$XCAT_FETCH_URL" "$1" "$XCAT_FETCH_SHA256") || {
xcat_fetch_log warning "Cannot build a UFTP request for $XCAT_FETCH_URL"
return 1
}
XCAT_FETCH_PORT=$1
XCAT_FETCH_FP=$2
}
# xcat_uftp_rootimg <imgurl> <file> <checksum file> <listener port> <server fingerprint>
# The checksum file is the sha256sum file published with the image, which
# names the archive to the listener.
xcat_uftp_rootimg() {
XCAT_FETCH_URL=$1
XCAT_FETCH_FILE=${1##*/}
# uftpd takes only absolute directories.
case "$2" in
/*) XCAT_FETCH_DEST=$2 ;;
*) XCAT_FETCH_DEST=${PWD%/}/$2 ;;
esac
XCAT_FETCH_DIR=${XCAT_FETCH_DEST%/*}/
XCAT_FETCH_STAGING=
XCAT_UFTP_PID=
XCAT_FETCH_SHA256=$(xcat_listed_sha256 "$3" "$XCAT_FETCH_FILE") || {
xcat_fetch_log warning "$XCAT_FETCH_URL.sha256 lists no SHA-256 checksum for $XCAT_FETCH_FILE"
return 1
}
xcat_uftp_usable "$4" "$5" || return 1
trap xcat_fetch_interrupted INT TERM HUP
_xf_attempt=1
while [ "$_xf_attempt" -le "$XCAT_UFTP_ATTEMPTS" ]; do
if xcat_uftp_attempt "$_xf_attempt" && mv -f "$XCAT_FETCH_STAGING/dest/$XCAT_FETCH_FILE" "$XCAT_FETCH_DEST"; then
xcat_uftp_cleanup
trap - INT TERM HUP
xcat_fetch_log info "Received $XCAT_FETCH_FILE over UFTP"
return 0
fi
xcat_uftp_cleanup
_xf_attempt=$((_xf_attempt + 1))
done
trap - INT TERM HUP
return 1
}
@@ -8,6 +8,16 @@ dracut_install ethtool
inst "$moddir/xcat-updateflag" "/tmp/updateflag"
inst "$moddir/xcatroot" "/sbin/xcatroot"
inst_hook cmdline 10 "$moddir/xcat-cmdline.sh"
# genimage adds the UFTP helper only when the image has a UFTP 5 receiver.
if [ -r "$moddir/xcat-rootimg-fetch.sh" ]; then
inst "$moddir/xcat-rootimg-fetch.sh" "/lib/xcat-rootimg-fetch.sh"
for uftpd in /usr/sbin/uftpd /usr/bin/uftpd; do
if [ -x "$uftpd" ]; then
dracut_install "$uftpd"
break
fi
done
fi
for file in /etc/udev/rules.d/*;do
grep -qi xcat $file && inst $file $file
done
@@ -52,7 +52,23 @@ rootimg_matches_checksum() {
return 1
}
# Downloads the image at $1 to the file $2, over UFTP the first time when nodeset asks for it.
rootimg_download() {
if [ -n "$rootimg_uftp" ]; then
rootimg_uftp_port=$rootimg_uftp
rootimg_uftp=
# nodeset asks for UFTP only with the checksum, which the caller checks on the received file.
if [ "$rootimgchecksum" = yes ] && [ -r /lib/xcat-rootimg-fetch.sh ] && . /lib/xcat-rootimg-fetch.sh &&
xcat_uftp_rootimg "$1" "$2" /tmp/rootimg.sha256.before "$rootimg_uftp_port" "$(getarg xcat.uftpfp=)"; then
return 0
fi
logger $SYSLOGHOST -t $log_label -p local4.warning "Cannot receive $1 over UFTP, downloading it over HTTP..."
fi
wget -nv "$1"
}
rootimgchecksum="$(getarg rootimgchecksum=)"
rootimg_uftp="$(getarg xcat.uftp=)"
imgurl="$(getarg imgurl=)";
if [ ! -z "$imgurl" ]; then
@@ -69,7 +85,7 @@ if [ ! -z "$imgurl" ]; then
[ "$xcatdebugmode" > "0" ] && logger -t $log_label -p local4.debug "Downloading $imgurl..."
echo Getting $imgurl...
[ "$rootimgchecksum" != yes ] || rootimg_fetch_checksum "$imgurl" /tmp/rootimg.sha256.before || rm -f /tmp/rootimg.sha256.before
if ! wget -nv $imgurl; then
if ! rootimg_download "$imgurl" "$FILENAME"; then
logger $SYSLOGHOST -t $log_label -p local4.error "Downloading $imgurl FAILED, retrying..."
rm -f $FILENAME
echo Failed to get the image, waiting for next retrying...
@@ -8,6 +8,16 @@ dracut_install ethtool
inst "$moddir/xcat-updateflag" "/tmp/updateflag"
inst "$moddir/xcatroot" "/sbin/xcatroot"
inst_hook cmdline 10 "$moddir/xcat-cmdline.sh"
# genimage adds the UFTP helper only when the image has a UFTP 5 receiver.
if [ -r "$moddir/xcat-rootimg-fetch.sh" ]; then
inst "$moddir/xcat-rootimg-fetch.sh" "/lib/xcat-rootimg-fetch.sh"
for uftpd in /usr/sbin/uftpd /usr/bin/uftpd; do
if [ -x "$uftpd" ]; then
dracut_install "$uftpd"
break
fi
done
fi
for file in /etc/udev/rules.d/*;do
grep -qi xcat $file && inst $file $file
done
@@ -53,7 +53,23 @@ rootimg_matches_checksum() {
return 1
}
# Downloads the image at $1 to the file $2, over UFTP the first time when nodeset asks for it.
rootimg_download() {
if [ -n "$rootimg_uftp" ]; then
rootimg_uftp_port=$rootimg_uftp
rootimg_uftp=
# nodeset asks for UFTP only with the checksum, which the caller checks on the received file.
if [ "$rootimgchecksum" = yes ] && [ -r /lib/xcat-rootimg-fetch.sh ] && . /lib/xcat-rootimg-fetch.sh &&
xcat_uftp_rootimg "$1" "$2" /tmp/rootimg.sha256.before "$rootimg_uftp_port" "$(getarg xcat.uftpfp=)"; then
return 0
fi
logger $SYSLOGHOST -t $log_label -p local4.warning "Cannot receive $1 over UFTP, downloading it over HTTP..."
fi
curl --fail "$1" -o "$2"
}
rootimgchecksum="$(getarg rootimgchecksum=)"
rootimg_uftp="$(getarg xcat.uftp=)"
imgurl="$(getarg imgurl=)";
if [ ! -z "$imgurl" ]; then
@@ -70,7 +86,7 @@ if [ ! -z "$imgurl" ]; then
[ "$xcatdebugmode" > "0" ] && logger -t $log_label -p local4.debug "Downloading $imgurl..."
echo Getting $imgurl...
[ "$rootimgchecksum" != yes ] || rootimg_fetch_checksum "$imgurl" /tmp/rootimg.sha256.before || rm -f /tmp/rootimg.sha256.before
if ! curl --fail $imgurl -o $FILENAME; then
if ! rootimg_download "$imgurl" "$FILENAME"; then
logger $SYSLOGHOST -t $log_label -p local4.error "Downloading $imgurl FAILED, retrying..."
rm -f $FILENAME
echo Failed to get the image, waiting for next retrying...
@@ -28,6 +28,17 @@ install() {
# cmdline hook
inst_hook cmdline 10 "$moddir/xcat-cmdline.sh"
# genimage adds the UFTP helper only when the image has a UFTP 5 receiver.
if [ -r "$moddir/xcat-rootimg-fetch.sh" ]; then
inst_simple "$moddir/xcat-rootimg-fetch.sh" "/lib/xcat-rootimg-fetch.sh"
for uftpd in /usr/sbin/uftpd /usr/bin/uftpd; do
if [ -x "$uftpd" ]; then
dracut_install "$uftpd"
break
fi
done
fi
# udev rules with "xcat"
for file in /etc/udev/rules.d/*; do
if grep -qi xcat "$file"; then
@@ -53,7 +53,23 @@ rootimg_matches_checksum() {
return 1
}
# Downloads the image at $1 to the file $2, over UFTP the first time when nodeset asks for it.
rootimg_download() {
if [ -n "$rootimg_uftp" ]; then
rootimg_uftp_port=$rootimg_uftp
rootimg_uftp=
# nodeset asks for UFTP only with the checksum, which the caller checks on the received file.
if [ "$rootimgchecksum" = yes ] && [ -r /lib/xcat-rootimg-fetch.sh ] && . /lib/xcat-rootimg-fetch.sh &&
xcat_uftp_rootimg "$1" "$2" /tmp/rootimg.sha256.before "$rootimg_uftp_port" "$(getarg xcat.uftpfp=)"; then
return 0
fi
logger $SYSLOGHOST -t $log_label -p local4.warning "Cannot receive $1 over UFTP, downloading it over HTTP..."
fi
curl --fail "$1" -o "$2"
}
rootimgchecksum="$(getarg rootimgchecksum=)"
rootimg_uftp="$(getarg xcat.uftp=)"
imgurl="$(getarg imgurl=)";
if [ ! -z "$imgurl" ]; then
@@ -70,7 +86,7 @@ if [ ! -z "$imgurl" ]; then
[ "$xcatdebugmode" > "0" ] && logger -t $log_label -p local4.debug "Downloading $imgurl..."
echo Getting $imgurl...
[ "$rootimgchecksum" != yes ] || rootimg_fetch_checksum "$imgurl" /tmp/rootimg.sha256.before || rm -f /tmp/rootimg.sha256.before
if ! curl --fail $imgurl -o $FILENAME; then
if ! rootimg_download "$imgurl" "$FILENAME"; then
logger $SYSLOGHOST -t $log_label -p local4.error "Downloading $imgurl FAILED, retrying..."
rm -f $FILENAME
echo Failed to get the image, waiting for next retrying...
+18 -2
View File
@@ -1085,6 +1085,10 @@ sub mkinitrd_dracut {
my $dracutmpath = $dracutmoduledir . "98xcat/";
mkpath($dracutmpath);
# Only a stateless initial ramdisk that checks the root image receives it over UFTP.
my $uftp_receiver = eval { imgutils::add_uftp_receiver($rootimg_dir, $arch, $dracutmpath, $mode eq 'stateless' && $dracutver >= "033") };
xdie($@) if $@;
if ($dracutver >= "105") {
# Copy xcat module to the image depending on the mode
my %files = (
@@ -1299,6 +1303,7 @@ sub mkinitrd_dracut {
!system("chroot $rootimg_dir dracut $additional_options -f /tmp/initrd.$$.gz $kernelver")
or xdie("Error: failed to generate the initial ramdisk for $mode.\n");
umount_chroot($rootimg_dir);
eval { imgutils::check_uftp_initrd($rootimg_dir, "/tmp/initrd.$$.gz", $mode) } or xdie($@) if $uftp_receiver;
if ($native_bootdir) {
eval {
imgutils::publish_boot_files($native_bootdir, $destdir,
@@ -1306,11 +1311,15 @@ sub mkinitrd_dracut {
} or xdie($@);
print "the initial ramdisk for $mode is generated successfully.\n";
mark_rootimg_check($mode);
mark_uftp_receiver($mode, $uftp_receiver);
return;
}
print "the initial ramdisk for $mode is generated successfully.\n";
print("Move $rootimg_dir/tmp/initrd.$$.gz -> $destdir/initrd-$mode.gz");
move("$rootimg_dir/tmp/initrd.$$.gz", "$destdir/initrd-$mode.gz") and mark_rootimg_check($mode);
print("Move $rootimg_dir/tmp/initrd.$$.gz -> $destdir/initrd-$mode.gz\n");
if (move("$rootimg_dir/tmp/initrd.$$.gz", "$destdir/initrd-$mode.gz")) {
mark_rootimg_check($mode);
mark_uftp_receiver($mode, $uftp_receiver);
}
}
# nodeset warns unless this marker says that the stateless initial ramdisk checks the root image checksum.
@@ -1322,6 +1331,13 @@ sub mark_rootimg_check {
print "Warning: $error\n" if $error;
}
# nodeset tells a node to receive the root image over UFTP only when this marker matches its initial ramdisk.
sub mark_uftp_receiver {
my ($mode, $receiver) = @_;
return unless $mode eq 'stateless';
imgutils::mark_uftp_receiver("$destdir/initrd-$mode.gz", $receiver);
}
sub mkinitrd {
my ($mode) = @_; # statelite or stateless
@@ -7,3 +7,13 @@ dracut_install parted mke2fs bc mkswap swapon chmod mkfs mkfs.ext4 mkfs.xfs xfs_
inst "$moddir/xcat-updateflag" "/tmp/updateflag"
inst "$moddir/xcatroot" "/sbin/xcatroot"
inst_hook cmdline 10 "$moddir/xcat-cmdline.sh"
# genimage adds the UFTP helper only when the image has a UFTP 5 receiver.
if [ -r "$moddir/xcat-rootimg-fetch.sh" ]; then
inst "$moddir/xcat-rootimg-fetch.sh" "/lib/xcat-rootimg-fetch.sh"
for uftpd in /usr/sbin/uftpd /usr/bin/uftpd; do
if [ -x "$uftpd" ]; then
dracut_install "$uftpd"
break
fi
done
fi
@@ -57,7 +57,23 @@ rootimg_matches_checksum() {
return 1
}
# Downloads the image at $1 to the file $2, over UFTP the first time when nodeset asks for it.
rootimg_download() {
if [ -n "$rootimg_uftp" ]; then
rootimg_uftp_port=$rootimg_uftp
rootimg_uftp=
# nodeset asks for UFTP only with the checksum, which the caller checks on the received file.
if [ "$rootimgchecksum" = yes ] && [ -r /lib/xcat-rootimg-fetch.sh ] && . /lib/xcat-rootimg-fetch.sh &&
xcat_uftp_rootimg "$1" "$2" /tmp/rootimg.sha256.before "$rootimg_uftp_port" "$(getarg xcat.uftpfp=)"; then
return 0
fi
logger $SYSLOGHOST -t $log_label -p local4.warning "Cannot receive $1 over UFTP, downloading it over HTTP..."
fi
wget -nv "$1"
}
rootimgchecksum="$(getarg rootimgchecksum=)"
rootimg_uftp="$(getarg xcat.uftp=)"
if [ ! -z "$imgurl" ]; then
if [ xhttp = x${imgurl%%:*} ] || [ xhttps = x${imgurl%%:*} ]; then
@@ -73,7 +89,7 @@ if [ ! -z "$imgurl" ]; then
[ "$xcatdebugmode" > "0" ] && logger -t $log_label -p debug "downloading $imgurl...."
echo Getting $imgurl...
[ "$rootimgchecksum" != yes ] || rootimg_fetch_checksum "$imgurl" /tmp/rootimg.sha256.before || rm -f /tmp/rootimg.sha256.before
if ! wget -nv $imgurl; then
if ! rootimg_download "$imgurl" "$FILENAME"; then
[ "$xcatdebugmode" > "0" ] && logger -t $log_label -p debug "downloading $imgurl failed,retrying...."
rm -f $FILENAME
sleep 27
+9 -1
View File
@@ -1057,6 +1057,10 @@ sub mkinitrd_dracut {
my $dracutmpath = $dracutmoduledir . "97xcat/";
mkpath($dracutmpath);
# Only a stateless initial ramdisk that checks the root image receives it over UFTP.
my $uftp_receiver = eval { imgutils::add_uftp_receiver($rootimg_dir, $arch, $dracutmpath, $mode eq 'stateless' && $dracutdir eq 'dracut_033') };
xdie($@) if $@;
my $perm = (stat("$fullpath/$dracutdir/check"))[2];
cp("$fullpath/$dracutdir/check", $dracutmpath);
chmod($perm & 07777, "$dracutmpath/check");
@@ -1177,8 +1181,12 @@ sub mkinitrd_dracut {
print "\nchroot $rootimg_dir dracut $additional_options -f /tmp/initrd.$$.gz $kernelver\n";
!system("chroot $rootimg_dir dracut $additional_options -f /tmp/initrd.$$.gz $kernelver")
or die("Error: failed to generate the initial ramdisk for $mode.\n");
eval { imgutils::check_uftp_initrd($rootimg_dir, "/tmp/initrd.$$.gz", $mode) } or xdie($@) if $uftp_receiver;
print "the initial ramdisk for $mode is generated successfully.\n";
move("$rootimg_dir/tmp/initrd.$$.gz", "$destdir/initrd-$mode.gz") and mark_rootimg_check($mode);
if (move("$rootimg_dir/tmp/initrd.$$.gz", "$destdir/initrd-$mode.gz")) {
mark_rootimg_check($mode);
imgutils::mark_uftp_receiver("$destdir/initrd-$mode.gz", $uftp_receiver) if $mode eq 'stateless';
}
}
@@ -5,6 +5,16 @@ dracut_install grep hostname awk egrep dirname expr ip sed tr cat
dracut_install mount.nfs
inst "$moddir/xcatroot" "/sbin/xcatroot"
inst_hook cmdline 10 "$moddir/xcat-cmdline.sh"
# genimage adds the UFTP helper only when the image has a UFTP 5 receiver.
if [ -r "$moddir/xcat-rootimg-fetch.sh" ]; then
inst "$moddir/xcat-rootimg-fetch.sh" "/lib/xcat-rootimg-fetch.sh"
for uftpd in /usr/sbin/uftpd /usr/bin/uftpd; do
if [ -x "$uftpd" ]; then
dracut_install "$uftpd"
break
fi
done
fi
# install netroot dispatcher if available (dracut 051+ with network-legacy)
if [ -f "$moddir/../40network/netroot.sh" ]; then
inst "$moddir/../40network/netroot.sh" "/sbin/netroot"
@@ -24,6 +24,25 @@ rootimg_matches_checksum() {
return 1
}
# Downloads the image at $1 to the file $2, over UFTP the first time when nodeset asks for it.
rootimg_download() {
if [ -n "$rootimg_uftp" ]; then
rootimg_uftp_port=$rootimg_uftp
rootimg_uftp=
# nodeset asks for UFTP only with the checksum, which the caller checks on the received file.
if [ "$rootimgchecksum" = yes ] && [ -r /lib/xcat-rootimg-fetch.sh ] && . /lib/xcat-rootimg-fetch.sh &&
xcat_uftp_rootimg "$1" "$2" /tmp/rootimg.sha256.before "$rootimg_uftp_port" "$rootimg_uftp_fp"; then
return 0
fi
echo "Cannot receive $1 over UFTP, downloading it over HTTP..."
fi
wget "$1"
}
# The kernel does not pass arguments with a dot to init as variables, so getarg reads them.
rootimg_uftp=$(. /lib/dracut-lib.sh && getarg xcat.uftp=)
rootimg_uftp_fp=$(. /lib/dracut-lib.sh && getarg xcat.uftpfp=)
if [ ! -z "$imgurl" ]; then
if [ xhttp = x${imgurl%%:*} ] || [ xhttps = x${imgurl%%:*} ]; then
NFS=0
@@ -36,7 +55,7 @@ if [ ! -z "$imgurl" ]; then
while [ ! -r "$FILENAME" ]; do
echo Getting $imgurl...
[ "$rootimgchecksum" != yes ] || rootimg_fetch_checksum "$imgurl" /tmp/rootimg.sha256.before || rm -f /tmp/rootimg.sha256.before
if ! wget $imgurl; then
if ! rootimg_download "$imgurl" "$FILENAME"; then
rm -f $FILENAME
sleep 27
elif [ "$rootimgchecksum" = yes ] && ! rootimg_check=$(rootimg_matches_checksum "$imgurl" "$FILENAME"); then
@@ -981,6 +981,10 @@ sub mkinitrd_dracut {
}
mkpath($dracutmpath);
# Only a stateless initial ramdisk that checks the root image receives it over UFTP.
my $uftp_receiver = eval { imgutils::add_uftp_receiver($rootimg_dir, $arch, $dracutmpath, $mode eq 'stateless') };
xdie($@) if $@;
my $perm = (stat("$fullpath/$dracutdir/check"))[2];
cp("$fullpath/$dracutdir/check", $dracutmpath);
chmod($perm & 07777, "$dracutmpath/check");
@@ -1055,8 +1059,12 @@ sub mkinitrd_dracut {
!system("chroot $rootimg_dir dracut -f /tmp/initrd.$$.gz $kernelver")
or die("Error: failed to generate the initial ramdisk for $mode.\n");
eval { imgutils::check_uftp_initrd($rootimg_dir, "/tmp/initrd.$$.gz", $mode) } or xdie($@) if $uftp_receiver;
print "the initial ramdisk for $mode is generated successfully.\n";
move("$rootimg_dir/tmp/initrd.$$.gz", "$destdir/initrd-$mode.gz") and mark_rootimg_check($mode);
if (move("$rootimg_dir/tmp/initrd.$$.gz", "$destdir/initrd-$mode.gz")) {
mark_rootimg_check($mode);
imgutils::mark_uftp_receiver("$destdir/initrd-$mode.gz", $uftp_receiver) if $mode eq 'stateless';
}
}
# nodeset warns unless this marker says that the stateless initial ramdisk checks the root image checksum.
+3
View File
@@ -385,6 +385,7 @@ cp etc/init.d/xcatd $RPM_BUILD_ROOT/%{prefix}/share/xcat/scripts/xcatd
chmod 755 $RPM_BUILD_ROOT/%{prefix}/share/xcat/scripts/xcatd
mkdir -p $RPM_BUILD_ROOT/usr/lib/systemd/system
cp etc/init.d/xcatd.service $RPM_BUILD_ROOT/usr/lib/systemd/system
cp etc/init.d/xcat-uftp-listener.service $RPM_BUILD_ROOT/usr/lib/systemd/system
%else
mkdir -p $RPM_BUILD_ROOT/etc/init.d
cp etc/init.d/xcatd $RPM_BUILD_ROOT/etc/init.d
@@ -453,6 +454,7 @@ rm -rf $RPM_BUILD_ROOT
# The compatibility helper owns runtime cleanup. Listing this path as %ghost
# would let RPM unlink preserved administrator content after %preun.
/usr/lib/systemd/system/xcatd.service
/usr/lib/systemd/system/xcat-uftp-listener.service
%else
/etc/init.d/xcatd
%endif
@@ -632,6 +634,7 @@ if [ $1 == 0 ]; then #This means only on -e
if [ -f "/proc/cmdline" ]; then # prevent running it during install into chroot image
if "$xcatd_init_compat" can-use-systemctl; then
systemctl stop xcatd.service
systemctl disable --now xcat-uftp-listener.service >/dev/null 2>&1 || true
elif ! "$xcatd_init_compat" uses-systemd --explicit-target && [ -x /etc/init.d/xcatd ]; then
/etc/init.d/xcatd stop
fi
+63
View File
@@ -0,0 +1,63 @@
#!/usr/bin/env bats
load 'helpers/shell_source'
setup()
{
[ "$(uname -s)" = Linux ] || skip 'package scriptlets run in a Linux sandbox'
bwrap=$(PATH=/usr/bin:/bin type -P bwrap) || {
echo 'Install bubblewrap to run the package removal test' >&2
return 1
}
fixture="$BATS_TEST_TMPDIR/fixture"
mkdir -p "$fixture/bin" "$fixture/opt/xcat/share/xcat/scripts"
cat >"$fixture/bin/systemctl" <<'SH'
#!/bin/sh
printf '%s\n' "$*" >>/tmp/fixture/systemctl.log
SH
cat >"$fixture/opt/xcat/share/xcat/scripts/xcatd-init-compat" <<'SH'
#!/bin/sh
printf '%s\n' "$*" >>/tmp/fixture/compat.log
exit 0
SH
chmod +x "$fixture/bin/systemctl" "$fixture/opt/xcat/share/xcat/scripts/xcatd-init-compat"
awk '/^%preun$/ { copy = 1; next } copy && !/^%/' "$(repo_path xCAT-server/xCAT-server.spec)" >"$fixture/preun"
[ -s "$fixture/preun" ]
sandbox=("$bwrap" --unshare-all --die-with-parent --new-session
--ro-bind / / --tmpfs /tmp --tmpfs /opt --tmpfs /usr/sbin --proc /proc --dev /dev
--bind "$fixture" /tmp/fixture --bind "$fixture/opt/xcat" /opt/xcat
--ro-bind "$(repo_path xCAT-server/debian/prerm)" /tmp/prerm
--setenv PATH /tmp/fixture/bin:/usr/bin:/bin --setenv RPM_INSTALL_PREFIX0 /opt/xcat)
}
@test 'RPM erase stops and disables the UFTP listener' {
run "${sandbox[@]}" /bin/bash /tmp/fixture/preun 0
[ "$status" -eq 0 ]
grep -qx 'stop xcatd.service' "$fixture/systemctl.log"
grep -qx 'disable --now xcat-uftp-listener.service' "$fixture/systemctl.log"
}
@test 'RPM upgrade leaves the UFTP listener alone' {
run "${sandbox[@]}" /bin/bash /tmp/fixture/preun 1
[ "$status" -eq 0 ]
if grep -q uftp "$fixture/systemctl.log" 2>/dev/null; then
echo 'an upgrade touched the listener'
return 1
fi
}
@test 'Debian removal stops and disables the UFTP listener' {
run "${sandbox[@]}" /bin/sh /tmp/prerm remove
[ "$status" -eq 0 ]
grep -qx 'stop xcatd.service' "$fixture/systemctl.log"
grep -qx 'disable --now xcat-uftp-listener.service' "$fixture/systemctl.log"
}
@test 'Debian upgrade leaves the UFTP listener alone' {
run "${sandbox[@]}" /bin/sh /tmp/prerm upgrade 2.19.1
[ "$status" -eq 0 ]
if grep -q uftp "$fixture/systemctl.log" 2>/dev/null; then
echo 'an upgrade touched the listener'
return 1
fi
}
+482
View File
@@ -0,0 +1,482 @@
#!/usr/bin/env bats
load 'helpers/shell_source'
FP=31:94:09:9D:B1:24:3F:32:C0:B9:1A:BB:AC:F4:74:2B:80:F9:77:B2
SERVER=192.0.2.1
NAME=rootimg.cpio.xz
URL=http://$SERVER:80//install/netboot/rhels9/x86_64/compute/$NAME
setup()
{
command -v sha256sum >/dev/null || skip 'sha256sum is required'
export FX="$BATS_TEST_TMPDIR/fx"
mkdir -p "$FX/bin" "$FX/root"
head -c 300000 /dev/urandom >"$FX/payload"
SHA=$(sha256sum "$FX/payload")
SHA=${SHA%% *}
printf '%s %s\n' "$SHA" "$NAME" >"$FX/sums"
cat >"$FX/bin/logger" <<'SH'
#!/bin/sh
printf '%s\n' "$*" >>"$FX/log"
SH
cat >"$FX/bin/curl" <<'SH'
#!/bin/sh
out=
maxtime=
for arg do
if [ "$prev" = -o ]; then out=$arg; fi
if [ "$prev" = --max-time ]; then maxtime=$arg; fi
case "$arg" in *://*) url=$arg ;; esac
prev=$arg
done
printf '%s\n' "$url" >>"$FX/curl.log"
case "$url" in
*/xcat-uftp/v1/transfer/*)
[ -r "$FX/ack" ] || exit 7
cp "$FX/ack" "$out"
;;
*/xcat-uftp/v1/job/*)
if [ -r "$FX/job_delay" ]; then
delay=$(cat "$FX/job_delay")
if [ -n "$maxtime" ] && [ "$maxtime" -lt "$delay" ]; then
sleep "$maxtime"
exit 28
fi
sleep "$delay"
fi
[ -r "$FX/job" ] || exit 22
cp "$FX/job" "$out"
;;
*) exit 22 ;;
esac
SH
cat >"$FX/bin/uftpd" <<'SH'
#!/bin/sh
echo $$ >"$FX/receiver.pid"
printf '%s\n' "$*" >"$FX/uftpd.args"
while [ $# -gt 0 ]; do
case "$1" in
-D) dest=$2; shift ;;
-T) temp=$2; shift ;;
-F) status=$2; shift ;;
-S) cp "$2" "$FX/servers"; shift ;;
-p|-M|-I|-x) shift ;;
esac
shift
done
# uftpd takes only absolute directories.
for dir in "$dest" "$temp"; do
case "$dir" in
/*) ;;
*) echo "ERROR: must specify absolute pathname for $dir" >&2; exit 1 ;;
esac
done
sleeper=
trap 'echo terminated >>"$FX/uftpd.events"; [ "$mode" != swap ] || { rm -f "$dest/rootimg.cpio.xz"; "$LN" -s "$FX/payload" "$dest/rootimg.cpio.xz"; }; [ -z "$sleeper" ] || kill "$sleeper" 2>/dev/null; exit 0' TERM
mode=$(cat "$FX/uftpd.mode" 2>/dev/null || echo deliver)
server=C0000201
case "$mode" in
exit) echo 'Error binding to interface: the receiver failed' >&2; exit 3 ;;
delayed:*) sleep "${mode#delayed:}"; mode=deliver ;;
stale) server=0A0A0A0A ;;
esac
case "$mode" in
deliver|stale|link|swap)
printf 'CONNECT;2026/10/07-21:05:38;%s;438BAE90;192.0.2.1;mn\n' "$server" >>"$status"
cp "$FX/payload" "$temp/rootimg.cpio.xz"
printf 'RESULT;2026/10/07-21:05:39;%s;438BAE90;rootimg.cpio.xz;293KB;copied\n' "$server" >>"$status"
if [ "$mode" = link ]; then
rm "$temp/rootimg.cpio.xz"
"$LN" -s "$FX/payload" "$dest/rootimg.cpio.xz"
else
mv "$temp/rootimg.cpio.xz" "$dest/rootimg.cpio.xz"
fi
;;
esac
while :; do
sleep 1 &
sleeper=$!
wait "$sleeper"
done
SH
chmod +x "$FX/bin/"*
mkdir "$FX/tools"
# The initramfs has no tail or head.
for tool in sh cat cp mv rm mkdir sleep kill; do
path=$(PATH=/usr/bin:/bin command -v "$tool") || skip "$tool is required"
ln -s "$path" "$FX/tools/$tool"
done
LN=$(PATH=/usr/bin:/bin command -v ln) || skip 'ln is required'
export LN
PATH="$FX/bin:$PATH"
export XCAT_UFTP_ATTEMPTS=1 XCAT_UFTP_GRACE=2 XCAT_UFTP_POLL=1 XCAT_UFTP_STOP_WAIT=3
. "$(repo_path xCAT-server/share/xcat/netboot/imgutils/xcat-rootimg-fetch.sh)"
}
fetch()
{
PATH="$FX/bin:$FX/tools" xcat_uftp_rootimg "${1:-$URL}" "$FX/root/$NAME" "$FX/sums" "${2-1045}" "${3-$FP}"
}
# xcatroot names the image file relative to its working directory.
fetch_relative()
{
(cd "$FX/root" && PATH="$FX/bin:$FX/tools" xcat_uftp_rootimg "$URL" "$NAME" "$FX/sums" 1045 "$FP")
}
ack()
{
printf '%s\n' status=queued job=0123456789abcdef "file=$NAME" size=300000 port=1044 \
mcast=230.4.4.1 server_id=0xC0000201 "server_ip=$SERVER" client_ip=192.0.2.50 "$@" >"$FX/ack"
}
refute_grep()
{
if grep -q -- "$1" "$2" 2>/dev/null; then
echo "unexpected '$1' in $2"
return 1
fi
}
no_leftovers()
{
[ -z "$(find "$FX/root" -name '.xcat-uftp*')" ]
if [ -r "$FX/receiver.pid" ] && kill -0 "$(cat "$FX/receiver.pid")" 2>/dev/null; then
echo 'the receiver is still running'
return 1
fi
if pgrep -f "$FX/bin/uftpd" >/dev/null; then
echo 'a receiver process survived'
return 1
fi
}
received()
{
cmp "$FX/payload" "$FX/root/$NAME"
}
not_received()
{
[ ! -e "$FX/root/$NAME" ]
}
@test 'listener URLs replace only the authority port and keep the scheme' {
run xcat_uftp_url "$URL" 1045 "$SHA"
[ "$output" = "http://$SERVER:1045/xcat-uftp/v1/transfer/$SHA/install/netboot/rhels9/x86_64/compute/$NAME" ]
run xcat_uftp_url http://10.80.0.80/install/x80/$NAME 1045 "$SHA"
[ "$output" = "http://10.80.0.80:1045/xcat-uftp/v1/transfer/$SHA/install/x80/$NAME" ]
run xcat_uftp_url http://mn.cluster:8080/install/img/$NAME 2045 "$SHA"
[ "$output" = "http://mn.cluster:2045/xcat-uftp/v1/transfer/$SHA/install/img/$NAME" ]
run xcat_uftp_url 'http://[2001:db8::1]:80/install/img/rootimg.sfs' 1045 "$SHA"
[ "$output" = "http://[2001:db8::1]:1045/xcat-uftp/v1/transfer/$SHA/install/img/rootimg.sfs" ]
run xcat_uftp_url 'http://[2001:db8::1]/install/img/rootimg.sfs' 1045 "$SHA"
[ "$output" = "http://[2001:db8::1]:1045/xcat-uftp/v1/transfer/$SHA/install/img/rootimg.sfs" ]
run xcat_uftp_url https://mn/install/img/$NAME 1045 "$SHA"
[ "$output" = "https://mn:1045/xcat-uftp/v1/transfer/$SHA/install/img/$NAME" ]
}
@test 'implicit ports follow the scheme' {
xcat_url_parse http://mn/install/x
[ "$XCAT_URL_PORT" = 80 ] && [ "$XCAT_URL_PATH" = /install/x ]
xcat_url_parse https://mn/install/x
[ "$XCAT_URL_PORT" = 443 ] && [ "$XCAT_URL_SCHEME" = https ]
xcat_url_parse 'http://[fe80::1]:8080'
[ "$XCAT_URL_HOST" = '[fe80::1]' ] && [ "$XCAT_URL_PORT" = 8080 ] && [ "$XCAT_URL_PATH" = / ]
}
@test 'malformed or ambiguous image URLs are refused' {
for bad in ftp://mn/install/x/$NAME http://user@mn/install/$NAME http://2001:db8::1/install/$NAME \
http://mn:0/install/$NAME http://mn:99999/install/$NAME http://mn:8a/install/$NAME \
http://mn/install/../etc/$NAME http://mn/install/./$NAME http://mn/install/.uftp/$NAME \
"http://mn/install/$NAME?x=1" http://mn/install/%2e%2e/$NAME 'http://[zz::1]/install/x' \
'http://[2001:db8::1/install/x' http:///install/x; do
if xcat_uftp_url "$bad" 1045 "$SHA" >/dev/null; then
echo "accepted $bad"
return 1
fi
done
}
@test 'the request names the first checksum that the checksum file lists for the image' {
other=$(printf '%064d' 7)
printf '%s rootimg.cpio.gz\n%s *%s\n%s %s\n' "$other" "$SHA" "$NAME" "$other" "$NAME" >"$FX/sums"
run xcat_listed_sha256 "$FX/sums" "$NAME"
[ "$status" -eq 0 ] && [ "$output" = "$SHA" ]
printf 'not-a-digest %s\n%s %s' "$NAME" "$SHA" "$NAME" >"$FX/sums"
run xcat_listed_sha256 "$FX/sums" "$NAME"
[ "$status" -eq 0 ] && [ "$output" = "$SHA" ]
for bad in "$(printf '%s rootimg.cpio.gz' "$SHA")" "$(printf '%s %s' "${SHA%?}" "$NAME")" \
"$(printf '%s %s' "$(printf '%s' "$SHA" | tr a-f A-F)" "$NAME")" '<html>busy</html>'; do
printf '%s\n' "$bad" >"$FX/sums"
if xcat_listed_sha256 "$FX/sums" "$NAME" >/dev/null; then
echo "accepted $bad"
return 1
fi
done
run xcat_listed_sha256 "$FX/missing" "$NAME"
[ "$status" -eq 1 ] && [ -z "$output" ]
}
@test 'a checksum file without the image is never sent to the listener' {
ack start_in=0 timeout=5
printf '%064d rootimg.cpio.gz\n' 0 >"$FX/sums"
run fetch
[ "$status" -eq 1 ]
not_received
grep -q "lists no SHA-256 checksum for $NAME" "$FX/log"
[ ! -e "$FX/curl.log" ]
}
@test 'listener replies are validated and the batch delay defaults to zero' {
ack start_in=7 timeout=60
xcat_uftp_read_ack "$FX/ack" "$NAME"
[ "$XCAT_ACK_START_IN" = 7 ] && [ "$XCAT_ACK_TIMEOUT" = 60 ]
ack timeout=60
xcat_uftp_read_ack "$FX/ack" "$NAME"
[ "$XCAT_ACK_START_IN" = 0 ]
ack start_in=0 timeout=60
xcat_uftp_read_ack "$FX/ack" "$NAME"
[ "$XCAT_ACK_START_IN" = 0 ]
for bad in 'start_in=abc timeout=60' 'start_in=-1 timeout=60' 'start_in=3601 timeout=60' \
'timeout=0' 'start_in=00 timeout=60' 'timeout=60 file=rootimg.cpio.gz' \
'timeout=60 server_id=0xc0000201' 'timeout=60 job=xyz' 'timeout=60 port=70000' \
'timeout=60 client_ip=192.0.2.256' 'timeout=60 status=refused'; do
ack $bad
if xcat_uftp_read_ack "$FX/ack" "$NAME"; then
echo "accepted $bad"
return 1
fi
done
printf 'status=queued\nnot a key value\n' >"$FX/ack"
if xcat_uftp_read_ack "$FX/ack" "$NAME"; then
echo 'accepted a line without a key'
return 1
fi
}
@test 'a UFTP transfer puts the image in place' {
ack start_in=0 timeout=5
run fetch
[ "$status" -eq 0 ]
received
no_leftovers
grep -q "Received $NAME over UFTP" "$FX/log"
[ "$(cat "$FX/curl.log")" = "http://$SERVER:1045/xcat-uftp/v1/transfer/$SHA/install/netboot/rhels9/x86_64/compute/$NAME" ]
[ "$(cat "$FX/servers")" = "0xC0000201|$SERVER|0|$FP" ]
args=" $(cat "$FX/uftpd.args") "
for option in -d -q -E '-p 1044' '-M 230.4.4.1' '-I 192.0.2.50'; do
case "$args" in
*" $option "*) ;;
*) echo "missing $option"; return 1 ;;
esac
done
grep -q -- "-D $FX/root/.xcat-uftp.[0-9]*.1/dest -T $FX/root/.xcat-uftp.[0-9]*.1/tmp" "$FX/uftpd.args"
grep -q terminated "$FX/uftpd.events"
}
@test 'an image file named relative to the working directory gets absolute receiver directories' {
ack start_in=0 timeout=5
run fetch_relative
[ "$status" -eq 0 ]
received
no_leftovers
grep -q -- "-D $FX/root/.xcat-uftp.[0-9]*.1/dest -T $FX/root/.xcat-uftp.[0-9]*.1/tmp" "$FX/uftpd.args"
}
@test 'completion lines from another server or a stale log do not count' {
ack start_in=0 timeout=1
echo stale >"$FX/uftpd.mode"
mkdir -p "$FX/root/tmp"
printf 'File transfer complete\nRESULT;x;C0000201;1;%s;1KB;copied\n' "$NAME" >"$FX/root/tmp/uftpd.log"
run fetch
[ "$status" -eq 1 ]
grep -q 'did not finish in 3s' "$FX/log"
not_received
no_leftovers
}
@test 'a symbolic link that the receiver delivers is never put in place of the image' {
ack start_in=0 timeout=5
echo link >"$FX/uftpd.mode"
run fetch
[ "$status" -eq 1 ]
[ ! -L "$FX/root/$NAME" ]
not_received
no_leftovers
grep -q "$NAME from UFTP is not a regular file" "$FX/log"
}
@test 'a symbolic link that replaces the image while the receiver stops is never put in place' {
ack start_in=0 timeout=5
echo swap >"$FX/uftpd.mode"
run fetch
[ "$status" -eq 1 ]
[ ! -L "$FX/root/$NAME" ]
not_received
no_leftovers
grep -q terminated "$FX/uftpd.events"
grep -q "$NAME from UFTP is not a regular file" "$FX/log"
}
@test 'a missing receiver sends no request' {
rm "$FX/bin/uftpd"
ack start_in=0 timeout=5
run fetch
[ "$status" -eq 1 ]
not_received
grep -q 'uftpd is not in the initramfs' "$FX/log"
[ ! -e "$FX/curl.log" ]
}
@test 'an unavailable listener is asked once for each attempt' {
XCAT_UFTP_ATTEMPTS=2
run fetch
[ "$status" -eq 1 ]
not_received
no_leftovers
grep -q 'did not accept the request' "$FX/log"
[ "$(grep -c /xcat-uftp/v1/transfer/ "$FX/curl.log")" -eq 2 ]
}
@test 'a sender failure reported by the server ends the wait early' {
ack start_in=0 timeout=60
echo hang >"$FX/uftpd.mode"
printf 'state=failed\ntimeout=60\nreason=uftp exited with status 7\n' >"$FX/job"
start=$(date +%s)
run fetch
[ "$status" -eq 1 ]
[ $(( $(date +%s) - start )) -lt 20 ]
grep -q 'failed on the server: uftp exited with status 7' "$FX/log"
not_received
no_leftovers
}
@test 'a stalled status request cannot extend the transfer deadline' {
ack start_in=0 timeout=2
echo hang >"$FX/uftpd.mode"
echo 30 >"$FX/job_delay"
printf 'state=running\ntimeout=2\n' >"$FX/job"
start=$(date +%s)
run fetch
[ "$status" -eq 1 ]
[ $(( $(date +%s) - start )) -lt 12 ]
grep -q 'did not finish in 4s' "$FX/log"
not_received
no_leftovers
}
@test 'a trickling wget status reply cannot extend the transfer deadline' {
rm "$FX/bin/curl"
cat >"$FX/bin/wget" <<'SH'
#!/bin/sh
out=
for arg do
if [ "$prev" = -O ]; then out=$arg; fi
case "$arg" in *://*) url=$arg ;; esac
prev=$arg
done
printf '%s\n' "$url" >>"$FX/curl.log"
case "$url" in
*/xcat-uftp/v1/transfer/*) cp "$FX/ack" "$out" ;;
*/xcat-uftp/v1/job/*)
while :; do
printf x >>"$out"
sleep 1
done
;;
*) exit 8 ;;
esac
SH
chmod +x "$FX/bin/wget"
ack start_in=0 timeout=2
echo hang >"$FX/uftpd.mode"
start=$(date +%s)
run fetch
[ "$status" -eq 1 ]
[ $(( $(date +%s) - start )) -lt 15 ]
grep -q 'did not finish in 4s' "$FX/log"
not_received
no_leftovers
if pgrep -f "$FX/bin/wget" >/dev/null; then
echo 'a wget request survived'
return 1
fi
}
@test 'a receiver that exits is detected' {
ack start_in=0 timeout=60
echo exit >"$FX/uftpd.mode"
run fetch
[ "$status" -eq 1 ]
grep -q 'UFTP receiver stopped: .*the receiver failed' "$FX/log"
not_received
no_leftovers
}
@test 'a transfer that does not finish in time stops its receiver' {
ack start_in=0 timeout=1
echo hang >"$FX/uftpd.mode"
run fetch
[ "$status" -eq 1 ]
grep -q 'did not finish in 3s' "$FX/log"
grep -q terminated "$FX/uftpd.events"
not_received
no_leftovers
}
@test 'a pending job extends the wait within the server deadline' {
ack start_in=0 timeout=3
echo delayed:6 >"$FX/uftpd.mode"
printf 'state=pending\nstart_in=3\ntimeout=3\n' >"$FX/job"
run fetch
[ "$status" -eq 0 ]
grep -q "Received $NAME over UFTP" "$FX/log"
received
}
@test 'an interrupted transfer stops its receiver and removes its files' {
ack start_in=0 timeout=60
echo hang >"$FX/uftpd.mode"
helper=$(repo_path xCAT-server/share/xcat/netboot/imgutils/xcat-rootimg-fetch.sh)
PATH="$FX/bin:$FX/tools" sh -c '. "$1"; xcat_uftp_rootimg "$2" "$3" "$4" 1045 "$5"' fetch "$helper" "$URL" \
"$FX/root/$NAME" "$FX/sums" "$FP" >"$FX/interrupted.out" 2>&1 &
fetcher=$!
for _ in $(seq 50); do
[ -s "$FX/receiver.pid" ] && break
sleep 0.1
done
[ -s "$FX/receiver.pid" ]
sleep 1
kill -TERM "$fetcher"
wait "$fetcher" || rc=$?
[ "${rc:-0}" -eq 130 ]
grep -q 'interrupted' "$FX/log"
not_received
no_leftovers
}
@test 'an HTTPS image URL is never sent to the listener over HTTP' {
ack start_in=0 timeout=5
run fetch "https://mn/install/netboot/img/$NAME"
[ "$status" -eq 1 ]
not_received
[ ! -e "$FX/curl.log" ]
grep -q 'UFTP is used only with http image URLs' "$FX/log"
}
@test 'malformed kernel arguments send no request' {
ack start_in=0 timeout=5
for args in '70000 '"$FP" "1045 ${FP%:*}" '1045 31:94:09:9d:B1:24:3F:32:C0:B9:1A:BB:AC:F4:74:2B:80:F9:77:B2' "0 $FP"; do
set -- $args
run fetch "$URL" "$1" "$2"
[ "$status" -eq 1 ]
done
run fetch "$URL" '' "$FP"
[ "$status" -eq 1 ]
not_received
[ "$(grep -c 'Ignoring malformed xcat.uftp' "$FX/log")" -eq 5 ]
[ ! -e "$FX/curl.log" ]
}
+97 -9
View File
@@ -3,6 +3,20 @@
load 'helpers/shell_source'
URL=http://192.0.2.1:80//install/netboot/img/rootimg.cpio.gz
FP=31:94:09:9D:B1:24:3F:32:C0:B9:1A:BB:AC:F4:74:2B:80:F9:77:B2
ALL_SCRIPTS='xCAT-server/share/xcat/netboot/rh/dracut_033/xcatroot bash
xCAT-server/share/xcat/netboot/rh/dracut_047/xcatroot bash
xCAT-server/share/xcat/netboot/rh/dracut_105/stateless/xcatroot bash
xCAT-server/share/xcat/netboot/sles/dracut_033/xcatroot bash
xCAT-server/share/xcat/netboot/ubuntu/dracut/xcatroot dash'
# The scripts that receive the root image over UFTP when nodeset asks for it.
UFTP_SCRIPTS='xCAT-server/share/xcat/netboot/rh/dracut_033/xcatroot bash
xCAT-server/share/xcat/netboot/rh/dracut_047/xcatroot bash
xCAT-server/share/xcat/netboot/rh/dracut_105/stateless/xcatroot bash
xCAT-server/share/xcat/netboot/sles/dracut_033/xcatroot bash
xCAT-server/share/xcat/netboot/ubuntu/dracut/xcatroot dash'
setup()
{
@@ -25,6 +39,12 @@ setup()
printf 'packed image\n' >"$FX/tree/etc/marker"
(cd "$FX/tree" && find . | cpio -H newc -o 2>/dev/null | gzip -c) >"$FX/served/rootimg.cpio.gz"
(cd "$FX/served" && sha256sum rootimg.cpio.gz >rootimg.cpio.gz.sha256)
SHA=$(cat "$FX/served/rootimg.cpio.gz.sha256")
SHA=${SHA%% *}
TRANSFER=http://192.0.2.1:1045/xcat-uftp/v1/transfer/$SHA/install/netboot/img/rootimg.cpio.gz
cp "$FX/served/rootimg.cpio.gz" "$FX/payload"
printf '%s\n' status=queued job=0123456789abcdef file=rootimg.cpio.gz port=1044 mcast=230.4.4.1 \
server_id=0xC0000201 server_ip=192.0.2.1 client_ip=192.0.2.50 start_in=0 timeout=5 >"$FX/ack"
cat >"$FX/lib/dracut-lib.sh" <<'SH'
getarg() {
@@ -50,6 +70,10 @@ SH
#!/bin/sh
url=$1 out=$2
printf '%s\n' "$url" >>/fx/downloads
case "$url" in
*/xcat-uftp/v1/transfer/*) [ -f /fx/ack ] && cp /fx/ack "$out" && echo 200 || echo 503; exit ;;
*/xcat-uftp/v1/job/*) echo 404; exit ;;
esac
name=${url##*/}
case "$name" in *.sha256) plan=/fx/checksum.plan ;; *) plan=/fx/image.plan ;; esac
set -- $(cat "$plan" 2>/dev/null)
@@ -95,9 +119,37 @@ SH
for tool in logger mount; do
printf '#!/bin/sh\nprintf "%%s\\n" "$*" >>/fx/%s\n' "$tool" >"$FX/bin/$tool"
done
# uftpd delivers /fx/payload in a session with the server that the listener named.
cat >"$FX/bin/uftpd" <<'SH'
#!/bin/sh
while [ $# -gt 0 ]; do
case "$1" in
-D) dest=$2; shift ;;
-T) temp=$2; shift ;;
-F) status=$2; shift ;;
-p|-M|-I|-x|-S) shift ;;
esac
shift
done
# uftpd takes only absolute directories.
for dir in "$dest" "$temp"; do
case "$dir" in
/*) ;;
*) echo "ERROR: must specify absolute pathname for $dir" >&2; exit 1 ;;
esac
done
printf 'CONNECT;t;C0000201;1;192.0.2.1;mn\n' >>"$status"
cp /fx/payload "$temp/rootimg.cpio.gz"
printf 'RESULT;t;C0000201;1;rootimg.cpio.gz;1KB;copied\n' >>"$status"
mv "$temp/rootimg.cpio.gz" "$dest/rootimg.cpio.gz"
trap 'exit 0' TERM
while :; do sleep 1 & wait $!; done
SH
# A node retries for ever, so the stub stops xcatroot after /fx/maxwaits waits.
# The UFTP receiver and its helper poll every second, which really waits.
cat >"$FX/bin/sleep" <<'SH'
#!/bin/sh
[ "$*" != 1 ] || exec /usr/bin/sleep 1
printf '%s\n' "$*" >>/fx/sleep
[ "$(grep -c . /fx/sleep)" -lt "$(cat /fx/maxwaits)" ] || kill -TERM "$PPID"
SH
@@ -115,12 +167,14 @@ run_xcatroot()
--bind "$FX" /fx --ro-bind "$FX/lib/dracut-lib.sh" /lib/dracut-lib.sh
--ro-bind "$(repo_path "$script")" /xcatroot --chdir /
--setenv PATH /fx/bin:/usr/bin:/bin --setenv imgurl "$URL" --setenv XCAT 192.0.2.1:3001
--setenv rootimgchecksum "${CMDLINE#rootimgchecksum=}" "$@")
--setenv rootimgchecksum "${CMDLINE#rootimgchecksum=}" --setenv XCAT_UFTP_POLL 60 "$@")
[ ! -e /usr/lib64 ] || sandbox+=(--symlink usr/lib64 /lib64)
for multiarch in /usr/lib/*-linux-gnu; do
[ ! -d "$multiarch" ] || sandbox+=(--ro-bind "$multiarch" "/lib/${multiarch##*/}")
done
echo "imgurl=$URL XCAT=192.0.2.1:3001 nonodestatus $CMDLINE" >"$FX/cmdline"
# genimage puts the helper in every initial ramdisk that can receive the root image over UFTP.
[ -n "$NO_UFTP_HELPER" ] || sandbox+=(--ro-bind "$(repo_path xCAT-server/share/xcat/netboot/imgutils/xcat-rootimg-fetch.sh)" /lib/xcat-rootimg-fetch.sh)
echo "imgurl=$URL XCAT=192.0.2.1:3001 nonodestatus $CMDLINE $UFTP" >"$FX/cmdline"
echo "$IMAGE_PLAN" >"$FX/image.plan"
echo "$CHECKSUM_PLAN" >"$FX/checksum.plan"
echo "${MAXWAITS:-100}" >"$FX/maxwaits"
@@ -144,13 +198,7 @@ check_each_script()
[ "$(cat "$FX/sleep" 2>/dev/null | grep -c .)" = "$waits" ] || { echo "$script waited: $(cat "$FX/sleep" 2>/dev/null)"; return 1; }
case "$script" in */ubuntu/*) log="$FX/console" ;; *) log="$FX/logger" ;; esac
[ -z "$message" ] || grep -q -- "$message" "$log" || { echo "$script did not log: $message"; return 1; }
done <<SCRIPTS
xCAT-server/share/xcat/netboot/rh/dracut_033/xcatroot bash
xCAT-server/share/xcat/netboot/rh/dracut_047/xcatroot bash
xCAT-server/share/xcat/netboot/rh/dracut_105/stateless/xcatroot bash
xCAT-server/share/xcat/netboot/sles/dracut_033/xcatroot bash
xCAT-server/share/xcat/netboot/ubuntu/dracut/xcatroot dash
SCRIPTS
done <<<"${SCRIPTS:-$ALL_SCRIPTS}"
}
@test 'an image that matches its published checksum is extracted' {
@@ -221,3 +269,43 @@ SCRIPTS
CMDLINE=rootimgchecksum=no
check_each_script 'packed image' "$URL " 0 'did not ask for a check of rootimg.cpio.gz, so it is not checked'
}
@test 'an image received over UFTP that matches its checksum is extracted without an HTTP download' {
SCRIPTS=$UFTP_SCRIPTS UFTP="xcat.uftp=1045 xcat.uftpfp=$FP"
check_each_script 'packed image' "$URL.sha256 $TRANSFER $URL.sha256 " 0 'Received rootimg.cpio.gz over UFTP'
}
@test 'an image received over UFTP that does not match its checksum is downloaded again over HTTP' {
head -c 100 "$FX/served/rootimg.cpio.gz" >"$FX/payload"
SCRIPTS=$UFTP_SCRIPTS UFTP="xcat.uftp=1045 xcat.uftpfp=$FP"
check_each_script 'packed image' "$URL.sha256 $TRANSFER $URL.sha256 $URL.sha256 $URL $URL.sha256 " 1 'rootimg.cpio.gz does not match'
}
@test 'an image received over UFTP while packimage replaces it matches the checksum file fetched before the request' {
CHECKSUM_PLAN='ok other'
SCRIPTS=$UFTP_SCRIPTS UFTP="xcat.uftp=1045 xcat.uftpfp=$FP"
check_each_script 'packed image' "$URL.sha256 $TRANSFER $URL.sha256 " 0 'Received rootimg.cpio.gz over UFTP'
}
@test 'an unavailable listener is followed by an HTTP download without a wait' {
rm "$FX/ack"
SCRIPTS=$UFTP_SCRIPTS UFTP="xcat.uftp=1045 xcat.uftpfp=$FP"
check_each_script 'packed image' "$URL.sha256 $TRANSFER $TRANSFER $URL $URL.sha256 " 0 'Cannot receive .*rootimg.cpio.gz over UFTP'
}
@test 'a checksum file that cannot be downloaded before the request is followed by an HTTP download' {
CHECKSUM_PLAN=503
SCRIPTS=$UFTP_SCRIPTS UFTP="xcat.uftp=1045 xcat.uftpfp=$FP"
check_each_script 'packed image' "$URL.sha256 $URL $URL.sha256 " 0 'lists no SHA-256 checksum for rootimg.cpio.gz'
}
@test 'an initial ramdisk without the UFTP helper downloads the image over HTTP' {
NO_UFTP_HELPER=1 SCRIPTS=$UFTP_SCRIPTS UFTP="xcat.uftp=1045 xcat.uftpfp=$FP"
check_each_script 'packed image' "$URL.sha256 $URL $URL.sha256 " 0 'Cannot receive .*rootimg.cpio.gz over UFTP'
}
@test 'UFTP is not used for an image that is not checked' {
rm "$FX/served/rootimg.cpio.gz.sha256"
CMDLINE= SCRIPTS=$UFTP_SCRIPTS UFTP="xcat.uftp=1045 xcat.uftpfp=$FP"
check_each_script 'packed image' "$URL " 0 'Cannot receive .*rootimg.cpio.gz over UFTP'
}
+16
View File
@@ -144,6 +144,22 @@ Compressor and ctorrent fixtures record what nodes can download during the
pack, or fail on request. The image lock and the ilitefile query are test
doubles. This case does not boot the image.
The pull-request workflow runs the UFTP listener case explicitly. It starts the
checkout's `xcat-uftp-listener` on loopback addresses and sends it requests
over TCP. It needs Bubblewrap with unprivileged user namespaces, GNU timeout,
the loopback address 127.0.0.2, and the Perl modules of the unit suite,
including DBD::SQLite and Capture::Tiny. Run it as an ordinary user:
```
prove xCAT-test/native/uftp_listener.t
```
Fixtures replace `uftp` and `uftp_keymgt`. The `uftp` fixture records its
arguments, its host list and the bytes it reads from descriptor 3, or exits
or blocks on request. The case checks key creation, the runtime check on a
read-only log directory, sender processes, timeouts, shutdown and connection
limits. It does not send multicast or boot a node.
The disk, kdump, sudoer and remoteshell cases require bats-core 1.4 or newer,
Bubblewrap, GNU timeout, diffutils, Capture::Tiny, File::Slurper and sudo's
`visudo`. The remoteshell case also needs `/usr/bin/openssl` and `ssh-keygen`
+387
View File
@@ -0,0 +1,387 @@
#!/usr/bin/env perl
use strict;
use warnings;
use FindBin;
use lib "$FindBin::Bin/../lib";
use lib "$FindBin::Bin/../../perl-xCAT";
use lib "$FindBin::Bin/../../xCAT-server/lib/perl";
use Cwd qw(realpath);
use Digest::SHA qw(sha256_hex);
use File::Path qw(make_path);
use File::Slurper qw(read_binary write_binary);
use File::Temp qw(tempdir);
use IO::Select;
use IO::Socket::INET;
use POSIX qw(WNOHANG);
use Test::More;
use Time::HiRes qw(sleep time);
use XCAT::Test::File qw(repo_path);
use XCAT::Test::Process qw(run_command);
use xCAT::SvrUtils;
plan skip_all => 'the UFTP listener test binds loopback addresses and uses Linux namespaces' unless $^O eq 'linux';
my ($bwrap) = grep { -x $_ } qw(/usr/bin/bwrap /bin/bwrap);
die "The UFTP listener test requires bubblewrap\n" unless $bwrap;
die "The UFTP listener test requires GNU timeout\n" unless grep { -x $_ } qw(/usr/bin/timeout /bin/timeout);
my $probe_client = IO::Socket::INET->new(LocalAddr => '127.0.0.2', LocalPort => 0, Proto => 'tcp')
or die "The UFTP listener test cannot bind 127.0.0.2: $!\n";
close($probe_client);
my $listener = repo_path('xCAT-server/sbin/xcat-uftp-listener');
my $modules = repo_path('xCAT-server/lib/perl');
my $tmp = realpath(tempdir(CLEANUP => 1));
my $calls = "$tmp/calls";
my $root = "$tmp/install/netboot";
my $fp = '31:94:09:9D:B1:24:3F:32:C0:B9:1A:BB:AC:F4:74:2B:80:F9:77:B2';
make_path($calls, $root, "$tmp/xcatroot/lib/perl", "$tmp/log");
local $ENV{XCATROOT} = "$tmp/xcatroot";
local $ENV{FAKE_UFTP_DIR} = $calls;
# The sender fixture records its arguments, its host list and the bytes of /dev/fd/3.
write_binary("$tmp/fake-uftp", <<'EOF');
#!/usr/bin/env perl
# The listener looks for: UFTP version 5.0.4 (xCAT build 1)
use strict;
use warnings;
my $dir = $ENV{FAKE_UFTP_DIR};
my @args = @ARGV;
my %value;
for (my $i = 0; $i < $#args; $i++) {
$value{ $args[$i] } = $args[ $i + 1 ] if $args[$i] =~ /^-[A-Za-z]$/;
}
open(my $argv, '>', "$dir/$$.argv") or die;
print {$argv} join("\n", @args), "\n";
close($argv);
if (($value{'-H'} // '') =~ /^\@(.+)$/ && open(my $hosts, '<', $1)) {
local $/;
my $list = <$hosts>;
open(my $copy, '>', "$dir/$$.hosts") or die;
print {$copy} $list;
close($copy);
}
open(my $image, '<:raw', $args[-1]) or exit 66;
my $bytes = do { local $/; <$image> };
open(my $out, '>:raw', "$dir/$$.bytes") or die;
print {$out} $bytes;
close($out);
my $mode = 'exit:0';
if (open(my $control, '<', "$dir/mode")) {
$mode = <$control>;
chomp($mode);
}
exit $1 if $mode =~ /^exit:(\d+)$/;
if ($mode =~ /^block:(\d+)$/) {
sleep $1;
exit 0;
}
exit 0;
EOF
write_binary("$tmp/fake-keymgt", <<"EOF");
#!/bin/sh
if [ "\$1" = -g ]; then
printf 'fixture private key\\n' > "\$3"
exit 0
fi
[ -r "\$1" ] || exit 1
printf '%s: ECDSA, curve prime256v1, fingerprint: $fp\\n' "\$1" >&2
EOF
chmod(0755, "$tmp/fake-uftp", "$tmp/fake-keymgt");
# packimage publishes the archive with its checksum, and the listener sends only such an archive.
sub publish {
my ($dir, $content) = @_;
make_path($dir);
chmod(0755, $dir);
my ($stage, $error) = xCAT::SvrUtils->rootimg_stage($dir);
die $error unless $stage;
write_binary("$stage/rootimg.cpio.gz", $content);
chmod(0644, "$stage/rootimg.cpio.gz");
$error = xCAT::SvrUtils->write_rootimg_checksum("$stage/rootimg.cpio.gz")
// xCAT::SvrUtils->publish_rootimg("$stage/rootimg.cpio.gz", "$dir/rootimg.cpio.gz");
die $error if $error;
return sha256_hex($content);
}
sub free_port {
my $probe = IO::Socket::INET->new(LocalAddr => '127.0.0.1', LocalPort => 0, Proto => 'tcp', Listen => 1)
or die "probe: $!";
my $port = $probe->sockport();
close($probe);
return $port;
}
my $config_seq = 0;
sub config {
my (%override) = @_;
my %settings = (
listen_address => '127.0.0.1',
listen_port => free_port(),
image_roots => $root,
batch_delay => 1,
transfer_timeout => 30,
request_timeout => 2,
key_file => "$tmp/server.key",
fingerprint_file => "$tmp/server.fingerprint",
uftp => "$tmp/fake-uftp",
uftp_keymgt => "$tmp/fake-keymgt",
log_dir => "$tmp/log",
%override,
);
my $path = "$tmp/listener." . ++$config_seq . '.conf';
write_binary($path, join('', map { "$_ = $settings{$_}\n" } sort keys %settings));
return ($path, $settings{listen_port});
}
sub run_listener {
my ($config, @args) = @_;
return run_command('timeout', '20', $^X, "-I$modules", $listener, '--config', $config, @args);
}
my %running;
sub start_listener {
my ($config) = @_;
my $log = "$tmp/listener.$config_seq.log";
my $pid = fork();
die "fork: $!" unless defined($pid);
if ($pid == 0) {
open(STDOUT, '>', $log) or die;
open(STDERR, '>&', \*STDOUT) or die;
exec($^X, "-I$modules", $listener, '--config', $config) or die;
}
$running{$pid} = 1;
return ($pid, $log);
}
sub stop_listener {
my ($pid) = @_;
delete $running{$pid};
kill('TERM', $pid);
my $deadline = time() + 15;
while (time() < $deadline) {
return $? if waitpid($pid, WNOHANG) == $pid;
sleep(0.1);
}
kill('KILL', $pid);
waitpid($pid, 0);
return -1;
}
END {
for my $pid (keys %running) {
kill('KILL', $pid);
waitpid($pid, 0);
}
}
sub http {
my ($port, $raw, $client) = @_;
my $sock = IO::Socket::INET->new(
PeerAddr => '127.0.0.1',
PeerPort => $port,
LocalAddr => $client || '127.0.0.2',
Proto => 'tcp',
Timeout => 5,
) or return { code => 0, error => "$!" };
print {$sock} $raw;
my $response = '';
my $select = IO::Select->new($sock);
while ($select->can_read(5)) {
last unless sysread($sock, my $chunk, 4096);
$response .= $chunk;
}
close($sock);
my ($head, $body) = split /\r\n\r\n/, $response, 2;
my ($status) = ($head // '') =~ m{^HTTP/1\.0 (\d{3}) };
my %record = map { split /=/, $_, 2 } grep { /=/ } split /\n/, $body // '';
return { %record, code => $status // 0 };
}
sub request {
my ($port, $digest, $path, $client) = @_;
return http($port, "GET /xcat-uftp/v1/transfer/$digest$path HTTP/1.0\r\nHost: test\r\n\r\n", $client);
}
sub job_state {
my ($port, $job, $client) = @_;
return http($port, "GET /xcat-uftp/v1/job/$job HTTP/1.0\r\n\r\n", $client);
}
sub wait_state {
my ($port, $job, $client, @states) = @_;
my %want = map { $_ => 1 } @states;
my $deadline = time() + 20;
my $state;
while (time() < $deadline) {
$state = job_state($port, $job, $client);
return $state if $want{ $state->{state} // '' };
sleep(0.2);
}
return $state;
}
sub wait_port {
my ($port) = @_;
my $deadline = time() + 10;
while (time() < $deadline) {
my $sock = IO::Socket::INET->new(PeerAddr => '127.0.0.1', PeerPort => $port, Proto => 'tcp');
if ($sock) {
close($sock);
return 1;
}
sleep(0.1);
}
return 0;
}
sub first_request {
my ($port, @args) = @_;
my $deadline = time() + 10;
my $reply;
while (time() < $deadline) {
$reply = request($port, @args);
return $reply if $reply->{code};
sleep(0.05);
}
return $reply;
}
sub sender_calls {
opendir(my $dh, $calls) or return ();
my @pids = sort { $a <=> $b } map { /^(\d+)\.argv$/ ? $1 : () } readdir($dh);
closedir($dh);
return map {
my $pid = $_;
{ pid => $pid, argv => [ split /\n/, read_binary("$calls/$pid.argv") ],
hosts => -e "$calls/$pid.hosts" ? read_binary("$calls/$pid.hosts") : '',
bytes => -e "$calls/$pid.bytes" ? read_binary("$calls/$pid.bytes") : undef }
} @pids;
}
sub wait_call {
my ($hosts) = @_;
my $deadline = time() + 10;
while (time() < $deadline) {
my ($call) = grep { $_->{hosts} eq $hosts && defined($_->{bytes}) } sender_calls();
return $call if $call;
sleep(0.1);
}
return undef;
}
sub reset_calls {
unlink(glob("$calls/*"));
}
sub mode {
write_binary("$calls/mode", "$_[0]\n");
}
my ($check_config) = config();
my ($rc, $out) = run_listener($check_config, '--check');
is($rc, 1, 'the listener refuses to start without a server key');
like($out, qr/run xcat-uftp-listener --init-key/, 'the missing key error says how to create it');
($rc, $out) = run_listener($check_config, '--init-key');
is($rc, 0, 'the server key is created') or diag($out);
is((stat("$tmp/server.key"))[2] & 07777, 0600, 'the server key is readable only by its owner');
is(scalar(read_binary("$tmp/server.fingerprint")), "$fp\n", 'the fingerprint that uftp_keymgt reports is published for nodeset');
($rc, $out) = run_listener($check_config, '--check');
is($rc, 0, 'the configuration check passes') or diag($out);
like($out, qr/server key fingerprint \Q$fp\E/, 'the check reports the fingerprint of the server key');
# The service sandbox mounts every path except its LogsDirectory read-only, and the listener runs as root.
make_path("$tmp/readonly-log");
my ($readonly_config) = config(log_dir => "$tmp/readonly-log");
($rc, $out) = run_command('timeout', '20', $bwrap, '--dev-bind', '/', '/', '--ro-bind', "$tmp/readonly-log", "$tmp/readonly-log",
'--unshare-user', '--uid', '0', '--gid', '0', '--', $^X, "-I$modules", $listener, '--config', $readonly_config, '--check');
isnt($rc, 0, 'the check fails as root when log_dir is on a read-only mount');
like($out, qr/Cannot write to \Q$tmp\E\/readonly-log: Read-only file system/, 'the check names the log directory and the reason');
my $image_a = "$root/rhels9/x86_64/compute";
my $content_a = "image A generation 1\n" x 200;
my $sum_a = publish($image_a, $content_a);
my $path_a = "$image_a/rootimg.cpio.gz";
my ($main_config, $port) = config(batch_delay => 2);
my ($pid, $main_log) = start_listener($main_config);
my $first = first_request($port, $sum_a, $path_a, '127.0.0.2');
is($first->{code}, 200, 'the first connection after startup is answered as a transfer request');
is($first->{client_ip}, '127.0.0.2', 'the reply gives the address of the connection');
($rc, $out) = run_listener($main_config);
is($rc, 1, 'a second listener on an occupied port exits with an error');
like($out, qr/Cannot listen on 127\.0\.0\.1:$port: Address already in use/, 'the occupied port is reported');
is(request($port, $sum_a, $path_a, '127.0.0.3')->{job}, $first->{job}, 'a request from another node joins the job');
my $done = wait_state($port, $first->{job}, '127.0.0.2', 'done', 'failed');
is($done->{state}, 'done', 'the job completes when its sender exits');
my @sent = sender_calls();
is(scalar(@sent), 1, 'one sender serves the job');
is($sent[0]{hosts}, "127.0.0.2\n127.0.0.3\n", 'the sender reads the host list of the job');
is($sent[0]{bytes}, $content_a, 'the sender reads the published bytes from its inherited descriptor');
is_deeply([ @{ $sent[0]{argv} }[ -3 .. -1 ] ], [ '-D', 'rootimg.cpio.gz', '/dev/fd/3' ], 'the sender reads the image from descriptor 3');
like(scalar(read_binary("$tmp/log/job-$first->{job}.log")), qr/^starting: \Q$tmp\E\/fake-uftp /m, 'the job log records the sender command');
like(scalar(read_binary($main_log)), qr/job \Q$first->{job}\E: done$/m, 'the listener log reports the finished job');
reset_calls();
mode('exit:7');
my $failing = request($port, $sum_a, $path_a, '127.0.0.5');
my $failed = wait_state($port, $failing->{job}, '127.0.0.5', 'failed', 'done');
is($failed->{state}, 'failed', 'a sender failure fails the job');
is($failed->{reason}, 'uftp exited with status 7', 'the job reports the exit status of the sender');
mode('exit:0');
reset_calls();
my $idle = IO::Socket::INET->new(PeerAddr => '127.0.0.1', PeerPort => $port, LocalAddr => '127.0.0.2', Proto => 'tcp');
my $idle_started = time();
my $idle_read = sysread($idle, my $nothing, 10);
ok(defined($idle_read) && $idle_read == 0 && time() - $idle_started < 6, 'an idle connection is closed after the request timeout');
close($idle);
is(stop_listener($pid), 0, 'the listener stops cleanly');
my $image_b = "$root/rhels9/x86_64/service";
my $sum_b = publish($image_b, "image B\n" x 100);
my ($limited, $limited_port) = config(batch_delay => 0, max_transfers => 1, transfer_timeout => 3);
($pid) = start_listener($limited);
wait_port($limited_port);
mode('block:20');
my $blocking = request($limited_port, $sum_a, $path_a, '127.0.0.2');
is(wait_state($limited_port, $blocking->{job}, '127.0.0.2', 'running')->{state}, 'running', 'the transfer starts');
# A waiting job fails after transfer_timeout, so it must arrive at least a second after the running one started.
sleep(1.5);
my $waiting = request($limited_port, $sum_b, "$image_b/rootimg.cpio.gz", '127.0.0.3');
is(job_state($limited_port, $waiting->{job}, '127.0.0.3')->{state}, 'pending', 'a second image waits while the transfer limit is reached');
my $timed_out = wait_state($limited_port, $blocking->{job}, '127.0.0.2', 'failed', 'done');
like($timed_out->{reason}, qr/transfer exceeded 3s/, 'a sender that exceeds its timeout fails the job');
my $blocked_call = wait_call("127.0.0.2\n");
ok($blocked_call && !kill(0, $blocked_call->{pid}), 'the timed out sender was stopped');
is(wait_state($limited_port, $waiting->{job}, '127.0.0.3', 'running')->{state}, 'running', 'the waiting transfer starts when the slot frees');
my $held = wait_call("127.0.0.3\n");
is(stop_listener($pid), 0, 'the listener stops while a transfer runs');
ok($held && !kill(0, $held->{pid}), 'stopping the listener stops its senders');
mode('exit:0');
reset_calls();
my ($crowded, $crowded_port) = config(max_connections => 2, request_timeout => 30, allowed_networks => '127.0.0.2/32');
($pid) = start_listener($crowded);
wait_port($crowded_port);
sleep(0.5);
my @held = map {
IO::Socket::INET->new(PeerAddr => '127.0.0.1', PeerPort => $crowded_port, LocalAddr => '127.0.0.2', Proto => 'tcp')
} 1 .. 2;
sleep(0.5);
my $fds_before = () = glob("/proc/$pid/fd/*");
my @refused;
for (1 .. 20) {
my $started = time();
my $reply = http($crowded_port, '');
push @refused, $reply->{code} == 503 && time() - $started < 2 ? 1 : 0;
}
is(scalar(grep { $_ } @refused), 20, 'connections beyond max_connections are refused at once');
my $fds_after = () = glob("/proc/$pid/fd/*");
is($fds_after, $fds_before, 'refused connections are not kept open');
close($_) for @held;
sleep(0.5);
is(request($crowded_port, $sum_a, $path_a, '127.0.0.3')->{code}, 403, 'the address of the connection is checked against allowed_networks');
is(request($crowded_port, $sum_a, $path_a, '127.0.0.2')->{code}, 200, 'the listener serves requests again when connections close');
stop_listener($pid);
done_testing();
+32
View File
@@ -209,6 +209,38 @@ for my $failed (qw(rootimg.cpio.gz.sha256 rootimg.cpio.gz)) {
'the marker of the bundled initial ramdisk is placed next to it');
}
{
# nodeset sends a UFTP image only to an initial ramdisk with a matching marker, so an exported image keeps it.
my $dir = "$fixture/install/netboot/rhels9.6/x86_64/uftp";
make_path($dir);
write_text("$dir/$_", "$_\n") for qw(kernel initrd-stateless.gz rootimg.cpio.gz compute.pkglist);
write_text("$dir/initrd-stateless.gz.uftp", "sha256=marker of the ramdisk with the receiver\n");
my $attrs = { osimage => { provmethod => 'netboot', osvers => 'rhels9.6', osarch => 'x86_64', profile => 'uftp' },
linuximage => { rootimgdir => $dir, pkglist => "$dir/compute.pkglist" } };
my @errors;
xCAT_plugin::imgport::get_files('uftp', sub { push @errors, @{ $_[0]{error} || [] } }, $attrs);
is(join("\n", @errors), '', 'the files of an image with the UFTP marker are found');
is($attrs->{ramdiskuftp}, "$dir/initrd-stateless.gz.uftp", 'the export carries the UFTP marker of the initial ramdisk');
my ($placed, $errors, $dest) = import_image('rootimg.cpio.gz', sub {
write_text("$fixture/bundle-$sequence/initrd-stateless.gz.uftp", "sha256=marker of the bundled ramdisk\n");
}, ramdiskuftp => 'DEST/initrd-stateless.gz.uftp');
is($errors, '', 'a bundle with the UFTP marker is placed');
is(read_binary("$dest/initrd-stateless.gz.uftp"), "sha256=marker of the bundled ramdisk\n",
'the UFTP marker of the bundled initial ramdisk is placed next to it');
# A bundle that names the UFTP marker but lacks it changes nothing.
($placed, $errors, $dest) = import_image('rootimg.cpio.gz', sub {
write_text("$_[0]/initrd-stateless.gz.uftp", "sha256=marker of the published ramdisk\n");
}, ramdiskuftp => 'DEST/initrd-stateless.gz.uftp');
ok(!$placed, 'an import fails when the bundle lacks the UFTP marker that it names');
like($errors, qr/The image bundle lacks initrd-stateless\.gz\.uftp\./, 'the missing UFTP marker is reported');
is(read_binary("$dest/rootimg.cpio.gz"), $published, 'the published image stays when the bundle lacks the UFTP marker');
is(read_binary("$dest/initrd-stateless.gz"), "published ramdisk\n", 'the published ramdisk stays when the bundle lacks the UFTP marker');
is(read_binary("$dest/initrd-stateless.gz.uftp"), "sha256=marker of the published ramdisk\n",
'the UFTP marker of the published ramdisk stays');
}
for my $nodes (qw(psnode undefined-node)) {
# The nodes that get the postscripts of the image exist, or not.
my ($placed, $errors, $dest, $archive) = import_image('rootimg.cpio.gz', undef, nodes => $nodes);
+137
View File
@@ -0,0 +1,137 @@
#!/usr/bin/env perl
use strict;
use warnings;
use Cwd qw(realpath);
use File::Path qw(make_path);
use File::Slurper qw(read_binary write_binary);
use File::Temp qw(tempdir);
use FindBin;
use lib "$FindBin::Bin/../../perl-xCAT";
use lib "$FindBin::Bin/../../xCAT-server/share/xcat/netboot/imgutils";
use lib "$FindBin::Bin/../../xCAT-server/lib/perl";
use Test::More;
use imgutils;
use xCAT::UFTP;
my $tmp = realpath(tempdir(CLEANUP => 1));
my $helper = realpath("$FindBin::Bin/../../xCAT-server/share/xcat/netboot/imgutils/xcat-rootimg-fetch.sh");
# Returns what the code printed and what it returned or died with.
sub output_of {
my ($code) = @_;
my $out = '';
open(my $capture, '>', \$out) or die $!;
my $previous = select($capture);
my @result = eval { $code->() };
my $error = $@;
select($previous);
close($capture);
return ($out, $error, @result);
}
# An image root with an x86_64 uftpd of the UFTP version $version.
sub image_root {
my ($name, $version) = @_;
my $root = "$tmp/image-$name";
make_path("$root/usr/bin");
my $elf = "\x7fELF\x02\x01\x01" . ("\0" x 9) . pack('v', 2) . pack('v', 62) . ("\0" x 44);
if ($version) {
write_binary("$root/usr/bin/uftpd", $elf . "\0UFTP version $version Copyright\0");
chmod(0755, "$root/usr/bin/uftpd");
}
return $root;
}
my $module = "$tmp/98xcat";
my $copy = "$module/xcat-rootimg-fetch.sh";
make_path($module);
my ($out, $error, $receiver) = output_of(sub { imgutils::add_uftp_receiver(image_root('uftp5', '5.0.3 (xCAT build 1)'), 'x86_64', $module, 1) });
is($error, '', 'a stateless image with a UFTP 5 receiver is accepted');
is($receiver, '/usr/bin/uftpd', 'the receiver of the image is returned');
is(scalar(read_binary($copy)), scalar(read_binary($helper)), 'the dracut module gets the UFTP fetch helper');
is((stat($copy))[2] & 07777, 0644, 'the dracut module installs the helper itself, so the copy is not executable');
like($out, qr{Adding the UFTP receiver /usr/bin/uftpd to the initial ramdisk}, 'genimage reports the receiver');
($out, $error, $receiver) = output_of(sub { imgutils::add_uftp_receiver(image_root('uftp5', '5.0.3 (xCAT build 1)'), 'x86_64', $module, 0) });
ok(!defined($receiver) && $error eq '', 'an initial ramdisk that must not receive over UFTP gets no receiver');
ok(!-e $copy, 'the helper that an earlier stateless build left in the module is removed');
write_binary($copy, "older helper\n");
($out, $error, $receiver) = output_of(sub { imgutils::add_uftp_receiver(image_root('none'), 'x86_64', $module, 1) });
ok(!defined($receiver) && $error eq '', 'an image without uftpd gets no receiver');
is($out, '', 'an image without uftpd gets no warning');
ok(!-e $copy, 'the helper is removed when the image no longer has uftpd');
symlink("$tmp/missing", $copy) or die $!;
($out, $error, $receiver) = output_of(sub { imgutils::add_uftp_receiver(image_root('uftp4', '4.10.2'), 'x86_64', $module, 1) });
ok(!defined($receiver) && $error eq '', 'a UFTP 4 receiver is left out without failing the image build');
like($out, qr/Warning: .*UFTP 5\.x is required/, 'a UFTP 4 receiver gets a warning');
ok(!-e $copy && !-l $copy, 'a dangling helper link is removed');
($out, $error, $receiver) = output_of(sub { imgutils::add_uftp_receiver(image_root('stock', '5.0.3'), 'x86_64', $module, 1) });
ok(!defined($receiver) && $error eq '', 'a UFTP 5 receiver without the xCAT build mark is left out');
like($out, qr/Warning: .*UFTP 5\.0\.3 without the xCAT fixes/, 'a UFTP 5 receiver without the xCAT build mark gets a warning');
($out, $error, $receiver) = output_of(sub { imgutils::add_uftp_receiver(image_root('uftp5', '5.0.3 (xCAT build 1)'), 'ppc64le', $module, 1) });
ok(!defined($receiver), 'a receiver for another architecture than the image is left out');
like($out, qr/Warning: .*another architecture than ppc64le/, 'a foreign receiver gets a warning');
ok(!-e $copy, 'no helper is added without a usable receiver');
# unlink and copy fail on these paths for root too.
make_path("$copy/older");
(undef, $error) = output_of(sub { imgutils::add_uftp_receiver(image_root('none'), 'x86_64', $module, 1) });
like($error, qr/^Error: cannot remove \Q$copy\E/, 'an older helper that cannot be removed fails the image build');
rmdir("$copy/older") && rmdir($copy) or die $!;
my $missing_module = "$tmp/missing/98xcat";
(undef, $error) = output_of(sub { imgutils::add_uftp_receiver(image_root('uftp5', '5.0.3 (xCAT build 1)'), 'x86_64', $missing_module, 1) });
like($error, qr{^Error: cannot copy \S+/imgutils/xcat-rootimg-fetch\.sh to \Q$missing_module/xcat-rootimg-fetch.sh\E},
'a helper that cannot be added fails the image build');
# lsinitrd runs in the image, so the test replaces the listing that it returns.
my (@listed, $listing);
{
no warnings 'redefine';
*imgutils::initrd_listing = sub { push @listed, [@_]; return $listing };
}
$listing = [
"-rwxr-xr-x 1 root root 180000 Jan 1 00:00 usr/bin/uftpd\n",
"-rwxr-xr-x 1 root root 40000 Jan 1 00:00 usr/bin/sha256sum\n",
"-rw-r--r-- 1 root root 12000 Jan 1 00:00 usr/lib/xcat-rootimg-fetch.sh\n",
];
my $root = image_root('uftp5', '5.0.3 (xCAT build 1)');
(undef, $error, my $checked) = output_of(sub { imgutils::check_uftp_initrd($root, '/tmp/initrd.1.gz', 'stateless') });
ok($checked && $error eq '', 'an initial ramdisk with the receiver files passes');
is_deeply(\@listed, [ [ $root, '/tmp/initrd.1.gz' ] ], 'the initial ramdisk is listed in the image');
$listing = [ @$listing[ 1, 2 ] ];
(undef, $error) = output_of(sub { imgutils::check_uftp_initrd($root, '/tmp/initrd.1.gz', 'stateless') });
like($error, qr/^Error: the initial ramdisk for stateless lacks the UFTP receiver files: uftpd$/m,
'an initial ramdisk without uftpd fails the image build');
$listing = undef;
(undef, $error) = output_of(sub { imgutils::check_uftp_initrd($root, '/tmp/initrd.1.gz', 'stateless') });
like($error, qr/lacks the UFTP receiver files: a readable file list/, 'an initial ramdisk that cannot be listed fails the image build');
my $initrd = "$tmp/initrd-stateless.gz";
write_binary($initrd, "initial ramdisk with the receiver\n");
($out, $error, my $marked) = output_of(sub { imgutils::mark_uftp_receiver($initrd, '/usr/bin/uftpd') });
ok($marked && $error eq '', 'an initial ramdisk with the receiver is marked');
is(xCAT::UFTP::verify_initrd_marker($initrd), undef, 'nodeset accepts the marked initial ramdisk');
like($out, qr/\Q$initrd\E can receive the root image over UFTP/, 'genimage reports the UFTP receiver');
write_binary($initrd, "initial ramdisk without the receiver\n");
($out, $error, $marked) = output_of(sub { imgutils::mark_uftp_receiver($initrd, undef) });
ok($marked && $error eq '' && $out eq '', 'an initial ramdisk without the receiver is not marked');
ok(!-e "$initrd.uftp", 'the marker of the previous initial ramdisk is removed');
like(xCAT::UFTP::verify_initrd_marker($initrd), qr/was not built with the UFTP receiver/, 'nodeset refuses UFTP for it');
# A file cannot replace a directory, for root too.
make_path("$initrd.uftp/older");
($out, $error, $marked) = output_of(sub { imgutils::mark_uftp_receiver($initrd, '/usr/bin/uftpd') });
is($error, '', 'a marker that cannot be written does not fail the image build');
ok(!$marked, 'a marker that cannot be written is reported to the caller');
like($out, qr/^Warning: Cannot publish \Q$initrd\E\.uftp: /, 'a marker that cannot be written gets a warning');
is(scalar(() = glob("$tmp/.initrd-stateless.gz.uftp.tmp*")), 0, 'a marker that cannot be written leaves no temporary file');
done_testing();
+189
View File
@@ -0,0 +1,189 @@
#!/usr/bin/env perl
use strict;
use warnings;
use FindBin;
use lib "$FindBin::Bin/../lib";
use lib "$FindBin::Bin/../../perl-xCAT";
use lib "$FindBin::Bin/../../xCAT-server/lib/perl";
use Cwd qw(realpath);
use File::Path qw(make_path);
use File::Slurper qw(read_text write_text);
use File::Temp qw(tempdir);
use Test::More;
use XCAT::Test::File qw(repo_path);
my $fixture = realpath(tempdir(CLEANUP => 1));
make_path("$fixture/db", "$fixture/root/lib/perl", "$fixture/tftpboot", "$fixture/etc");
symlink(repo_path('perl-xCAT/xCAT'), "$fixture/root/lib/perl/xCAT") or die $!;
$ENV{XCATROOT} = "$fixture/root";
$ENV{XCATCFG} = "SQLite:$fixture/db";
require xCAT::Table;
require xCAT::SvrUtils;
require xCAT::UFTP;
require xCAT::DBobjUtils;
require(repo_path('xCAT-server/lib/xcat/plugins/anaconda.pm'));
require(repo_path('xCAT-server/lib/xcat/plugins/sles.pm'));
require(repo_path('xCAT-server/lib/xcat/plugins/debian.pm'));
my $fp = '31:94:09:9D:B1:24:3F:32:C0:B9:1A:BB:AC:F4:74:2B:80:F9:77:B2';
my $uftp = qr/(?:^| )xcat\.uftp=2045 xcat\.uftpfp=\Q$fp\E /;
write_text("$fixture/etc/server.fingerprint", "$fp\n");
write_text("$fixture/etc/listener.conf", "listen_address = 192.0.2.1\nlisten_port = 2045\n"
. "image_roots = $fixture/install/netboot\nfingerprint_file = $fixture/etc/server.fingerprint\n");
{
no warnings 'once';
$xCAT::UFTP::CONFIG_FILE = "$fixture/etc/listener.conf";
}
my %plugin = (anaconda => 'rhels9.6', sles => 'sle15.6', debian => 'ubuntu24.04');
my %dir;
my %tables = (
site => [ [ { key => 'installdir' }, { value => "$fixture/install" } ], [ { key => 'tftpdir' }, { value => "$fixture/tftpboot" } ] ],
nodelist => [ map { [ { node => $_ }, { groups => 'compute' } ] } qw(n1 n2 n3) ],
noderes => [ map { [ { node => $_ }, { xcatmaster => '192.0.2.1' } ] } qw(n1 n2 n3) ],
);
for my $name (keys %tables) {
my $table = xCAT::Table->new($name, -create => 1) or die "cannot open $name";
$table->setAttribs(@$_) for @{ $tables{$name} };
}
my $nodetype = xCAT::Table->new('nodetype', -create => 1);
my $osimage = xCAT::Table->new('osimage', -create => 1);
my $linuximage = xCAT::Table->new('linuximage', -create => 1);
for my $name (sort keys %plugin) {
$dir{$name} = "$fixture/install/netboot/$name";
make_path($dir{$name});
write_text("$dir{$name}/$_", "$_ of $name\n") for qw(kernel initrd-stateless.gz rootimg.cpio.gz);
$osimage->setAttribs({ imagename => $name }, { osvers => $plugin{$name}, osarch => 'x86_64', profile => 'compute', provmethod => 'netboot' });
$linuximage->setAttribs({ imagename => $name }, { rootimgdir => $dir{$name} });
}
# The anaconda plugin also sets up a statelite node whose ramdisk root image is downloaded.
$dir{ramdisk} = "$fixture/install/netboot/ramdisk";
make_path($dir{ramdisk});
write_text("$dir{ramdisk}/$_", "$_ of ramdisk\n") for qw(kernel initrd-stateless.gz initrd-statelite.gz rootimg-statelite.gz);
$osimage->setAttribs({ imagename => 'ramdisk' },
{ osvers => 'rhels9.6', osarch => 'x86_64', profile => 'compute', provmethod => 'statelite', rootfstype => 'ramdisk' });
$linuximage->setAttribs({ imagename => 'ramdisk' }, { rootimgdir => $dir{ramdisk} });
# genimage built every initial ramdisk with the UFTP receiver and the root image check.
sub mark {
my ($name) = @_;
is(xCAT::UFTP::write_initrd_marker("$dir{$name}/initrd-stateless.gz"), undef, "genimage marks the initial ramdisk of $name");
is(xCAT::SvrUtils->write_initrd_check_marker("$dir{$name}/initrd-stateless.gz"), undef, "genimage marks the check of $name");
}
mark($_) for (sort keys %plugin), 'ramdisk';
# Runs nodeset for n1 and n2 with the image of $name, and n3 with the statelite ramdisk image for anaconda.
# Returns the kernel command lines and the errors.
sub nodeset {
my ($name, %opt) = @_;
$nodetype->setAttribs({ node => $_ }, { os => $plugin{$name}, arch => 'x86_64', profile => 'compute', provmethod => $name }) for qw(n1 n2);
$nodetype->setAttribs({ node => 'n3' }, { os => 'rhels9.6', arch => 'x86_64', profile => 'compute', provmethod => 'ramdisk' });
no warnings 'once';
local %::XCATSITEVALS = (installdir => "$fixture/install", tftpdir => "$fixture/tftpboot", httpmethod => 'http', %{ $opt{site} || {} });
my $bootparams = { map { ($_ => [ { addkcmdline => $opt{node_addkcmdline}{$_} } ]) } keys %{ $opt{node_addkcmdline} || {} } };
my @responses;
my @requests = ([ mknetboot => qw(n1 n2) ]);
push @requests, [ mkstatelite => 'n3' ] if $name eq 'anaconda';
for my $request (@requests) {
my ($command, @nodes) = @$request;
no strict 'refs';
&{"xCAT_plugin::${name}::process_request"}(
{ command => [$command], node => \@nodes, bootparams => \$bootparams, noupdateinitrd => $opt{noupdateinitrd} },
sub { push @responses, @_ }, sub { });
}
my %kcmdline = map { $_ => $bootparams->{$_}[0]{kcmdline} } grep { $bootparams->{$_}[0]{kcmdline} } keys %$bootparams;
my @errors = map { (@{ $_->{error} || [] }, map { @{ $_->{error} || [] } } @{ $_->{node} || [] }) } @responses;
return (\%kcmdline, join("\n", @errors));
}
for my $name (sort keys %plugin) {
my ($kcmdline, $errors) = nodeset($name);
is($errors, '', "$name: nodeset reports no error");
unlike($kcmdline->{n1} // '', qr/xcat\.uftp/, "$name: nodes download the root image over HTTP by default");
$linuximage->setAttribs({ imagename => $name }, { rootimgtransport => 'uftp' });
($kcmdline, $errors) = nodeset($name);
is($errors, '', "$name: an image set to uftp reports no error");
for my $node (qw(n1 n2)) {
like($kcmdline->{$node} // '', $uftp, "$name: $node gets the listener port and the server key fingerprint");
like($kcmdline->{$node} // '', qr{imgurl=http://\S+/rootimg\.cpio\.gz .*rootimgchecksum=yes }, "$name: $node keeps the HTTP image URL and the check");
}
($kcmdline, $errors) = nodeset($name, node_addkcmdline => { n2 => 'quiet xcat.uftpfp=AA' });
like($kcmdline->{n1} // '', $uftp, "$name: other nodes are set up when one node sets a managed argument");
ok(!exists($kcmdline->{n2}), "$name: a node whose bootparams.addkcmdline sets a managed argument gets no boot entry");
like($errors, qr/bootparams\.addkcmdline must not set xcat\.uftpfp/, "$name: the managed argument of the node is reported");
$linuximage->setAttribs({ imagename => $name }, { addkcmdline => 'console=ttyS0 xcat.uftp=1' });
($kcmdline, $errors) = nodeset($name);
ok(!exists($kcmdline->{n1}) && !exists($kcmdline->{n2}), "$name: no node gets a boot entry while the image sets a managed argument");
like($errors, qr/linuximage\.addkcmdline must not set xcat\.uftp;/, "$name: the managed argument of the image is reported");
$linuximage->setAttribs({ imagename => $name }, { addkcmdline => '' });
$linuximage->setAttribs({ imagename => $name }, { rootimgchecksum => 'no' });
($kcmdline, $errors) = nodeset($name);
ok(!exists($kcmdline->{n1}), "$name: a node gets no boot entry when its image sends over UFTP without the check");
like($errors, qr/UFTP needs the root image checksum/, "$name: the missing check is reported");
$linuximage->setAttribs({ imagename => $name }, { rootimgchecksum => '' });
# A refused node keeps its boot entry, so the kernel and initial ramdisk that it names must stay too.
my $tftp = "$fixture/tftpboot/xcat/osimage/$name";
my %booted = map { ($_ => read_text("$tftp/$_")) } qw(kernel initrd-stateless.gz);
write_text("$dir{$name}/kernel", "kernel of $name rebuilt\n");
write_text("$dir{$name}/initrd-stateless.gz", "initial ramdisk of $name rebuilt without the receiver\n");
xCAT::SvrUtils->write_initrd_check_marker("$dir{$name}/initrd-stateless.gz");
($kcmdline, $errors) = nodeset($name);
ok(!exists($kcmdline->{n1}), "$name: a node gets no boot entry when its initial ramdisk lacks the UFTP receiver");
like($errors, qr{\Q$dir{$name}\E/initrd-stateless\.gz is not the initial ramdisk that genimage built with the UFTP receiver},
"$name: the initial ramdisk of the image is named");
is(read_text("$tftp/$_"), $booted{$_}, "$name: nodes keep the $_ that they boot now") for sort keys %booted;
mark($name);
rename($xCAT::UFTP::CONFIG_FILE, "$xCAT::UFTP::CONFIG_FILE.off") or die $!;
($kcmdline, $errors) = nodeset($name);
rename("$xCAT::UFTP::CONFIG_FILE.off", $xCAT::UFTP::CONFIG_FILE) or die $!;
like($errors, qr/UFTP listener configuration: Cannot read/, "$name: a missing listener configuration is reported");
is(read_text("$tftp/$_"), $booted{$_}, "$name: a missing listener configuration keeps the $_ that nodes boot") for sort keys %booted;
($kcmdline, $errors) = nodeset($name);
like($kcmdline->{n1} // '', $uftp, "$name: a rebuilt initial ramdisk with the receiver is accepted");
# The debian plugin has no --noupdateinitrd.
if ($name ne 'debian') {
write_text("$fixture/tftpboot/xcat/osimage/$name/initrd-stateless.gz", "initial ramdisk that nodes boot now\n");
($kcmdline, $errors) = nodeset($name, noupdateinitrd => 1);
like($errors, qr/is not the initial ramdisk that genimage built with the UFTP receiver/,
"$name: with --noupdateinitrd, the initial ramdisk that nodes boot decides");
}
$linuximage->setAttribs({ imagename => $name }, { rootimgtransport => 'tftp' });
($kcmdline, $errors) = nodeset($name);
ok(!exists($kcmdline->{n1}), "$name: a node gets no boot entry when its image has an unknown transport");
like($errors, qr/The rootimgtransport attribute of the image is 'tftp', but only http and uftp are supported/,
"$name: the unknown transport is reported");
$linuximage->setAttribs({ imagename => $name }, { rootimgtransport => '' });
($kcmdline, $errors) = nodeset($name, site => { rootimgtransport => 'uftp' });
like($kcmdline->{n1} // '', $uftp, "$name: site.rootimgtransport sets the transport of an image that does not set it");
$linuximage->setAttribs({ imagename => $name }, { rootimgtransport => 'http' });
($kcmdline, $errors) = nodeset($name, site => { rootimgtransport => 'uftp' });
unlike($kcmdline->{n1} // '', qr/xcat\.uftp/, "$name: an image set to http overrides site.rootimgtransport");
$linuximage->setAttribs({ imagename => $name }, { rootimgtransport => '' });
}
{
my ($kcmdline, $errors) = nodeset('anaconda', site => { rootimgtransport => 'uftp' });
is($errors, '', 'site.rootimgtransport=uftp reports no error for a statelite image');
like($kcmdline->{n3} // '', qr{imgurl=http://\S+/rootimg-statelite\.gz }, 'the statelite ramdisk node downloads its archive');
unlike($kcmdline->{n3} // '', qr/xcat\.uftp/, 'the statelite ramdisk node does not use UFTP');
}
for my $name (qw(anaconda sles)) {
$linuximage->setAttribs({ imagename => $name }, { rootimgtransport => 'uftp' });
my ($kcmdline, $errors) = nodeset($name, site => { httpmethod => 'https' });
ok(!exists($kcmdline->{n1}), "$name: a node gets no boot entry when its image URL uses https");
like($errors, qr/requires site\.httpmethod=http, not https/, "$name: the https image URL is reported");
$linuximage->setAttribs({ imagename => $name }, { rootimgtransport => '' });
}
done_testing();
@@ -0,0 +1,52 @@
#!/usr/bin/env perl
use strict;
use warnings;
use FindBin;
use lib "$FindBin::Bin/../lib";
use lib "$FindBin::Bin/../../perl-xCAT";
use lib "$FindBin::Bin/../../xCAT-server/lib/perl";
use File::Temp qw(tempdir);
use Test::More;
my $fixture = tempdir(CLEANUP => 1);
$ENV{XCATCFG} = "SQLite:$fixture";
require xCAT::SvrUtils;
require xCAT::Table;
require xCAT::DBobjUtils;
sub label {
my ($image, $site) = @_;
return sprintf('image %s and site %s', map({ defined $_ ? "'$_'" : 'unset' } $image, $site));
}
# The image value wins, and the site value applies only when the image does not set one.
for my $case ([ undef, undef, 'http' ], [ '', undef, 'http' ], [ ' ', 'uftp', 'uftp' ], [ undef, 'uftp', 'uftp' ],
[ undef, ' UFTP ', 'uftp' ], [ 'http', 'uftp', 'http' ], [ 'Uftp', 'http', 'uftp' ], [ 'uftp', undef, 'uftp' ]) {
my ($image, $site, $expected) = @$case;
no warnings 'once';
local %::XCATSITEVALS = (master => '192.0.2.1', defined $site ? (rootimgtransport => $site) : ());
my ($transport, $error) = xCAT::SvrUtils->rootimg_transport($image);
is($transport, $expected, label($image, $site) . " select $expected");
is($error, undef, label($image, $site) . ' give no error');
}
# Another value is refused, and the error names the attribute that holds it.
for my $case ([ 'tftp', 'uftp', qr/^The rootimgtransport attribute of the image is 'tftp'/ ],
[ undef, 'multicast', qr/^site\.rootimgtransport is 'multicast'/ ], [ '', 'yes', qr/^site\.rootimgtransport is 'yes'/ ]) {
my ($image, $site, $message) = @$case;
no warnings 'once';
local %::XCATSITEVALS = (master => '192.0.2.1', rootimgtransport => $site);
my ($transport, $error) = xCAT::SvrUtils->rootimg_transport($image);
is($transport, undef, label($image, $site) . ' select no transport');
like($error // '', $message, label($image, $site) . ' name the wrong value');
}
# The osimage object shows the attribute that the linuximage table holds.
xCAT::Table->new('osimage', -create => 1)->setAttribs({ imagename => 'image' },
{ imagetype => 'linux', provmethod => 'netboot', osvers => 'rhels9.6', osarch => 'x86_64', profile => 'compute' });
my (undef, $error) = xCAT::Table->new('linuximage', -create => 1)->setAttribs({ imagename => 'image' }, { rootimgtransport => 'uftp' });
is($error, undef, 'the linuximage table stores rootimgtransport');
my %objects = xCAT::DBobjUtils->getobjdefs({ image => 'osimage' });
is($objects{image}{rootimgtransport}, 'uftp', 'the osimage object shows rootimgtransport');
done_testing();
+105
View File
@@ -0,0 +1,105 @@
#!/usr/bin/env perl
use strict;
use warnings;
use Cwd qw(realpath);
use File::Path qw(make_path);
use File::Slurper qw(write_binary);
use File::Temp qw(tempdir);
use FindBin;
use lib "$FindBin::Bin/../../xCAT-server/lib/perl";
use Test::More;
use xCAT::UFTP;
my $tmp = realpath(tempdir(CLEANUP => 1));
my $fp = '31:94:09:9D:B1:24:3F:32:C0:B9:1A:BB:AC:F4:74:2B:80:F9:77:B2';
# genimage marks the ramdisk in the image directory, and nodes boot its copy in TFTP.
my $root = "$tmp/install/netboot";
my $image = "$root/rhels9/x86_64/compute";
my $tftp = "$tmp/tftpboot/xcat/osimage/compute";
make_path($image, $tftp, "$tmp/etc");
write_binary("$image/initrd-stateless.gz", 'initramfs with receiver');
write_binary("$tftp/initrd-stateless.gz", 'initramfs with receiver');
write_binary("$image/rootimg.cpio.gz", "root image\n");
is(xCAT::UFTP::write_initrd_marker("$image/initrd-stateless.gz"), undef, 'the ramdisk with the receiver is marked');
write_binary("$tmp/etc/server.fingerprint", "$fp\n");
write_binary("$tmp/etc/listener.conf",
"listen_address = 192.0.2.1\nlisten_port = 2045\nimage_roots = $root\n"
. "fingerprint_file = $tmp/etc/server.fingerprint\n");
my %base = (
rootimgdir => $image,
artifact => 'rootimg.cpio.gz',
initrd => "$tftp/initrd-stateless.gz",
marker => "$image/initrd-stateless.gz.uftp",
httpmethod => 'http',
checksum => 1,
config => "$tmp/etc/listener.conf",
);
sub arguments {
my (%override) = @_;
return xCAT::UFTP::boot_arguments(%base, %override);
}
my $args = "xcat.uftp=2045 xcat.uftpfp=$fp ";
is_deeply([ arguments() ], [ $args, undef ], 'nodeset passes the listener port and the server key fingerprint');
is_deeply([ arguments(addkcmdline => 'console=ttyS0,115200 xcat.uftpx=1 uftp=true xcat.imgsha256=abc') ],
[ $args, undef ], 'other addkcmdline tokens are left to the bootloader');
for my $token ('xcat.uftp=1', 'xcat.uftpfp=AA') {
my (undef, $refusal) = arguments(addkcmdline => "quiet $token");
like($refusal, qr/linuximage\.addkcmdline must not set/, "a managed token in addkcmdline is refused: $token");
(undef, $refusal) = arguments(node_addkcmdline => "quiet $token");
like($refusal, qr/^bootparams\.addkcmdline must not set/, "a managed token in the addkcmdline of a node is refused: $token");
}
is_deeply([ arguments(node_addkcmdline => 'console=ttyS1,115200 xcat.uftpx=1') ], [ $args, undef ],
'other tokens in the addkcmdline of a node are left to the bootloader');
my (undef, $unchecked) = arguments(checksum => 0);
like($unchecked, qr/UFTP needs the root image checksum/, 'UFTP is refused for an image whose checksum is turned off');
my (undef, $statelite) = arguments(statelite => 1);
like($statelite, qr/only the compressed root image of a stateless image/, 'UFTP is refused for a statelite image');
my (undef, $https) = arguments(httpmethod => 'https');
like($https, qr/requires site\.httpmethod=http, not https/, 'UFTP is refused for an HTTPS image URL');
for my $artifact (undef, 'rootimg.gz') {
my (undef, $unknown) = arguments(artifact => $artifact);
like($unknown, qr/has no root image that UFTP can send; run packimage/,
sprintf('an image whose archive is %s is refused', $artifact // 'missing'));
}
write_binary("$image/rootimg.cpio.gz.metainfo", 'torrent');
my (undef, $torrent) = arguments();
like($torrent, qr/cannot be combined with packimage --tracker/, 'UFTP is refused for a BitTorrent image');
unlink("$image/rootimg.cpio.gz.metainfo");
write_binary("$tftp/initrd-stateless.gz", 'initramfs rebuilt without receiver');
my (undef, $stale_initrd) = arguments();
like($stale_initrd, qr/is not the initial ramdisk that genimage built with the UFTP receiver/,
'a TFTP ramdisk that differs from the marked one is refused');
write_binary("$tftp/initrd-stateless.gz", 'initramfs with receiver');
xCAT::UFTP::remove_initrd_marker("$image/initrd-stateless.gz");
my (undef, $no_marker) = arguments();
like($no_marker, qr/was not built with the UFTP receiver/, 'a ramdisk without the receiver is refused');
xCAT::UFTP::write_initrd_marker("$image/initrd-stateless.gz");
my $outside = "$tmp/images/rhels9/x86_64/compute";
make_path($outside);
my (undef, $foreign) = arguments(rootimgdir => $outside);
like($foreign, qr/^\Q$outside\E is outside the image_roots of the UFTP listener in \Q$tmp\E\/etc\/listener\.conf$/,
'an image that the listener does not serve is refused');
my $spaced = "$root/rhels9/x86_64/compute two";
make_path($spaced);
my (undef, $unsendable) = arguments(rootimgdir => $spaced);
like($unsendable, qr/has a character that a UFTP request cannot carry/, 'an image path that a request cannot name is refused');
my (undef, $no_config) = arguments(config => "$tmp/etc/missing.conf");
like($no_config, qr/UFTP listener configuration: Cannot read/, 'a missing listener configuration is refused');
write_binary("$tmp/etc/server.fingerprint", "not a fingerprint\n");
my (undef, $bad_fp) = arguments();
like($bad_fp, qr/does not hold a UFTP key fingerprint/, 'a malformed server fingerprint is refused');
unlink("$tmp/etc/server.fingerprint");
my (undef, $no_fp) = arguments();
like($no_fp, qr/Cannot read UFTP server fingerprint/, 'a missing server fingerprint is refused');
done_testing();
+359
View File
@@ -0,0 +1,359 @@
#!/usr/bin/env perl
use strict;
use warnings;
use Cwd qw(realpath);
use Digest::SHA qw(sha256_hex);
use File::Path qw(make_path);
use File::Slurper qw(read_binary write_binary);
use File::Temp qw(tempdir);
use FindBin;
use Test::More;
use lib "$FindBin::Bin/../../perl-xCAT";
use lib "$FindBin::Bin/../../xCAT-server/lib/perl";
use xCAT::SvrUtils;
use xCAT::UFTP;
my $tmp = realpath(tempdir(CLEANUP => 1));
my $root = "$tmp/install/netboot";
my $fp = '31:94:09:9D:B1:24:3F:32:C0:B9:1A:BB:AC:F4:74:2B:80:F9:77:B2';
make_path($root, "$tmp/log");
# packimage publishes the archive with its checksum, and the listener sends only such an archive.
sub publish {
my ($dir, $content, $name) = @_;
$name ||= 'rootimg.cpio.gz';
make_path($dir);
chmod(0755, $dir);
my ($stage, $error) = xCAT::SvrUtils->rootimg_stage($dir);
die $error unless $stage;
write_binary("$stage/$name", $content);
chmod(0644, "$stage/$name");
$error = xCAT::SvrUtils->write_rootimg_checksum("$stage/$name") // xCAT::SvrUtils->publish_rootimg("$stage/$name", "$dir/$name");
die $error if $error;
return sha256_hex($content);
}
my $config_seq = 0;
sub config {
my (%override) = @_;
my %settings = (
listen_address => '192.0.2.1',
image_roots => $root,
batch_delay => 2,
transfer_timeout => 30,
job_retention => 60,
key_file => "$tmp/server.key",
fingerprint_file => "$tmp/server.fingerprint",
uftp => "$tmp/uftp",
log_dir => "$tmp/log",
%override,
);
my $path = "$tmp/listener." . ++$config_seq . '.conf';
write_binary($path, join('', map { "$_ = $settings{$_}\n" } sort keys %settings));
my ($config, $error) = xCAT::UFTP::load_listener_config($path);
die $error if $error;
return $config;
}
# The listener starts uftp in a child process, so the test records each start instead.
my (@log, @senders, @signals, $spawn_error);
my $next_pid = 4000;
sub spawn {
my ($job, $log, @command) = @_;
return (undef, $spawn_error) if $spawn_error;
my ($hostlist) = map { /^\@(.+)$/ ? $1 : () } @command;
sysseek($job->{fh}, 0, 0) or die "sysseek: $!";
my $bytes = '';
while (sysread($job->{fh}, my $chunk, 65536)) {
$bytes .= $chunk;
}
push @senders, {
pid => ++$next_pid,
log => $log,
command => \@command,
hostlist => $hostlist,
hosts => scalar(read_binary($hostlist)),
mode => (stat($hostlist))[2] & 07777,
bytes => $bytes,
};
return ($next_pid, undef);
}
sub listener {
my $config = config(@_);
@log = @senders = @signals = ();
return xCAT::UFTP::listener_state($config, workdir => tempdir(DIR => $tmp), spawn => \&spawn,
log => sub { push @log, "$_[0]: $_[1]" },
signal => sub { push @signals, "$_[0] $_[1]" });
}
sub reply {
my ($state, $peer, $raw, $now) = @_;
my ($status, $body) = xCAT::UFTP::listener_reply($state, $peer, $raw, $now);
return { code => $status, body => $body, map { split /=/, $_, 2 } grep { /=/ } split /\n/, $body // '' };
}
sub transfer {
my ($state, $peer, $digest, $path, $now) = @_;
return reply($state, $peer, "GET /xcat-uftp/v1/transfer/$digest$path HTTP/1.0\r\nHost: test\r\n\r\n", $now);
}
sub job_state {
my ($state, $peer, $job, $now) = @_;
return reply($state, $peer, "GET /xcat-uftp/v1/job/$job HTTP/1.0\r\n\r\n", $now);
}
sub runtime {
my ($config, $key_fingerprint) = @_;
return xCAT::UFTP::check_listener_runtime($config, $key_fingerprint || sub { ($fp, undef) });
}
write_binary("$tmp/uftp", ("\0" x 64) . "UFTP version 5.0.4 (xCAT build 1) Copyright\0");
write_binary("$tmp/uftp4", "\0UFTP version 4.10.2 Copyright\0");
write_binary("$tmp/uftp-stock", "\0UFTP version 5.0.4 Copyright\0");
chmod(0755, "$tmp/uftp", "$tmp/uftp4", "$tmp/uftp-stock");
my $checked = config();
my ($fingerprint, $error) = runtime($checked);
like($error, qr/^UFTP server key \Q$tmp\E\/server\.key is missing; run xcat-uftp-listener --init-key$/,
'a missing server key is refused with the command that creates it');
write_binary("$tmp/server.key", "fixture private key\n");
chmod(0600, "$tmp/server.key");
($fingerprint, $error) = runtime($checked);
like($error, qr/^Cannot read UFTP server fingerprint .*; run xcat-uftp-listener --init-key$/,
'a missing fingerprint file is refused');
write_binary("$tmp/server.fingerprint", "$fp\n");
($fingerprint, $error) = runtime($checked);
is($error, undef, 'the runtime check passes');
is($fingerprint, $fp, 'the runtime check returns the fingerprint of the server key');
is(scalar(() = glob("$tmp/log/.write-check*")), 0, 'the write check leaves no file in log_dir');
(undef, $error) = runtime($checked, sub { (undef, "$tmp/uftp_keymgt failed for $tmp/server.key") });
is($error, "$tmp/uftp_keymgt failed for $tmp/server.key", 'a key that uftp_keymgt cannot read is refused');
chmod(0640, "$tmp/server.key");
(undef, $error) = runtime($checked);
like($error, qr/must not be accessible by other users/, 'a private key that others can read is refused');
chmod(0600, "$tmp/server.key");
write_binary("$tmp/server.fingerprint", "00:11:22:33:44:55:66:77:88:99:AA:BB:CC:DD:EE:FF:00:11:22:33\n");
(undef, $error) = runtime($checked);
like($error, qr/server\.fingerprint does not match \Q$tmp\E\/server\.key; run xcat-uftp-listener --init-key$/,
'a fingerprint file for another key is refused');
write_binary("$tmp/server.fingerprint", "$fp\n");
chmod(0644, "$tmp/uftp");
(undef, $error) = runtime($checked);
like($error, qr/UFTP sender \Q$tmp\E\/uftp is not executable; install the uftp package/, 'a sender that cannot run is refused');
chmod(0755, "$tmp/uftp");
(undef, $error) = runtime(config(uftp => "$tmp/uftp4"));
like($error, qr/UFTP 4\.10\.2; UFTP 5\.x is required/, 'a UFTP 4 sender is refused');
(undef, $error) = runtime(config(uftp => "$tmp/uftp-stock"));
like($error, qr/UFTP 5\.0\.4 without the xCAT fixes/, 'a UFTP 5 sender without the xCAT build mark is refused');
write_binary("$tmp/log-file", "not a directory\n");
(undef, $error) = runtime(config(log_dir => "$tmp/log-file"));
is($error, "Cannot create $tmp/log-file", 'a log_dir that cannot be created is refused');
(undef, $error) = runtime(config(log_dir => "$tmp/new/log"));
ok(!defined($error) && -d "$tmp/new/log", 'a missing log_dir is created');
my $image_a = "$root/rhels9/x86_64/compute";
my $content_a = "image A generation 1\n" x 200;
my $sum_a = publish($image_a, $content_a);
my $path_a = "$image_a/rootimg.cpio.gz";
my $t = 1000;
my $state = listener();
my $first = transfer($state, '192.0.2.10', $sum_a, $path_a, $t);
is($first->{code}, 200, 'a request for a published image is accepted');
like($first->{job}, qr/^[0-9a-f]{16}$/, 'the reply names the transfer job');
is($first->{file}, 'rootimg.cpio.gz', 'the reply names the artifact');
is($first->{size}, length($content_a), 'the reply gives the artifact size');
is($first->{port}, 1044, 'the reply gives the server-owned UDP port');
is($first->{mcast}, '230.4.4.1', 'the reply gives the public multicast address');
is($first->{server_id}, '0xC0000201', 'the reply gives the UFTP server ID');
is($first->{server_ip}, '192.0.2.1', 'the reply gives the sending address');
is($first->{client_ip}, '192.0.2.10', 'the reply gives the address the server saw');
is($first->{start_in}, 2, 'the reply gives the batch delay');
is($first->{timeout}, 30, 'the reply gives the transfer timeout');
my $again = transfer($state, '192.0.2.10', $sum_a, $path_a, $t + 1);
is($again->{job}, $first->{job}, 'a duplicate request joins the same job');
is($again->{start_in}, 1, 'a later request gets the remaining batch delay');
is(transfer($state, '192.0.2.11', $sum_a, "/$path_a", $t + 1)->{code}, 400, 'an unnormalized image path is refused');
is(transfer($state, '192.0.2.11', $sum_a, $path_a, $t + 1)->{job}, $first->{job},
'a compatible request from another node joins the same job');
my @added = map { /: job \Q$first->{job}\E: added (\S+)$/ ? $1 : () } @log;
is_deeply(\@added, [ '192.0.2.10', '192.0.2.11' ], 'the log adds each node to the job once');
ok((grep { $_ eq "info: job $first->{job}: 192.0.2.10 asked again" } @log), 'the log reports the repeated request');
my $pending = job_state($state, '192.0.2.10', $first->{job}, $t + 1);
is($pending->{state}, 'pending', 'a member sees the job wait for its batch');
is($pending->{start_in}, 1, 'a waiting job reports the remaining batch delay');
xCAT::UFTP::run_jobs($state, $t + 1);
is(scalar(@senders), 0, 'no sender starts before the batch delay ends');
xCAT::UFTP::run_jobs($state, $t + 2);
is(scalar(@senders), 1, 'one sender serves the coalesced requests');
my $sent = $senders[0];
is($sent->{hosts}, "192.0.2.10\n192.0.2.11\n", 'the closed group lists each requesting node once');
is($sent->{mode}, 0600, 'only the listener can read the host list');
is($sent->{bytes}, $content_a, 'the sender gets the published bytes');
is($sent->{log}, "$tmp/log/job-$first->{job}.log", 'the sender writes the log of its job');
is_deeply($sent->{command}, [ "$tmp/uftp", '-I', '192.0.2.1', '-U', '0xC0000201', '-p', '1044', '-M', '230.4.4.1',
'-P', '230.5.5.x', '-t', '1', '-R', '100000', '-b', '1300', '-N', '98', '-Y', 'aes256-gcm',
'-h', 'sha256', '-k', "$tmp/server.key", '-H', "\@$sent->{hostlist}", '-x', '2', '-l',
'-D', 'rootimg.cpio.gz', '/dev/fd/3' ], 'the sender gets only server-owned arguments');
is_deeply([ xCAT::UFTP::sender_pids($state) ], [ $sent->{pid} ], 'the listener tracks the running sender');
is(job_state($state, '192.0.2.10', $first->{job}, $t + 2)->{state}, 'running', 'a member sees the job run');
is(job_state($state, '192.0.2.99', $first->{job}, $t + 2)->{code}, 404, 'a node outside the job cannot query it');
my $late = transfer($state, '192.0.2.12', $sum_a, $path_a, $t + 3);
isnt($late->{job}, $first->{job}, 'a node arriving after the transfer started gets a new job');
xCAT::UFTP::sender_exited($state, $sent->{pid}, 0, $t + 4);
is(job_state($state, '192.0.2.10', $first->{job}, $t + 4)->{state}, 'done', 'a sender that exits with status 0 completes the job');
ok(!-e $sent->{hostlist}, 'the host list is removed when the job finishes');
is(scalar(xCAT::UFTP::sender_pids($state)), 0, 'a finished sender is no longer tracked');
xCAT::UFTP::sender_exited($state, 999999, 0, $t + 4);
is(job_state($state, '192.0.2.10', $first->{job}, $t + 4)->{state}, 'done', 'the exit of an unknown process changes no job');
xCAT::UFTP::run_jobs($state, $t + 63);
is(job_state($state, '192.0.2.10', $first->{job}, $t + 63)->{state}, 'done', 'a finished job stays for job_retention');
xCAT::UFTP::run_jobs($state, $t + 64);
is(job_state($state, '192.0.2.10', $first->{job}, $t + 64)->{code}, 404, 'a finished job is forgotten after job_retention');
$state = listener();
is(reply($state, '192.0.2.10', "GET / HTTP/1.0\r\nX-Image: $path_a\r\nReferer: /xcat-uftp/v1/transfer/$sum_a$path_a\r\n\r\n", $t)->{code},
404, 'an image path in a header does not select an image');
is(transfer($state, '192.0.2.10', $sum_a, '/etc/passwd', $t)->{code}, 400, 'a request for an unrelated file is refused');
ok((grep { /^warning: 192\.0\.2\.10: rejected request: unsupported image artifact$/ } @log), 'the log reports why a request is refused');
write_binary("$image_a/rootimg.tar.gz", "unrelated archive\n");
chmod(0644, "$image_a/rootimg.tar.gz");
is(transfer($state, '192.0.2.10', sha256_hex("unrelated archive\n"), "$image_a/rootimg.tar.gz", $t)->{code}, 403,
'an existing image file that was not published is refused');
is(transfer($state, '192.0.2.10', $sum_a, "$root/rhels9/../../../etc/rootimg.cpio.gz", $t)->{code}, 400, 'traversal is refused');
is(reply($state, '192.0.2.10', "GET /xcat-uftp/v1/transfer/$sum_a/install/%2e%2e/rootimg.cpio.gz HTTP/1.0\r\n\r\n", $t)->{code},
400, 'percent-encoded paths are refused');
is(transfer($state, '192.0.2.10', 'f' x 64, $path_a, $t)->{code}, 403, 'an unpublished digest is refused');
my $outside = "$tmp/outside/compute";
my $sum_out = publish($outside, "outside\n");
symlink($outside, "$root/escape") or die $!;
is(transfer($state, '192.0.2.10', $sum_out, "$root/escape/rootimg.cpio.gz", $t)->{code}, 403,
'a symbolic link escaping the image roots is refused');
is(reply($state, '192.0.2.10', "GET /xcat-uftp/v1/job/0123456789abcdef HTTP/1.0\r\n\r\n", $t)->{code}, 404,
'an unknown job is not found');
is(reply($state, '192.0.2.10', 'GET /x HTTP/1.0' . ('y' x 9000) . "\r\n\r\n", $t)->{code}, 413, 'an oversized request is refused');
is(scalar(my @none = xCAT::UFTP::listener_reply($state, '192.0.2.10', "GET / HTTP/1.0\r\n", $t)), 0,
'an incomplete request gets no reply yet');
is(reply($state, '192.0.2.10', "POST /xcat-uftp/v1/transfer/$sum_a$path_a HTTP/1.0\r\n\r\n", $t)->{code}, 405,
'a method other than GET is refused');
is(scalar(keys %{ $state->{jobs} }), 0, 'refused requests create no job');
my $odd = "$root/-H/-x";
my $sum_odd = publish($odd, "option-looking directory\n", 'rootimg.sfs');
is(transfer($state, '192.0.2.10', $sum_odd, "$odd/rootimg.sfs", $t)->{code}, 200, 'an image in an option-looking directory is accepted');
xCAT::UFTP::run_jobs($state, $t + 2);
is_deeply([ @{ $senders[0]{command} }[ -3 .. -1 ] ], [ '-D', 'rootimg.sfs', '/dev/fd/3' ],
'the sender gets the file name of the image');
is(scalar(grep { index($_, '/-H/-x') >= 0 } @{ $senders[0]{command} }), 0, 'no request text is passed to the sender');
is($senders[0]{bytes}, "option-looking directory\n", 'the option-looking image is sent intact');
my $image_b = "$root/rhels9/x86_64/service";
my $gen1 = "image B generation 1\n" x 100;
my $gen2 = "image B generation 2\n" x 120;
$state = listener();
my $sum_b1 = publish($image_b, $gen1);
my $old_gen = transfer($state, '192.0.2.20', $sum_b1, "$image_b/rootimg.cpio.gz", $t);
my $sum_b2 = publish($image_b, $gen2);
is(transfer($state, '192.0.2.21', $sum_b1, "$image_b/rootimg.cpio.gz", $t)->{code}, 403, 'a repacked generation is no longer offered');
my $new_gen = transfer($state, '192.0.2.21', $sum_b2, "$image_b/rootimg.cpio.gz", $t);
isnt($new_gen->{job}, $old_gen->{job}, 'different image generations never share a job');
xCAT::UFTP::run_jobs($state, $t + 2);
my %by_hosts = map { $_->{hosts} => $_->{bytes} } @senders;
is($by_hosts{"192.0.2.20\n"}, $gen1, 'a queued job still sends the generation it validated');
is($by_hosts{"192.0.2.21\n"}, $gen2, 'the new generation is sent to its own job');
$state = listener();
my $failing = transfer($state, '192.0.2.30', $sum_a, $path_a, $t);
xCAT::UFTP::run_jobs($state, $t + 2);
xCAT::UFTP::sender_exited($state, $senders[-1]{pid}, 7 << 8, $t + 3);
my $failed = job_state($state, '192.0.2.30', $failing->{job}, $t + 3);
is($failed->{state}, 'failed', 'a sender failure fails the job');
is($failed->{reason}, 'uftp exited with status 7', 'the job reports why the sender failed');
ok((grep { $_ eq "error: job $failing->{job}: failed (uftp exited with status 7)" } @log), 'the log reports the failed job');
my $retry = transfer($state, '192.0.2.30', $sum_a, $path_a, $t + 3);
isnt($retry->{job}, $failing->{job}, 'a retry after a failure gets a new job');
xCAT::UFTP::run_jobs($state, $t + 5);
xCAT::UFTP::sender_exited($state, $senders[-1]{pid}, 9, $t + 6);
is(job_state($state, '192.0.2.30', $retry->{job}, $t + 6)->{reason}, 'uftp killed by signal 9', 'the job reports the signal that stopped the sender');
my $unstarted = transfer($state, '192.0.2.31', $sum_a, $path_a, $t + 6);
$spawn_error = 'fork failed: Resource temporarily unavailable';
xCAT::UFTP::run_jobs($state, $t + 8);
undef $spawn_error;
my $unstarted_state = job_state($state, '192.0.2.31', $unstarted->{job}, $t + 8);
is($unstarted_state->{state}, 'failed', 'a sender that cannot start fails the job');
is($unstarted_state->{reason}, 'fork failed: Resource temporarily unavailable', 'the job reports why the sender did not start');
is(scalar(() = glob("$state->{workdir}/*.hosts")), 0, 'no host list stays after the jobs finish');
$state = listener(batch_delay => 0, max_transfers => 1, max_queued_jobs => 2, max_clients_per_job => 2, transfer_timeout => 3);
my $c_image = "$root/rhels9/x86_64/login";
my $sum_c = publish($c_image, "image C\n");
my $blocking = transfer($state, '192.0.2.40', $sum_a, $path_a, $t);
xCAT::UFTP::run_jobs($state, $t);
is(job_state($state, '192.0.2.40', $blocking->{job}, $t)->{state}, 'running', 'a zero batch delay starts the transfer at once');
my $waiting = transfer($state, '192.0.2.41', $sum_b2, "$image_b/rootimg.cpio.gz", $t);
xCAT::UFTP::run_jobs($state, $t + 1);
is(job_state($state, '192.0.2.41', $waiting->{job}, $t + 1)->{state}, 'pending', 'a second image waits while the transfer limit is reached');
is(transfer($state, '192.0.2.42', $sum_c, "$c_image/rootimg.cpio.gz", $t + 1)->{code}, 503, 'the queue limit refuses further images');
is(transfer($state, '192.0.2.42', $sum_b2, "$image_b/rootimg.cpio.gz", $t + 1)->{job}, $waiting->{job}, 'a node joins the waiting job');
my $full = transfer($state, '192.0.2.43', $sum_b2, "$image_b/rootimg.cpio.gz", $t + 1);
is($full->{code}, 503, 'the per-job client limit refuses further nodes');
is($full->{error}, 'transfer is full', 'the reply says the job is full');
xCAT::UFTP::run_jobs($state, $t + 2);
is_deeply(\@signals, [], 'a sender within its timeout keeps running');
xCAT::UFTP::run_jobs($state, $t + 3);
is_deeply(\@signals, ["TERM $senders[0]{pid}"], 'a sender that exceeds transfer_timeout is asked to stop');
xCAT::UFTP::sender_exited($state, $senders[0]{pid}, 15, $t + 3);
my $timed_out = job_state($state, '192.0.2.40', $blocking->{job}, $t + 3);
is($timed_out->{state}, 'failed', 'a transfer that exceeds its timeout fails');
is($timed_out->{reason}, 'transfer exceeded 3s', 'the timeout is reported');
xCAT::UFTP::run_jobs($state, $t + 3);
is(job_state($state, '192.0.2.41', $waiting->{job}, $t + 3)->{state}, 'running', 'the waiting transfer starts when the slot frees');
is($senders[1]{hosts}, "192.0.2.41\n192.0.2.42\n", 'the waiting transfer sends to the nodes that joined it');
@signals = ();
xCAT::UFTP::run_jobs($state, $t + 6);
xCAT::UFTP::run_jobs($state, $t + 10);
is_deeply(\@signals, ["TERM $senders[1]{pid}"], 'a sender gets the kill grace time after it is asked to stop');
xCAT::UFTP::run_jobs($state, $t + 11);
is_deeply(\@signals, [ "TERM $senders[1]{pid}", "KILL $senders[1]{pid}" ], 'a sender that ignores the request to stop is killed');
xCAT::UFTP::sender_exited($state, $senders[1]{pid}, 9, $t + 11);
is(job_state($state, '192.0.2.41', $waiting->{job}, $t + 11)->{reason}, 'transfer exceeded 3s', 'a killed sender reports the timeout');
$state = listener(batch_delay => 0, max_transfers => 1, transfer_timeout => 3);
transfer($state, '192.0.2.50', $sum_a, $path_a, $t);
xCAT::UFTP::run_jobs($state, $t);
my $starved = transfer($state, '192.0.2.51', $sum_c, "$c_image/rootimg.cpio.gz", $t);
xCAT::UFTP::run_jobs($state, $t + 3);
is(job_state($state, '192.0.2.51', $starved->{job}, $t + 3)->{state}, 'pending', 'a waiting job waits up to transfer_timeout for a slot');
xCAT::UFTP::run_jobs($state, $t + 4);
my $starved_state = job_state($state, '192.0.2.51', $starved->{job}, $t + 4);
is($starved_state->{state}, 'failed', 'a job that gets no slot within transfer_timeout fails');
is($starved_state->{reason}, 'no transfer slot became free', 'the job reports that no slot became free');
$state = listener();
my $queued = transfer($state, '192.0.2.60', $sum_a, $path_a, $t);
my $sending = transfer($state, '192.0.2.61', $sum_c, "$c_image/rootimg.cpio.gz", $t - 2);
xCAT::UFTP::run_jobs($state, $t);
xCAT::UFTP::sender_exited($state, $senders[0]{pid}, 15, $t, 'listener stopped');
xCAT::UFTP::stop_jobs($state, $t);
is(job_state($state, '192.0.2.61', $sending->{job}, $t)->{reason}, 'listener stopped', 'a sender stopped with the listener fails its job');
my $stopped = job_state($state, '192.0.2.60', $queued->{job}, $t);
is($stopped->{state}, 'failed', 'a queued job fails when the listener stops');
is($stopped->{reason}, 'listener stopped', 'the queued job reports that the listener stopped');
$state = listener(max_connections => 2, allowed_networks => '192.0.2.8/30');
is_deeply([ xCAT::UFTP::connection_refusal($state, 1, '192.0.2.9') ], [], 'an address inside allowed_networks is accepted');
is_deeply([ xCAT::UFTP::connection_refusal($state, 2, '192.0.2.9') ], [ 503, "error=too many connections\n" ],
'a connection beyond max_connections is refused');
is_deeply([ xCAT::UFTP::connection_refusal($state, 0, '192.0.2.12') ], [ 403, "error=client address is not allowed\n" ],
'an address outside allowed_networks is refused');
is_deeply(\@log, ['warning: 192.0.2.12: address is not in allowed_networks'], 'the log names the refused address');
$state = listener();
is_deeply([ xCAT::UFTP::connection_refusal($state, 0, '198.51.100.7') ], [], 'any address is accepted without allowed_networks');
done_testing();
+179
View File
@@ -0,0 +1,179 @@
#!/usr/bin/env perl
use strict;
use warnings;
use Digest::SHA qw(sha256_hex);
use Errno ();
use File::Path qw(make_path);
use File::Slurper qw(read_binary write_binary);
use File::Temp qw(tempdir);
use FindBin;
use lib "$FindBin::Bin/../../xCAT-server/lib/perl";
use Test::More;
our $fail_write;
BEGIN {
*CORE::GLOBAL::syswrite = sub {
if ($fail_write) {
$! = Errno::ENOSPC();
return undef;
}
return CORE::syswrite($_[0], $_[1], $_[2] // length($_[1]), $_[3] // 0);
};
}
use xCAT::UFTP;
my $tmp = tempdir(CLEANUP => 1);
sub entries {
my ($dir) = @_;
opendir(my $dh, $dir) or return ();
my @names = sort grep { $_ ne '.' && $_ ne '..' } readdir($dh);
closedir($dh);
return @names;
}
# genimage records the initial ramdisk it built with the receiver, and nodeset checks the copy that nodes boot.
my $initrd = "$tmp/initrd-stateless.gz";
write_binary($initrd, 'initramfs with receiver');
is(xCAT::UFTP::verify_initrd_marker($initrd) // '',
"$initrd was not built with the UFTP receiver; install the uftp package in the image and run genimage again",
'an initial ramdisk without a marker cannot receive over UFTP');
is(xCAT::UFTP::write_initrd_marker($initrd), undef, 'genimage records a ramdisk with the receiver');
is(scalar(read_binary("$initrd.uftp")), 'sha256=' . sha256_hex('initramfs with receiver') . "\n",
'the marker holds the SHA-256 of the ramdisk');
is((stat("$initrd.uftp"))[2] & 07777, 0644, 'the marker is readable');
is(xCAT::UFTP::verify_initrd_marker($initrd), undef, 'the recorded ramdisk is accepted');
write_binary("$tmp/tftp-copy.gz", 'initramfs with receiver');
is(xCAT::UFTP::verify_initrd_marker("$tmp/tftp-copy.gz", "$initrd.uftp"), undef,
'an identical copy of the ramdisk is accepted under the marker of the original');
write_binary("$tmp/other-initrd.gz", 'initramfs regenerated without uftp');
like(xCAT::UFTP::verify_initrd_marker("$tmp/other-initrd.gz", "$initrd.uftp"),
qr/is not the initial ramdisk that genimage built with the UFTP receiver/,
'another ramdisk is not accepted under an old marker');
write_binary("$tmp/broken.uftp", "format=1\n");
like(xCAT::UFTP::verify_initrd_marker($initrd, "$tmp/broken.uftp"), qr/is not a UFTP receiver marker/,
'a marker in another format is refused');
is(xCAT::UFTP::remove_initrd_marker($initrd), undef, 'the marker is removed');
is(xCAT::UFTP::remove_initrd_marker($initrd), undef, 'removing a missing marker succeeds');
like(xCAT::UFTP::verify_initrd_marker($initrd), qr/was not built with the UFTP receiver/,
'a ramdisk rebuilt without the receiver loses the marker');
my $atomic = "$tmp/atomic";
make_path($atomic);
write_binary("$atomic/server.fingerprint", "old fingerprint\n");
{
local $fail_write = 1;
like(xCAT::UFTP::write_file_atomic("$atomic/server.fingerprint", "new fingerprint\n", 0644),
qr/Cannot write/, 'a failed write is reported');
}
is(scalar(read_binary("$atomic/server.fingerprint")), "old fingerprint\n", 'a failed write keeps the existing file');
{
no warnings qw(redefine once);
local *IO::Handle::sync = sub { $! = Errno::EIO(); return 0 };
like(xCAT::UFTP::write_file_atomic("$atomic/server.fingerprint", "new fingerprint\n", 0644),
qr/Cannot flush/, 'a failed flush is reported');
}
is(scalar(read_binary("$atomic/server.fingerprint")), "old fingerprint\n", 'a failed flush keeps the existing file');
is_deeply([ entries($atomic) ], ['server.fingerprint'], 'failed writes leave no temporary file');
is(xCAT::UFTP::write_file_atomic("$atomic/server.fingerprint", "new fingerprint\n", 0644), undef,
'a complete write replaces the file');
is(scalar(read_binary("$atomic/server.fingerprint")), "new fingerprint\n", 'the replacement has the new content');
is((stat("$atomic/server.fingerprint"))[2] & 07777, 0644, 'the replacement has the requested mode');
sub elf {
my ($path, $class, $data, $machine) = @_;
my $ident = "\x7fELF" . chr($class) . chr($data) . "\x01" . ("\0" x 9);
my $type = $data == 2 ? pack('n', 2) : pack('v', 2);
my $mach = $data == 2 ? pack('n', $machine) : pack('v', $machine);
write_binary($path, $ident . $type . $mach . ("\0" x 44));
chmod(0755, $path);
}
elf("$tmp/x86_64", 2, 1, 62);
elf("$tmp/ppc64le", 2, 1, 21);
elf("$tmp/ppc64", 2, 2, 21);
elf("$tmp/aarch64", 2, 1, 183);
elf("$tmp/riscv64", 2, 1, 243);
is(xCAT::UFTP::elf_arch_error("$tmp/$_", $_), undef, "a $_ receiver matches a $_ image")
for qw(x86_64 ppc64le ppc64 aarch64 riscv64);
like(xCAT::UFTP::elf_arch_error("$tmp/x86_64", 'ppc64le'), qr/built for another architecture than ppc64le/,
'a management node binary is refused for another image architecture');
like(xCAT::UFTP::elf_arch_error("$tmp/ppc64le", 'ppc64'), qr/another architecture/,
'byte order distinguishes ppc64le from ppc64');
is(xCAT::UFTP::elf_arch_error("$tmp/ppc64le", 'ppc64el'), undef, 'a ppc64le receiver matches an Ubuntu ppc64el image');
like(xCAT::UFTP::elf_arch_error("$tmp/ppc64", 'ppc64el'), qr/another architecture/,
'a big-endian receiver is refused for an Ubuntu ppc64el image');
like(xCAT::UFTP::elf_arch_error("$tmp/aarch64", 'x86_64'), qr/another architecture/,
'an aarch64 receiver is refused for an x86_64 image');
write_binary("$tmp/script", "#!/bin/sh\n");
chmod(0755, "$tmp/script");
like(xCAT::UFTP::elf_arch_error("$tmp/script", 'x86_64'), qr/not an ELF executable/,
'a script is not accepted as a receiver');
like(xCAT::UFTP::elf_arch_error("$tmp/x86_64", 'sparc'), qr/not supported for architecture sparc/,
'unknown architectures are refused');
sub receiver_image {
my ($name, $class, $data, $machine, $version) = @_;
my $root = "$tmp/receiver-$name";
make_path("$root/usr/bin");
elf("$root/usr/bin/uftpd", $class, $data, $machine);
open(my $fh, '>>:raw', "$root/usr/bin/uftpd") or die $!;
print {$fh} "\0UFTP version $version Copyright\0";
close($fh);
return $root;
}
is_deeply([ xCAT::UFTP::usable_receiver(receiver_image('ok', 2, 1, 62, '5.0.3 (xCAT build 1)'), 'x86_64') ],
[ '/usr/bin/uftpd', undef ], 'a UFTP 5 receiver of xcat-dep for the image architecture is used');
my ($stock_receiver, $stock_warning) = xCAT::UFTP::usable_receiver(receiver_image('stock', 2, 1, 62, '5.0.3'), 'x86_64');
ok(!defined($stock_receiver), 'a UFTP 5 receiver without the xCAT build mark is left out');
like($stock_warning, qr/UFTP 5\.0\.3 without the xCAT fixes; use the uftp package of xcat-dep; the initial ramdisk will not/,
'a UFTP 5 receiver without the xCAT build mark gets a warning');
my ($none_receiver, $none_warning) = xCAT::UFTP::usable_receiver("$tmp/receiver-absent", 'x86_64');
ok(!defined($none_receiver) && !defined($none_warning), 'an image without uftpd gets no receiver and no warning');
my ($old_receiver, $old_warning) = xCAT::UFTP::usable_receiver(receiver_image('old', 2, 1, 62, '4.10.2'), 'x86_64');
ok(!defined($old_receiver), 'a UFTP 4 receiver is left out');
like($old_warning, qr/UFTP 5\.x is required; the initial ramdisk will not receive the root image over UFTP/,
'a UFTP 4 receiver gets a warning instead of failing the image build');
is_deeply([ xCAT::UFTP::usable_receiver(receiver_image('ppc64el', 2, 1, 21, '5.0.3 (xCAT build 1)'), 'ppc64el') ],
[ '/usr/bin/uftpd', undef ], 'a ppc64le receiver of xcat-dep is used for an Ubuntu ppc64el image');
my ($foreign_receiver, $foreign_warning) = xCAT::UFTP::usable_receiver(receiver_image('foreign', 2, 1, 183, '5.0.3 (xCAT build 1)'), 'x86_64');
ok(!defined($foreign_receiver), 'a receiver for another architecture is left out');
like($foreign_warning, qr/another architecture than x86_64/, 'a foreign receiver gets a warning');
write_binary("$tmp/uftp5", ("\0" x 70000) . "UFTP version 5.0.3 (xCAT build 1) Copyright (C) 2001-2023\0");
chmod(0755, "$tmp/uftp5");
write_binary("$tmp/uftp5-stock", "\0UFTP version 5.0.3 Copyright (C) 2001-2023\0");
chmod(0755, "$tmp/uftp5-stock");
write_binary("$tmp/uftp4", "\0UFTP version 4.10.2 Copyright\0");
chmod(0755, "$tmp/uftp4");
write_binary("$tmp/uftp-none", "\0no version here\0");
chmod(0755, "$tmp/uftp-none");
is(xCAT::UFTP::uftp_version_error("$tmp/uftp5"), undef, 'UFTP 5 binaries with the xCAT build mark are accepted');
like(xCAT::UFTP::uftp_version_error("$tmp/uftp5-stock"), qr/is UFTP 5\.0\.3 without the xCAT fixes/,
'UFTP 5 binaries without the xCAT build mark are refused');
like(xCAT::UFTP::uftp_version_error("$tmp/uftp4"), qr/is UFTP 4\.10\.2; UFTP 5\.x is required/,
'UFTP 4 binaries are refused');
like(xCAT::UFTP::uftp_version_error("$tmp/uftp-none"), qr/Cannot find the UFTP version/,
'binaries without a version string are refused');
my @listing = (
'drwxr-xr-x 2 root root 0 Jan 1 00:00 usr/bin',
'-rwxr-xr-x 1 root root 180000 Jan 1 00:00 usr/bin/uftpd',
'-rwxr-xr-x 1 root root 40000 Jan 1 00:00 usr/bin/sha256sum',
'-rw-r--r-- 1 root root 12000 Jan 1 00:00 lib/xcat-rootimg-fetch.sh',
);
is_deeply([ xCAT::UFTP::initrd_missing_files(\@listing) ], [], 'a complete initramfs listing passes');
my @sbin = @listing;
$sbin[1] =~ s{usr/bin/uftpd}{usr/sbin/uftpd};
is_deeply([ xCAT::UFTP::initrd_missing_files(\@sbin) ], [], 'a receiver in usr/sbin is accepted');
my @usrmerge = @listing;
$usrmerge[3] =~ s{lib/xcat-rootimg-fetch\.sh}{usr/lib/xcat-rootimg-fetch.sh};
push @usrmerge, 'lrwxrwxrwx 1 root root 7 Jan 1 00:00 lib -> usr/lib';
is_deeply([ xCAT::UFTP::initrd_missing_files(\@usrmerge) ], [], 'a helper under usr/lib in a usrmerged initramfs is accepted');
is_deeply([ xCAT::UFTP::initrd_missing_files([ @listing[0, 2, 3] ]) ], ['uftpd'],
'a missing receiver is reported');
is_deeply([ xCAT::UFTP::initrd_missing_files([ @listing[0, 1] ]) ],
[ 'sha256sum', 'xcat-rootimg-fetch.sh' ], 'missing verification files are reported');
done_testing();
+273
View File
@@ -0,0 +1,273 @@
#!/usr/bin/env perl
use strict;
use warnings;
use Cwd qw(realpath);
use Digest::SHA qw(sha256_hex);
use File::Path qw(make_path);
use File::Slurper qw(write_binary);
use File::Temp qw(tempdir);
use FindBin;
use lib "$FindBin::Bin/../../perl-xCAT";
use lib "$FindBin::Bin/../../xCAT-server/lib/perl";
use Test::More;
use xCAT::SvrUtils;
use xCAT::UFTP;
# The image checks refuse files that other users can write.
umask(022);
my $tmp = realpath(tempdir(CLEANUP => 1));
# packimage publishes the archive with its checksum, and the listener sends only such an archive.
sub publish {
my ($dir, $name, $content) = @_;
make_path($dir);
chmod(0755, $dir);
my ($stage, $error) = xCAT::SvrUtils->rootimg_stage($dir);
die $error unless $stage;
write_binary("$stage/$name", $content);
$error = xCAT::SvrUtils->write_rootimg_checksum("$stage/$name") // xCAT::SvrUtils->publish_rootimg("$stage/$name", "$dir/$name");
die $error if $error;
return sha256_hex($content);
}
my $sha = sha256_hex('x');
my $request = xCAT::UFTP::parse_http_request(
"GET /xcat-uftp/v1/transfer/$sha/install/netboot/img/rootimg.cpio.gz HTTP/1.1\r\n"
. "Host: 192.0.2.1:1045\r\nUser-Agent: curl/8\r\n\r\n");
is_deeply($request, { method => 'GET', target => "/xcat-uftp/v1/transfer/$sha/install/netboot/img/rootimg.cpio.gz" },
'the request target is parsed from the request line');
is_deeply(xCAT::UFTP::parse_http_request("GET /a HTTP/1.0\n\n"), { method => 'GET', target => '/a' },
'bare line feeds are accepted');
ok(xCAT::UFTP::parse_http_request("GET /a HTTP/1.0\r\nHost: x\r\n")->{incomplete},
'a request without the end of its headers is incomplete');
my $header_only = xCAT::UFTP::parse_http_request(
"GET / HTTP/1.0\r\nX-Image: /install/netboot/img/rootimg.cpio.gz\r\n\r\n");
is($header_only->{target}, '/', 'an image path in a header is not the request target');
for my $case (
[ "POST /a HTTP/1.0\r\n\r\n", 405 ],
[ "GET /a HTTP/2.0\r\n\r\n", 400 ],
[ "GET /a HTTP/1.0\r\n\r\n", 400 ],
[ "GET /a HTTP/1.0 extra\r\n\r\n", 400 ],
[ "GET /install/%2e%2e/etc/rootimg.cpio.gz HTTP/1.0\r\n\r\n", 400 ],
[ "GET /a?image=/install/x/rootimg.cpio.gz HTTP/1.0\r\n\r\n", 400 ],
[ "GET /a#frag HTTP/1.0\r\n\r\n", 400 ],
[ "GET /a\tb HTTP/1.0\r\n\r\n", 400 ],
[ "GET /a HTTP/1.0\r\nno colon here\r\n\r\n", 400 ],
[ "GET /a HTTP/1.0\r\n" . ("X: y\r\n" x 65) . "\r\n", 400 ],
) {
(my $label = $case->[0]) =~ s/[\r\n].*//s;
is(xCAT::UFTP::parse_http_request($case->[0])->{status}, $case->[1], "'$label' is answered with $case->[1]");
}
is(xCAT::UFTP::normalize_image_path('//install//netboot/rhels9/x86_64/compute/'),
'/install/netboot/rhels9/x86_64/compute', 'repeated and trailing slashes are normalized');
for my $path ('install/x', '/install/../etc', '/install/./x', '/install/.hidden/x', '/a b',
'/install/x%2e', '/install/x?y', '/' . ('a/' x 70), '/' . ('a' x 300)) {
is(xCAT::UFTP::normalize_image_path($path), undef, "image path '$path' is refused");
}
my $prefix = "/xcat-uftp/v1/transfer/$sha";
my ($digest, $path, $error) = xCAT::UFTP::parse_transfer_target("$prefix/install/netboot/img/rootimg.tar.xz");
is_deeply([ $digest, $path, $error ], [ $sha, '/install/netboot/img/rootimg.tar.xz', undef ],
'a transfer target yields the digest and the image path');
for my $target (
"$prefix/install/netboot/img/../../../etc/rootimg.cpio.gz",
"$prefix//install/netboot/img/rootimg.cpio.gz",
"$prefix/install/netboot/img/rootimg.cpio.gz/",
"$prefix/install/netboot/img/.hidden/rootimg.cpio.gz",
"$prefix/etc/passwd",
"$prefix/install/netboot/img/rootimg.gz",
'/xcat-uftp/v1/transfer/' . ('A' x 64) . '/install/netboot/img/rootimg.cpio.gz',
'/xcat-uftp/v1/transfer/' . ('a' x 63) . '/install/netboot/img/rootimg.cpio.gz',
'/xcat-uftp/v2/transfer/' . $sha . '/install/netboot/img/rootimg.cpio.gz',
) {
my (undef, undef, $target_error) = xCAT::UFTP::parse_transfer_target($target);
ok($target_error, "transfer target '$target' is refused");
}
is_deeply([ xCAT::UFTP::parse_job_target('/xcat-uftp/v1/job/0123456789abcdef') ], [ '0123456789abcdef', undef ],
'a job target yields the job identifier');
ok((xCAT::UFTP::parse_job_target('/xcat-uftp/v1/job/../../x'))[1], 'a malformed job target is refused');
my $root = "$tmp/install/netboot";
my $image_dir = "$root/rhels9/x86_64/compute";
my $digest_published = publish($image_dir, 'rootimg.cpio.gz', "published image\n");
my @roots = (realpath($root));
my ($image, $resolve_error) = xCAT::UFTP::resolve_artifact(\@roots, "$image_dir/rootimg.cpio.gz", $digest_published);
is($resolve_error, undef, 'a published image under an image root resolves');
is($image->{name}, 'rootimg.cpio.gz', 'the transfer keeps the archive name');
is($image->{size}, length("published image\n"), 'the resolved image has the published size');
my @published = stat("$image_dir/rootimg.cpio.gz");
is("$image->{dev}:$image->{ino}", "$published[0]:$published[1]", 'the job is keyed to the published inode');
# A batch keeps the bytes it opened when packimage publishes the next archive.
my $next = publish($image_dir, 'rootimg.cpio.gz', "next image\n");
my $bytes = do { local $/; my $fh = $image->{fh}; <$fh> };
is($bytes, "published image\n", 'the open handle reads the archive of its batch after a new publication');
close($image->{fh});
sub refused {
my ($roots, $path, $digest, $pattern, $label) = @_;
my ($resolved, $refusal) = xCAT::UFTP::resolve_artifact($roots, $path, $digest);
close($resolved->{fh}) if $resolved;
like($refusal // '', $pattern, $label);
}
refused(\@roots, "$image_dir/rootimg.cpio.gz", $digest_published,
qr/not the published one/, 'the digest of a replaced archive is refused');
refused(\@roots, "$image_dir/rootimg.cpio.gz", sha256_hex('another generation'),
qr/not the published one/, 'a digest that the checksum file does not list is refused');
write_binary("$image_dir/rootimg.cpio.gz.sha256", "$next rootimg.cpio.gz\n$digest_published rootimg.cpio.gz\n");
my ($during) = xCAT::UFTP::resolve_artifact(\@roots, "$image_dir/rootimg.cpio.gz", $digest_published);
ok($during, 'while packimage replaces the archive, the previous digest in the checksum file is accepted');
close($during->{fh}) if $during;
write_binary("$image_dir/rootimg.cpio.gz.sha256", "$next rootimg.cpio.xz\n");
refused(\@roots, "$image_dir/rootimg.cpio.gz", $next, qr/not the published one/,
'a checksum line for another archive name is refused');
write_binary("$image_dir/rootimg.cpio.gz.sha256", "$next rootimg.cpio.gz\n");
write_binary("$image_dir/rootimg.cpio.xz", "unrelated existing file\n");
refused(\@roots, "$image_dir/rootimg.cpio.xz", sha256_hex("unrelated existing file\n"),
qr/image is not published: .*rootimg\.cpio\.xz\.sha256 does not exist/, 'an archive without a checksum file is refused');
my $outside = "$tmp/srv/images/compute";
my $outside_digest = publish($outside, 'rootimg.cpio.gz', "outside image\n");
refused(\@roots, "$outside/rootimg.cpio.gz", $outside_digest,
qr/outside the configured image roots/, 'a published image outside the image roots is refused');
my $sibling = "$tmp/install/netboot2/compute";
my $sibling_digest = publish($sibling, 'rootimg.cpio.gz', "sibling image\n");
refused(\@roots, "$sibling/rootimg.cpio.gz", $sibling_digest,
qr/outside the configured image roots/, 'a directory sharing a name prefix with a root is refused');
my ($configured) = xCAT::UFTP::resolve_artifact([ @roots, realpath("$tmp/srv/images") ],
"$outside/rootimg.cpio.gz", $outside_digest);
ok($configured, 'an explicitly configured image root outside /install is accepted');
close($configured->{fh}) if $configured;
symlink($outside, "$root/escape") or die $!;
refused(\@roots, "$root/escape/rootimg.cpio.gz", $outside_digest,
qr/outside the configured image roots/, 'a symbolic link that escapes the image roots is refused');
symlink($image_dir, "$root/alias") or die $!;
my ($alias) = xCAT::UFTP::resolve_artifact(\@roots, "$root/alias/rootimg.cpio.gz", $next);
my @current = stat("$image_dir/rootimg.cpio.gz");
ok($alias && $alias->{ino} == $current[1], 'a symbolic link that stays inside an image root is resolved');
close($alias->{fh}) if $alias;
my $swapped = "$root/swapped";
my $swapped_digest = publish($swapped, 'rootimg.cpio.gz', "swapped archive\n");
write_binary("$tmp/secret", "secret readable by the service\n");
unlink("$swapped/rootimg.cpio.gz") or die $!;
symlink("$tmp/secret", "$swapped/rootimg.cpio.gz") or die $!;
refused(\@roots, "$swapped/rootimg.cpio.gz", $swapped_digest,
qr/is a symbolic link/, 'an archive replaced by a symbolic link is refused');
my $linked_sums = "$root/linked-sums";
my $linked_digest = publish($linked_sums, 'rootimg.cpio.gz', "linked checksum\n");
rename("$linked_sums/rootimg.cpio.gz.sha256", "$tmp/moved.sha256") or die $!;
symlink("$tmp/moved.sha256", "$linked_sums/rootimg.cpio.gz.sha256") or die $!;
refused(\@roots, "$linked_sums/rootimg.cpio.gz", $linked_digest,
qr/is a symbolic link/, 'a checksum file replaced by a symbolic link is refused');
my $writable = "$root/writable";
my $writable_digest = publish($writable, 'rootimg.cpio.gz', "writable\n");
chmod(0777, $writable) or die $!;
refused(\@roots, "$writable/rootimg.cpio.gz", $writable_digest,
qr/writable by other users/, 'an image directory writable by others is refused');
chmod(0755, $writable) or die $!;
chmod(0666, "$writable/rootimg.cpio.gz") or die $!;
refused(\@roots, "$writable/rootimg.cpio.gz", $writable_digest,
qr/writable by other users/, 'an archive writable by others is refused');
my $empty = "$root/empty";
make_path($empty);
chmod(0755, $empty) or die $!;
write_binary("$empty/rootimg.cpio.gz", '');
write_binary("$empty/rootimg.cpio.gz.sha256", sha256_hex('') . " rootimg.cpio.gz\n");
refused(\@roots, "$empty/rootimg.cpio.gz", sha256_hex(''), qr/is empty/, 'an empty archive is refused');
refused(\@roots, "$root/missing/rootimg.cpio.gz", $next, qr/does not exist/,
'a missing image directory is refused');
my $config_file = "$tmp/listener.conf";
sub config {
my ($text) = @_;
write_binary($config_file, $text);
return xCAT::UFTP::load_listener_config($config_file);
}
my ($config, $config_error) = config("listen_address = 192.0.2.1\nimage_roots = $root\n");
is($config_error, undef, 'a minimal configuration loads');
is($config->{listen_port}, 1045, 'the listener port defaults to 1045');
is($config->{udp_port}, 1044, 'the UFTP port defaults to 1044');
is($config->{batch_delay}, 30, 'the batch delay defaults to 30 seconds');
is($config->{block_size}, 1300, 'the block size defaults to one that fits an MTU of 1500');
is($config->{multicast_interface}, '192.0.2.1', 'multicast is sent from the listen address by default');
is($config->{server_id}, '0xC0000201', 'the UFTP server ID is the sending address');
is_deeply($config->{image_roots}, \@roots, 'image roots are canonical paths');
($config) = config("listen_address = 192.0.2.1\nimage_roots = $root\nbatch_delay = 0\n");
is($config->{batch_delay}, 0, 'a zero batch delay is supported');
($config, $config_error) = config("listen_address = 192.0.2.1\nimage_roots = $root\nmax_transfers =\nblock_size =\n"
. "uftp =\nallowed_networks =\nmulticast_interface =\n");
is($config_error, undef, 'a configuration with empty values loads');
is($config->{max_transfers}, 4, 'an empty max_transfers takes its default');
is($config->{block_size}, 1300, 'an empty block_size takes its default');
is($config->{uftp}, '/usr/bin/uftp', 'an empty uftp path takes its default');
ok(xCAT::UFTP::address_allowed($config->{allowed_networks}, '198.51.100.8'), 'empty allowed_networks allow any address');
is($config->{multicast_interface}, '192.0.2.1', 'an empty multicast_interface sends from the listen address');
for my $size (512, 8000, 8800) {
my ($sized, $sized_error) = config("listen_address = 192.0.2.1\nimage_roots = $root\nblock_size = $size\n");
is($sized_error, undef, "a block size of $size is accepted");
is($sized->{block_size}, $size, "the block size of $size is kept");
}
($config) = config("listen_address = 192.0.2.1\nimage_roots = $root, $tmp/srv/images\n"
. "allowed_networks = 192.0.2.0/24, 198.51.100.7/32\nmulticast_interface = 192.0.2.9\n");
ok(xCAT::UFTP::address_allowed($config->{allowed_networks}, '192.0.2.200'), 'an address inside allowed_networks is allowed');
ok(xCAT::UFTP::address_allowed($config->{allowed_networks}, '198.51.100.7'), 'a host network is allowed');
ok(!xCAT::UFTP::address_allowed($config->{allowed_networks}, '198.51.100.8'), 'other addresses are refused');
is($config->{server_id}, '0xC0000209', 'a separate multicast interface sets the server ID');
is(scalar(@{ $config->{image_roots} }), 2, 'several image roots are accepted');
for my $case (
[ "image_roots = $root\n", qr/listen_address is required/ ],
[ "listen_address =\nimage_roots = $root\n", qr/listen_address is required/ ],
[ "listen_address = ::1\nimage_roots = $root\n", qr/listen_address must be an IPv4 address/ ],
[ "listen_address = 192.0.2.1\nimage_roots = $root\nlisten_port = 0\n", qr/between 1 and 65535/ ],
[ "listen_address = 192.0.2.1\nimage_roots = $root\nlisten_port = 65536\n", qr/between 1 and 65535/ ],
[ "listen_address = 192.0.2.1\nimage_roots = $root\nudp_port = 10a\n", qr/udp_port must be an integer/ ],
[ "listen_address = 192.0.2.1\nimage_roots = $root\nbatch_delay = -1\n", qr/batch_delay must be an integer/ ],
[ "listen_address = 192.0.2.1\nimage_roots = $root\nbatch_delay = 3601\n", qr/between 0 and 3600/ ],
[ "listen_address = 192.0.2.1\nimage_roots = $root\nbatch_delay = 030\n", qr/must be an integer/ ],
[ "listen_address = 192.0.2.1\nimage_roots = $root\ntransfer_timeout = 0\n", qr/between 1 and 86400/ ],
[ "listen_address = 192.0.2.1\nimage_roots = $root\nblock_size = 511\n", qr/block_size must be between 512 and 8800/ ],
[ "listen_address = 192.0.2.1\nimage_roots = $root\nblock_size = 8801\n", qr/block_size must be between 512 and 8800/ ],
[ "listen_address = 192.0.2.1\nimage_roots = $root\npublic_multicast = 10.0.0.1\n", qr/multicast address/ ],
[ "listen_address = 192.0.2.1\nimage_roots = $root\nencryption = none\n", qr/encryption must be one of/ ],
[ "listen_address = 192.0.2.1\nimage_roots = $root\nuftp = uftp\n", qr/uftp must be an absolute path/ ],
[ "listen_address = 192.0.2.1\nimage_roots = relative\n", qr/must be absolute paths/ ],
[ "listen_address = 192.0.2.1\nimage_roots = $tmp/none\n", qr/is not a directory/ ],
[ "listen_address = 192.0.2.1\nimage_roots = $root\nallowed_networks = 192.0.2.0/33\n", qr/address\/prefix/ ],
[ "listen_address = 192.0.2.1\nimage_roots = $root\nrate = 5\n", qr/unknown key 'rate'/ ],
[ "listen_address = 192.0.2.1\nlisten_address = 192.0.2.2\nimage_roots = $root\n", qr/duplicate key/ ],
[ "listen_address 192.0.2.1\n", qr/expected key = value/ ],
) {
my (undef, $refusal) = config($case->[0]);
like($refusal // '', $case->[1], "configuration is refused: $case->[1]");
}
is(xCAT::UFTP::parse_fingerprint("/etc/xcat/uftp/server.key: ECDSA, curve prime256v1, fingerprint: "
. "31:94:09:9D:B1:24:3F:32:C0:B9:1A:BB:AC:F4:74:2B:80:F9:77:B2\n"),
'31:94:09:9D:B1:24:3F:32:C0:B9:1A:BB:AC:F4:74:2B:80:F9:77:B2', 'uftp_keymgt output yields the fingerprint');
is(xCAT::UFTP::parse_fingerprint("31:94:09\n"), undef, 'a short fingerprint is refused');
($config) = config("listen_address = 192.0.2.1\nimage_roots = $root\nrate_kbps = 900000\n");
is_deeply([ xCAT::UFTP::uftp_command($config, 'rootimg.cpio.gz', '/run/x/job.hosts') ], [
'/usr/bin/uftp', '-I', '192.0.2.1', '-U', '0xC0000201', '-p', 1044, '-M', '230.4.4.1',
'-P', '230.5.5.x', '-t', 1, '-R', 900000, '-b', 1300, '-N', 98, '-Y', 'aes256-gcm',
'-h', 'sha256', '-k', '/etc/xcat/uftp/server.key', '-H', '@/run/x/job.hosts', '-x', 2, '-l',
'-D', 'rootimg.cpio.gz', '/dev/fd/3' ],
'the sender argument vector holds only server-owned values and the inherited image descriptor');
($config) = config("listen_address = 192.0.2.1\nimage_roots = $root\nblock_size = 8000\n");
my @command = xCAT::UFTP::uftp_command($config, 'rootimg.cpio.gz', '/run/x/job.hosts');
my ($block_size) = map { $command[ $_ + 1 ] } grep { $command[$_] eq '-b' } 0 .. $#command - 1;
is($block_size, 8000, 'the configured block size reaches the sender');
done_testing();