diff --git a/.clang-format-version b/.clang-format-version index 209e3ef4..2bd5a0a9 100644 --- a/.clang-format-version +++ b/.clang-format-version @@ -1 +1 @@ -20 +22 diff --git a/.github/workflows/clang-format-check.yml b/.github/workflows/clang-format-check.yml index aa789a3e..d882c2fa 100644 --- a/.github/workflows/clang-format-check.yml +++ b/.github/workflows/clang-format-check.yml @@ -36,11 +36,40 @@ jobs: set -euo pipefail REQUIRED_CLANG_FORMAT_VERSION="$(tr -d '[:space:]' < .clang-format-version)" - CLANG_FORMAT_BIN="" - if [[ -n "${REQUIRED_CLANG_FORMAT_VERSION}" ]] && command -v "clang-format-${REQUIRED_CLANG_FORMAT_VERSION}" >/dev/null 2>&1; then - CLANG_FORMAT_BIN="$(command -v "clang-format-${REQUIRED_CLANG_FORMAT_VERSION}")" - elif command -v clang-format >/dev/null 2>&1; then - CLANG_FORMAT_BIN="$(command -v clang-format)" + resolve_clang_format_bin() { + CLANG_FORMAT_BIN="" + if [[ -n "${REQUIRED_CLANG_FORMAT_VERSION}" ]] && command -v "clang-format-${REQUIRED_CLANG_FORMAT_VERSION}" >/dev/null 2>&1; then + CLANG_FORMAT_BIN="$(command -v "clang-format-${REQUIRED_CLANG_FORMAT_VERSION}")" + elif command -v clang-format >/dev/null 2>&1; then + CLANG_FORMAT_BIN="$(command -v clang-format)" + fi + } + + clang_format_major_version() { + "${CLANG_FORMAT_BIN}" --version | sed -nE 's/.* ([0-9]+)(\.[0-9]+)+.*/\1/p' | head -n 1 + } + + resolve_clang_format_bin + + if [[ -n "${REQUIRED_CLANG_FORMAT_VERSION}" ]]; then + current_major="" + if [[ -n "${CLANG_FORMAT_BIN}" ]]; then + current_major="$(clang_format_major_version)" + fi + + if [[ "${current_major}" != "${REQUIRED_CLANG_FORMAT_VERSION}" ]]; then + command -v python3 >/dev/null 2>&1 || { + echo "python3 is required to install clang-format ${REQUIRED_CLANG_FORMAT_VERSION}" >&2 + exit 1 + } + + next_version="$((REQUIRED_CLANG_FORMAT_VERSION + 1))" + python3 -m pip install --user --upgrade \ + "clang-format>=${REQUIRED_CLANG_FORMAT_VERSION}.0,<${next_version}.0" + + export PATH="$(python3 -m site --user-base)/bin:${PATH}" + resolve_clang_format_bin + fi fi [[ -n "${CLANG_FORMAT_BIN}" ]] || { diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index a3cd7d23..eeae52bc 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -210,3 +210,99 @@ jobs: rpmbuild -ba ~/rpmbuild/SPECS/opencattus.spec ls -l ~/rpmbuild/RPMS/*/opencattus-installer*.rpm ' + + deb-build: + name: DEB Build (Ubuntu 24.04) + needs: fast-tests + runs-on: + - self-hosted + - Linux + - X64 + timeout-minutes: 60 + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Verify Podman runner state + run: | + set -euo pipefail + command -v podman >/dev/null || { + echo "Missing required command on self-hosted runner: podman" >&2 + exit 1 + } + + podman system migrate || true + podman info >/dev/null + + - name: Prepare Conan cache + run: | + set -euo pipefail + mkdir -p "${HOME}/.cache/opencattus/conan/ubuntu24" + mkdir -p "${HOME}/.cache/opencattus/cpm/ubuntu24" + mkdir -p "${HOME}/.cache/opencattus/sources/ubuntu24" + + - name: Build DEB in Ubuntu 24.04 container + run: | + set -euo pipefail + podman run --rm \ + -v "${GITHUB_WORKSPACE}:${GITHUB_WORKSPACE}:z" \ + -v "${HOME}/.cache/opencattus/conan/ubuntu24:/root/.conan2:z" \ + -v "${HOME}/.cache/opencattus/cpm/ubuntu24:/root/.cache/CPM:z" \ + -v "${HOME}/.cache/opencattus/sources/ubuntu24:/root/.cache/opencattus/sources:z" \ + -w "${GITHUB_WORKSPACE}" \ + docker.io/library/ubuntu:24.04 \ + bash -lc ' + set -euo pipefail + export DEBIAN_FRONTEND=noninteractive + apt update + apt install -y ca-certificates curl git dpkg-dev \ + build-essential cmake ninja-build \ + pkg-config python3 python3-pip python3-venv \ + autoconf automake libtool meson perl \ + libglibmm-2.4-dev libnewt-dev libsystemd-dev + export OPENCATTUS_SOURCE_CACHE="/root/.cache/opencattus/sources" + mkdir -p "${OPENCATTUS_SOURCE_CACHE}" + fetch_source() { + local url="$1" + local path="$2" + local tmp="${path}.tmp" + + if [ -s "${path}" ] && python3 -m zipfile -t "${path}" >/dev/null 2>&1; then + return 0 + fi + + rm -f "${path}" "${tmp}" + for attempt in 1 2 3 4 5; do + if curl --fail --location --retry 8 --retry-delay 5 \ + --connect-timeout 20 --output "${tmp}" "${url}" && + python3 -m zipfile -t "${tmp}" >/dev/null 2>&1; then + mv "${tmp}" "${path}" + return 0 + fi + + rm -f "${tmp}" + sleep 5 + done + + echo "Failed to fetch ${url}" >&2 + return 1 + } + export OPENCATTUS_FARGS_ARCHIVE="${OPENCATTUS_SOURCE_CACHE}/cmake-forward-arguments-8c50d1f956172edb34e95efa52a2d5cb1f686ed2.zip" + export OPENCATTUS_YCM_ARCHIVE="${OPENCATTUS_SOURCE_CACHE}/ycm-v0.13.0.zip" + fetch_source \ + "https://github.com/polysquare/cmake-forward-arguments/archive/8c50d1f956172edb34e95efa52a2d5cb1f686ed2.zip" \ + "${OPENCATTUS_FARGS_ARCHIVE}" + fetch_source \ + "https://github.com/robotology/ycm/archive/refs/tags/v0.13.0.zip" \ + "${OPENCATTUS_YCM_ARCHIVE}" + python3 -m venv /tmp/conan-venv + /tmp/conan-venv/bin/pip install conan + export PATH="/tmp/conan-venv/bin:${PATH}" + conan profile detect --force + git config --global --add safe.directory "$(pwd)" + cmake -S . -B build-deb -G Ninja -DCMAKE_BUILD_TYPE=Debug -DBUILD_TESTING=OFF + cmake --build build-deb --target opencattus -j"$(nproc)" + scripts/build-deb.sh build-deb out/deb + ls -l out/deb/opencattus-installer*.deb + ' diff --git a/ansible/Dockerfile b/ansible/Dockerfile index 3d5c5481..9b853628 100644 --- a/ansible/Dockerfile +++ b/ansible/Dockerfile @@ -2,5 +2,5 @@ FROM vagrantlibvirt/vagrant-libvirt:latest ENV VAGRANT_DISABLE_STRICT_DEPENDENCY_ENFORCEMENT=1 ARG DEBIAN_FRONTEND=noninteractive ENV TZ=America/Sao_Paulo -RUN apt-get update -y && apt-get install ansible -y +RUN apt update -y && apt install ansible -y RUN vagrant plugin install vagrant-scp diff --git a/format-changed.sh b/format-changed.sh index d8dfad0d..c66987ea 100755 --- a/format-changed.sh +++ b/format-changed.sh @@ -52,7 +52,6 @@ resolve_clang_format_bin() { candidates+=("/opt/homebrew/opt/llvm@${required_version}/bin/clang-format") fi - candidates+=("clang-format-19") candidates+=("clang-format") for candidate in "${candidates[@]}"; do diff --git a/include/opencattus/models/os.h b/include/opencattus/models/os.h index ff4efc81..e7e33cc8 100644 --- a/include/opencattus/models/os.h +++ b/include/opencattus/models/os.h @@ -48,13 +48,13 @@ class OS { * @enum Platform * @brief Enumeration representing different platforms of the OS. */ - enum class Platform { el8, el9, el10 }; + enum class Platform { el8, el9, el10, ubuntu24 }; /** * @enum Distro * @brief Enumeration representing different distributions of the OS. */ - enum class Distro { RHEL, OL, Rocky, AlmaLinux }; + enum class Distro { RHEL, OL, Rocky, AlmaLinux, Ubuntu }; /** * @enum PackageManager @@ -68,8 +68,8 @@ class OS { std::variant m_platform; std::variant m_distro; std::optional m_kernel; // kernel version may be uninitialized - unsigned m_majorVersion {}; - unsigned m_minorVersion {}; + unsigned m_majorVersion { }; + unsigned m_minorVersion { }; void setMajorVersion(unsigned int majorVersion); diff --git a/include/opencattus/services/xcat.h b/include/opencattus/services/xcat.h index 1d4d403f..2e41725c 100644 --- a/include/opencattus/services/xcat.h +++ b/include/opencattus/services/xcat.h @@ -32,7 +32,7 @@ namespace opencattus::services { class XCAT : public Provisioner { public: struct Image { - std::vector otherpkgs = {}; + std::vector otherpkgs = { }; // @TODO: We need to support more than one osimage (: // this can be a default osimage though std::string osimage; @@ -230,6 +230,11 @@ class XCAT : public Provisioner { */ static void configureEL9(); + /** + * @brief Configures xCAT netboot templates for Ubuntu 24.04. + */ + static void configureUbuntu24(); + public: XCAT(); @@ -275,7 +280,7 @@ class XCAT : public Provisioner { */ void createImage(ImageType = ImageType::Netboot, NodeType = NodeType::Compute, - const std::vector& customizations = {}); + const std::vector& customizations = { }); /** * @brief Adds nodes to the provisioning system. diff --git a/scripts/build-deb.sh b/scripts/build-deb.sh new file mode 100755 index 00000000..a4415db2 --- /dev/null +++ b/scripts/build-deb.sh @@ -0,0 +1,62 @@ +#!/usr/bin/env bash + +set -Eeuo pipefail + +build_dir=${1:-build-host} +output_dir=${2:-out/deb} +package_name=${OPENCATTUS_DEB_PACKAGE_NAME:-opencattus-installer-debug} + +version=$(awk '/^Version:/ {print $2; exit}' rpmspecs/opencattus.spec) +release=$(awk '/^Release:/ {print $2; exit}' rpmspecs/opencattus.spec) +architecture=$(dpkg --print-architecture) +package_root="${output_dir}/${package_name}_${version}-${release}_${architecture}" +binary_path="${build_dir}/src/opencattus" + +[[ -x "${binary_path}" ]] || { + echo "OpenCATTUS binary not found or not executable: ${binary_path}" >&2 + exit 1 +} + +rm -rf "${package_root}" +install -d \ + "${package_root}/DEBIAN" \ + "${package_root}/usr/bin" \ + "${package_root}/opt/opencattus/conf/repos" + +install -m 755 "${binary_path}" "${package_root}/usr/bin/opencattus" +install -m 644 repos/repos.conf "${package_root}/opt/opencattus/conf/repos/repos.conf" +install -m 644 repos/alma.conf "${package_root}/opt/opencattus/conf/repos/alma.conf" +install -m 644 repos/rhel.conf "${package_root}/opt/opencattus/conf/repos/rhel.conf" +install -m 644 repos/oracle.conf "${package_root}/opt/opencattus/conf/repos/oracle.conf" +install -m 644 repos/rocky-upstream.conf \ + "${package_root}/opt/opencattus/conf/repos/rocky-upstream.conf" +install -m 644 repos/rocky-vault.conf \ + "${package_root}/opt/opencattus/conf/repos/rocky-vault.conf" + +depends="" +if command -v dpkg-shlibdeps >/dev/null 2>&1; then + depends=$(dpkg-shlibdeps -O -e"${package_root}/usr/bin/opencattus" 2>/dev/null \ + | sed -n 's/^shlibs:Depends=//p' || true) +fi + +if [[ -z "${depends}" ]]; then + depends="libnewt0.52" +fi + +installed_size=$(du -sk "${package_root}" | awk '{print $1}') +cat >"${package_root}/DEBIAN/control" < +Depends: ${depends} +Installed-Size: ${installed_size} +Homepage: https://github.com/versatushpc/opencattus +Description: OpenCATTUS Installer + OpenCATTUS installs and configures an HPC cluster from a single + head node. +EOF + +dpkg-deb --build --root-owner-group "${package_root}" "${output_dir}" diff --git a/src/NFS.cpp b/src/NFS.cpp index 82f92d68..9ef70a8d 100644 --- a/src/NFS.cpp +++ b/src/NFS.cpp @@ -45,19 +45,28 @@ opencattus::services::ScriptBuilder NFS::installScript(const OS& osinfo) { using namespace opencattus; services::ScriptBuilder builder(osinfo); + const auto nfsServerPackage + = osinfo.getPackageType() == OS::PackageType::DEB + ? std::string_view("nfs-kernel-server") + : std::string_view("nfs-utils"); + const auto nfsServerServices + = osinfo.getPackageType() == OS::PackageType::DEB + ? std::string_view("rpcbind nfs-kernel-server") + : std::string_view("rpcbind nfs-server"); builder.addNewLine() .addCommand("# Variables") .addCommand("HEADNODE=$(hostname -s)") .addNewLine() .addCommand("# install packages") - .addPackage("nfs-utils") + .addPackage(nfsServerPackage) .addNewLine() .addCommand("# Add exports to /etc/exports") - .addLineToFile("/etc/exports", "/home", + .addLineToFile("/etc/exports", "^/home[[:space:]]", "/home *(rw,no_subtree_check,fsid={},no_root_squash)", 10) .addLineToFile("/etc/exports", "/opt/ohpc/pub", "/opt/ohpc/pub *(ro,no_subtree_check,fsid={})", 11) - .addLineToFile("/etc/exports", "/opt/spack", "/opt/spack *(ro)"); + .addLineToFile( + "/etc/exports", "/opt/spack", "/opt/spack *(ro,no_subtree_check)"); if (utils::singleton::answerfile()->system.provisioner == "xcat") { builder .addLineToFile("/etc/exports", "/tftpboot", @@ -66,7 +75,7 @@ opencattus::services::ScriptBuilder NFS::installScript(const OS& osinfo) "/install *(rw,no_root_squash,sync,no_subtree_check)") .addNewLine(); } - builder.enableService("rpcbind nfs-server") + builder.enableService(nfsServerServices) .addCommand("exportfs -a > /dev/null 2>&1 || :") .addNewLine() .addCommand(R"(# Update firewall rules @@ -82,6 +91,10 @@ opencattus::services::ScriptBuilder NFS::imageInstallScript( { using namespace opencattus; services::ScriptBuilder builder(osinfo); + const auto nfsClientPackage + = osinfo.getPackageType() == OS::PackageType::DEB + ? std::string_view("nfs-common") + : std::string_view("nfs-utils"); builder.addNewLine() .addCommand("# Define variables (for shell script execution)") .addCommand("IMAGE=\"{}\"", args.imageName) @@ -96,7 +109,7 @@ opencattus::services::ScriptBuilder NFS::imageInstallScript( .addCommand("chmod +x \"${{POSTINSTALL}}\"") .addNewLine() .addCommand("# Add required packages to the image") - .addLineToFile("${PKGLIST}", "nfs-utils", "nfs-utils") + .addLineToFile("${PKGLIST}", nfsClientPackage, "{}", nfsClientPackage) .addLineToFile("${PKGLIST}", "autofs", "autofs") .addNewLine() .addCommand("# Configure autofs") @@ -144,7 +157,9 @@ TEST_CASE("installScript") CHECK(script.contains("systemctl is-active --quiet firewalld.service")); CHECK( script.contains("/home *(rw,no_subtree_check,fsid=10,no_root_squash)")); + CHECK(script.contains(R"(grep -q "^/home[[:space:]]" "/etc/exports")")); CHECK(script.contains("/opt/ohpc/pub *(ro,no_subtree_check,fsid=11)")); + CHECK(script.contains("/opt/spack *(ro,no_subtree_check)")); CHECK(script.contains( "/tftpboot *(rw,no_root_squash,sync,no_subtree_check)")); CHECK( @@ -185,6 +200,41 @@ TEST_CASE("installImageScript") R"(chdef -t osimage ${IMAGE} postinstall="${POSTINSTALL}")")); }; +TEST_CASE("installImageScript uses Debian package names for Ubuntu images") +{ + const OS osinfo + = opencattus::models::OS(OS::Distro::Ubuntu, OS::Platform::ubuntu24, 4); + const auto builder = NFS::imageInstallScript(osinfo, + { .imageName = "ubuntu24.04-x86_64-netboot-compute", + .rootfs = "/install/netboot/ubuntu24.04/x86_64/compute/rootimg", + .postinstall = "/install/custom/netboot/compute.postinstall", + .pkglist = "/install/custom/netboot/compute.otherpkglist" }); + const std::string script = builder.toString(); + CHECK(script.contains(R"(echo "nfs-common" >> "${PKGLIST}")")); + CHECK_FALSE(script.contains(R"(echo "nfs-utils" >> "${PKGLIST}")")); +}; + +TEST_CASE("installScript uses Debian NFS server package names on Ubuntu") +{ + const OS osinfo + = opencattus::models::OS(OS::Distro::Ubuntu, OS::Platform::ubuntu24, 4); + opencattus::services::initializeSingletonsOptions( + std::make_unique()); + opencattus::Singleton::init( + []() -> std::unique_ptr { + auto answerfile = std::make_unique( + "test/sample/answerfile/rocky9-xcat.ini"); + return answerfile; + }); + const auto builder = NFS::installScript(osinfo); + const auto scriptStr = builder.toString(); + const auto script = std::string_view(scriptStr); + + CHECK(script.contains("DEBIAN_FRONTEND=noninteractive apt install -y " + "nfs-kernel-server")); + CHECK(script.contains("systemctl enable --now rpcbind nfs-kernel-server")); +} + } TEST_SUITE_END(); diff --git a/src/connection.cpp b/src/connection.cpp index a4ebb55b..b3ec7989 100644 --- a/src/connection.cpp +++ b/src/connection.cpp @@ -56,7 +56,7 @@ namespace { */ class ifaddrslist { private: - ifaddrs* m_ptr {}; + ifaddrs* m_ptr { }; public: // The const iterator class @@ -270,7 +270,7 @@ std::vector Connection::fetchInterfaces() interfaces.emplace(ifa.ifa_name); } - return interfaces | std::ranges::to>(); + return { interfaces.begin(), interfaces.end() }; } std::optional Connection::getMAC() const { return m_mac; } @@ -378,7 +378,7 @@ address Connection::fetchAddress(const std::string& interface) } freeifaddrs(ifaddr); - return {}; + return { }; throw std::runtime_error(fmt::format( "Interface {} does not have an IP address defined", interface)); } diff --git a/src/diskImage.cpp b/src/diskImage.cpp index e3d3b229..b01c8f9f 100644 --- a/src/diskImage.cpp +++ b/src/diskImage.cpp @@ -50,6 +50,8 @@ bool DiskImage::isKnownImage(const std::filesystem::path& path) return opencattus::models::OS::Distro::OL; } else if (imageView.starts_with("AlmaLinux")) { return opencattus::models::OS::Distro::AlmaLinux; + } else if (imageView.starts_with("ubuntu-")) { + return opencattus::models::OS::Distro::Ubuntu; } return std::nullopt; }; diff --git a/src/main.cpp b/src/main.cpp index a0045ec8..199346f5 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -65,8 +65,13 @@ int runTestCommand(const std::string& testCommand, runner->checkCommand(testCommandArgs[0]); } else if (testCommand == "initialize-repos") { repoManager->initializeDefaultRepositories(); - runner->checkCommand( - R"(bash -c "dnf config-manager --set-enabled '*' && dnf makecache -y" )"); + if (cluster->getHeadnode().getOS().getPackageType() + == opencattus::models::OS::PackageType::DEB) { + runner->checkCommand("DEBIAN_FRONTEND=noninteractive apt update"); + } else { + runner->checkCommand( + R"(bash -c "dnf config-manager --set-enabled '*' && dnf makecache -y" )"); + } } else if (testCommand == "create-http-repo") { assert(testCommandArgs.size() > 0); opencattus::functions::createHTTPRepo(testCommandArgs[0]); @@ -323,9 +328,9 @@ int runApplication(int argc, const char** argv) auto model = std::make_unique(); LOG_INFO("Model initialized"); std::unique_ptr answerfile; - auto tuiDraftState = services::tui::DraftState {}; - auto tuiDraftPath = std::filesystem::path {}; - auto tuiView = std::unique_ptr {}; + auto tuiDraftState = services::tui::DraftState { }; + auto tuiDraftPath = std::filesystem::path { }; + auto tuiView = std::unique_ptr { }; if (opts->enableTUI) { tuiView = std::make_unique(); } diff --git a/src/models/answerfile.cpp b/src/models/answerfile.cpp index a3d396c7..bd0551d2 100644 --- a/src/models/answerfile.cpp +++ b/src/models/answerfile.cpp @@ -162,7 +162,7 @@ namespace { if (error == std::errc::result_out_of_range) { fail("value is out of range"); } - if (error != std::errc {} || ptr != end) { + if (error != std::errc { } || ptr != end) { fail("not a number"); } if (!allowZero && parsed == 0) { @@ -1017,11 +1017,14 @@ AFNode AnswerFile::validateNode(AFNode node, const std::string& section) auto AnswerFile::AFNodes::nodesNames() const -> std::vector { std::uint32_t nodeIdx = 0; - return nodes | std::views::transform([&](const auto& node) { + std::vector output; + output.reserve(nodes.size()); + for (const auto& node : nodes) { nodeIdx++; - return utils::optional::unwrap( - node.hostname, "hostname missing for node {}", nodeIdx); - }) | std::ranges::to(); + output.emplace_back(utils::optional::unwrap( + node.hostname, "hostname missing for node {}", nodeIdx)); + } + return output; } bool AnswerFile::checkEnabled(const std::string& section) @@ -1121,7 +1124,7 @@ void AnswerFile::loadRepositories() return; } - repositories.enabled = std::vector {}; + repositories.enabled = std::vector { }; const auto enabled = m_keyfile.getStringOpt("repositories", "enabled"); if (!enabled.has_value() || enabled->empty()) { return; @@ -1139,7 +1142,7 @@ void AnswerFile::loadOHPC() return; } - ohpc.enabled = std::vector {}; + ohpc.enabled = std::vector { }; const auto enabled = m_keyfile.getStringOpt("ohpc", "enabled"); if (!enabled.has_value() || enabled->empty()) { return; diff --git a/src/models/cluster.cpp b/src/models/cluster.cpp index e6939a38..f1b18a73 100644 --- a/src/models/cluster.cpp +++ b/src/models/cluster.cpp @@ -510,14 +510,15 @@ void Cluster::fillTestData() // TODO: Pass network connection as object std::list connections1 { - { &getNetwork(Network::Profile::Management), {}, "00:0c:29:9b:0c:75", + { &getNetwork(Network::Profile::Management), { }, "00:0c:29:9b:0c:75", "172.26.0.1" }, - { &getNetwork(Network::Profile::Application), "eno1", {}, "172.27.0.1" } + { &getNetwork(Network::Profile::Application), "eno1", { }, + "172.27.0.1" } }; std::list connections2 { { &getNetwork( Network::Profile::Management), - {}, "de:ad:be:ff:00:00", "172.26.0.2" } }; + { }, "de:ad:be:ff:00:00", "172.26.0.2" } }; BMC bmc { "172.25.0.2", "ADMIN", "ADMIN", 0, 115200, BMC::kind::IPMI }; @@ -548,20 +549,29 @@ auto& getNetworkField(AnswerFile& answerfile, Network::Profile profile) } } -void validateProvisionerSupport(const OS& os, Cluster::Provisioner provisioner) +void validateProvisionerSupport( + const OS& headnodeOS, const OS& nodeOS, Cluster::Provisioner provisioner) { - switch (os.getPlatform()) { + if (provisioner == Cluster::Provisioner::xCAT + && headnodeOS.getPackageType() == OS::PackageType::DEB) { + throw std::runtime_error("xCAT on DEB head nodes is not implemented " + "yet; use confluent instead"); + } + + switch (nodeOS.getPlatform()) { case OS::Platform::el10: if (provisioner == Cluster::Provisioner::xCAT) { throw std::runtime_error(fmt::format( "xCAT is not supported on EL{}; use confluent instead", - os.getMajorVersion())); + nodeOS.getMajorVersion())); } return; case OS::Platform::el8: return; case OS::Platform::el9: return; + case OS::Platform::ubuntu24: + return; default: std::unreachable(); } @@ -1050,7 +1060,8 @@ void Cluster::fillData(const AnswerFile& answerfil) opencattus::functions::abort("Invalid provisioner {}", provisioner); }(); - validateProvisionerSupport(nodeOS, selectedProvisioner); + validateProvisionerSupport( + getHeadnode().getOS(), nodeOS, selectedProvisioner); setProvisioner(selectedProvisioner); setComputeNodeOS(nodeOS); diff --git a/src/models/os.cpp b/src/models/os.cpp index 3bb68684..445c22e0 100644 --- a/src/models/os.cpp +++ b/src/models/os.cpp @@ -35,7 +35,7 @@ namespace opencattus::models { OS::OS() { LOG_INFO("Initializing OS (ctr 1)"); - struct utsname system {}; + struct utsname system { }; // @FIXME: Unfortunately this runs during the initialization of the // cluster instance. Which prevents us of running this during testing // in a machine that does not have /etc/os-release file. @@ -124,6 +124,9 @@ OS::OS(const Distro& distro, const Platform& platform, case OS::Platform::el8: m_majorVersion = 8; break; + case OS::Platform::ubuntu24: + m_majorVersion = 24; + break; default: opencattus::functions::abort("Invalid platform: {}", opencattus::utils::enums::toString(platform)); @@ -203,6 +206,9 @@ std::string OS::getDistroString() const case OS::Distro::OL: distro = "ol"; break; + case OS::Distro::Ubuntu: + distro = "ubuntu"; + break; default: std::unreachable(); } @@ -218,6 +224,8 @@ OS::PackageType OS::getPackageType() const case Distro::Rocky: case Distro::AlmaLinux: return PackageType::RPM; + case Distro::Ubuntu: + return PackageType::DEB; default: throw std::runtime_error("Unknonw distro type"); }; @@ -231,6 +239,9 @@ void OS::setDistro(std::string_view distro) auto normalizedDistro = utils::string::lower(std::string(distro)); if (normalizedDistro == "alma") { normalizedDistro = "almalinux"; + } else if (normalizedDistro == "ubuntu24" + || normalizedDistro == "ubuntu24.04") { + normalizedDistro = "ubuntu"; } if (const auto& rval = enums::ofStringOpt( @@ -261,9 +272,12 @@ void OS::setMajorVersion(unsigned int majorVersion) case 10: m_platform = OS::Platform::el10; break; + case 24: + m_platform = OS::Platform::ubuntu24; + break; default: throw std::runtime_error(fmt::format( - "Unsupported release: EL{} is not supported.", majorVersion)); + "Unsupported release: {} is not supported.", majorVersion)); } m_majorVersion = majorVersion; @@ -278,6 +292,11 @@ void OS::setMinorVersion(unsigned int minorVersion) std::string OS::getVersion() const { + if (getPlatform() == OS::Platform::ubuntu24) { + return fmt::format( + "{}.{}", m_majorVersion, fmt::format("{:02}", m_minorVersion)); + } + return fmt::format("{}.{}", m_majorVersion, m_minorVersion); } diff --git a/src/ofed.cpp b/src/ofed.cpp index 51e49195..3d3bdc24 100644 --- a/src/ofed.cpp +++ b/src/ofed.cpp @@ -11,6 +11,7 @@ #include #include #include +#include #include #include #include @@ -158,6 +159,34 @@ auto buildDocaHostInstallCommand() -> std::string "doca-ofed mlnx-fw-updater"; } +auto buildInboxOFEDInstalledCommand(const opencattus::models::OS& osinfo) + -> std::string +{ + switch (osinfo.getPackageType()) { + case opencattus::models::OS::PackageType::RPM: + return "dnf group info \"Infiniband Support\""; + case opencattus::models::OS::PackageType::DEB: + return "dpkg-query -W rdma-core ibverbs-providers " + "infiniband-diags"; + } + + std::unreachable(); +} + +auto buildInboxOFEDInstallCommand(const opencattus::models::OS& osinfo) + -> std::string +{ + switch (osinfo.getPackageType()) { + case opencattus::models::OS::PackageType::RPM: + return "Infiniband Support"; + case opencattus::models::OS::PackageType::DEB: + return "DEBIAN_FRONTEND=noninteractive apt install -y " + "rdma-core ibverbs-providers infiniband-diags perftest"; + } + + std::unreachable(); +} + auto rockyKernelPackageRepositoryComponent(const opencattus::models::OS& osinfo, std::string_view packageName) -> std::string_view { @@ -255,7 +284,7 @@ auto latestInstalledKernelCore(IRunner& runner) -> std::string "bash -lc \"rpm -q kernel-core --qf '%{INSTALLTIME} " "%{VERSION}-%{RELEASE}.%{ARCH}\\n'\""); - auto newestKernel = std::string {}; + auto newestKernel = std::string { }; long long newestInstallTime = 0; auto foundKernel = false; @@ -336,12 +365,19 @@ bool OFED::installed() const } auto runner = opencattus::Singleton::get(); + const auto osinfo = opencattus::utils::singleton::os(); switch (m_kind) { case OFED::Kind::Doca: + if (osinfo.getPackageType() + == opencattus::models::OS::PackageType::DEB) { + throw std::runtime_error( + "DOCA OFED is only implemented for Enterprise Linux head " + "nodes"); + } return runner->executeCommand("rpm -q doca-ofed") == 0; case OFED::Kind::Inbox: return runner->executeCommand( - "dnf group info \"Infiniband Support\"") + buildInboxOFEDInstalledCommand(osinfo)) == 0; case OFED::Kind::Oracle: throw std::logic_error("Not implemented"); @@ -367,10 +403,18 @@ void OFED::install() const } switch (m_kind) { - case OFED::Kind::Inbox: - opencattus::utils::singleton::osservice()->groupInstall( - "Infiniband Support"); + case OFED::Kind::Inbox: { + const auto osinfo = opencattus::utils::singleton::os(); + const auto installCommand = buildInboxOFEDInstallCommand(osinfo); + if (osinfo.getPackageType() + == opencattus::models::OS::PackageType::DEB) { + opencattus::services::runner::shell::cmd(installCommand); + } else { + opencattus::utils::singleton::osservice()->groupInstall( + installCommand); + } break; + } case OFED::Kind::Doca: { auto runner @@ -636,6 +680,23 @@ TEST_CASE("buildDocaHostInstallCommand excludes kernel package upgrades") "doca-ofed mlnx-fw-updater"); } +TEST_CASE("buildInboxOFEDInstalledCommand uses APT package probes on Ubuntu") +{ + CHECK(buildInboxOFEDInstalledCommand( + opencattus::models::OS(opencattus::models::OS::Distro::Ubuntu, + opencattus::models::OS::Platform::ubuntu24, 4)) + == "dpkg-query -W rdma-core ibverbs-providers infiniband-diags"); +} + +TEST_CASE("buildInboxOFEDInstallCommand installs Ubuntu RDMA packages") +{ + CHECK(buildInboxOFEDInstallCommand( + opencattus::models::OS(opencattus::models::OS::Distro::Ubuntu, + opencattus::models::OS::Platform::ubuntu24, 4)) + == "DEBIAN_FRONTEND=noninteractive apt install -y " + "rdma-core ibverbs-providers infiniband-diags perftest"); +} + TEST_CASE("DOCA DKMS priming runs when a newer kernel is installed") { CHECK(shouldPrimeDocaDkmsForInstalledKernel( @@ -662,7 +723,7 @@ TEST_CASE("latestInstalledKernelCore uses install time instead of version sort") opencattus::services::CommandProxy executeCommandIter( const std::string&, opencattus::services::Stream) override { - return {}; + return { }; } void checkCommand(const std::string&) override { } std::vector checkOutput( @@ -684,7 +745,7 @@ TEST_CASE("latestInstalledKernelCore uses install time instead of version sort") } }; - auto runner = KernelQueryRunner {}; + auto runner = KernelQueryRunner { }; CHECK( latestInstalledKernelCore(runner) == "4.18.0-553.111.1.el8_10.x86_64"); CHECK(runner.queriedCommand diff --git a/src/presenter/PresenterNodesOperationalSystem.cpp b/src/presenter/PresenterNodesOperationalSystem.cpp index ac8f20e2..b793dfef 100644 --- a/src/presenter/PresenterNodesOperationalSystem.cpp +++ b/src/presenter/PresenterNodesOperationalSystem.cpp @@ -38,6 +38,16 @@ auto defaultVersionComboFor(const OS& os) -> PresenterNodesVersionCombo static_cast(os.getMinorVersion()), os.getArch() }; } +auto defaultVersionComboFor(const OS::Distro distro, const OS& fallbackOS) + -> PresenterNodesVersionCombo +{ + if (distro == OS::Distro::Ubuntu) { + return { 24, 4, fallbackOS.getArch() }; + } + + return defaultVersionComboFor(fallbackOS); +} + auto parseVersionString(std::string_view raw) -> std::optional> { @@ -59,7 +69,7 @@ auto parseVersionString(std::string_view raw) auto parseArchitecture(std::string_view raw) -> std::optional { const auto normalized = opencattus::utils::string::lower(std::string(raw)); - if (normalized.contains("x86_64")) { + if (normalized.contains("x86_64") || normalized.contains("amd64")) { return OS::Arch::x86_64; } @@ -85,10 +95,18 @@ auto makeOperatingSystem( case 10: return OS(distro, OS::Platform::el10, static_cast(minorVersion), arch); + case 24: + if (distro == OS::Distro::Ubuntu && minorVersion == 4) { + return OS(distro, OS::Platform::ubuntu24, + static_cast(minorVersion), arch); + } + break; default: - throw std::runtime_error(fmt::format( - "Unsupported OS version {}.{}", majorVersion, minorVersion)); + break; } + + throw std::runtime_error(fmt::format( + "Unsupported OS version {}.{}", majorVersion, minorVersion)); } auto inferVersionComboFromIso(OS::Distro distro, std::string_view isoName) @@ -112,6 +130,16 @@ auto inferVersionComboFromIso(OS::Distro distro, std::string_view isoName) std::stoi(match[2].str()), arch.value() }; } + if (distro == OS::Distro::Ubuntu) { + const std::regex ubuntuPattern(R"(ubuntu-([0-9]+)\.([0-9]+))"); + if (!std::regex_search(isoString, match, ubuntuPattern)) { + return std::nullopt; + } + + return PresenterNodesVersionCombo { std::stoi(match[1].str()), + std::stoi(match[2].str()), arch.value() }; + } + const std::regex genericPattern(R"(([0-9]+)\.([0-9]+))"); if (!std::regex_search(isoString, match, genericPattern)) { return std::nullopt; @@ -132,6 +160,8 @@ auto isoMatchesDistro(const OS::Distro distro, std::string_view isoName) -> bool return isoName.contains("Rocky"); case OS::Distro::AlmaLinux: return isoName.contains("AlmaLinux"); + case OS::Distro::Ubuntu: + return isoName.contains("ubuntu-"); } return false; @@ -148,16 +178,20 @@ auto isoSearchToken(const OS::Distro distro) -> std::string_view return "Rocky"; case OS::Distro::AlmaLinux: return "AlmaLinux"; + case OS::Distro::Ubuntu: + return "ubuntu-"; } - return {}; + return { }; } auto exampleIsoName(const OS::Distro distro, const PresenterNodesVersionCombo& version) -> std::string { const auto [major, minor, arch] = version; - const auto architecture = opencattus::utils::enums::toString(arch); + const auto architecture = distro == OS::Distro::Ubuntu + ? std::string("amd64") + : opencattus::utils::enums::toString(arch); switch (distro) { case OS::Distro::RHEL: @@ -172,9 +206,12 @@ auto exampleIsoName(const OS::Distro distro, case OS::Distro::AlmaLinux: return fmt::format( "AlmaLinux-{}.{}-{}-dvd.iso", major, minor, architecture); + case OS::Distro::Ubuntu: + return fmt::format( + "ubuntu-{}.04.4-live-server-{}.iso", major, architecture); } - return {}; + return { }; } auto formatNoMatchingIsoMessage(const fs::path& directory, @@ -233,7 +270,10 @@ std::string PresenterNodesOperationalSystem::getDownloadURL( auto [majorVersion, minorVersion, arch] = version; fmt::dynamic_format_arg_store store; - store.push_back(fmt::arg("arch", opencattus::utils::enums::toString(arch))); + store.push_back(fmt::arg("arch", + distro == OS::Distro::Ubuntu + ? std::string("amd64") + : opencattus::utils::enums::toString(arch))); store.push_back(fmt::arg("major", majorVersion)); store.push_back(fmt::arg("minor", minorVersion)); @@ -256,6 +296,14 @@ std::string PresenterNodesOperationalSystem::getDownloadURL( "{major}.{minor}/isos/{arch}/" "AlmaLinux-{major}.{minor}-{arch}-dvd.iso", store); + case OS::Distro::Ubuntu: + if (majorVersion != 24 || minorVersion != 4) { + throw std::runtime_error( + "Only Ubuntu 24.04 ISO download is supported"); + } + return fmt::vformat("https://releases.ubuntu.com/24.04/" + "ubuntu-24.04.4-live-server-{arch}.iso", + store); } return "?"; @@ -265,7 +313,7 @@ PresenterNodesVersionCombo PresenterNodesOperationalSystem::promptVersion( OS::Distro distro, std::optional initial) { auto [defaultMajor, defaultMinor, defaultArch] = initial.value_or( - defaultVersionComboFor(m_model->getHeadnode().getOS())); + defaultVersionComboFor(distro, m_model->getHeadnode().getOS())); auto metadata = std::to_array>( { { Messages::OperationalSystemVersion::version, @@ -306,12 +354,14 @@ PresenterNodesOperationalSystem::PresenterNodesOperationalSystem( distroNames.emplace_back("AlmaLinux"); distroNames.emplace_back("Rocky Linux"); distroNames.emplace_back("Oracle Linux"); + distroNames.emplace_back("Ubuntu"); std::map distros; distros["Red Hat Enterprise Linux"] = OS::Distro::RHEL; distros["AlmaLinux"] = OS::Distro::AlmaLinux; distros["Rocky Linux"] = OS::Distro::Rocky; distros["Oracle Linux"] = OS::Distro::OL; + distros["Ubuntu"] = OS::Distro::Ubuntu; const auto downloadSelectedDistro = [&](const std::string&, OS::Distro distro) -> bool { @@ -406,7 +456,8 @@ PresenterNodesOperationalSystem::PresenterNodesOperationalSystem( if (isos->empty()) { const auto noneFoundMessage = formatNoMatchingIsoMessage( isoRoot, selectedDistro->second, - defaultVersionComboFor(m_model->getHeadnode().getOS())); + defaultVersionComboFor(selectedDistro->second, + m_model->getHeadnode().getOS())); m_view->message(Messages::title, noneFoundMessage.c_str()); if (m_view->yesNoQuestion(Messages::title, Messages::OperationalSystem::downloadMissing, @@ -460,6 +511,16 @@ TEST_CASE("inferVersionComboFromIso parses Oracle Linux ISO names") CHECK(inferred.value() == expected); } +TEST_CASE("inferVersionComboFromIso parses Ubuntu ISO names") +{ + const auto inferred = inferVersionComboFromIso( + OS::Distro::Ubuntu, "ubuntu-24.04.4-live-server-amd64.iso"); + + REQUIRE(inferred.has_value()); + const PresenterNodesVersionCombo expected { 24, 4, OS::Arch::x86_64 }; + CHECK(inferred.value() == expected); +} + TEST_CASE("makeOperatingSystem maps major versions to supported platforms") { const auto os @@ -467,4 +528,11 @@ TEST_CASE("makeOperatingSystem maps major versions to supported platforms") CHECK(os.getPlatform() == OS::Platform::el10); CHECK(os.getVersion() == "10.1"); + + const auto ubuntu + = makeOperatingSystem(OS::Distro::Ubuntu, { 24, 4, OS::Arch::x86_64 }); + + CHECK(ubuntu.getPlatform() == OS::Platform::ubuntu24); + CHECK(ubuntu.getPackageType() == OS::PackageType::DEB); + CHECK(ubuntu.getVersion() == "24.04"); } diff --git a/src/presenter/PresenterProvisioner.cpp b/src/presenter/PresenterProvisioner.cpp index 447dc4ce..1de2be06 100644 --- a/src/presenter/PresenterProvisioner.cpp +++ b/src/presenter/PresenterProvisioner.cpp @@ -23,6 +23,11 @@ using OS = opencattus::models::OS; auto supportedProvisionersFor(const OS& os) -> std::vector { switch (os.getPlatform()) { + case OS::Platform::ubuntu24: + return { + Cluster::Provisioner::xCAT, + Cluster::Provisioner::Confluent, + }; case OS::Platform::el10: return { Cluster::Provisioner::Confluent }; case OS::Platform::el8: @@ -115,6 +120,19 @@ TEST_CASE("supportedProvisionersFor keeps EL9 xcat and confluent available") }); } +TEST_CASE("supportedProvisionersFor keeps Ubuntu 24.04 xcat and confluent " + "available") +{ + const auto supported = supportedProvisionersFor( + OS(OS::Distro::Ubuntu, OS::Platform::ubuntu24, 4)); + + CHECK(supported + == std::vector { + Cluster::Provisioner::xCAT, + Cluster::Provisioner::Confluent, + }); +} + TEST_CASE("supportedProvisionersFor checks headnode and compute node releases") { const auto supported diff --git a/src/presenter/PresenterRepository.cpp b/src/presenter/PresenterRepository.cpp index 3ae16f79..3645f32f 100644 --- a/src/presenter/PresenterRepository.cpp +++ b/src/presenter/PresenterRepository.cpp @@ -73,11 +73,11 @@ PresenterRepository::PresenterRepository( return; } - auto allReposUIAdapter = optionalRepos - | std::views::transform([](const auto& entry) { - return std::make_tuple(entry.id, entry.name, entry.enabled); - }) - | std::ranges::to(); + UISelectionAdapterTy allReposUIAdapter; + allReposUIAdapter.reserve(optionalRepos.size()); + for (const auto& entry : optionalRepos) { + allReposUIAdapter.emplace_back(entry.id, entry.name, entry.enabled); + } const auto& [ret, toEnable] = m_view->checkboxSelectionMenu(Messages::title, Messages::General::question, Messages::General::help, diff --git a/src/services/ansible/roles/aide.cpp b/src/services/ansible/roles/aide.cpp index 68188537..7d3c03b5 100644 --- a/src/services/ansible/roles/aide.cpp +++ b/src/services/ansible/roles/aide.cpp @@ -30,10 +30,16 @@ ScriptBuilder installScript( .addPackage("aide") .addNewLine() .addCommand("# Skip if aide database exists") - .addCommand("test -f /var/lib/aide/aide.db.gz && exit 0") + .addCommand( + "test -f /var/lib/aide/aide.db.gz -o -f /var/lib/aide/aide.db && " + "exit 0") .addCommand("# Initialize AIDE database") - .addCommand("aide --init") - .addCommand("mv /var/lib/aide/aide.db.new.gz /var/lib/aide/aide.db.gz"); + .addCommand("aide --init || aideinit") + .addCommand(R"(if test -f /var/lib/aide/aide.db.new.gz; then + mv /var/lib/aide/aide.db.new.gz /var/lib/aide/aide.db.gz +elif test -f /var/lib/aide/aide.db.new; then + mv /var/lib/aide/aide.db.new /var/lib/aide/aide.db +fi)"); return builder; } diff --git a/src/services/ansible/roles/audit.cpp b/src/services/ansible/roles/audit.cpp index 46fbd8a1..85b20977 100644 --- a/src/services/ansible/roles/audit.cpp +++ b/src/services/ansible/roles/audit.cpp @@ -25,10 +25,18 @@ ScriptBuilder installScript( using namespace opencattus; ScriptBuilder builder(osinfo); + const auto auditPackage + = osinfo.getPackageType() == opencattus::models::OS::PackageType::DEB + ? std::string_view("auditd") + : std::string_view("audit"); + builder.addNewLine() .addCommand("# Install audit packages") - .addPackage("audit") - .addPackage("audispd-plugins") + .addPackage(auditPackage) + .addCommand( + "DEBIAN_FRONTEND=noninteractive apt install -y " + "audispd-plugins 2>/dev/null || dnf install -y audispd-plugins " + "2>/dev/null || :") .addCommand("# Configure auditd") .addFileTemplate("/etc/audit/auditd.conf", R"EOF( # diff --git a/src/services/ansible/roles/base.cpp b/src/services/ansible/roles/base.cpp index 494eb32c..0ef1ddae 100644 --- a/src/services/ansible/roles/base.cpp +++ b/src/services/ansible/roles/base.cpp @@ -31,6 +31,10 @@ ScriptBuilder installScript( builder.addNewLine().addCommand("# Install EPEL repositories if needed"); switch (osinfo.getDistro()) { + case models::OS::Distro::Ubuntu: + builder.addCommand("# Ubuntu does not use EPEL repositories"); + break; + case models::OS::Distro::RHEL: case models::OS::Distro::Rocky: case models::OS::Distro::AlmaLinux: @@ -64,19 +68,38 @@ ScriptBuilder installScript( builder.addNewLine().addCommand("# Install general base packages"); - // "python3-dnf-plugin-versionlock" is conflicting with dnf-plugins-core - // during the first install - // TODO: CFL initscripts is only required by xCAT - std::set allPackages = { - "wget", - "curl", - "dnf-plugins-core", - "chkconfig", - "initscripts", // @FIXME: This is only required if the provisioner is - // xCAT - "jq", - "tar", - }; + std::set allPackages; + switch (osinfo.getPackageType()) { + case models::OS::PackageType::RPM: + // "python3-dnf-plugin-versionlock" is conflicting with + // dnf-plugins-core during the first install. + // TODO: CFL initscripts is only required by xCAT. + allPackages = { + "wget", + "curl", + "dnf-plugins-core", + "chkconfig", + "initscripts", // @FIXME: This is only required if the + // provisioner is xCAT + "jq", + "tar", + }; + break; + case models::OS::PackageType::DEB: + allPackages = { + "ca-certificates", + "curl", + "gnupg", + "iproute2", + "jq", + "lsb-release", + "network-manager", + "openssh-server", + "tar", + "wget", + }; + break; + } if (const auto iter = role.vars().find("base_packages"); iter != role.vars().end()) { for (const auto& pkg : diff --git a/src/services/ansible/roles/check.cpp b/src/services/ansible/roles/check.cpp index e58a11db..14511379 100644 --- a/src/services/ansible/roles/check.cpp +++ b/src/services/ansible/roles/check.cpp @@ -1,4 +1,5 @@ #include +#include #include #include #include @@ -31,6 +32,14 @@ void run(const Role& role) { using namespace opencattus::utils; + if (singleton::os().getPackageType() + == opencattus::models::OS::PackageType::DEB) { + runner::shell::cmd("DEBIAN_FRONTEND=noninteractive apt update"); + LOG_WARN("Skipping kernel freshness check on Ubuntu; the existing " + "check is RPM-specific"); + return; + } + // OFED installation fails with ISO kernel, require update and reboot runner::shell::cmd("dnf makecache"); const auto kernelAvailable diff --git a/src/services/ansible/roles/dump.cpp b/src/services/ansible/roles/dump.cpp index c1bd464f..f2faf66c 100644 --- a/src/services/ansible/roles/dump.cpp +++ b/src/services/ansible/roles/dump.cpp @@ -22,6 +22,25 @@ std::string buildFirewallDumpCommand() "echo 'firewalld inactive or firewall-cmd not installed'; fi"; } +std::string buildRepositoryDumpCommand() +{ + return "if command -v apt-cache >/dev/null 2>&1; then " + "grep -RHE '^[[:space:]]*(deb|deb-src) ' " + "/etc/apt/sources.list /etc/apt/sources.list.d 2>/dev/null || true; " + "elif test -d /etc/yum.repos.d; then " + "grep -EH '^(mirrorlist|baseurl)' /etc/yum.repos.d/*.repo " + "2>/dev/null || true; " + "else echo 'No supported repository configuration found'; fi"; +} + +std::string buildPackageDumpCommand() +{ + return "if command -v rpm >/dev/null 2>&1; then rpm -qa; " + "elif command -v dpkg-query >/dev/null 2>&1; then " + "dpkg-query -W -f='${binary:Package}\\t${Version}\\n'; " + "else echo 'No supported package database found'; fi"; +} + void dumpPreInstallState() { using namespace opencattus::services::runner; @@ -33,11 +52,10 @@ void dumpPreInstallState() shell::cmd("cat /etc/os-release"); LOG_INFO("Repositories URLs"); - shell::cmd( - "grep -EH '^(mirrorlist|baseurl)' /etc/yum.repos.d/*.repo || true"); + shell::cmd(buildRepositoryDumpCommand()); LOG_INFO("Packages installed"); - shell::cmd("rpm -qa"); + shell::cmd(buildPackageDumpCommand()); LOG_INFO("Network configuration"); shell::cmd("ip a"); @@ -66,11 +84,24 @@ void run(const Role& role) { dumpPreInstallState(); } } -TEST_CASE("buildFirewallDumpCommand only queries firewalld when the service is active") +TEST_CASE("buildFirewallDumpCommand only queries firewalld when the service is " + "active") { const auto command = buildFirewallDumpCommand(); - CHECK(command.contains("command -v firewall-cmd >/dev/null 2>&1 && systemctl is-active --quiet firewalld.service")); + CHECK(command.contains("command -v firewall-cmd >/dev/null 2>&1 && " + "systemctl is-active --quiet firewalld.service")); CHECK(command.contains("firewall-cmd --list-all-zones")); CHECK(command.contains("firewalld inactive or firewall-cmd not installed")); } + +TEST_CASE("dump commands support RPM and APT systems") +{ + const auto repositories = buildRepositoryDumpCommand(); + const auto packages = buildPackageDumpCommand(); + + CHECK(repositories.contains("/etc/apt/sources.list")); + CHECK(repositories.contains("/etc/yum.repos.d")); + CHECK(packages.contains("rpm -qa")); + CHECK(packages.contains("dpkg-query")); +} diff --git a/src/services/ansible/roles/firewall.cpp b/src/services/ansible/roles/firewall.cpp index b77dd35d..eb9cb5a7 100644 --- a/src/services/ansible/roles/firewall.cpp +++ b/src/services/ansible/roles/firewall.cpp @@ -1,5 +1,6 @@ #include #include +#include #ifdef BUILD_TESTING #include @@ -16,6 +17,19 @@ void configureFirewall() { LOG_INFO("Setting up firewall") + if (cluster()->getHeadnode().getOS().getPackageType() + == opencattus::models::OS::PackageType::DEB) { + if (cluster()->isFirewall()) { + LOG_WARN("Ubuntu firewall configuration is not implemented yet; " + "leaving the existing firewall state unchanged"); + } else { + opencattus::services::runner::shell::cmd( + "systemctl disable --now ufw || :"); + LOG_WARN("UFW has been disabled if it was installed") + } + return; + } + if (cluster()->isFirewall()) { osservice()->enableService("firewalld"); diff --git a/src/services/ansible/roles/network.cpp b/src/services/ansible/roles/network.cpp index 3e951a46..66394462 100644 --- a/src/services/ansible/roles/network.cpp +++ b/src/services/ansible/roles/network.cpp @@ -46,8 +46,9 @@ sleep 0.2 nmcli connection up "{conn_name}" )", fmt::arg("iface", connection.getInterface().value()), - fmt::arg("conn_name", opencattus::utils::enums::toString( - connection.getNetwork()->getProfile())), + fmt::arg("conn_name", + opencattus::utils::enums::toString( + connection.getNetwork()->getProfile())), fmt::arg("type", connection.getNetwork()->getType()), fmt::arg("mtu", connection.getMTU()), fmt::arg("ip", connection.getAddress().to_string()), @@ -75,6 +76,38 @@ void disableNetworkManagerDNSOverride() osservice()->restartService("NetworkManager"); } +[[nodiscard]] bool headnodeUsesUbuntu() +{ + return cluster()->getHeadnode().getOS().getDistro() + == opencattus::models::OS::Distro::Ubuntu; +} + +[[nodiscard]] std::string buildNetplanNetworkManagerRendererScript() +{ + return R"( +if command -v netplan >/dev/null 2>&1 && test -d /etc/netplan; then + cat > /etc/netplan/01-opencattus-networkmanager.yaml <<'EOF' +network: + version: 2 + renderer: NetworkManager +EOF + chmod 600 /etc/netplan/01-opencattus-networkmanager.yaml + netplan generate + netplan apply +fi +)"; +} + +void ensureNetplanUsesNetworkManager() +{ + if (!headnodeUsesUbuntu()) { + return; + } + + LOG_INFO("Configuring netplan to let NetworkManager manage interfaces") + shell::cmd(buildNetplanNetworkManagerRendererScript()); +} + // WARNING: We used to do this in a DRY way, but each connection has its own // idissiocracies. Keep connection setup splitted from now on @@ -119,6 +152,7 @@ void configureServiceNetwork(const Connection& connection) void configureNetworks(const std::list& connections) { osservice()->enableService("NetworkManager"); + ensureNetplanUsesNetworkManager(); disableNetworkManagerDNSOverride(); for (const auto& connection : std::as_const(connections)) { @@ -203,7 +237,7 @@ TEST_CASE("renderStaticConnectionScript activates the generated profile") } for (auto* current = interfaces; current != nullptr; - current = current->ifa_next) { + current = current->ifa_next) { if (current->ifa_name == nullptr) { continue; } @@ -231,9 +265,9 @@ TEST_CASE("renderStaticConnectionScript activates the generated profile") CHECK(script.contains("nmcli connection up \"Management\"")); CHECK(script.contains( - fmt::format("awk -F: '$2==\"{}\" {{print $1}}' | while read -r ", - interfaceName) - + "existing_connection; do")); + fmt::format( + "awk -F: '$2==\"{}\" {{print $1}}' | while read -r ", interfaceName) + + "existing_connection; do")); CHECK(script.contains( fmt::format("nmcli device set {} managed yes", interfaceName))); CHECK(script.contains( @@ -242,3 +276,14 @@ TEST_CASE("renderStaticConnectionScript activates the generated profile") interfaceName))); CHECK_FALSE(script.contains("nmcli device connect oc-mgmt0")); } + +TEST_CASE( + "buildNetplanNetworkManagerRendererScript sets NetworkManager renderer") +{ + const auto script = buildNetplanNetworkManagerRendererScript(); + + CHECK(script.contains("/etc/netplan/01-opencattus-networkmanager.yaml")); + CHECK(script.contains("renderer: NetworkManager")); + CHECK(script.contains("netplan generate")); + CHECK(script.contains("netplan apply")); +} diff --git a/src/services/ansible/roles/ohpc.cpp b/src/services/ansible/roles/ohpc.cpp index 06f3fcd0..fd3ccadd 100644 --- a/src/services/ansible/roles/ohpc.cpp +++ b/src/services/ansible/roles/ohpc.cpp @@ -1,5 +1,6 @@ #include #include +#include #include #ifdef BUILD_TESTING @@ -55,6 +56,14 @@ namespace { "spack-ohpc", "valgrind-ohpc" }; } + std::set defaultPackagesForUbuntu24() + { + return { "gnu15-compilers-ohpc", "openmpi5-gnu15-ohpc", + "mpich-ucx-gnu15-ohpc", "mvapich2-gnu15-ohpc", "lmod-ohpc", + "lmod-defaults-gnu15-openmpi5-ohpc", "ohpc-autotools", "hwloc-ohpc", + "spack-ohpc", "valgrind-ohpc" }; + } + std::set bundlePackagesForEl8(std::string_view bundleId) { if (bundleId == bundleSerialLibraries) { @@ -64,10 +73,10 @@ namespace { return { "ohpc-gnu12-parallel-libs" }; } if (bundleId == bundleIntelOneAPI) { - return {}; + return { }; } - return {}; + return { }; } std::set bundlePackagesForEl9(std::string_view bundleId) @@ -84,7 +93,7 @@ namespace { "ohpc-intel-serial-libs", "ohpc-intel-impi-parallel-libs" }; } - return {}; + return { }; } std::set bundlePackagesForEl10(std::string_view bundleId) @@ -101,7 +110,24 @@ namespace { "ohpc-intel-serial-libs", "ohpc-intel-impi-parallel-libs" }; } - return {}; + return { }; + } + + std::set bundlePackagesForUbuntu24(std::string_view bundleId) + { + if (bundleId == bundleSerialLibraries) { + return { "ohpc-gnu15-serial-libs" }; + } + if (bundleId == bundleParallelLibraries) { + return { "ohpc-gnu15-parallel-libs" }; + } + if (bundleId == bundleIntelOneAPI) { + return { "intel-oneapi-toolkit-release-ohpc", + "intel-compilers-devel-ohpc", "intel-mpi-devel-ohpc", + "ohpc-intel-serial-libs", "ohpc-intel-impi-parallel-libs" }; + } + + return { }; } std::set defaultPackagesFor(const models::OS& os) @@ -113,6 +139,8 @@ namespace { return defaultPackagesForEl9(); case models::OS::Platform::el10: return defaultPackagesForEl10(); + case models::OS::Platform::ubuntu24: + return defaultPackagesForUbuntu24(); default: std::unreachable(); } @@ -124,6 +152,7 @@ namespace { case models::OS::Platform::el8: case models::OS::Platform::el9: case models::OS::Platform::el10: + case models::OS::Platform::ubuntu24: return { std::string(bundleSerialLibraries), std::string(bundleParallelLibraries) }; default: @@ -141,6 +170,8 @@ namespace { return bundlePackagesForEl9(bundleId); case models::OS::Platform::el10: return bundlePackagesForEl10(bundleId); + case models::OS::Platform::ubuntu24: + return bundlePackagesForUbuntu24(bundleId); default: std::unreachable(); } @@ -166,6 +197,22 @@ namespace { return packages; } + std::string buildPackageInstallCommand( + const models::OS& os, const std::set& packages) + { + const auto packageList = fmt::format("{}", fmt::join(packages, " ")); + switch (os.getPackageType()) { + case models::OS::PackageType::RPM: + return packageList; + case models::OS::PackageType::DEB: + return fmt::format( + "DEBIAN_FRONTEND=noninteractive apt install -y {}", + packageList); + } + + std::unreachable(); + } + } void run(const Role& role) @@ -176,15 +223,22 @@ void run(const Role& role) auto ohpcPackages = resolvePackages(utils::singleton::os(), utils::singleton::cluster()->getEnabledOpenHPCBundles(), utils::singleton::options()->ohpcPackages); - utils::singleton::osservice()->install( - fmt::format("{}", fmt::join(ohpcPackages, " "))); + const auto installCommand + = buildPackageInstallCommand(utils::singleton::os(), ohpcPackages); + + if (utils::singleton::os().getPackageType() + == models::OS::PackageType::DEB) { + opencattus::services::runner::shell::cmd(installCommand); + } else { + utils::singleton::osservice()->install(installCommand); + } } TEST_CASE("resolvePackages keeps the current EL8 defaults explicit") { const auto packages = resolvePackages( models::OS(models::OS::Distro::Rocky, models::OS::Platform::el8, 10), - std::nullopt, {}); + std::nullopt, { }); CHECK(packages.contains("gnu12-compilers-ohpc")); CHECK(packages.contains("openmpi4-gnu12-ohpc")); @@ -208,7 +262,7 @@ TEST_CASE("resolvePackages keeps the current EL9 defaults explicit") { const auto packages = resolvePackages( models::OS(models::OS::Distro::Rocky, models::OS::Platform::el9, 7), - std::nullopt, {}); + std::nullopt, { }); CHECK(packages.contains("gnu15-compilers-ohpc")); CHECK(packages.contains("openmpi5-pmix-gnu15-ohpc")); @@ -231,7 +285,7 @@ TEST_CASE("resolvePackages switches EL10 defaults to OpenHPC 4 toolchains") { const auto packages = resolvePackages( models::OS(models::OS::Distro::Rocky, models::OS::Platform::el10, 1), - std::nullopt, {}); + std::nullopt, { }); CHECK(packages.contains("gnu15-compilers-ohpc")); CHECK(packages.contains("openmpi5-pmix-gnu15-ohpc")); @@ -251,6 +305,29 @@ TEST_CASE("resolvePackages switches EL10 defaults to OpenHPC 4 toolchains") CHECK_FALSE(packages.contains("openmpi5-gnu15-ohpc")); } +TEST_CASE("resolvePackages keeps Ubuntu 24.04 OpenHPC fork defaults explicit") +{ + const auto packages + = resolvePackages(models::OS(models::OS::Distro::Ubuntu, + models::OS::Platform::ubuntu24, 4), + std::nullopt, { }); + + CHECK(packages.contains("gnu15-compilers-ohpc")); + CHECK(packages.contains("openmpi5-gnu15-ohpc")); + CHECK(packages.contains("mpich-ucx-gnu15-ohpc")); + CHECK(packages.contains("mvapich2-gnu15-ohpc")); + CHECK(packages.contains("lmod-ohpc")); + CHECK(packages.contains("lmod-defaults-gnu15-openmpi5-ohpc")); + CHECK(packages.contains("ohpc-autotools")); + CHECK(packages.contains("hwloc-ohpc")); + CHECK(packages.contains("ohpc-gnu15-serial-libs")); + CHECK(packages.contains("ohpc-gnu15-parallel-libs")); + CHECK(packages.contains("spack-ohpc")); + CHECK(packages.contains("valgrind-ohpc")); + CHECK_FALSE(packages.contains("openmpi5-pmix-gnu15-ohpc")); + CHECK_FALSE(packages.contains("mpich-ofi-gnu15-ohpc")); +} + TEST_CASE("resolvePackages preserves explicit package overrides") { const std::set explicitPackages { "custom-ohpc-package" }; @@ -261,6 +338,18 @@ TEST_CASE("resolvePackages preserves explicit package overrides") CHECK(packages == explicitPackages); } +TEST_CASE("buildPackageInstallCommand uses apt on Ubuntu") +{ + const auto command + = buildPackageInstallCommand(models::OS(models::OS::Distro::Ubuntu, + models::OS::Platform::ubuntu24, 4), + { "gnu15-compilers-ohpc", "openmpi5-gnu15-ohpc" }); + + CHECK(command + == "DEBIAN_FRONTEND=noninteractive apt install -y " + "gnu15-compilers-ohpc openmpi5-gnu15-ohpc"); +} + TEST_CASE("resolvePackages adds Intel oneAPI and Intel MPI when requested") { const auto packages = resolvePackages( @@ -268,7 +357,7 @@ TEST_CASE("resolvePackages adds Intel oneAPI and Intel MPI when requested") std::vector { std::string(bundleSerialLibraries), std::string(bundleParallelLibraries), std::string(bundleIntelOneAPI) }, - {}); + { }); CHECK(packages.contains("ohpc-gnu15-serial-libs")); CHECK(packages.contains("ohpc-gnu15-parallel-libs")); diff --git a/src/services/ansible/roles/selinux.cpp b/src/services/ansible/roles/selinux.cpp index 40102bcb..2781158c 100644 --- a/src/services/ansible/roles/selinux.cpp +++ b/src/services/ansible/roles/selinux.cpp @@ -31,6 +31,12 @@ void configureSELinuxMode() { LOG_INFO("Setting up SELinux") + if (cluster()->getHeadnode().getOS().getPackageType() + == opencattus::models::OS::PackageType::DEB) { + LOG_INFO("Skipping SELinux configuration on Ubuntu"); + return; + } + switch (cluster()->getSELinux()) { case opencattus::models::Cluster::SELinuxMode::Permissive: ::runner()->executeCommand("setenforce 0"); diff --git a/src/services/ansible/roles/slurm.cpp b/src/services/ansible/roles/slurm.cpp index 0ee03b16..33a59f12 100644 --- a/src/services/ansible/roles/slurm.cpp +++ b/src/services/ansible/roles/slurm.cpp @@ -1,4 +1,5 @@ #include +#include #include #include #include @@ -77,6 +78,51 @@ done sacct )slurm"; } + +std::string buildSlurmServerInstallCommand(const opencattus::models::OS& osinfo) +{ + switch (osinfo.getPackageType()) { + case opencattus::models::OS::PackageType::RPM: + return "dnf -y install ohpc-slurm-server mariadb-server mariadb"; + case opencattus::models::OS::PackageType::DEB: + return "DEBIAN_FRONTEND=noninteractive apt install -y " + "ohpc-slurm-server mariadb-server mariadb-client"; + } + + std::unreachable(); +} + +std::string buildSlurmConfigSeedScript() +{ + return R"slurm( +if test -f /etc/slurm/slurm.conf.ohpc; then + \cp /etc/slurm/slurm.conf.ohpc "$slurm_conf" +elif test -f /etc/slurm/slurm.conf.example; then + \cp /etc/slurm/slurm.conf.example "$slurm_conf" +else + cat > "$slurm_conf" <<'EOF' +ClusterName=cluster +SlurmctldHost=localhost +MpiDefault=none +ProctrackType=proctrack/cgroup +ReturnToService=1 +SlurmctldPidFile=/var/run/slurmctld.pid +SlurmctldPort=6817 +SlurmdPidFile=/var/run/slurmd.pid +SlurmdPort=6818 +SlurmdSpoolDir=/var/spool/slurmd +SlurmUser=slurm +StateSaveLocation=/var/spool/slurmctld +SwitchType=switch/none +SchedulerType=sched/backfill +SelectType=select/cons_tres +SelectTypeParameters=CR_Core_Memory +SlurmctldLogFile=/var/log/slurmctld.log +SlurmdLogFile=/var/log/slurmd.log +EOF +fi +)slurm"; +} } // namespace namespace opencattus::services::ansible::roles::slurm { @@ -120,14 +166,14 @@ void run(const Role& role) node.hostname, "hostname missing for node {}", nodeIndex); const auto nodeAddress = optional::unwrap( node.start_ip, "node_ip missing for node {}", nodeName); - nodeDeclarations.emplace_back(buildNodeDeclaration(nodeName, - nodeAddress.to_string(), cpusPerNode, sockets, realMemory, - coresPerSocket, threadsPerCore)); + nodeDeclarations.emplace_back( + buildNodeDeclaration(nodeName, nodeAddress.to_string(), cpusPerNode, + sockets, realMemory, coresPerSocket, threadsPerCore)); } runner::shell::fmt(R"del( # SLURM configuration -dnf -y install ohpc-slurm-server mariadb-server mariadb +{slurmServerInstallCommand} {controllerStartupScript} # Secure the installation, `mysql -u root` will exit with @@ -201,6 +247,8 @@ chmod 600 /etc/slurm/slurmdbd.conf )del", fmt::arg("controllerStartupScript", buildControllerStartupScript()), + fmt::arg( + "slurmServerInstallCommand", buildSlurmServerInstallCommand(os())), fmt::arg("hostname", answerfile()->hostname.hostname), fmt::arg( "mariadb_root_pass", answerfile()->slurm.mariadb_root_password), @@ -209,7 +257,7 @@ chmod 600 /etc/slurm/slurmdbd.conf runner::shell::fmt(R"del( # Minimal /etc/slurm/slurm.conf slurm_conf=/etc/slurm/slurm.conf -\cp /etc/slurm/slurm.conf.ohpc $slurm_conf +{slurmConfigSeedScript} sed -i \ -e "s/ClusterName=.*/ClusterName={cluster_name}/" \ @@ -228,6 +276,7 @@ sed -i \ "$slurm_conf" )del", + fmt::arg("slurmConfigSeedScript", buildSlurmConfigSeedScript()), fmt::arg("hostname", answerfile()->hostname.hostname), fmt::arg("cluster_name", answerfile()->information.cluster_name)); @@ -248,8 +297,8 @@ EOF {controllerActivationScript} )del", - fmt::arg("controllerActivationScript", - buildControllerActivationScript()), + fmt::arg( + "controllerActivationScript", buildControllerActivationScript()), fmt::arg("hostname", answerfile()->hostname.hostname), fmt::arg("node_declarations", fmt::join(nodeDeclarations, "\n")), fmt::arg("node_prefix", nodesPrefix), @@ -261,8 +310,8 @@ EOF TEST_CASE("buildNodeDeclaration pins the management address") { - CHECK(buildNodeDeclaration("n01", "192.168.30.1", "1", "1", "4096", "1", - "1") + CHECK( + buildNodeDeclaration("n01", "192.168.30.1", "1", "1", "4096", "1", "1") == "NodeName=n01 NodeAddr=192.168.30.1 NodeHostName=n01 CPUs=1 " "Sockets=1 RealMemory=4096 CoresPerSocket=1 ThreadsPerCore=1 " "State=UNKNOWN"); @@ -274,7 +323,8 @@ TEST_CASE("buildControllerStartupScript keeps service ordering explicit") CHECK(script.contains("systemctl enable munge slurmctld slurmdbd mariadb")); CHECK(script.contains("systemctl start munge mariadb")); - CHECK_FALSE(script.contains("systemctl enable --now munge slurmctld slurmdbd mariadb")); + CHECK_FALSE(script.contains( + "systemctl enable --now munge slurmctld slurmdbd mariadb")); } TEST_CASE("buildControllerActivationScript waits for slurmdbd before slurmctld") @@ -289,3 +339,12 @@ TEST_CASE("buildControllerActivationScript waits for slurmdbd before slurmctld") CHECK(script.contains("systemctl is-active --quiet slurmctld")); CHECK(script.contains("sacct >/dev/null 2>&1")); } + +TEST_CASE("buildSlurmConfigSeedScript falls back to Ubuntu example config") +{ + const auto script = buildSlurmConfigSeedScript(); + + CHECK(script.contains("/etc/slurm/slurm.conf.ohpc")); + CHECK(script.contains("/etc/slurm/slurm.conf.example")); + CHECK(script.contains("ClusterName=cluster")); +} diff --git a/src/services/ansible/roles/timesync.cpp b/src/services/ansible/roles/timesync.cpp index d3fc7fc7..b615b4a8 100644 --- a/src/services/ansible/roles/timesync.cpp +++ b/src/services/ansible/roles/timesync.cpp @@ -32,7 +32,10 @@ ScriptBuilder installScript( ->getHeadnode() .getConnections(); - std::string_view filename = CHROOT "/etc/chrony.conf"; + const auto filename + = osinfo.getPackageType() == opencattus::models::OS::PackageType::DEB + ? std::string_view(CHROOT "/etc/chrony/chrony.conf") + : std::string_view(CHROOT "/etc/chrony.conf"); for (const auto& connection : connections) { if ((connection.getNetwork()->getProfile() == Network::Profile::Management) @@ -49,7 +52,10 @@ ScriptBuilder installScript( } } - builder.enableService("chronyd"); + builder.enableService( + osinfo.getPackageType() == opencattus::models::OS::PackageType::DEB + ? std::string_view("chrony") + : std::string_view("chronyd")); return builder; } diff --git a/src/services/confluent.cpp b/src/services/confluent.cpp index a5aa61bd..7afd05ab 100644 --- a/src/services/confluent.cpp +++ b/src/services/confluent.cpp @@ -136,6 +136,40 @@ std::string buildConfluentRepoRpmUrl(const models::OS& os) std::string buildConfluentBootstrapCommands(const models::OS& os) { + if (os.getPackageType() == models::OS::PackageType::DEB) { + return R"( +# Add the Confluent repository +DEBIAN_FRONTEND=noninteractive apt update +DEBIAN_FRONTEND=noninteractive apt install -y ca-certificates wget +wget -q -O /etc/apt/trusted.gpg.d/confluent.gpg https://hpc.lenovo.com/apt/latest/lenovo-hpc.key +cat > /etc/apt/sources.list.d/confluent.sources <<'EOF' +Types: deb +URIs: https://hpc.lenovo.com/apt/latest/noble +Suites: noble +Components: main +Signed-By: /etc/apt/trusted.gpg.d/confluent.gpg +EOF + +# Install required packages +DEBIAN_FRONTEND=noninteractive apt update +DEBIAN_FRONTEND=noninteractive apt install -y lenovo-confluent tftpd-hpa dnsmasq +systemctl enable confluent --now +systemctl enable apache2 --now +systemctl enable tftpd-hpa --now +systemctl stop dnsmasq || : +systemctl enable dnsmasq + +# Enable the Confluent environment +test -f /etc/profile.d/confluent_env.sh +set +xeu # confluent_env.sh has undefined variables +source /etc/profile.d/confluent_env.sh +set -xeu +command -v confluent2hosts >/dev/null +command -v osdeploy >/dev/null +command -v imgutil >/dev/null +)"; + } + return fmt::format(R"( # Add the Confluent repository rpm -q lenovo-hpc-yum >/dev/null 2>&1 || rpm -Uvh {repoRpmUrl} @@ -178,6 +212,9 @@ std::string buildConfluentImageName(const models::OS& os) case models::OS::Distro::OL: distro = "ol"; break; + case models::OS::Distro::Ubuntu: + return fmt::format("ubuntu{}-{}", os.getVersion(), + opencattus::utils::enums::toString(os.getArch())); default: std::unreachable(); } @@ -186,6 +223,163 @@ std::string buildConfluentImageName(const models::OS& os) opencattus::utils::enums::toString(os.getArch())); } +std::string buildConfluentImageSourceResolutionScript( + const models::OS& os, std::string_view image) +{ + if (os.getDistro() == models::OS::Distro::Ubuntu) { + return R"(# Ubuntu imgutil builds from the build system repositories. +# Confluent does not support imgutil build --source for Ubuntu. +)"; + } + + return fmt::format(R"(export confluent_image_source="{image}" +if ! test -d "/var/lib/confluent/distributions/${{confluent_image_source}}"; then + echo "Unable to locate Confluent distribution source ${{confluent_image_source}}" >&2 + find /var/lib/confluent/distributions -maxdepth 1 -mindepth 1 -type d -printf '%f\n' >&2 || : + exit 1 +fi +)", + fmt::arg("image", image)); +} + +std::string buildConfluentDnsmasqCommands(const models::OS& os, + std::string_view internalNic, std::string_view headnodeAddress, + std::string_view domain) +{ + if (os.getPackageType() != models::OS::PackageType::DEB) { + return { }; + } + + return fmt::format(R"( +# Keep dnsmasq away from systemd-resolved's loopback stub on Ubuntu. +cat > /etc/dnsmasq.d/opencattus-confluent.conf <<'EOF' +interface={internalNic} +bind-interfaces +except-interface=lo +listen-address={headnodeAddress} +domain={domain} +expand-hosts +local=/{domain}/ +EOF +systemctl restart dnsmasq +systemctl is-active --quiet dnsmasq +)", + fmt::arg("internalNic", internalNic), + fmt::arg("headnodeAddress", headnodeAddress), + fmt::arg("domain", domain)); +} + +std::string buildConfluentHttpPublishCommands(const models::OS& os) +{ + if (os.getPackageType() != models::OS::PackageType::DEB) { + return { }; + } + + return R"( +# Publish Confluent boot assets and API through Apache on Ubuntu. +cat > /etc/apache2/conf-available/opencattus-confluent-public.conf <<'EOF' +Alias /confluent-public/ /var/lib/confluent/public/ +Alias /confluent-public /var/lib/confluent/public + + + Options FollowSymLinks + AllowOverride None + Require all granted + +EOF + +install -d -m 0755 /etc/confluent/apache +export opencattus_apache_fqdn=$(hostname -f 2>/dev/null || hostname) +export opencattus_apache_san=/etc/confluent/apache/opencattus-apache-san.cnf +cat > "${opencattus_apache_san}" </dev/null | tr ' ' '\n' | sort -u | while read -r name; do + if [ -n "${name}" ]; then + printf 'DNS.%s=%s\n' "${opencattus_dns_idx}" "${name}" >> "${opencattus_apache_san}" + opencattus_dns_idx=$((opencattus_dns_idx + 1)) + fi +done + +opencattus_ip_idx=1 +( + ip -o addr show scope global up | awk '{ print $4 }' | cut -d/ -f1 + ip -o -6 addr show scope link up | awk '{ print $4 }' | cut -d/ -f1 | cut -d% -f1 +) | sort -u | while read -r addr; do + if [ -n "${addr}" ]; then + printf 'IP.%s=%s\n' "${opencattus_ip_idx}" "${addr}" >> "${opencattus_apache_san}" + opencattus_ip_idx=$((opencattus_ip_idx + 1)) + fi +done + +openssl req -new -nodes -newkey rsa:2048 \ + -keyout /etc/confluent/apache/opencattus-apache.key \ + -out /etc/confluent/apache/opencattus-apache.csr \ + -config "${opencattus_apache_san}" +openssl x509 -req \ + -in /etc/confluent/apache/opencattus-apache.csr \ + -CA /etc/confluent/tls/cacert.pem \ + -CAkey /etc/confluent/tls/ca/private/cakey.pem \ + -CAcreateserial \ + -out /etc/confluent/apache/opencattus-apache.crt \ + -days 3650 -sha256 \ + -extensions v3_req -extfile "${opencattus_apache_san}" +chmod 0644 /etc/confluent/apache/opencattus-apache.crt +chmod 0600 /etc/confluent/apache/opencattus-apache.key +chown root:root /etc/confluent/apache/opencattus-apache.* + +cat > /etc/apache2/sites-available/opencattus-confluent-ssl.conf < + ServerName ${opencattus_apache_fqdn} + SSLEngine on + SSLCertificateFile /etc/confluent/apache/opencattus-apache.crt + SSLCertificateKeyFile /etc/confluent/apache/opencattus-apache.key + + ProxyPreserveHost On + ProxyPass /confluent-api/ http://127.0.0.1:4005/confluent-api/ + ProxyPassReverse /confluent-api/ http://127.0.0.1:4005/confluent-api/ + + Alias /confluent-public/ /var/lib/confluent/public/ + Alias /confluent-public /var/lib/confluent/public + + Options FollowSymLinks + AllowOverride None + Require all granted + + +EOF + +a2enmod ssl proxy proxy_http headers +a2enconf opencattus-confluent-public +a2ensite opencattus-confluent-ssl +apache2ctl configtest +systemctl reload apache2 +)"; +} + +std::string buildImgutilBuildCommand(const models::OS& os) +{ + if (os.getDistro() == models::OS::Distro::Ubuntu) { + return "imgutil build -y $scratchdir"; + } + + return "imgutil build -y -s \"$confluent_image_source\" $scratchdir"; +} + std::string buildSpackModuleTree(const models::OS& os) { std::string distro; @@ -202,6 +396,9 @@ std::string buildSpackModuleTree(const models::OS& os) case models::OS::Distro::OL: distro = "ol"; break; + case models::OS::Distro::Ubuntu: + return fmt::format("linux-ubuntu{}-{}", os.getVersion(), + opencattus::utils::enums::toString(os.getArch())); default: std::unreachable(); } @@ -229,6 +426,8 @@ std::string buildNodeImageRepoFiles(const models::OS& os) return "{epel,OpenHPC,rhel}.repo"; case models::OS::Distro::OL: return "{epel,OpenHPC,oracle}.repo"; + case models::OS::Distro::Ubuntu: + return ""; default: std::unreachable(); } @@ -244,6 +443,8 @@ std::string buildNodeImagePackages(const models::OS& os) return "ohpc-base-compute ohpc-slurm-client lmod-ohpc hwloc-libs"; case models::OS::Platform::el10: return "ohpc-base-compute ohpc-slurm-client lmod-ohpc hwloc-libs"; + case models::OS::Platform::ubuntu24: + return "ohpc-base-compute ohpc-slurm-client lmod-ohpc hwloc-ohpc"; default: std::unreachable(); } @@ -262,11 +463,138 @@ std::string buildNodeImageInstallCommand(const models::OS& os) case models::OS::Platform::el10: return "dnf install -y --nogpg " "ohpc-base-compute ohpc-slurm-client lmod-ohpc hwloc-libs"; + case models::OS::Platform::ubuntu24: + return "DEBIAN_FRONTEND=noninteractive apt update && " + "DEBIAN_FRONTEND=noninteractive apt install -y " + "ca-certificates && " + "DEBIAN_FRONTEND=noninteractive apt update && " + "DEBIAN_FRONTEND=noninteractive apt install -y " + "ohpc-base-compute ohpc-slurm-client lmod-ohpc hwloc-ohpc"; default: std::unreachable(); } } +std::string buildConfluentHeadnodeSELinuxCommands(const models::OS& os) +{ + if (os.getPackageType() == models::OS::PackageType::DEB) { + return "# SELinux is not configured on Ubuntu head nodes"; + } + + return "# Configure SELinux to allow httpd to make connections\n" + "setsebool -P httpd_can_network_connect=on"; +} + +std::string buildNodeImageRepositorySyncCommands( + const models::OS& os, std::string_view nodeImageRepoFiles) +{ + if (os.getPackageType() == models::OS::PackageType::DEB) { + return R"( +# Install the APT sources and keys into the scratch image +install -d $scratchdir/etc/apt/sources.list.d +install -d $scratchdir/etc/apt/trusted.gpg.d +if test -f /etc/apt/sources.list; then + \cp -va /etc/apt/sources.list $scratchdir/etc/apt/sources.list +fi +find /etc/apt/sources.list.d -maxdepth 1 \( -name '*.list' -o -name '*.sources' \) -exec \cp -va {} $scratchdir/etc/apt/sources.list.d/ \; +find /etc/apt/trusted.gpg.d -maxdepth 1 -type f -exec \cp -va {} $scratchdir/etc/apt/trusted.gpg.d/ \; +)"; + } + + return fmt::format(R"( +# Install the GPG keys & repos +\cp -va /etc/yum.repos.d/{nodeImageRepoFiles} $scratchdir/etc/yum.repos.d/ +)", + fmt::arg("nodeImageRepoFiles", nodeImageRepoFiles)); +} + +std::string buildNodeImageChronyCommands( + const models::OS& os, std::string_view hnIp) +{ + if (os.getPackageType() == models::OS::PackageType::DEB) { + return fmt::format(R"( +# Install and configure chrony +imgutil exec $scratchdir <> /etc/chrony/chrony.conf +grep -HE '^server' /etc/chrony/chrony.conf +systemctl enable chrony +EOF +)", + fmt::arg("hnIp", hnIp)); + } + + return fmt::format(R"( +# Install and configure chrony +imgutil exec $scratchdir <> /etc/chrony.conf +grep -HE '^server' /etc/chrony.conf +systemctl enable chronyd +EOF +)", + fmt::arg("hnIp", hnIp)); +} + +std::string buildNodeImageAutofsCommands( + const models::OS& os, std::string_view hnIp) +{ + const auto installCommand + = os.getPackageType() == models::OS::PackageType::DEB + ? "DEBIAN_FRONTEND=noninteractive apt update && " + "DEBIAN_FRONTEND=noninteractive apt install -y autofs nfs-common" + : "dnf install -y autofs"; + + return fmt::format(R"( +# Install and configure nfs and autofs +imgutil exec $scratchdir < /etc/auto.master.d/ohpc.autofs +echo "* -nfsvers=4 {hnIp}:/opt/ohpc/pub/&" > /etc/auto.ohpc + +echo "/home /etc/auto.home" > /etc/auto.master.d/home.autofs +echo "* -nfsvers=4 {hnIp}:/home/&" > /etc/auto.home + +echo "/opt/spack /etc/auto.spack" > /etc/auto.master.d/spack.autofs +echo "* -nfsvers=4 {hnIp}:/opt/spack/&" > /etc/auto.spack + +echo "/opt/intel /etc/auto.intel" > /etc/auto.master.d/intel.autofs +echo "* -nfsvers=4 {hnIp}:/opt/intel/&" > /etc/auto.intel + +echo "/opt/nvidia /etc/auto.nvidia" > /etc/auto.master.d/nvidia.autofs +echo "* -nfsvers=4 {hnIp}:/opt/nvidia/&" > /etc/auto.nvidia + +# Configure scratch area +echo "/scratch /etc/auto.scratch" > /etc/auto.master.d/scratch.autofs +echo "local -fstype=xfs,rw :/dev/sda1" > /etc/auto.scratch + +# Check that the mounts are correct +grep -H "{hnIp}" /etc/auto.{{home,ohpc,spack,intel,nvidia}} 2> /dev/null + +systemctl enable autofs +EOF +)", + fmt::arg("installCommand", installCommand), fmt::arg("hnIp", hnIp)); +} + +std::string buildNodeImageSlurmDefaultsCommand( + const models::OS& os, std::string_view hnIp) +{ + if (os.getPackageType() == models::OS::PackageType::DEB) { + return fmt::format("echo SLURMD_OPTIONS=\\\"--conf-server {hnIp}\\\" > " + "/etc/default/slurmd", + fmt::arg("hnIp", hnIp)); + } + + return fmt::format("echo SLURMD_OPTIONS=\\\"--conf-server {hnIp}\\\" > " + "/etc/sysconfig/slurmd", + fmt::arg("hnIp", hnIp)); +} + std::optional selectConfluentImageKernelVersion( const std::optional& ofed, std::optional configuredKernel, @@ -401,6 +729,13 @@ std::string buildNodeImageOFEDCommands(const models::OS& os, switch (ofed->getKind()) { case OFED::Kind::Inbox: + if (os.getPackageType() == models::OS::PackageType::DEB) { + return R"( +imgutil exec $scratchdir <> /etc/chrony.conf -grep -HE '^server' /etc/chrony.conf -systemctl enable chronyd -EOF +{nodeImageChronyCommands} -# Install and configure nfs and autofs -imgutil exec $scratchdir < /etc/auto.master.d/ohpc.autofs -echo "* -nfsvers=4 {hnIp}:/opt/ohpc/pub/&" > /etc/auto.ohpc - -echo "/home /etc/auto.home" > /etc/auto.master.d/home.autofs -echo "* -nfsvers=4 {hnIp}:/home/&" > /etc/auto.home - -echo "/opt/spack /etc/auto.spack" > /etc/auto.master.d/spack.autofs -echo "* -nfsvers=4 {hnIp}:/opt/spack/&" > /etc/auto.spack - -echo "/opt/intel /etc/auto.intel" > /etc/auto.master.d/intel.autofs -echo "* -nfsvers=4 {hnIp}:/opt/intel/&" > /etc/auto.intel - -echo "/opt/nvidia /etc/auto.nvidia" > /etc/auto.master.d/nvidia.autofs -echo "* -nfsvers=4 {hnIp}:/opt/nvidia/&" > /etc/auto.nvidia - -# Configure scratch area -echo "/scratch /etc/auto.scratch" > /etc/auto.master.d/scratch.autofs -echo "local -fstype=xfs,rw :/dev/sda1" > /etc/auto.scratch - -# Check that the mounts are correct -grep -H "{hnIp}" /etc/auto.{{home,ohpc,spack,intel,nvidia}} 2> /dev/null - -systemctl enable autofs -EOF +{nodeImageAutofsCommands} # Slurm node configuration \install -vD -m 0400 -o munge -g munge /etc/munge/munge.key $scratchdir/etc/munge/munge.key \install -vD -m 0644 -o root -g root /etc/slurm/slurm.conf $scratchdir/etc/slurm/slurm.conf -# Install the GPG keys & repos -\cp -va /etc/yum.repos.d/{nodeImageRepoFiles} $scratchdir/etc/yum.repos.d/ +{nodeImageRepositorySyncCommands} imgutil exec $scratchdir <> /etc/pam.d/sshd -echo SLURMD_OPTIONS=\"--conf-server {hnIp}\" > /etc/sysconfig/slurmd +{nodeImageSlurmDefaultsCommand} chown munge: /etc/munge/munge.key systemctl enable munge systemctl enable slurmd @@ -674,11 +984,53 @@ rm -rf $scratchdir || : fmt::arg("domain", cluster()->getDomainName()), fmt::arg("confluentBootstrapCommands", buildConfluentBootstrapCommands(os())), + fmt::arg("confluentHttpPublishCommands", + buildConfluentHttpPublishCommands(os())), + fmt::arg("confluentDnsmasqCommands", + buildConfluentDnsmasqCommands(os(), + utils::optional::unwrap(answerfile()->management.con_interface, + "Internal interface not found in [network_management]"), + cluster() + ->getHeadnode() + .getConnection(Network::Profile::Management) + .getAddress() + .to_string(), + cluster()->getDomainName())), + fmt::arg("confluentImageSourceResolutionScript", + buildConfluentImageSourceResolutionScript(computeNodeOS, image)), + fmt::arg( + "imgutilBuildCommand", buildImgutilBuildCommand(computeNodeOS)), + fmt::arg("headnodeSELinuxCommands", + buildConfluentHeadnodeSELinuxCommands(os())), fmt::arg("spackModulePathExport", buildUserSpackModulePathExport(computeNodeOS)), + fmt::arg("nodeImageChronyCommands", + buildNodeImageChronyCommands(computeNodeOS, + cluster() + ->getHeadnode() + .getConnection(Network::Profile::Management) + .getAddress() + .to_string())), + fmt::arg("nodeImageAutofsCommands", + buildNodeImageAutofsCommands(computeNodeOS, + cluster() + ->getHeadnode() + .getConnection(Network::Profile::Management) + .getAddress() + .to_string())), fmt::arg("nodeImageInstallCommand", buildNodeImageInstallCommand(computeNodeOS)), fmt::arg("nodeImageRepoFiles", buildNodeImageRepoFiles(computeNodeOS)), + fmt::arg("nodeImageRepositorySyncCommands", + buildNodeImageRepositorySyncCommands( + computeNodeOS, buildNodeImageRepoFiles(computeNodeOS))), + fmt::arg("nodeImageSlurmDefaultsCommand", + buildNodeImageSlurmDefaultsCommand(computeNodeOS, + cluster() + ->getHeadnode() + .getConnection(Network::Profile::Management) + .getAddress() + .to_string())), fmt::arg("hnIp", cluster() ->getHeadnode() @@ -844,7 +1196,7 @@ TEST_CASE("buildConfluentRepoRpmUrl uses upstream by default") using opencattus::services::Options; opencattus::Singleton::init( - std::make_unique(Options {})); + std::make_unique(Options { })); CHECK(buildConfluentRepoRpmUrl(OS(OS::Distro::Rocky, OS::Platform::el9, 7)) == "https://hpc.lenovo.com/yum/latest/el9/x86_64/" @@ -897,6 +1249,68 @@ TEST_CASE("buildConfluentBootstrapCommands validates Confluent tools before " CHECK_FALSE(script.contains("lenovo-confluent tftp-server dnsmasq || :")); } +TEST_CASE("buildConfluentBootstrapCommands uses Lenovo APT on Ubuntu") +{ + using opencattus::models::OS; + + const auto script = buildConfluentBootstrapCommands( + OS(OS::Distro::Ubuntu, OS::Platform::ubuntu24, 4)); + + CHECK(script.contains("https://hpc.lenovo.com/apt/latest/noble")); + CHECK(script.contains( + "DEBIAN_FRONTEND=noninteractive apt install -y lenovo-confluent " + "tftpd-hpa dnsmasq")); + CHECK(script.contains("systemctl enable apache2 --now")); + CHECK(script.contains("systemctl enable dnsmasq")); + CHECK_FALSE(script.contains("systemctl enable dnsmasq --now")); + CHECK(script.contains("command -v confluent2hosts >/dev/null")); +} + +TEST_CASE("buildConfluentDnsmasqCommands binds Ubuntu dnsmasq to management") +{ + using opencattus::models::OS; + + const auto script = buildConfluentDnsmasqCommands( + OS(OS::Distro::Ubuntu, OS::Platform::ubuntu24, 4), "oc-mgmt0", + "172.31.38.254", "cluster.example.com"); + + CHECK(script.contains("interface=oc-mgmt0")); + CHECK(script.contains("bind-interfaces")); + CHECK(script.contains("listen-address=172.31.38.254")); + CHECK(script.contains("systemctl is-active --quiet dnsmasq")); + + CHECK(buildConfluentDnsmasqCommands( + OS(OS::Distro::Rocky, OS::Platform::el9, 7), "eth1", "172.31.38.254", + "cluster.example.com") + .empty()); +} + +TEST_CASE("buildConfluentHttpPublishCommands exposes Ubuntu boot assets") +{ + using opencattus::models::OS; + + const auto script = buildConfluentHttpPublishCommands( + OS(OS::Distro::Ubuntu, OS::Platform::ubuntu24, 4)); + + CHECK(script.contains("Alias /confluent-public/ " + "/var/lib/confluent/public/")); + CHECK(script.contains("Require all granted")); + CHECK(script.contains("ProxyPass /confluent-api/ " + "http://127.0.0.1:4005/confluent-api/")); + CHECK(script.contains("SSLCertificateFile " + "/etc/confluent/apache/opencattus-apache.crt")); + CHECK(script.contains("openssl x509 -req")); + CHECK(script.contains("a2enmod ssl proxy proxy_http headers")); + CHECK(script.contains("a2enconf opencattus-confluent-public")); + CHECK(script.contains("a2ensite opencattus-confluent-ssl")); + CHECK(script.contains("apache2ctl configtest")); + CHECK(script.contains("systemctl reload apache2")); + + CHECK(buildConfluentHttpPublishCommands( + OS(OS::Distro::Rocky, OS::Platform::el9, 7)) + .empty()); +} + TEST_CASE("buildSpackModuleTree matches Spack's Enterprise Linux module naming") { using opencattus::models::OS; @@ -907,6 +1321,9 @@ TEST_CASE("buildSpackModuleTree matches Spack's Enterprise Linux module naming") == "linux-rocky9-x86_64"); CHECK(buildSpackModuleTree(OS(OS::Distro::Rocky, OS::Platform::el10, 0)) == "linux-rocky10-x86_64"); + CHECK( + buildSpackModuleTree(OS(OS::Distro::Ubuntu, OS::Platform::ubuntu24, 4)) + == "linux-ubuntu24.04-x86_64"); } TEST_CASE("buildConfluentImageName matches osdeploy distribution ids") @@ -922,6 +1339,21 @@ TEST_CASE("buildConfluentImageName matches osdeploy distribution ids") == "rhel-9.7-x86_64"); CHECK(buildConfluentImageName(OS(OS::Distro::OL, OS::Platform::el9, 5)) == "ol-9.5-x86_64"); + CHECK(buildConfluentImageName( + OS(OS::Distro::Ubuntu, OS::Platform::ubuntu24, 4)) + == "ubuntu24.04-x86_64"); +} + +TEST_CASE("buildImgutilBuildCommand omits unsupported Ubuntu source") +{ + using opencattus::models::OS; + + CHECK(buildImgutilBuildCommand( + OS(OS::Distro::Ubuntu, OS::Platform::ubuntu24, 4)) + == "imgutil build -y $scratchdir"); + + CHECK(buildImgutilBuildCommand(OS(OS::Distro::Rocky, OS::Platform::el9, 7)) + == "imgutil build -y -s \"$confluent_image_source\" $scratchdir"); } TEST_CASE("buildUserSpackModulePathExport tolerates an unset MODULEPATH") @@ -938,6 +1370,11 @@ TEST_CASE("buildUserSpackModulePathExport tolerates an unset MODULEPATH") == "export " "MODULEPATH=/opt/spack/share/spack/lmod/linux-rocky10-x86_64/" "Core${MODULEPATH:+:$MODULEPATH}"); + CHECK(buildUserSpackModulePathExport( + OS(OS::Distro::Ubuntu, OS::Platform::ubuntu24, 4)) + == "export " + "MODULEPATH=/opt/spack/share/spack/lmod/linux-ubuntu24.04-x86_64/" + "Core${MODULEPATH:+:$MODULEPATH}"); } TEST_CASE("buildNodeImageRepoFiles keeps distro repo filenames explicit") @@ -953,6 +1390,9 @@ TEST_CASE("buildNodeImageRepoFiles keeps distro repo filenames explicit") == "{epel,OpenHPC,rhel}.repo"); CHECK(buildNodeImageRepoFiles(OS(OS::Distro::OL, OS::Platform::el9, 5)) == "{epel,OpenHPC,oracle}.repo"); + CHECK(buildNodeImageRepoFiles( + OS(OS::Distro::Ubuntu, OS::Platform::ubuntu24, 4)) + == ""); } TEST_CASE("buildNodeImagePackages keeps EL8 and newer node images explicit") @@ -974,6 +1414,11 @@ TEST_CASE("buildNodeImagePackages keeps EL8 and newer node images explicit") = buildNodeImagePackages(OS(OS::Distro::Rocky, OS::Platform::el10, 1)); CHECK(el10Packages == "ohpc-base-compute ohpc-slurm-client lmod-ohpc hwloc-libs"); + + const auto ubuntuPackages = buildNodeImagePackages( + OS(OS::Distro::Ubuntu, OS::Platform::ubuntu24, 4)); + CHECK(ubuntuPackages + == "ohpc-base-compute ohpc-slurm-client lmod-ohpc hwloc-ohpc"); } TEST_CASE("buildNodeImageInstallCommand keeps EL8, EL9, and EL10 explicit") @@ -995,6 +1440,51 @@ TEST_CASE("buildNodeImageInstallCommand keeps EL8, EL9, and EL10 explicit") OS(OS::Distro::Rocky, OS::Platform::el10, 1)) == "dnf install -y --nogpg " "ohpc-base-compute ohpc-slurm-client lmod-ohpc hwloc-libs"); + + CHECK(buildNodeImageInstallCommand( + OS(OS::Distro::Ubuntu, OS::Platform::ubuntu24, 4)) + == "DEBIAN_FRONTEND=noninteractive apt update && " + "DEBIAN_FRONTEND=noninteractive apt install -y " + "ca-certificates && DEBIAN_FRONTEND=noninteractive apt update " + "&& DEBIAN_FRONTEND=noninteractive apt install -y " + "ohpc-base-compute ohpc-slurm-client lmod-ohpc hwloc-ohpc"); +} + +TEST_CASE("buildNodeImageChronyCommands refreshes APT metadata on Ubuntu") +{ + using opencattus::models::OS; + + const auto script = buildNodeImageChronyCommands( + OS(OS::Distro::Ubuntu, OS::Platform::ubuntu24, 4), "172.31.38.254"); + + CHECK(script.contains("DEBIAN_FRONTEND=noninteractive apt update\n" + "DEBIAN_FRONTEND=noninteractive apt install -y " + "chrony")); +} + +TEST_CASE("buildNodeImageAutofsCommands refreshes APT metadata on Ubuntu") +{ + using opencattus::models::OS; + + const auto script = buildNodeImageAutofsCommands( + OS(OS::Distro::Ubuntu, OS::Platform::ubuntu24, 4), "172.31.38.254"); + + CHECK(script.contains("DEBIAN_FRONTEND=noninteractive apt update && " + "DEBIAN_FRONTEND=noninteractive apt install -y " + "autofs nfs-common")); +} + +TEST_CASE("buildNodeImageRepositorySyncCommands copies APT sources on Ubuntu") +{ + using opencattus::models::OS; + + const auto script = buildNodeImageRepositorySyncCommands( + OS(OS::Distro::Ubuntu, OS::Platform::ubuntu24, 4), ""); + + CHECK(script.contains("/etc/apt/sources.list.d")); + CHECK(script.contains("/etc/apt/trusted.gpg.d")); + CHECK(script.contains("*.sources")); + CHECK_FALSE(script.contains("/etc/yum.repos.d")); } TEST_CASE("buildNodeImageOFEDCommands skips OFED runtime staging without an " @@ -1026,6 +1516,19 @@ TEST_CASE("buildNodeImageOFEDCommands uses inbox packages for inbox OFED") CHECK_FALSE(script.contains("doca-ofed")); } +TEST_CASE("buildNodeImageOFEDCommands uses Ubuntu inbox RDMA packages") +{ + using opencattus::models::OS; + + const auto script = buildNodeImageOFEDCommands( + OS(OS::Distro::Ubuntu, OS::Platform::ubuntu24, 4), true, + std::optional(OFED(OFED::Kind::Inbox, "")), std::nullopt); + + CHECK(script.contains("apt install -y rdma-core ibverbs-providers " + "perftest")); + CHECK_FALSE(script.contains("dnf group install")); +} + TEST_CASE( "selectConfluentImageKernelVersion keeps DOCA images on the staged kernel") { diff --git a/src/services/files.cpp b/src/services/files.cpp index 209e42da..cd686e03 100644 --- a/src/services/files.cpp +++ b/src/services/files.cpp @@ -89,9 +89,12 @@ KeyFile::~KeyFile() = default; namespace { std::vector toStrings(const std::vector& input) { - return input | std::views::transform([](const auto& group) { - return group.raw(); - }) | std::ranges::to>(); + std::vector output; + output.reserve(input.size()); + for (const auto& group : input) { + output.emplace_back(group.raw()); + } + return output; } } @@ -100,9 +103,13 @@ std::vector KeyFile::listAllPrefixedEntries( { auto groups = getGroups(); - return groups | std::views::filter([&](const auto& group) { - return group.starts_with(prefix); - }) | std::ranges::to(); + std::vector output; + for (auto& group : groups) { + if (group.starts_with(prefix)) { + output.emplace_back(std::move(group)); + } + } + return output; } std::vector KeyFile::getGroups() const diff --git a/src/services/osservice.cpp b/src/services/osservice.cpp index c8e0b3da..a943addb 100644 --- a/src/services/osservice.cpp +++ b/src/services/osservice.cpp @@ -160,6 +160,137 @@ class ELOSService final : public IOSService { }; }; +class UbuntuOSService final : public IOSService { + OS m_osinfo; + +public: + explicit UbuntuOSService(const OS& osinfo) + : m_osinfo(osinfo) { }; + + [[nodiscard]] std::string getKernelInstalled() const override + { + auto output = Singleton::get()->checkOutput( + "bash -c \"dpkg-query -W -f='${Version} ${Package}\\n' " + "'linux-image-*' 2>/dev/null | sort -Vr | head -1 | awk " + "'{print $1}'\""); + return output.empty() ? getKernelRunning() : output[0]; + } + + [[nodiscard]] std::string getKernelRunning() const override + { + return Singleton::get()->checkOutput("uname -r")[0]; + } + + [[nodiscard]] std::string getLocale() const override + { + return Singleton::get()->checkOutput( + R"(bash -c "localectl status | awk -F'=' '/System Locale: / {print $2}'")") + [0]; + } + + [[nodiscard]] std::vector getAvailableLocales() const override + { + return Singleton::get()->checkOutput("locale -a"); + } + + [[nodiscard]] bool install(std::string_view package) const override + { + return Singleton::get()->executeCommand(fmt::format( + "DEBIAN_FRONTEND=noninteractive apt install -y {}", package)) + != 0; + } + + [[nodiscard]] bool reinstall(std::string_view package) const override + { + return Singleton::get()->executeCommand(fmt::format( + "DEBIAN_FRONTEND=noninteractive apt install --reinstall " + "-y {}", + package)) + != 0; + } + + [[nodiscard]] bool groupInstall(std::string_view package) const override + { + return install(package); + } + + [[nodiscard]] bool remove(std::string_view package) const override + { + return Singleton::get()->executeCommand(fmt::format( + "DEBIAN_FRONTEND=noninteractive apt remove -y {}", package)) + != 0; + } + + [[nodiscard]] bool update(std::string_view package) const override + { + return Singleton::get()->executeCommand( + fmt::format("DEBIAN_FRONTEND=noninteractive apt install " + "--only-upgrade -y {}", + package)) + != 0; + } + + [[nodiscard]] bool update() const override + { + return Singleton::get()->executeCommand( + "DEBIAN_FRONTEND=noninteractive apt update && " + "DEBIAN_FRONTEND=noninteractive apt upgrade -y") + != 0; + } + + void check() const override + { + Singleton::get()->executeCommand("apt check"); + } + + void pinOSVersion() const override { static_cast(m_osinfo); } + + void clean() const override + { + Singleton::get()->executeCommand("apt clean"); + } + + [[nodiscard]] std::vector repolist() const override + { + return Singleton::get()->checkOutput("apt-cache policy"); + } + + [[nodiscard]] bool enableService(std::string_view service) const override + { + return Singleton::get()->executeCommand( + fmt::format("systemctl enable --now {}", service)) + == 0; + }; + + [[nodiscard]] bool disableService(std::string_view service) const override + { + return Singleton::get()->executeCommand( + fmt::format("systemctl disable --now {}", service)) + == 0; + }; + + [[nodiscard]] bool startService(std::string_view service) const override + { + return Singleton::get()->executeCommand( + fmt::format("systemctl start {}", service)) + == 0; + }; + + [[nodiscard]] bool stopService(std::string_view service) const override + { + return Singleton::get()->executeCommand( + fmt::format("systemctl stop {}", service)) + == 0; + }; + + [[nodiscard]] bool restartService(std::string_view service) const override + { + return Singleton::get()->executeCommand( + fmt::format("systemctl restart {}", service)) + == 0; + }; +}; + std::unique_ptr IOSService::factory(const OS& osinfo) { switch (osinfo.getDistro()) { @@ -168,6 +299,8 @@ std::unique_ptr IOSService::factory(const OS& osinfo) case OS::Distro::AlmaLinux: case OS::Distro::OL: return makeUniqueDerived(osinfo); + case OS::Distro::Ubuntu: + return makeUniqueDerived(osinfo); default: throw std::logic_error("Not implemented"); } diff --git a/src/services/repos.cpp b/src/services/repos.cpp index 963b86bc..ad198b26 100644 --- a/src/services/repos.cpp +++ b/src/services/repos.cpp @@ -578,7 +578,7 @@ struct RepoAssembler final { const UpstreamRepo& upstream, const bool enabled = false, const bool forceUpstream = false) { - auto repo = RPMRepository {}; + auto repo = RPMRepository { }; repo.group(static_cast(repoid.id)); repo.id(static_cast(repoid.id)); repo.name(static_cast(repoid.name)); @@ -666,7 +666,7 @@ class RepoConfFile final { void insert(const std::string& filename, RepoConfig& value) { if (!m_files.contains(filename)) { - m_files.emplace(filename, std::vector {}); + m_files.emplace(filename, std::vector { }); } m_files.at(filename).emplace_back(value); } @@ -682,9 +682,12 @@ class RepoConfFile final { // RepoConfigs grouped by file name [[nodiscard]] std::vector filesnames() const { - return m_files - | std::views::transform([](const auto& pair) { return pair.first; }) - | std::ranges::to(); + std::vector output; + output.reserve(m_files.size()); + for (const auto& [filename, _configs] : m_files) { + output.emplace_back(filename); + } + return output; } // Find a RepoConfig by repository id, if it exists @@ -1070,9 +1073,11 @@ std::string defaultOpenHPCVersionFor(const OS& osinfo) return "3"; case OS::Platform::el10: return "4"; + case OS::Platform::ubuntu24: + return "versatushpc-4"; default: throw std::runtime_error( - fmt::format("Unsupported OpenHPC repository baseline for EL{}", + fmt::format("Unsupported OpenHPC repository baseline for {}", osinfo.getMajorVersion())); } } @@ -1139,6 +1144,10 @@ bool usesDocaMajorVersionRepo(std::string_view ofedVersion) std::string defaultDOCARepoTargetFor( const OS& osinfo, std::string_view ofedVersion) { + if (osinfo.getDistro() == OS::Distro::Ubuntu) { + return ""; + } + if (usesDocaMajorVersionRepo(ofedVersion)) { switch (osinfo.getPlatform()) { case OS::Platform::el8: @@ -1178,15 +1187,21 @@ std::string defaultCUDAGPGKeyFor(const OS& osinfo) return "D42D0685.pub"; case OS::Platform::el10: return "CDF6BA43.pub"; + case OS::Platform::ubuntu24: + return ""; default: throw std::runtime_error( - fmt::format("Unsupported CUDA repository baseline for EL{}", + fmt::format("Unsupported CUDA repository baseline for {}", osinfo.getMajorVersion())); } } std::string defaultRHELCodeReadyMirrorRepoFor(const OS& osinfo) { + if (osinfo.getDistro() == OS::Distro::Ubuntu) { + return ""; + } + const auto arch = opencattus::utils::enums::toString(osinfo.getArch()); switch (osinfo.getPlatform()) { case OS::Platform::el8: @@ -1205,6 +1220,10 @@ std::string defaultRHELCodeReadyMirrorRepoFor(const OS& osinfo) std::string defaultRHELBaseMirrorGPGKeyFor(const OS& osinfo) { + if (osinfo.getDistro() == OS::Distro::Ubuntu) { + return ""; + } + switch (osinfo.getPlatform()) { case OS::Platform::el8: case OS::Platform::el9: @@ -1220,6 +1239,10 @@ std::string defaultRHELBaseMirrorGPGKeyFor(const OS& osinfo) std::string defaultRHELCodeReadyMirrorGPGKeyFor(const OS& osinfo) { + if (osinfo.getDistro() == OS::Distro::Ubuntu) { + return ""; + } + switch (osinfo.getPlatform()) { case OS::Platform::el8: case OS::Platform::el9: @@ -1658,6 +1681,9 @@ TEST_CASE("defaultOpenHPCVersionFor maps the supported EL releases") CHECK(defaultOpenHPCVersionFor( OS(models::OS::Distro::Rocky, OS::Platform::el10, 1)) == "4"); + CHECK(defaultOpenHPCVersionFor( + OS(models::OS::Distro::Ubuntu, OS::Platform::ubuntu24, 4)) + == "versatushpc-4"); } // Installs and enable/disable RPM repositories @@ -1745,15 +1771,15 @@ class RPMRepoManager final { { try { auto path = fmt::format("{}/{}", basedir, repoFileName); - auto repos = RPMRepositoryFile(path).repos() - // We copy to cons unique to express that these values cannot - // be changed through this API - | std::views::transform([](auto&& pair) { - return std::make_unique( - *pair.second); - }) - | std::ranges::to< - std::vector>>(); + const auto repoFileRepos = RPMRepositoryFile(path).repos(); + std::vector> repos; + repos.reserve(repoFileRepos.size()); + // We copy to const unique to express that these values cannot be + // changed through this API. + for (const auto& [_id, repo] : repoFileRepos) { + repos.emplace_back( + std::make_unique(*repo)); + } return repos; } catch (const std::out_of_range& e) { throw std::runtime_error( @@ -1785,7 +1811,7 @@ class RPMRepoManager final { void enable(const std::vector& repos, bool value) { auto byIdPtr = [](const std::shared_ptr& rptr) { - return std::hash {}(rptr->path()); + return std::hash { }(rptr->path()); }; std::unordered_set, decltype(byIdPtr)> @@ -1814,7 +1840,7 @@ class RPMRepoManager final { { // Function to iterate over map by id constexpr auto byId - = [](auto& repo) { return std::hash {}(repo.id()); }; + = [](auto& repo) { return std::hash { }(repo.id()); }; std::unordered_set output; for (auto& [_id1, repoFile] : m_filesIdx) { @@ -1823,9 +1849,12 @@ class RPMRepoManager final { } } - return output | std::views::transform([](auto&& repo) { - return std::make_unique(repo); - }) | std::ranges::to>>(); + std::vector> repos; + repos.reserve(output.size()); + for (const auto& repo : output) { + repos.emplace_back(std::make_unique(repo)); + } + return repos; } }; @@ -2077,7 +2106,7 @@ TEST_CASE("RepoNames") struct ShouldUseVaultService final { static bool shouldUseVault(const OS& osinfo) { return false; } }; - const auto enabler = RepoNames {}; + const auto enabler = RepoNames { }; const RepoConfigVars& vars = RepoConfigVars { .arch = "x86_64", .beegfsVersion = "beegfs_7.3.3", @@ -2382,7 +2411,7 @@ TEST_SUITE("opencattus::services::repos [slow]") #ifdef BUILD_TESTING using namespace opencattus::services; opencattus::services::initializeSingletonsOptions( - std::make_unique(Options {})); + std::make_unique(Options { })); const auto repos = std::filesystem::path("./repos"); REQUIRE(opencattus::functions::exists(repos / "repos.conf")); const auto confs @@ -2494,7 +2523,7 @@ std::vector expandSelectedRepositoryIds( std::vector expanded; expanded.reserve(repositoryIds.size() + 2); - auto seen = std::unordered_set {}; + auto seen = std::unordered_set { }; const auto append = [&](const std::string& repoId, auto&& appendSelf) -> void { @@ -2524,6 +2553,26 @@ RepoManager::defaultRepositoriesFor(const OS& osinfo, const std::optional>& enabledRepositories, const std::optional>& enabledOpenHPCBundles) { + if (osinfo.getDistro() == OS::Distro::Ubuntu) { + static_cast(ofedVersion); + static_cast(enabledRepositories); + static_cast(enabledOpenHPCBundles); + return { + { .id = "ubuntu-main", + .name = "Ubuntu 24.04 main, restricted, universe, multiverse", + .enabled = true }, + { .id = "ubuntu-updates", + .name = "Ubuntu 24.04 updates", + .enabled = true }, + { .id = "ubuntu-security", + .name = "Ubuntu 24.04 security", + .enabled = true }, + { .id = "OpenHPC", + .name = "VersatusHPC OpenHPC 4.x for Ubuntu 24.04", + .enabled = true }, + }; + } + struct NoVaultLookup final { static bool shouldUseVault(const OS& osinfo) { @@ -2589,7 +2638,7 @@ TEST_CASE("defaultRepositoriesFor keeps mandatory repositories enabled when " "optional repositories are selected") { opencattus::services::initializeSingletonsOptions( - std::make_unique(Options {})); + std::make_unique(Options { })); const auto osinfo = OS(models::OS::Distro::Rocky, OS::Platform::el9, 6, OS::Arch::x86_64); const auto selections = RepoManager::defaultRepositoriesFor( @@ -2618,7 +2667,7 @@ TEST_CASE("defaultRepositoriesFor enables oneAPI when the Intel OpenHPC " "bundle is selected") { opencattus::services::initializeSingletonsOptions( - std::make_unique(Options {})); + std::make_unique(Options { })); const auto osinfo = OS( models::OS::Distro::Rocky, OS::Platform::el10, 1, OS::Arch::x86_64); const auto selections = RepoManager::defaultRepositoriesFor(osinfo, @@ -2630,6 +2679,22 @@ TEST_CASE("defaultRepositoriesFor enables oneAPI when the Intel OpenHPC " CHECK(it->enabled); } +TEST_CASE("defaultRepositoriesFor exposes Ubuntu 24.04 mandatory repositories") +{ + opencattus::services::initializeSingletonsOptions( + std::make_unique(Options { })); + const auto osinfo = OS(models::OS::Distro::Ubuntu, OS::Platform::ubuntu24, + 4, OS::Arch::x86_64); + const auto selections = RepoManager::defaultRepositoriesFor( + osinfo, "latest", std::nullopt, std::nullopt); + + CHECK(selections.size() == 4); + CHECK(std::ranges::all_of( + selections, [](const auto& selection) { return selection.enabled; })); + CHECK(std::ranges::any_of(selections, + [](const auto& selection) { return selection.id == "OpenHPC"; })); +} + inline void RPMRepository::valid() const { auto isValid = (!id().empty() && !name().empty() @@ -2654,6 +2719,52 @@ struct RPMRepositoryGenerator { } }; +std::string ubuntuOpenHpcRepositoryUrl(const OS& osinfo) +{ + switch (osinfo.getPlatform()) { + case OS::Platform::ubuntu24: + return "https://repos.versatushpc.com.br/openhpc/" + "versatushpc-4/Ubuntu_24.04/"; + default: + throw std::runtime_error(fmt::format( + "Unsupported Ubuntu OpenHPC repository baseline for {}", + osinfo.getVersion())); + } +} + +TEST_CASE("ubuntuOpenHpcRepositoryUrl uses the VersatusHPC Noble fork") +{ + const auto osinfo = OS(models::OS::Distro::Ubuntu, OS::Platform::ubuntu24, + 4, OS::Arch::x86_64); + + CHECK(ubuntuOpenHpcRepositoryUrl(osinfo) + == "https://repos.versatushpc.com.br/openhpc/versatushpc-4/" + "Ubuntu_24.04/"); +} + +void initializeDebianHeadnodeRepositories(const OS& osinfo) +{ + if (osinfo.getDistro() != OS::Distro::Ubuntu) { + throw std::logic_error( + "Debian repository initialization is only implemented for Ubuntu"); + } + + // The VersatusHPC Ubuntu OpenHPC fork publishes a signed Release file, but + // the public key is not published next to the repository yet. Keep this + // explicit so apt can consume the repo while the repository signing path is + // finished. + runner::shell::fmt(R"( +DEBIAN_FRONTEND=noninteractive apt update +DEBIAN_FRONTEND=noninteractive apt install -y ca-certificates +install -d /etc/apt/sources.list.d +cat > /etc/apt/sources.list.d/opencattus-openhpc.list <<'EOF' +deb [trusted=yes] {openhpcUrl} ./ +EOF +DEBIAN_FRONTEND=noninteractive apt update +)", + fmt::arg("openhpcUrl", ubuntuOpenHpcRepositoryUrl(osinfo))); +} + void RepoManager::initializeDefaultRepositories() { auto opts = opencattus::utils::singleton::options(); @@ -2663,7 +2774,21 @@ void RepoManager::initializeDefaultRepositories() } LOG_INFO("RepoManager initialization"); auto cluster = opencattus::Singleton::get(); - const auto& osinfo = cluster->getComputeNodeOS(); + const auto& computeOS = cluster->getComputeNodeOS(); + const auto& headnodeOS = cluster->getHeadnode().getOS(); + const auto& osinfo = computeOS.getPackageType() == OS::PackageType::DEB + && headnodeOS.getPackageType() == OS::PackageType::RPM + ? headnodeOS + : computeOS; + + if (computeOS.getPackageType() == OS::PackageType::DEB + && headnodeOS.getPackageType() == OS::PackageType::RPM) { + LOG_WARN("Compute nodes use DEB repositories, but the head node uses " + "RPM. Initializing RPM repositories for head-node packages; " + "xCAT will attach Ubuntu APT repositories directly to the " + "compute image."); + } + const auto ofedVersion = cluster->getOFED().has_value() ? cluster->getOFED()->getVersion() : std::string("latest"); @@ -2700,7 +2825,7 @@ void RepoManager::initializeDefaultRepositories() "config-manager --save --setopt=keepcache=True"); } break; case OS::PackageType::DEB: - throw std::logic_error("DEB packages not implemented"); + initializeDebianHeadnodeRepositories(osinfo); break; } } diff --git a/src/services/runner.cpp b/src/services/runner.cpp index a9be9ffb..89920417 100644 --- a/src/services/runner.cpp +++ b/src/services/runner.cpp @@ -66,7 +66,7 @@ CommandProxy runCommandIter( } } - return CommandProxy {}; + return CommandProxy { }; } int runCommand(const std::string& command, std::list& output, @@ -202,7 +202,7 @@ std::optional CommandProxy::getline() } valid = false; - return std::string {}; + return std::string { }; }); valid = new_valid; @@ -222,7 +222,7 @@ std::optional CommandProxy::getUntil(char chr) } valid = false; - return std::string {}; + return std::string { }; }); valid = new_valid; @@ -283,7 +283,7 @@ std::vector Runner::checkOutput(const std::string& cmd) throw std::runtime_error( fmt::format("ERROR: Command failed '{}'", cmd)); } - return output | std::ranges::to(); + return { output.begin(), output.end() }; } int DryRunner::executeCommand(const std::string& cmd) @@ -313,14 +313,14 @@ std::vector DryRunner::checkOutput(const std::string& cmd) throw std::runtime_error( fmt::format("ERROR: Command failed '{}'", cmd)); } - return output | std::ranges::to(); + return { output.begin(), output.end() }; } CommandProxy DryRunner::executeCommandIter( const std::string& cmd, Stream /*out*/) { LOG_WARN("Dry Run: Would execute iterative command: {}", cmd); - return CommandProxy {}; // Return an invalid CommandProxy + return CommandProxy { }; // Return an invalid CommandProxy } int DryRunner::downloadFile(const std::string& url, const std::string& file) @@ -346,7 +346,7 @@ void MockRunner::checkCommand(const std::string& cmd) { } std::vector MockRunner::checkOutput(const std::string& /*cmd*/) { - return {}; + return { }; } const std::vector& MockRunner::listCommands() const @@ -358,7 +358,7 @@ CommandProxy MockRunner::executeCommandIter( const std::string& cmd, Stream /*out*/) { m_cmds.push_back(cmd); - return CommandProxy {}; // Return an invalid CommandProxy + return CommandProxy { }; // Return an invalid CommandProxy } int MockRunner::downloadFile(const std::string& url, const std::string& file) diff --git a/src/services/scriptbuilder.cpp b/src/services/scriptbuilder.cpp index 5344c3d4..497cd710 100644 --- a/src/services/scriptbuilder.cpp +++ b/src/services/scriptbuilder.cpp @@ -1,4 +1,5 @@ #include +#include #include #include @@ -52,17 +53,42 @@ ScriptBuilder& ScriptBuilder::stopService(const std::string_view service) ScriptBuilder& ScriptBuilder::addPackage(const std::string_view pkg) { - return addCommand("dnf install -y {}", pkg); + switch (m_os.getPackageType()) { + case OS::PackageType::RPM: + return addCommand("dnf install -y {}", pkg); + case OS::PackageType::DEB: + return addCommand( + "DEBIAN_FRONTEND=noninteractive apt install -y {}", pkg); + } + + std::unreachable(); }; ScriptBuilder& ScriptBuilder::addPackages(const std::set& pkgs) { - return addCommand("dnf install -y {}", fmt::join(pkgs, " ")); + switch (m_os.getPackageType()) { + case OS::PackageType::RPM: + return addCommand("dnf install -y {}", fmt::join(pkgs, " ")); + case OS::PackageType::DEB: + return addCommand( + "DEBIAN_FRONTEND=noninteractive apt install -y {}", + fmt::join(pkgs, " ")); + } + + std::unreachable(); } ScriptBuilder& ScriptBuilder::removePackage(const std::string_view pkg) { - return addCommand("dnf remove -y {}", pkg); + switch (m_os.getPackageType()) { + case OS::PackageType::RPM: + return addCommand("dnf remove -y {}", pkg); + case OS::PackageType::DEB: + return addCommand( + "DEBIAN_FRONTEND=noninteractive apt remove -y {}", pkg); + } + + std::unreachable(); } ScriptBuilder& ScriptBuilder::removeLineWithKeyFromFile( diff --git a/src/services/xcat.cpp b/src/services/xcat.cpp index de1c7504..c91d84c5 100644 --- a/src/services/xcat.cpp +++ b/src/services/xcat.cpp @@ -68,10 +68,60 @@ std::string getOSImageDistroVersion(const OS& nodeOS) osimage += "alma"; osimage += nodeOS.getVersion(); break; + case OS::Distro::Ubuntu: + osimage += "ubuntu"; + osimage += nodeOS.getVersion(); + break; } return osimage; } +bool isUbuntu24ComputeImage(const OS& nodeOS) +{ + return nodeOS.getPlatform() == OS::Platform::ubuntu24; +} + +std::vector ubuntu24XcatPkgdirEntries() +{ + return { + "http://archive.ubuntu.com/ubuntu noble main restricted universe " + "multiverse", + "http://archive.ubuntu.com/ubuntu noble-updates main restricted " + "universe multiverse", + "http://security.ubuntu.com/ubuntu noble-security main restricted " + "universe multiverse", + }; +} + +std::string ubuntu24OpenHpcOtherpkgdirEntry() +{ + return "[trusted=yes] " + "https://repos.versatushpc.com.br/openhpc/versatushpc-4/" + "Ubuntu_24.04/ ./"; +} + +std::string buildUbuntu24OSImageDefinitionCommand( + const opencattus::services::XCAT::Image& image) +{ + return fmt::format( + "mkdef -f -t osimage {image} " + "imagetype=linux " + "osname=Linux " + "osvers=ubuntu24.04 " + "osarch=x86_64 " + "osdistroname=ubuntu24.04-x86_64 " + "profile=compute " + "provmethod=netboot " + "rootimgdir={rootimg} " + "pkglist=/opt/xcat/share/xcat/netboot/ubuntu/" + "compute.ubuntu24.04.x86_64.pkglist " + "otherpkglist=/install/custom/netboot/compute.otherpkglist " + "postinstall=/install/custom/netboot/compute.postinstall " + "synclists=/install/custom/netboot/compute.synclists", + fmt::arg("image", image.osimage), + fmt::arg("rootimg", image.chroot.string())); +} + std::string getEnterpriseLinuxTemplateVersion(const OS& nodeOS) { switch (nodeOS.getPlatform()) { @@ -214,6 +264,18 @@ XcatInfinibandPlan buildXcatInfinibandPlan(const OFED& ofed, const OS& nodeOS, std::string_view runningKernel) { switch (nodeOS.getPlatform()) { + case OS::Platform::ubuntu24: + if (ofed.getKind() != OFED::Kind::Inbox) { + throw std::invalid_argument( + "xCAT Ubuntu 24.04 compute-node OFED staging only " + "supports inbox RDMA packages today"); + } + return XcatInfinibandPlan { + .otherPackages = { "rdma-core", "ibverbs-utils", + "ibverbs-providers", "infiniband-diags" }, + .kernelVersion = std::nullopt, + .localRepoName = std::nullopt, + }; case OS::Platform::el8: case OS::Platform::el9: break; @@ -237,7 +299,7 @@ XcatInfinibandPlan buildXcatInfinibandPlan(const OFED& ofed, const OS& nodeOS, ? std::string(configuredKernel.value()) : std::string(runningKernel); return XcatInfinibandPlan { - .otherPackages = {}, + .otherPackages = { }, .kernelVersion = kernelVersion, .localRepoName = fmt::format("doca-kernel-{}", kernelVersion), }; @@ -289,7 +351,7 @@ std::vector buildXcatKernelPackageNames(const OS& nodeOS) std::string buildXcatKernelPackages( const OS& nodeOS, std::string_view kernelVersion) { - auto output = std::string {}; + auto output = std::string { }; for (const auto packageName : buildXcatKernelPackageNames(nodeOS)) { if (!output.empty()) { output += " "; @@ -347,7 +409,7 @@ std::optional buildRockyXcatKernelDownloadFallbackCommand( return std::nullopt; } - auto fallbackDownloads = std::string {}; + auto fallbackDownloads = std::string { }; for (const auto packageName : buildXcatKernelPackageNames(nodeOS)) { const auto packageUrl = buildRockyKernelPackageUrl(nodeOS, packageName, kernelVersion); @@ -794,6 +856,7 @@ XCAT::Image XCAT::getImage() const { return m_stateless; } void XCAT::installPackages() { auto runner = opencattus::utils::singleton::runner(); + const auto& computeOS = cluster()->getComputeNodeOS(); runner->checkCommand("dnf -y install xCAT"); // xCAT's embedded Perl IPMI stack does not interoperate cleanly with // VirtualBMC on EL9, so keep ipmitool available as a fallback path. @@ -801,6 +864,9 @@ void XCAT::installPackages() // xCAT always prepends a local file:// otherpkgdir for osimages; ensure we // can publish metadata there even when we do not ship custom RPMs. runner->checkCommand("dnf -y install createrepo_c"); + if (computeOS.getPackageType() == OS::PackageType::DEB) { + runner->checkCommand("dnf -y install debootstrap"); + } } void XCAT::patchInstall() @@ -977,6 +1043,12 @@ void XCAT::genimage() const ? std::optional(configuredKernel.value()) : std::nullopt, runningKernel); + if (osinfo.getPackageType() == OS::PackageType::DEB + && kernelVersionOpt.has_value()) { + throw std::invalid_argument( + "Custom xCAT kernels are not supported for Ubuntu 24.04 images " + "yet"); + } if (!kernelVersionOpt) { shell::fmt("genimage {} ", m_stateless.osimage); @@ -1038,6 +1110,11 @@ void XCAT::createDirectoryTree() void XCAT::configureSELinux() { + if (cluster()->getNodes().front().getOS().getPackageType() + == OS::PackageType::DEB) { + return; + } + m_stateless.postinstall.emplace_back( fmt::format("echo \"SELINUX=disabled\nSELINUXTYPE=targeted\" > " "$IMG_ROOTIMGDIR/etc/selinux/config\n\n")); @@ -1045,7 +1122,11 @@ void XCAT::configureSELinux() void XCAT::configureOpenHPC() { - const auto packages = { "ohpc-base-compute", "lmod-ohpc", "lua" }; + const auto nodeOS = cluster()->getNodes().front().getOS(); + const auto packages = nodeOS.getPackageType() == OS::PackageType::DEB + ? std::vector { "ohpc-base-compute", "lmod-ohpc" } + : std::vector { "ohpc-base-compute", "lmod-ohpc", + "lua" }; m_stateless.otherpkgs.reserve(packages.size()); for (const auto& package : std::as_const(packages)) { @@ -1175,18 +1256,28 @@ void XCAT::configureInfiniband() void XCAT::configureSLURM() { // NOTE: hwloc-libs required to fix slurmd + const auto nodeOS = cluster()->getNodes().front().getOS(); m_stateless.otherpkgs.emplace_back("ohpc-slurm-client"); - m_stateless.otherpkgs.emplace_back("hwloc-libs"); + m_stateless.otherpkgs.emplace_back( + nodeOS.getPackageType() == OS::PackageType::DEB ? "hwloc-ohpc" + : "hwloc-libs"); // TODO: Deprecate this for SRV entries on DNS: _slurmctld._tcp 0 100 6817 + const auto slurmdOptionsPath + = nodeOS.getPackageType() == OS::PackageType::DEB + ? "/etc/default/slurmd" + : "/etc/sysconfig/slurmd"; m_stateless.postinstall.emplace_back( - fmt::format("echo SLURMD_OPTIONS=\\\"--conf-server {}\\\" > " - "$IMG_ROOTIMGDIR/etc/sysconfig/slurmd\n\n", + fmt::format("install -d $IMG_ROOTIMGDIR/{}\n" + "echo SLURMD_OPTIONS=\\\"--conf-server {}\\\" > " + "$IMG_ROOTIMGDIR{}\n\n", + std::filesystem::path(slurmdOptionsPath).parent_path().string(), cluster() ->getHeadnode() .getConnection(Network::Profile::Management) .getAddress() - .to_string())); + .to_string(), + slurmdOptionsPath)); // Diskless nodes need SSH reachable before they have joined SLURM. A // blanket pam_slurm gate locks xCAT and root out during first boot and @@ -1255,8 +1346,14 @@ void XCAT::generatePostinstallFile() "$IMG_ROOTIMGDIR/etc/security/limits.conf\n" "\n"); - m_stateless.postinstall.emplace_back( - "chroot $IMG_ROOTIMGDIR systemctl disable firewalld\n"); + const auto nodeOS = cluster()->getNodes().front().getOS(); + if (nodeOS.getPackageType() == OS::PackageType::RPM) { + m_stateless.postinstall.emplace_back( + "chroot $IMG_ROOTIMGDIR systemctl disable firewalld\n"); + } else { + m_stateless.postinstall.emplace_back( + "chroot $IMG_ROOTIMGDIR systemctl disable ufw 2>/dev/null || :\n"); + } for (const auto& entries : std::as_const(m_stateless.postinstall)) { functions::addStringToFile(filename, entries); @@ -1291,12 +1388,14 @@ void XCAT::configureOSImageDefinition() const { auto opts = opencattus::utils::singleton::options(); auto runner = opencattus::utils::singleton::runner(); - const auto localOtherPkgDir - = getLocalOtherPkgRepoPath(cluster()->getNodes().front().getOS()); - opencattus::services::runner::shell::cmd( - fmt::format("mkdir -p {} && createrepo_c --update {}", - shellSingleQuote(localOtherPkgDir.string()), - shellSingleQuote(localOtherPkgDir.string()))); + const auto nodeOS = cluster()->getNodes().front().getOS(); + if (nodeOS.getPackageType() == OS::PackageType::RPM) { + const auto localOtherPkgDir = getLocalOtherPkgRepoPath(nodeOS); + opencattus::services::runner::shell::cmd( + fmt::format("mkdir -p {} && createrepo_c --update {}", + shellSingleQuote(localOtherPkgDir.string()), + shellSingleQuote(localOtherPkgDir.string()))); + } runner->executeCommand( fmt::format("chdef -t osimage {} --plus otherpkglist=" @@ -1315,10 +1414,18 @@ void XCAT::configureOSImageDefinition() const /* Add external repositories to otherpkgdir */ if (!opts->dryRun) { - std::vector repos = getxCATOSImageRepos(); - runner->executeCommand( - fmt::format("chdef -t osimage {} --plus otherpkgdir={}", - m_stateless.osimage, fmt::join(repos, ","))); + if (nodeOS.getPackageType() == OS::PackageType::DEB) { + const auto pkgdirEntries = ubuntu24XcatPkgdirEntries(); + const auto pkgdir + = fmt::format("{}", fmt::join(pkgdirEntries, ",")); + runner->executeCommand(fmt::format("chdef -t osimage {} pkgdir={}", + m_stateless.osimage, shellSingleQuote(pkgdir))); + } + + const std::vector repos = getxCATOSImageRepos(); + runner->executeCommand(fmt::format( + "chdef -t osimage {} --plus otherpkgdir={}", m_stateless.osimage, + shellSingleQuote(fmt::format("{}", fmt::join(repos, ","))))); } } @@ -1377,6 +1484,41 @@ void XCAT::configureEL9() } } +void XCAT::configureUbuntu24() +{ + auto runner = opencattus::utils::singleton::runner(); + runner->executeCommand("install -d /opt/xcat/share/xcat/netboot/ubuntu"); + runner->executeCommand( + R"(bash -c "cat > /opt/xcat/share/xcat/netboot/ubuntu/compute.ubuntu24.04.x86_64.pkglist <<'EOF' +bash +nfs-common +openssl +isc-dhcp-client +libc-bin +linux-image-generic +openssh-server +openssh-client +wget +rsync +busybox-static +gawk +dnsutils +tar +gzip +xz-utils +cpio +chrony +EOF +for suffix in exlist postinstall; do + target=/opt/xcat/share/xcat/netboot/ubuntu/compute.ubuntu24.04.x86_64.${suffix} + if [ -e /opt/xcat/share/xcat/netboot/ubuntu/compute.ubuntu20.04.x86_64.${suffix} ]; then + ln -sf /opt/xcat/share/xcat/netboot/ubuntu/compute.ubuntu20.04.x86_64.${suffix} \"$target\" + elif [ -e /opt/xcat/share/xcat/netboot/ubuntu/compute.${suffix} ]; then + ln -sf /opt/xcat/share/xcat/netboot/ubuntu/compute.${suffix} \"$target\" + fi +done")"); +} + opencattus::services::XCAT::ImageInstallArgs XCAT::getImageInstallArgs( ImageType imageType, NodeType nodeType) { @@ -1396,6 +1538,9 @@ void XCAT::createImage(ImageType imageType, NodeType nodeType, const std::vector& customizations) { switch (cluster()->getNodes().front().getOS().getPlatform()) { + case OS::Platform::ubuntu24: + configureUbuntu24(); + break; case OS::Platform::el8: configureEL8(); break; @@ -1424,10 +1569,18 @@ void XCAT::createImage(ImageType imageType, NodeType nodeType, m_stateless.chroot, "/install/custom/netboot/compute.otherpkglist", "/install/custom/netboot/compute.postinstall")); + } else if (isUbuntu24ComputeImage( + cluster()->getNodes().front().getOS())) { + LOG_INFO("Skipping copycds for Ubuntu 24.04; xCAT will bootstrap " + "the stateless image from Ubuntu archive repositories"); } else { copycds(cluster()->getDiskImage().getPath()); } generateOSImagePath(imageType, nodeType); + if (isUbuntu24ComputeImage(cluster()->getNodes().front().getOS())) { + runner->executeCommand( + buildUbuntu24OSImageDefinitionCommand(m_stateless)); + } createDirectoryTree(); configureSELinux(); @@ -1605,6 +1758,10 @@ void XCAT::generateOSImagePath(ImageType imageType, NodeType nodeType) std::vector XCAT::getxCATOSImageRepos() { const auto& osinfo = cluster()->getComputeNodeOS(); + if (osinfo.getPackageType() == OS::PackageType::DEB) { + return { ubuntu24OpenHpcOtherpkgdirEntry() }; + } + const auto repoManager = opencattus::utils::singleton::repos(); std::vector repos; const auto addReposFromFile = [&](const std::string& filename) { @@ -1630,6 +1787,8 @@ std::vector XCAT::getxCATOSImageRepos() case OS::Distro::AlmaLinux: addReposFromFile("almalinux.repo"); break; + case OS::Distro::Ubuntu: + std::unreachable(); } addReposFromFile("epel.repo"); @@ -1644,7 +1803,7 @@ void XCAT::install() LOG_INFO("Setting up compute node images... This may take a while"); constexpr auto provisionerName = "xCAT"; const auto opts = singleton::options(); - const auto osinfo = singleton::os(); + const auto computeOS = cluster()->getComputeNodeOS(); auto repos = singleton::repos(); repos->enable("xcat-core"); repos->enable("xcat-dep"); @@ -1677,7 +1836,7 @@ void XCAT::install() // Customizations to the image const auto nfsImageInstallScript - = networkFileSystem.imageInstallScript(osinfo, imageInstallArgs); + = networkFileSystem.imageInstallScript(computeOS, imageInstallArgs); // Image role LOG_INFO("[{}] Creating node images", provisionerName); @@ -1719,6 +1878,50 @@ TEST_CASE("getOSImageDistroVersion uses node OS metadata") == "rhels9.7.0"); CHECK(getOSImageDistroVersion(OS(OS::Distro::OL, OS::Platform::el9, 7)) == "ol9.7.0"); + CHECK(getOSImageDistroVersion( + OS(OS::Distro::Ubuntu, OS::Platform::ubuntu24, 4)) + == "ubuntu24.04"); +} + +TEST_CASE("ubuntu24XcatPkgdirEntries uses Noble archive repositories") +{ + const auto entries = ubuntu24XcatPkgdirEntries(); + + CHECK(entries.size() == 3); + CHECK(entries[0] + == "http://archive.ubuntu.com/ubuntu noble main restricted universe " + "multiverse"); + CHECK(entries[1] + == "http://archive.ubuntu.com/ubuntu noble-updates main restricted " + "universe multiverse"); + CHECK(entries[2] + == "http://security.ubuntu.com/ubuntu noble-security main restricted " + "universe multiverse"); +} + +TEST_CASE("ubuntu24OpenHpcOtherpkgdirEntry uses VersatusHPC OpenHPC") +{ + CHECK(ubuntu24OpenHpcOtherpkgdirEntry() + == "[trusted=yes] " + "https://repos.versatushpc.com.br/openhpc/versatushpc-4/" + "Ubuntu_24.04/ ./"); +} + +TEST_CASE("buildUbuntu24OSImageDefinitionCommand defines the xCAT image") +{ + const auto command = buildUbuntu24OSImageDefinitionCommand( + opencattus::services::XCAT::Image { + .osimage = "ubuntu24.04-x86_64-netboot-compute", + .chroot = "/install/netboot/ubuntu24.04/x86_64/compute/rootimg" }); + + CHECK(command.contains( + "mkdef -f -t osimage ubuntu24.04-x86_64-netboot-compute")); + CHECK(command.contains("osvers=ubuntu24.04")); + CHECK(command.contains("osdistroname=ubuntu24.04-x86_64")); + CHECK(command.contains("pkglist=/opt/xcat/share/xcat/netboot/ubuntu/" + "compute.ubuntu24.04.x86_64.pkglist")); + CHECK(command.contains( + "rootimgdir=/install/netboot/ubuntu24.04/x86_64/compute/rootimg")); } TEST_CASE("buildEnterpriseLinuxTemplateAliasCommands uses explicit EL releases") @@ -1903,7 +2106,7 @@ TEST_CASE("buildXcatInfinibandPlan stages DOCA packages for EL8 compute nodes") = buildXcatInfinibandPlan(OFED(OFED::Kind::Doca, "latest-3.2-LTS"), OS(OS::Distro::Rocky, OS::Platform::el8, 10, OS::Arch::x86_64), std::nullopt, "4.18.0-553.75.1.el8_10.x86_64"); - const std::vector expectedPackages {}; + const std::vector expectedPackages { }; CHECK(plan.otherPackages == expectedPackages); REQUIRE(plan.kernelVersion.has_value()); diff --git a/test/answerfile.cpp b/test/answerfile.cpp index 0f2bb388..104c7572 100644 --- a/test/answerfile.cpp +++ b/test/answerfile.cpp @@ -910,6 +910,119 @@ TEST_SUITE("opencattus::models::answerfile") std::filesystem::remove(diskImagePath); } + TEST_CASE("fillData accepts Ubuntu 24.04 compute nodes with xcat") + { + initializeOptionsSingleton(); + + const auto interfaces = firstHostInterfaces(); + REQUIRE_FALSE(interfaces.empty()); + + const auto answerfilePath + = tempAnswerfilePath("opencattus-cluster-provisioner-ubuntu24"); + const auto diskImagePath + = tempIsoPath("opencattus-cluster-provisioner-ubuntu24"); + std::ofstream(diskImagePath).close(); + writeAnswerfile(answerfilePath, diskImagePath, interfaces.front(), + interfaces.front(), std::nullopt, true, "xcat", std::nullopt, + "ubuntu", "24.04"); + + try { + AnswerFile answerfile(answerfilePath); + Cluster cluster; + cluster.fillData(answerfile); + + CHECK(cluster.getProvisioner() == Cluster::Provisioner::xCAT); + CHECK(cluster.getComputeNodeOS().getDistro() + == opencattus::models::OS::Distro::Ubuntu); + CHECK(cluster.getComputeNodeOS().getPlatform() + == opencattus::models::OS::Platform::ubuntu24); + CHECK(cluster.getComputeNodeOS().getVersion() == "24.04"); + } catch (const std::exception& e) { + FAIL(std::string(e.what())); + } catch (...) { + FAIL("non-std exception while filling cluster for Ubuntu 24.04 " + "xCAT"); + } + + std::filesystem::remove(answerfilePath); + std::filesystem::remove(diskImagePath); + } + + TEST_CASE("fillData rejects xcat on Ubuntu 24.04 headnodes") + { + initializeOptionsSingleton(); + + const auto interfaces = firstHostInterfaces(); + REQUIRE_FALSE(interfaces.empty()); + + const auto answerfilePath = tempAnswerfilePath( + "opencattus-cluster-provisioner-ubuntu24-headnode-xcat"); + const auto diskImagePath = tempIsoPath( + "opencattus-cluster-provisioner-ubuntu24-headnode-xcat"); + std::ofstream(diskImagePath).close(); + writeAnswerfile(answerfilePath, diskImagePath, interfaces.front(), + interfaces.front(), std::nullopt, true, "xcat", std::nullopt, + "rocky", "9.6"); + + try { + AnswerFile answerfile(answerfilePath); + Cluster cluster; + cluster.getHeadnode().setOS( + opencattus::models::OS(opencattus::models::OS::Distro::Ubuntu, + opencattus::models::OS::Platform::ubuntu24, 4)); + + CHECK_THROWS_WITH(cluster.fillData(answerfile), + doctest::Contains("xCAT on DEB head nodes is not implemented " + "yet")); + } catch (const std::exception& e) { + FAIL(std::string(e.what())); + } catch (...) { + FAIL("non-std exception while validating Ubuntu 24.04 headnode " + "xCAT rejection"); + } + + std::filesystem::remove(answerfilePath); + std::filesystem::remove(diskImagePath); + } + + TEST_CASE("fillData accepts Ubuntu 24.04 compute nodes with confluent") + { + initializeOptionsSingleton(); + + const auto interfaces = firstHostInterfaces(); + REQUIRE_FALSE(interfaces.empty()); + + const auto answerfilePath = tempAnswerfilePath( + "opencattus-cluster-provisioner-ubuntu24-confluent"); + const auto diskImagePath + = tempIsoPath("opencattus-cluster-provisioner-ubuntu24-confluent"); + std::ofstream(diskImagePath).close(); + writeAnswerfile(answerfilePath, diskImagePath, interfaces.front(), + interfaces.front(), std::nullopt, true, "confluent", std::nullopt, + "ubuntu", "24.04"); + + try { + AnswerFile answerfile(answerfilePath); + Cluster cluster; + cluster.fillData(answerfile); + + CHECK(cluster.getProvisioner() == Cluster::Provisioner::Confluent); + CHECK(cluster.getComputeNodeOS().getDistro() + == opencattus::models::OS::Distro::Ubuntu); + CHECK(cluster.getComputeNodeOS().getPlatform() + == opencattus::models::OS::Platform::ubuntu24); + CHECK(cluster.getComputeNodeOS().getVersion() == "24.04"); + } catch (const std::exception& e) { + FAIL(std::string(e.what())); + } catch (...) { + FAIL("non-std exception while filling cluster for Ubuntu 24.04 " + "Confluent"); + } + + std::filesystem::remove(answerfilePath); + std::filesystem::remove(diskImagePath); + } + TEST_CASE("fillData accepts confluent on Rocky Linux 10") { initializeOptionsSingleton(); diff --git a/test/sample/answerfile/ubuntu24-xcat.ini b/test/sample/answerfile/ubuntu24-xcat.ini new file mode 100644 index 00000000..1b072cd4 --- /dev/null +++ b/test/sample/answerfile/ubuntu24-xcat.ini @@ -0,0 +1,69 @@ +# Example Ubuntu 24.04 compute-node answerfile for an EL xCAT head node. + +[information] +cluster_name=opencattus +company_name=opencattus-enterprises +administrator_email=foo@example.com + +[time] +timezone=America/Sao_Paulo +timeserver=0.br.pool.ntp.org +locale=en_US.utf8 + +[hostname] +hostname=opencattus +domain_name=cluster.example.com + +[network_external] +interface=enp2s1 +ip_address=192.168.20.254 +subnet_mask=255.255.255.0 +gateway=192.168.20.1 +domain_name=cluster.external.example.com +nameservers=192.168.20.1 + +[network_management] +interface=enp2s2 +ip_address=192.168.30.254 +subnet_mask=255.255.255.0 +gateway=192.168.30.1 +domain_name=cluster.management.example.com +nameservers=192.168.122.1 + +[system] +disk_image=/opt/iso/ubuntu-24.04.4-live-server-amd64.iso +distro=ubuntu +version=24.04 +provisioner=xcat + +[ofed] +kind=inbox +version=latest + +[slurm] +mariadb_root_password=xxxxxx +slurmdb_password=xxxxxx +storage_password=xxxxxx +partition_name=batch + +[ohpc] +enabled=serial-libs, parallel-libs + +[node] +prefix=n +padding=2 +node_ip=192.168.30.1 +node_root_password=pwd +sockets=1 +cpus_per_node=1 +cores_per_socket=1 +threads_per_core=1 +real_memory=4096 +bmc_username=admin +bmc_password=admin +bmc_serialport=0 +bmc_serialspeed=9600 + +[node.1] +mac_address=ca:fe:de:ad:be:ef +bmc_address=10.0.0.2 diff --git a/testing/libvirt/config/ubuntu24-confluent.env.example b/testing/libvirt/config/ubuntu24-confluent.env.example new file mode 100644 index 00000000..d8ab055e --- /dev/null +++ b/testing/libvirt/config/ubuntu24-confluent.env.example @@ -0,0 +1,43 @@ +# OpenCATTUS Ubuntu 24.04 libvirt/KVM lab configuration for the Confluent path. +# +# Example: +# cp testing/libvirt/config/ubuntu24-confluent.env.example /tmp/opencattus-ubuntu24-confluent.env +# vi /tmp/opencattus-ubuntu24-confluent.env +# testing/libvirt/opencattus-ubuntu24-lab.sh -c /tmp/opencattus-ubuntu24-confluent.env run + +LAB_NAME=opencattus-ubuntu24-confluent +DISTRO_ID=ubuntu +DISTRO_VERSION=24.04 +DISTRO_MAJOR=24 +PROVISIONER=confluent + +BASE_IMAGE=/var/lib/libvirt/images/opencattus-assets/noble-server-cloudimg-amd64.img +CLUSTER_ISO=/var/lib/libvirt/images/opencattus-assets/ubuntu-24.04.4-live-server-amd64.iso + +OPENCATTUS_SOURCE_DIR=/path/to/opencattus +IMAGE_ROOT=/var/lib/libvirt/images/opencattus-lab + +HEADNODE_MEMORY_MB=24576 +HEADNODE_VCPUS=8 +HEADNODE_DISK_GB=220 + +COMPUTE_COUNT=1 +COMPUTE_MEMORY_MB=8192 +COMPUTE_VCPUS=2 + +NODE_SOCKETS=1 +NODE_CPUS_PER_NODE=2 +NODE_CORES_PER_SOCKET=2 +NODE_THREADS_PER_CORE=1 +NODE_REAL_MEMORY_MB=4096 + +MPI_SMOKE_NODES=1 +MPI_SMOKE_TASKS=2 +REMOTE_BUILD_JOBS=4 + +CLUSTER_NAME=opencattus +CLUSTER_HOSTNAME=opencattus +CLUSTER_DOMAIN=cluster.example.com +TIMEZONE=UTC +TIMESERVER=pool.ntp.org +LOCALE=en_US.utf8 diff --git a/testing/libvirt/opencattus-el9-lab.sh b/testing/libvirt/opencattus-el9-lab.sh index 15789564..0e7b0936 100755 --- a/testing/libvirt/opencattus-el9-lab.sh +++ b/testing/libvirt/opencattus-el9-lab.sh @@ -121,18 +121,28 @@ is_distro_major_el10() { [[ "${DISTRO_MAJOR}" == "10" ]] } +is_distro_major_ubuntu24() { + [[ "${DISTRO_ID}" == "ubuntu" && "${DISTRO_MAJOR}" == "24" ]] +} + require_supported_distro_major() { + if is_distro_major_ubuntu24; then + return 0 + fi + case "${DISTRO_MAJOR}" in 8|9|10) ;; *) - die "Unsupported DISTRO_MAJOR ${DISTRO_MAJOR}; the shared lab supports explicit EL8, EL9, and EL10 paths only" + die "Unsupported DISTRO_MAJOR ${DISTRO_MAJOR}; the shared lab supports explicit EL8, EL9, EL10, and Ubuntu 24 paths only" ;; esac } default_remote_build_preset() { - if is_distro_major_el10; then + if is_distro_major_ubuntu24; then + printf 'ubuntu24-gcc-release' + elif is_distro_major_el10; then printf 'el10-gcc-release' elif is_distro_major_el8; then printf 'rhel8-gcc-toolset-14-release' @@ -144,7 +154,9 @@ default_remote_build_preset() { } default_remote_build_preset_build() { - if is_distro_major_el10; then + if is_distro_major_ubuntu24; then + printf 'ubuntu24-gcc-release-build' + elif is_distro_major_el10; then printf 'el10-gcc-release-build' elif is_distro_major_el8; then printf 'rhel8-gcc-toolset-14-release-build' @@ -156,7 +168,9 @@ default_remote_build_preset_build() { } headnode_glibmm_package() { - if is_distro_major_el10; then + if is_distro_major_ubuntu24; then + printf 'libglibmm-2.68-1t64' + elif is_distro_major_el10; then printf 'glibmm2.68' elif is_distro_major_el8; then printf 'glibmm24' @@ -168,7 +182,9 @@ headnode_glibmm_package() { } virt_install_osinfo_name() { - if is_distro_major_el10; then + if is_distro_major_ubuntu24; then + printf 'ubuntu24.04' + elif is_distro_major_el10; then printf 'generic' elif is_distro_major_el8; then printf 'rocky8' @@ -192,7 +208,9 @@ load_defaults() { require_supported_distro_major if [[ -z "${PROVISIONER+x}" ]]; then - if is_distro_major_el8; then + if is_distro_major_ubuntu24; then + PROVISIONER=confluent + elif is_distro_major_el8; then PROVISIONER=confluent elif is_distro_major_el9; then PROVISIONER=xcat @@ -603,6 +621,94 @@ render_headnode_cloud_init() { local pubkey pubkey=$(<"${SSH_PUBLIC_KEY}") + if is_distro_major_ubuntu24; then + cat >"$(headnode_user_data_path)" </dev/null | awk 'NF { print \$1; exit }' || true) + if [[ -n "\${vg_name}" ]]; then + pv_name=\$(pvs --noheadings -o pv_name -S "vg_name=\${vg_name}" 2>/dev/null | awk 'NF { print \$1; exit }' || true) + if [[ -n "\${pv_name}" ]]; then + pv_basename=\$(basename "\${pv_name}") + parent_disk=\$(lsblk -no PKNAME "\${pv_name}" 2>/dev/null | awk 'NF { print \$1; exit }' || true) + if [[ -r "/sys/class/block/\${pv_basename}/partition" ]]; then + part_number=\$(tr -d '[:space:]' < "/sys/class/block/\${pv_basename}/partition" || true) + else + part_number=\$(lsblk -no PARTN "\${pv_name}" 2>/dev/null | awk 'NF { print \$1; exit }' || true) + fi + if [[ -n "\${parent_disk}" && -n "\${part_number}" ]]; then + growpart "/dev/\${parent_disk}" "\${part_number}" || true + fi + pvresize "\${pv_name}" || true + fi + lvextend -l +100%FREE -r "\${root_source}" || true + fi + elif [[ "\${root_source}" == /dev/* ]]; then + root_basename=\$(basename "\${root_source}") + parent_disk=\$(lsblk -no PKNAME "\${root_source}" 2>/dev/null | awk 'NF { print \$1; exit }' || true) + if [[ -r "/sys/class/block/\${root_basename}/partition" ]]; then + part_number=\$(tr -d '[:space:]' < "/sys/class/block/\${root_basename}/partition" || true) + else + part_number=\$(lsblk -no PARTN "\${root_source}" 2>/dev/null | awk 'NF { print \$1; exit }' || true) + fi + if [[ -n "\${parent_disk}" && -n "\${part_number}" ]]; then + growpart "/dev/\${parent_disk}" "\${part_number}" || true + fi + case "\${root_fstype}" in + xfs) + xfs_growfs / || true + ;; + ext2|ext3|ext4) + resize2fs "\${root_source}" || true + ;; + esac + fi + + lsblk -o NAME,SIZE,FSTYPE,MOUNTPOINT + df -h / +users: + - default + - name: ${SSH_USER} + gecos: OpenCATTUS Lab + groups: [adm, sudo] + lock_passwd: true + shell: /bin/bash + sudo: ALL=(ALL) NOPASSWD:ALL + ssh_authorized_keys: + - ${pubkey} +runcmd: + - [bash, -lc, /usr/local/sbin/opencattus-grow-rootfs.sh] + - [systemctl, enable, --now, ssh] + - [systemctl, enable, --now, qemu-guest-agent] +EOF + else cat >"$(headnode_user_data_path)" <"$(headnode_meta_data_path)" </dev/null 2>&1 || true; + for attempt in 1 2 3; do + if sudo DEBIAN_FRONTEND=noninteractive apt update && + sudo DEBIAN_FRONTEND=noninteractive apt install -y \ + build-essential \ + ca-certificates \ + ccache \ + cmake \ + cppcheck \ + g++-14 \ + gcc-14 \ + git \ + libglibmm-2.68-dev \ + libnewt-dev \ + network-manager \ + ninja-build \ + pkg-config \ + python3-pip \ + qemu-guest-agent \ + rsync \ + tar \ + wget; then + break; + fi; + if [[ \$attempt -eq 3 ]]; then + exit 1; + fi; + sleep 5; + done" + ssh_remote "if [[ -e /dev/virtio-ports/org.qemu.guest_agent.0 ]]; then + sudo systemctl enable --now qemu-guest-agent + else + sudo systemctl enable qemu-guest-agent >/dev/null 2>&1 || true + fi" >/dev/null 2>&1 || true + return + fi + seed_rhel_local_mirror_repo log "Checking headnode repository state" @@ -1466,6 +1618,36 @@ ssh_remote() { ssh "${SSH_OPTIONS[@]}" "$(remote_host)" "$@" } +wait_for_ubuntu_apt() { + ssh_remote "set -euo pipefail + if command -v cloud-init >/dev/null 2>&1; then + sudo cloud-init status --wait >/dev/null 2>&1 || true + fi + sudo systemctl stop \ + apt-daily.service \ + apt-daily.timer \ + apt-daily-upgrade.service \ + apt-daily-upgrade.timer \ + packagekit \ + packagekit-offline-update >/dev/null 2>&1 || true + for attempt in \$(seq 1 120); do + if sudo fuser \ + /var/lib/dpkg/lock-frontend \ + /var/lib/dpkg/lock \ + /var/lib/apt/lists/lock \ + /var/cache/apt/archives/lock >/dev/null 2>&1; then + sleep 5 + else + break + fi + if [[ \${attempt} -eq 120 ]]; then + echo 'Timed out waiting for apt/dpkg locks' >&2 + exit 1 + fi + done + sudo dpkg --configure -a" +} + scp_to_remote() { scp "${SSH_OPTIONS[@]}" "$1" "$(remote_host):$2" } @@ -1507,6 +1689,8 @@ build_binary_in_guest() { local build_log="${LOG_DIR}/build.log" local compiler_setup_cmd local local_mirror_setup_cmd= + local cmake_compiler_args + local cmake_extra_args= local remote_build_dir="${REMOTE_SOURCE_DIR}/out/build/${REMOTE_BUILD_PRESET}" local remote_build_type="Release" local remote_cmd @@ -1519,12 +1703,34 @@ build_binary_in_guest() { sync_repo_to_remote - if is_distro_major_el10; then + if is_distro_major_ubuntu24; then + wait_for_ubuntu_apt + compiler_setup_cmd=$(cat <<'EOF' +sudo DEBIAN_FRONTEND=noninteractive apt update && +sudo DEBIAN_FRONTEND=noninteractive apt install -y \ + build-essential \ + ccache \ + cmake \ + g++-14 \ + gcc-14 \ + git \ + libglibmm-2.68-dev \ + libnewt-dev \ + ninja-build \ + pkg-config \ + python3-pip && +python3 -m pip install --user --break-system-packages --upgrade pip conan && +EOF +) + cmake_compiler_args="-DCMAKE_C_COMPILER=gcc-14 -DCMAKE_CXX_COMPILER=g++-14" + cmake_extra_args="-Dopencattus_ENABLE_CPPCHECK=OFF" + elif is_distro_major_el10; then compiler_setup_cmd=$(cat <<'EOF' chmod +x setupDevEnvironment.sh && ./setupDevEnvironment.sh && EOF ) + cmake_compiler_args="-DCMAKE_C_COMPILER=gcc -DCMAKE_CXX_COMPILER=g++" else compiler_setup_cmd=$(cat <<'EOF' chmod +x setupDevEnvironment.sh rhel-gcc-toolset-14.sh && @@ -1534,6 +1740,7 @@ source ./rhel-gcc-toolset-14.sh && set -u && EOF ) + cmake_compiler_args="-DCMAKE_C_COMPILER=gcc -DCMAKE_CXX_COMPILER=g++" fi if [[ -n "${OPENCATTUS_MIRROR_URL}" && "${DISTRO_ID}" == "rocky" ]]; then @@ -1596,8 +1803,8 @@ ${compiler_setup_cmd} conan profile detect --force && cmake -S . -B '${remote_build_dir}' \ -DCMAKE_BUILD_TYPE='${remote_build_type}' \ - -DCMAKE_C_COMPILER=gcc \ - -DCMAKE_CXX_COMPILER=g++ && + ${cmake_compiler_args} \ + ${cmake_extra_args} && cmake --build '${remote_build_dir}' --target opencattus -j '${REMOTE_BUILD_JOBS}' && install -m 0755 '${REMOTE_BUILD_BINARY}' '${REMOTE_BINARY_PATH}' EOF diff --git a/testing/libvirt/opencattus-ubuntu24-lab.sh b/testing/libvirt/opencattus-ubuntu24-lab.sh new file mode 100755 index 00000000..c8ec18ad --- /dev/null +++ b/testing/libvirt/opencattus-ubuntu24-lab.sh @@ -0,0 +1,21 @@ +#!/usr/bin/env bash + +set -Eeuo pipefail + +SCRIPT_DIR=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd) + +: "${DISTRO_ID:=ubuntu}" +: "${DISTRO_VERSION:=24.04}" +: "${DISTRO_MAJOR:=24}" +: "${PROVISIONER:=confluent}" +: "${MPI_SMOKE_NODES:=1}" +: "${MPI_SMOKE_TASKS:=2}" + +export DISTRO_ID +export DISTRO_VERSION +export DISTRO_MAJOR +export PROVISIONER +export MPI_SMOKE_NODES +export MPI_SMOKE_TASKS + +exec "${SCRIPT_DIR}/opencattus-el9-lab.sh" "$@" diff --git a/testing/libvirt/scripts/check-headnode.sh b/testing/libvirt/scripts/check-headnode.sh index 05406160..33cbcc9c 100755 --- a/testing/libvirt/scripts/check-headnode.sh +++ b/testing/libvirt/scripts/check-headnode.sh @@ -47,15 +47,27 @@ warn_check() { done } -common_services=( - chronyd - mariadb - munge - nfs-server - rpcbind - slurmctld - slurmdbd -) +if command -v apt >/dev/null 2>&1; then + common_services=( + chrony + mariadb + munge + nfs-server + rpcbind + slurmctld + slurmdbd + ) +else + common_services=( + chronyd + mariadb + munge + nfs-server + rpcbind + slurmctld + slurmdbd + ) +fi case "${provisioner}" in xcat) @@ -66,11 +78,19 @@ case "${provisioner}" in ) ;; confluent) - provisioner_services=( - confluent - dnsmasq - httpd - ) + if command -v apt >/dev/null 2>&1; then + provisioner_services=( + confluent + dnsmasq + apache2 + ) + else + provisioner_services=( + confluent + dnsmasq + httpd + ) + fi ;; *) echo "Unsupported provisioner: ${provisioner}" >&2 diff --git a/testing/libvirt/templates/ubuntu24-confluent.answerfile.ini b/testing/libvirt/templates/ubuntu24-confluent.answerfile.ini new file mode 100644 index 00000000..8275fbc0 --- /dev/null +++ b/testing/libvirt/templates/ubuntu24-confluent.answerfile.ini @@ -0,0 +1,55 @@ +# Autogenerated by testing/libvirt/opencattus-ubuntu24-lab.sh + +[information] +cluster_name=__CLUSTER_NAME__ +company_name=__COMPANY_NAME__ +administrator_email=__ADMIN_EMAIL__ + +[time] +timezone=__TIMEZONE__ +timeserver=__TIMESERVER__ +locale=__LOCALE__ + +[hostname] +hostname=__CLUSTER_HOSTNAME__ +domain_name=__CLUSTER_DOMAIN__ + +[network_external] +interface=__EXTERNAL_IFACE__ +domain_name=__EXTERNAL_DOMAIN__ + +[slurm] +mariadb_root_password=__MARIADB_ROOT_PASSWORD__ +slurmdb_password=__SLURMDB_PASSWORD__ +storage_password=__STORAGE_PASSWORD__ +partition_name=__PARTITION_NAME__ + +[network_management] +interface=__MANAGEMENT_IFACE__ +ip_address=__HEADNODE_MGMT_IP__ +subnet_mask=__MANAGEMENT_NETMASK__ +domain_name=__MANAGEMENT_DOMAIN__ + +[system] +disk_image=__REMOTE_ISO_PATH__ +distro=__DISTRO_ID__ +version=__DISTRO_VERSION__ +provisioner=__PROVISIONER__ + +[ohpc] +enabled=mpi,serial-libs,parallel-libs + +[node] +prefix=__NODE_PREFIX__ +padding=__NODE_PADDING__ +node_ip=__NODE_IP_START__ +node_root_password=__NODE_ROOT_PASSWORD__ +sockets=__NODE_SOCKETS__ +cpus_per_node=__NODE_CPUS_PER_NODE__ +cores_per_socket=__NODE_CORES_PER_SOCKET__ +threads_per_core=__NODE_THREADS_PER_CORE__ +real_memory=__NODE_REAL_MEMORY_MB__ +bmc_username=__NODE_BMC_USERNAME__ +bmc_password=__NODE_BMC_PASSWORD__ +bmc_serialport=__NODE_BMC_SERIALPORT__ +bmc_serialspeed=__NODE_BMC_SERIALSPEED__