diff --git a/.buildkite/pipeline.yml b/.buildkite/pipeline.yml index 72c9c14cdb..d3dda80ee9 100644 --- a/.buildkite/pipeline.yml +++ b/.buildkite/pipeline.yml @@ -26,10 +26,11 @@ steps: - exit_status: 255 # Forced agent shutdown timeout_in_minutes: 10 - wait - - label: "Package :debian: :openwrt:" + - label: "Package :debian: :openwrt: :rpm:" command: | make dist-deb BFLAGS="--file_name_version=${SCION_VERSION}" make dist-openwrt BFLAGS="--file_name_version=${SCION_VERSION}" + make dist-rpm BFLAGS="--file_name_version=${SCION_VERSION}" artifact_paths: - "installables/scion_*.tar.gz" plugins: @@ -42,6 +43,7 @@ steps: tar -chaf scion_${SCION_VERSION}_deb_i386.tar.gz *_${SCION_VERSION}_i386.deb tar -chaf scion_${SCION_VERSION}_deb_armel.tar.gz *_${SCION_VERSION}_armel.deb tar -chaf scion_${SCION_VERSION}_openwrt_x86_64.tar.gz *_${SCION_VERSION}_x86_64.ipk + tar -chaf scion_${SCION_VERSION}_rpm_x86_64.tar.gz *_${SCION_VERSION}_x86_64.rpm popd ls installables post-artifact: | @@ -53,6 +55,8 @@ steps: - armel #### Packages :openwrt: - x86_64 + #### Packages :rpm: + - x86_64 EOF key: dist retry: *automatic-retry diff --git a/Makefile b/Makefile index 52aa4a9acc..8a43295c70 100644 --- a/Makefile +++ b/Makefile @@ -31,6 +31,12 @@ dist-openwrt-testing: @ mkdir -p installables @ cd installables ; ln -sfv ../bazel-out/*/bin/dist/*.ipk . +dist-rpm: + bazel build //dist:rpm_all $(BFLAGS) + @ # These artefacts have unique names but varied locations. Link them somewhere convenient. + @ mkdir -p installables + @ cd installables ; ln -sfv ../bazel-out/*/bin/dist/*.rpm . + # all: performs the code-generation steps and then builds; the generated code # is git controlled, and therefore this is only necessary when changing the # sources for the code generation. diff --git a/WORKSPACE b/WORKSPACE index 64c7aaac18..33d58579ed 100644 --- a/WORKSPACE +++ b/WORKSPACE @@ -182,6 +182,7 @@ oci_pull( repository = "library/debian", ) +# Debian packaging http_archive( name = "rules_debian_packages", sha256 = "0ae3b332f9d894e57693ce900769d2bd1b693e1f5ea1d9cdd82fa4479c93bcc8", @@ -206,6 +207,14 @@ load("@tester_debian10_packages//:packages.bzl", tester_debian_packages_install_ tester_debian_packages_install_deps() +# RPM packaging +load("@rules_pkg//toolchains/rpm:rpmbuild_configure.bzl", "find_system_rpmbuild") + +find_system_rpmbuild( + name = "rules_pkg_rpmbuild", + verbose = False, +) + # Buf CLI http_archive( name = "buf", diff --git a/dist/BUILD.bazel b/dist/BUILD.bazel index 5422f1fae0..221e68638b 100644 --- a/dist/BUILD.bazel +++ b/dist/BUILD.bazel @@ -1,5 +1,4 @@ -load(":package.bzl", "scion_pkg_deb") -load(":package.bzl", "scion_pkg_ipk") +load(":package.bzl", "scion_pkg_deb", "scion_pkg_ipk", "scion_pkg_rpm") load(":platform.bzl", "multiplatform_filegroup") load(":git_version.bzl", "git_version") @@ -10,6 +9,10 @@ DEB_PLATFORMS = [ "@io_bazel_rules_go//go/toolchain:linux_arm", ] +RPM_PLATFORMS = [ + "@io_bazel_rules_go//go/toolchain:linux_amd64", +] + # TODO(jice@scion.org): # For now only a single openwrt platform can be in this list. If we allow several, they get # built in parallel, which breaks on non-reentrant openwrt makefiles. For a single platform @@ -221,3 +224,116 @@ multiplatform_filegroup( target_platforms = OPENWRT_PLATFORMS, visibility = ["//dist:__subpackages__"], ) + +scion_pkg_rpm( + name = "router_rpm", + depends = [ + "/sbin/adduser", + ], + description = "SCION inter-domain network architecture border router", + executables = { + "//router/cmd/router:router": "scion-router", + }, + package = "scion-router", + postinst = "rpm/scion.postinst", + systemds = ["systemd/scion-router@.service"], + version_file = ":git_version", +) + +scion_pkg_rpm( + name = "control_rpm", + configs = [], + depends = [ + "/sbin/adduser", + "scion-dispatcher", + ], + description = "SCION inter-domain network architecture control service", + executables = { + "//control/cmd/control:control": "scion-control", + }, + package = "scion-control", + systemds = ["systemd/scion-control@.service"], + version_file = ":git_version", +) + +scion_pkg_rpm( + name = "dispatcher_rpm", + configs = ["conffiles/dispatcher.toml"], + depends = [ + "/sbin/adduser", + ], + description = "SCION dispatcher", + executables = { + "//dispatcher/cmd/dispatcher:dispatcher": "scion-dispatcher", + }, + package = "scion-dispatcher", + postinst = "rpm/scion.postinst", + systemds = ["systemd/scion-dispatcher.service"], + version_file = ":git_version", +) + +scion_pkg_rpm( + name = "daemon_rpm", + configs = ["conffiles/daemon.toml"], + depends = [ + "/sbin/adduser", + ], + description = "SCION daemon", + executables = { + "//daemon/cmd/daemon:daemon": "scion-daemon", + }, + package = "scion-daemon", + postinst = "rpm/scion.postinst", + systemds = ["systemd/scion-daemon.service"], + version_file = ":git_version", +) + +scion_pkg_rpm( + name = "gateway_rpm", + configs = [ + "conffiles/gateway.json", + "conffiles/gateway.toml", + ], + depends = [ + "/sbin/adduser", + "scion-dispatcher", + "scion-daemon", + ], + description = "SCION-IP Gateway", + executables = { + "//gateway/cmd/gateway:gateway": "scion-ip-gateway", + }, + package = "scion-ip-gateway", + systemds = ["systemd/scion-ip-gateway.service"], + version_file = ":git_version", +) + +scion_pkg_rpm( + name = "tools_rpm", + depends = [ + "/sbin/adduser", + "scion-dispatcher", + "scion-daemon", + ], + description = "SCION tools", + executables = { + "//scion/cmd/scion:scion": "scion", + "//scion-pki/cmd/scion-pki:scion-pki": "scion-pki", + }, + package = "scion-tools", + version_file = ":git_version", +) + +multiplatform_filegroup( + name = "rpm", + srcs = [ + "control_rpm", + "daemon_rpm", + "dispatcher_rpm", + "gateway_rpm", + "router_rpm", + "tools_rpm", + ], + target_platforms = RPM_PLATFORMS, + visibility = ["//dist:__subpackages__"], +) diff --git a/dist/package.bzl b/dist/package.bzl index 88c9e1544e..06a80a7f9f 100644 --- a/dist/package.bzl +++ b/dist/package.bzl @@ -1,6 +1,8 @@ load("@rules_pkg//pkg:pkg.bzl", "pkg_deb", "pkg_tar") +load("@rules_pkg//pkg:rpm.bzl", "pkg_rpm") load("@bazel_skylib//rules:common_settings.bzl", "BuildSettingInfo") load("@rules_pkg//pkg:providers.bzl", "PackageVariablesInfo") +load("@rules_pkg//pkg:mappings.bzl", "pkg_attributes", "pkg_files") SCION_PKG_HOMEPAGE = "https://github.com/scionproto/scion" SCION_PKG_MAINTAINER = "SCION Contributors" @@ -30,6 +32,106 @@ name_elems = rule( }, ) +def scion_pkg_rpm(name, package, executables = {}, systemds = [], configs = [], **kwargs): + """ + The package content, the _data_ arg for the pkg_rpm rule, is assembled from: + + - executables: Map Label (the executable) -> string, the basename of the executable in the package + Executables are installed to /usr/bin/ + - systemds: List[string], the systemd unit files to be installed in /lib/systemd/system/ + - configs: List[string], the configuration files to be installed in /etc/scion/ + + The values for the following pkg_rpm args are set to a default value: + - url + - license + - architecture, set based on the platform. + + The caller needs to set: + - package: the name of the package (e.g. scion-router) + - description: one-liner + - version/version_file: One can use the label ":git_version" + and any of the optional control directives. + + The version string gets edited to meet rpm requirements: dashes are replaced with ^. + """ + + kwargs.setdefault("url", SCION_PKG_HOMEPAGE) + kwargs.setdefault("license", SCION_PKG_LICENSE) + + if "architecture" not in kwargs: + kwargs["architecture"] = select({ + "@platforms//cpu:x86_64": "x86_64", + "@platforms//cpu:x86_32": "i386", + "@platforms//cpu:aarch64": "arm64", + "@platforms//cpu:armv7": "armel", + "@platforms//cpu:s390x": "s390x", + # Note: some rules_go toolchains don't (currently) seem to map (cleanly) to @platforms//cpu. + # "@platforms//cpu:ppc": "ppc64", + # "@platforms//cpu:ppc64le": "ppc64le", + }) + + name_elems( + name = "package_file_naming_" + name, + file_name_version = "@@//:file_name_version", + architecture = kwargs["architecture"], + package = package, + ) + + # Note that our "executables" parameter is a dictionary label->file_name; exactly what pkg_files + # wants for its "renames" param. + pkg_files(name = "%s_configs" % name, prefix = "/etc/scion/", srcs = configs) + pkg_files(name = "%s_systemds" % name, prefix = "/lib/systemd/system/", srcs = systemds) + pkg_files( + name = "%s_execs" % name, + prefix = "/usr/bin/", + srcs = executables.keys(), + attributes = pkg_attributes(mode = "0755"), + renames = executables, + ) + + if kwargs.get("version_file"): + native.genrule( + name = "%s_version" % name, + srcs = [kwargs["version_file"]], + outs = ["%s_version_file" % name], + cmd = "sed 's/-/^/g' < $< > $@", + ) + kwargs.pop("version_file") + elif kwargs.get("version"): + native.genrule( + name = "%s_version" % name, + srcs = [], + outs = ["%s_version_file" % name], + cmd = "echo \"%s\" | sed 's/-/^/g' > $@" % kwargs["version"], + ) + kwargs.pop("version") + + # Use ethe same attributes as scion_pkg_deb, in view of may-be simplifying BUILD.bazel later. + deps = kwargs.get("depends") + if deps: + kwargs.pop("depends") + else: + deps = [] + + post = kwargs.get("postinst") + if post: + kwargs.pop("postinst") + + pkg_rpm( + name = name, + summary = kwargs["description"], + srcs = ["%s_configs" % name, "%s_systemds" % name, "%s_execs" % name], + target_compatible_with = ["@platforms//os:linux"], + package_file_name = "{package}_{file_name_version}_{architecture}.rpm", + package_variables = ":package_file_naming_" + name, + package_name = package, + release = "%autorelease", + version_file = ":%s_version" % name, + requires = deps, + post_scriptlet_file = post, + **kwargs + ) + def scion_pkg_deb(name, executables = {}, systemds = [], configs = [], **kwargs): """ The package content, the _data_ arg for the pkg_deb rule, is assembled from: diff --git a/dist/rpm/scion.postinst b/dist/rpm/scion.postinst new file mode 100644 index 0000000000..44d6dee5e4 --- /dev/null +++ b/dist/rpm/scion.postinst @@ -0,0 +1,10 @@ +#!/bin/bash +set -e + +# Create system user +adduser --system --user-group --create-home --home /var/lib/scion scion + +# Create configuration directory +mkdir /etc/scion/ >& /dev/null || true +chown scion:scion /etc/scion/ + diff --git a/dist/test/BUILD.bazel b/dist/test/BUILD.bazel index c2cd205e12..1d0f820c76 100644 --- a/dist/test/BUILD.bazel +++ b/dist/test/BUILD.bazel @@ -29,3 +29,19 @@ sh_test( "integration", ], ) + +sh_test( + name = "rpm_test", + srcs = ["rpm_test.sh"], + data = [ + "Dockerfile.rpm", + "//dist:rpm", + ], + env = { + "SCION_RPM_PACKAGES": "$(locations //dist:rpm)", + }, + tags = [ + "exclusive", + "integration", + ], +) diff --git a/dist/test/Dockerfile.rpm b/dist/test/Dockerfile.rpm new file mode 100644 index 0000000000..38a696f908 --- /dev/null +++ b/dist/test/Dockerfile.rpm @@ -0,0 +1,8 @@ +FROM fedora:40 + +RUN dnf --assumeyes install systemd + +ENV container docker + +# Only "boot" a minimal system with journald and nothing else +CMD ["/usr/lib/systemd/systemd", "--unit", "systemd-journald.service"] diff --git a/dist/test/rpm_test.sh b/dist/test/rpm_test.sh new file mode 100755 index 0000000000..fb62359081 --- /dev/null +++ b/dist/test/rpm_test.sh @@ -0,0 +1,144 @@ +#!/bin/bash + +set -euo pipefail + +set -x +if [ -n ${SCION_RPM_PACKAGES+x} ]; then + # Invocation from bazel: + # SCION_RPM_PACKAGES is a space-separated list of filenames of (symlinks to) .rpm packages. + # Below we mount this stuff into a docker container, which won't work with symlinks. + # Copy everything into a tmp directory. + tmpdir="${TEST_TMPDIR?}" + cp ${SCION_RPM_PACKAGES} "${tmpdir}" + SCION_RPM_PACKAGES_DIR=$(realpath ${tmpdir}) +else + SCION_ROOT=$(realpath $(dirname $0)/../../) + SCION_RPM_PACKAGES_DIR=${SCION_RPM_PACKAGES_DIR:-${SCION_ROOT}/rpm} +fi +DEBUG=${DEBUG:-0} +set +x + +function cleanup { + docker container rm -f fedora-systemd || true + docker image rm --no-prune fedora-systemd || true +} +cleanup + +if [ "$DEBUG" == 0 ]; then # if DEBUG: keep container fedora-systemd running after test + trap cleanup EXIT +fi + +# Note: specify absolute path to Dockerfile because docker will not follow bazel's symlinks. +# Luckily we don't need anything else in this directory. +docker build -t fedora-systemd -f $(realpath dist/test/Dockerfile.rpm) dist/test + +# Start container with systemd in PID 1. +# Note: there are ways to avoid --privileged, but its unreliable and appears to depend on the host system +docker run -d --rm --name fedora-systemd -t \ + --tmpfs /tmp \ + --tmpfs /run \ + --tmpfs /run/lock \ + --tmpfs /run/shm \ + -v $SCION_RPM_PACKAGES_DIR:/rpm \ + --privileged \ + fedora-systemd:latest + +docker exec -i fedora-systemd /bin/bash <<'EOF' + set -xeuo pipefail + arch=$(arch) + + # check that the rpm files are all here (avoid cryptic error from rpm) + stat /rpm/scion-{router,control,dispatcher,daemon,ip-gateway,tools}_*_${arch}.rpm > /dev/null + + # router + rpm -iv /rpm/scion-router_*_${arch}.rpm + cat > /etc/scion/br-1.toml < /etc/scion/topology.json < /etc/scion/cs-1.toml << INNER_EOF + general.id = "cs-1" + general.config_dir = "/etc/scion" + trust_db.connection = "/var/lib/scion/cs-1.trust.db" + beacon_db.connection = "/var/lib/scion/cs-1.beacon.db" + path_db.connection = "/var/lib/scion/cs-1.path.db" +INNER_EOF + systemctl enable --now scion-control@cs-1.service + sleep 1 + systemctl status scion-control@cs-1.service + systemctl is-active scion-dispatcher.service # should be re-started as dependency + systemctl stop scion-control@cs-1.service scion-dispatcher.service + + # daemon + systemctl enable --now scion-daemon.service + systemctl status scion-daemon.service + sleep 1 + systemctl is-active scion-dispatcher.service # should be re-started as dependency + # ... tools (continued) + # now with the daemon running, we can test `scion` e.g. to inspect our local SCION address + scion address + systemctl stop scion-daemon.service scion-dispatcher.service + + # scion-ip-gateway + rpm -i /rpm/scion-ip-gateway_*_${arch}.rpm + systemctl start scion-ip-gateway.service + sleep 1 + # Note: this starts even if the default sig.json is not a valid configuration + systemctl status scion-ip-gateway.service + systemctl is-active scion-dispatcher.service scion-daemon.service # should be re-started as dependency + # Note: the gateway will only create a tunnel device once a session with a + # neighbor is up. This is too complicated to arrange in this test. Instead, + # we just ensure that the process has the required capabilities to do so. + getpcaps $(pidof scion-ip-gateway) | tee /dev/stderr | grep -q "cap_net_admin" || echo "missing capability 'cap_net_admin'" + + echo "Success!" +EOF