diff --git a/.github/workflows/create-release-branch.yml b/.github/workflows/create-release-branch.yml new file mode 100644 index 0000000000..51aba4a0f2 --- /dev/null +++ b/.github/workflows/create-release-branch.yml @@ -0,0 +1,63 @@ +name: Create release branch + +on: + workflow_dispatch: + inputs: + jira_ticket: + description: "Jira ticket to create PR (e.g. BCF-7777)" + type: string + required: true + +permissions: + contents: write + pull-requests: write + +jobs: + create-release-branch: + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + fetch-depth: 0 + ref: develop + + - name: Configure Git + run: | + git config user.name "github-actions" + git config user.email "github-actions@github.com" + + - name: Authenticate GitHub CLI + run: echo "${{ secrets.GITHUB_TOKEN }}" | gh auth login --with-token + + - name: Determine version + id: determine_version + run: | + inc_version=$(git tag \ + | grep -E '^[0-9]+\.[0-9]+-[0-9]+-cove$' \ + | awk -F'-' '{print $(NF-1)}' \ + | sort -n \ + | tail -1) + inc_version=$((inc_version + 1)) + version="2.9-$inc_version-cove" + echo "Version: $version" + echo "version=$version" >> $GITHUB_OUTPUT + + - name: Create release branch + env: + VERSION: ${{ steps.determine_version.outputs.version }} + run: | + git checkout -b release/$VERSION + git push origin release/$VERSION + + - name: Create PR to master + env: + VERSION: ${{ steps.determine_version.outputs.version }} + JIRA_TICKET: ${{ github.event.inputs.jira_ticket }} + run: | + gh pr create \ + --base master \ + --head release/$VERSION \ + --title "${JIRA_TICKET}: Merge release/$VERSION to master" \ + --body "Automated release PR" diff --git a/.github/workflows/create-release-tag.yml b/.github/workflows/create-release-tag.yml new file mode 100644 index 0000000000..8b328c85e4 --- /dev/null +++ b/.github/workflows/create-release-tag.yml @@ -0,0 +1,45 @@ +name: Create release tag + +on: + push: + branches: + - master + +permissions: + contents: write + +jobs: + create-release-tag: + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + fetch-depth: 0 + ref: master + + - name: Configure Git + run: | + git config user.name "github-actions" + git config user.email "github-actions@github.com" + + - name: Determine version + id: determine_version + run: | + inc_version=$(git tag \ + | grep -E '^[0-9]+\.[0-9]+-[0-9]+-cove$' \ + | awk -F'-' '{print $(NF-1)}' \ + | sort -n \ + | tail -1) + inc_version=$((inc_version + 1)) + version="2.9-$inc_version-cove" + echo "Version: $version" + echo "version=$version" >> $GITHUB_OUTPUT + + - name: Create release tag + env: + VERSION: ${{ steps.determine_version.outputs.version }} + run: | + git tag $VERSION + git push origin $VERSION diff --git a/Jenkinsfile b/Jenkinsfile new file mode 100644 index 0000000000..7fc08b0684 --- /dev/null +++ b/Jenkinsfile @@ -0,0 +1,126 @@ +@Library('cove') +import nable.cove.helpers.ShellHelper +import nable.cove.SecretManager + +final String jobName = env.JOB_NAME.split('/')[-2] +final String branchName = env.CHANGE_BRANCH ?: env.BRANCH_NAME +final boolean isProd = jobName.endsWith('-prd') +final boolean isProdBranch = branchName == 'master' || branchName.startsWith('release') +final String envType = isProd ? 'prd' : 'dev' + +def String repositoryName = 'rear' + +def config = [ + cloud: "backup-${envType}", + serviceAccount: "backup", + buildImage: "${nsbuild.ecrHost()}/cove/onprem/develop/rear-builder:v1.7" +] + +def secrets = [ + jenkins: [ + 'github-app': [ + usernamePassword: [ + usernameVariable: 'GITHUB_USERNAME', + passwordVariable: 'GITHUB_PASSWORD' + ] + ] + ], + kubernetes: [ + 'artifactory': [ + 'JFROG_USERNAME': 'ARTIFACTORY_USERNAME_COVE', + 'JFROG_ACCESS_TOKEN': 'ARTIFACTORY_TOKEN_COVE' + ] + ] +] + +def secretManager +def shellHelper +def shouldBuild = true + +pipeline { + agent { + kubernetes { + cloud "${config.cloud}" + yaml nsbuild.agentYaml(config) + defaultContainer nsbuild.defaultContainer(config) + } + } + + options { + ansiColor('xterm') + } + + stages { + stage('Prepare') { + steps { + script { + if (isProd != isProdBranch) { + echo "Environment mismatch: isProd=${isProd}, isProdBranch=${isProdBranch}. Skipping build." + shouldBuild = false + } + + secretManager = new SecretManager( + this, + envType, + jenkinsWhitelist: [ + 'github-app' + ], + k8sWhitelist: [ + 'artifactory' + ] + ) + shellHelper = new ShellHelper(this, isUnix: true) + } + } + } + stage('Load secrets') { + when { + expression { shouldBuild } + } + agent { + kubernetes { + cloud "${config.cloud}" + yaml secretManager.getK8sPodYaml(secrets) + defaultContainer 'secrets' + customWorkspace 'w' + } + } + steps { + script { + secretManager.loadJenkinsSecrets(secrets.jenkins) + secretManager.loadK8sSecrets(secrets.kubernetes, shellHelper) + } + } + } + stage('Build') { + when { + expression { shouldBuild } + } + environment { + ARTIFACTORY_URL = 'https://mspsolarwinds.jfrog.io/artifactory' + } + steps { + script { + secretManager.withSecrets { + shellHelper.exec('Validate', """ + make validate + """) + shellHelper.exec('Run unit tests', """ + make test-cove + """) + shellHelper.exec('Build', """ + make dist + """) + def repository = (envType == 'dev') ? 'cove-generic-develop-local' : 'cove-generic-release-local' + shellHelper.exec('Upload', """ + PACKAGE="rear-\$(make version).tar.gz" + curl -sSf -X PUT -T dist/\${PACKAGE} \ + -u \${ARTIFACTORY_USERNAME_COVE}:\${ARTIFACTORY_TOKEN_COVE} \ + \${ARTIFACTORY_URL}/${repository}/rear/\${PACKAGE} + """) + } + } + } + } + } +} diff --git a/Makefile b/Makefile index 4097de5f6f..2e77d2959d 100644 --- a/Makefile +++ b/Makefile @@ -34,7 +34,7 @@ else git_date := $(shell git log -n 1 --format="%ai") git_ref := $(shell git rev-parse HEAD | cut -c 1-8) git_count := $(shell git rev-list HEAD --no-merges | wc -l) - git_branch_suffix = $(shell git symbolic-ref HEAD | sed -e 's,^.*/,,' -e "s/[^A-Za-z0-9]//g") + git_branch_suffix = $(shell echo "$${CHANGE_BRANCH:-$${BRANCH_NAME:-$$(git symbolic-ref HEAD || echo unknown)}}" | sed -e 's,^.*/,,' -e "s/[^A-Za-z0-9]//g") git_status := $(shell git status --porcelain) git_stamp := $(git_count).$(git_ref).$(git_branch_suffix) ifneq ($(git_status),) @@ -101,6 +101,7 @@ help: \n\ version - Show ReaR version used\n\ validate - Check source code\n\ + test-cove - Run COVE unit tests with bats\n\ install - Install Relax-and-Recover (may replace files)\n\ uninstall - Uninstall Relax-and-Recover (may remove files)\n\ dist - Create tar file in dist/\n\ @@ -153,6 +154,16 @@ validate: fi; \ done +test-cove: + @echo "== Running COVE unit tests ==" + @for test_file in tests/COVE/*.bats; do \ + echo ""; \ + echo "========================================"; \ + echo "Executing: $${test_file}"; \ + echo "========================================"; \ + bats "$${test_file}"; \ + done + man: @echo -e "\033[1m== Prepare manual ==\033[0;0m" $(MAKE) -C doc man diff --git a/README.md b/README.md index 8e9655dc14..83cf685bcc 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # Relax-and-Recover Linux Disaster Recovery & Bare Metal Restore -Relax-and-Recover (abbreviated ReaR) is the de facto standard disaster recovery framework on Linux. +Relax-and-Recover (abbreviated ReaR) is the de facto standard disaster recovery framework on Linux. It is in particular used on enterprise Linux distributions like Red Hat Enterprise Linux (RHEL) and SUSE Linux Enterprise Server (SLES). @@ -10,7 +10,7 @@ for bare metal disaster recovery with data backup restore on physical or virtual For bare metal disaster recovery the ReaR recovery system is booted on pristine replacement hardware. On replacement hardware first the storage setup/layout is recreated (disk partitioning, filesystems, mount points), -then a backup restore program is called to restore the data (system files) into the recreated storage, +then a backup restore program is called to restore the data (system files) into the recreated storage, and finally a boot loader is installed. System administrators use the ReaR framework to set up a disaster recovery procedure diff --git a/containers/builder/Dockerfile b/containers/builder/Dockerfile new file mode 100644 index 0000000000..aafd2cb0c3 --- /dev/null +++ b/containers/builder/Dockerfile @@ -0,0 +1,24 @@ +ARG CONTAINER_REPO=263262308774.dkr.ecr.eu-west-1.amazonaws.com + +FROM ${CONTAINER_REPO}/ns/mirror/prd/library/debian:12.11-slim + +RUN apt-get update && \ + apt-get install -y \ + git \ + bats \ + bash \ + coreutils \ + curl \ + make \ + && rm -rf /var/lib/apt/lists/* + +ARG user=build +ARG group=build +ARG uid=1000 +ARG gid=1000 + +RUN groupadd -g "${gid}" "${group}" \ + && useradd -l -c "Build user" -d "/${user}" -u "${uid}" -g "${gid}" \ + -s /bin/bash -m "${user}" + +USER build diff --git a/doc/user-guide/06-layout-configuration.adoc b/doc/user-guide/06-layout-configuration.adoc index f1f4a47326..3f90e72294 100644 --- a/doc/user-guide/06-layout-configuration.adoc +++ b/doc/user-guide/06-layout-configuration.adoc @@ -631,7 +631,7 @@ disk === Partitions === ---------------------------------- -part +part ---------------------------------- === Software RAID arrays === diff --git a/tests/COVE/000_save_future_dangling_efi_entries.bats b/tests/COVE/000_save_future_dangling_efi_entries.bats new file mode 100755 index 0000000000..41a62b5fbd --- /dev/null +++ b/tests/COVE/000_save_future_dangling_efi_entries.bats @@ -0,0 +1,166 @@ +setup_file() { + export REAR_SHARE_DIR="$(realpath "$BATS_TEST_DIRNAME/../../usr/share/rear")" + export USING_UEFI_BOOTLOADER="yes" + export COVE_TESTS="yes" +} + +setup() { + function is_cove_in_azure() { + false + } + + # shellcheck disable=SC1091 + source "$REAR_SHARE_DIR/lib/global-functions.sh" + + # shellcheck disable=SC1091 + source "$REAR_SHARE_DIR/layout/recreate/COVE/default/130_save_future_dangling_efi_entries.sh" +} + +teardown() { + # Clean up any test-specific state if needed + unset DISKS_TO_BE_OVERWRITTEN +} + +@test "find single future dangling EFI entry" { + function get_partuuids_of_disks_to_be_overwritten() { + echo "368a5c5b-26bf-4a63-b9ae-64a92d79d085" + echo "9bf08aed-f779-49fe-b310-9c21983289c1" + echo "bc6f95e7-893a-434b-8b76-f3d52e6ad28d" + echo "d96a0593-d527-424f-9174-b16f9e386dcb" + } + + function get_efi_entries() { + echo "BootCurrent: 0003" + echo "BootOrder: 0003,0000,0001,0002" + echo "Boot0000* EFI Virtual disk (0.0) PcieRoot(0x8)/Pci(0x0,0x0)/SCSI(0,0)" + echo "Boot0001* EFI VMware Virtual SATA CDROM Drive (0.0) PcieRoot(0x8)/Pci(0x2,0x0)/Sata(0,0,0)" + echo "Boot0002* EFI Network PcieRoot(0x8)/Pci(0x1,0x0)/MAC(00505698f752,1)" + # shellcheck disable=SC2028 + echo "Boot0003* Red Hat Enterprise Linux HD(1,GPT,bc6f95e7-893a-434b-8b76-f3d52e6ad28d,0x800,0x12c000)/\\EFI\\redhat\\shimx64.efi" + # shellcheck disable=SC2028 + echo "Boot0004* Red Hat Enterprise Linux HD(1,GPT,bc6f95e7-0000-0000-0000-f3d52e6ad28d,0x800,0x12c000)/\\EFI\\redhat\\shimx64.efi" + } + + local boot_number + boot_number="$(get_future_dangling_efi_entries)" + + [ "$boot_number" = "0003" ] +} + +@test "find multiple future dangling EFI entries" { + function get_partuuids_of_disks_to_be_overwritten() { + echo "bc6f95e7-893a-434b-8b76-f3d52e6ad28d" + } + + function get_efi_entries() { + # shellcheck disable=SC2028 + echo "Boot0003* Red Hat Enterprise Linux HD(1,GPT,bc6f95e7-893a-434b-8b76-f3d52e6ad28d,0x800,0x12c000)/\\EFI\\redhat\\shimx64.efi" + # shellcheck disable=SC2028 + echo "Boot0004* Red Hat Enterprise Linux HD(1,MBR,bc6f95e7-893a-434b-8b76-f3d52e6ad28d,0x800,0x12c000)/\\EFI\\redhat\\grubx64.efi" + } + + local boot_number + boot_number="$(get_future_dangling_efi_entries)" + + [ "$boot_number" = "0003 0004" ] +} + +@test "handle unexpected EFI entry format" { + function get_partuuids_of_disks_to_be_overwritten() { + echo "bc6f95e7-893a-434b-8b76-f3d52e6ad28d" + } + + function get_efi_entries() { + # shellcheck disable=SC2028 + echo "Bot0003 Red Hat Enterprise Linux HD(1,GPT,bc6f95e7-893a-434b-8b76-f3d52e6ad28d,0x800,0x12c000)/\\EFI\\redhat\\shimx64.efi" + } + + local boot_number + boot_number="$(get_future_dangling_efi_entries)" + + [ -z "$boot_number" ] +} + +@test "handle empty functions" { + function get_partuuids_of_disks_to_be_overwritten() { + : + } + + function get_efi_entries() { + : + } + + local boot_number + boot_number="$(get_future_dangling_efi_entries)" + + [ -z "$boot_number" ] +} + +@test "handle get_partuuids_of_disks_to_be_overwritten error" { + function get_partuuids_of_disks_to_be_overwritten() { + return 1 + } + + function get_efi_entries() { + : + } + + run get_future_dangling_efi_entries + [ "$status" -ne 0 ] +} + +@test "get partuuids for one disk to be overwritten" { + DISKS_TO_BE_OVERWRITTEN="/dev/sda" + local expected_partuuids="368a5c5b-26bf-4a63-b9ae-64a92d79d085" + + function get_disk_partuuids() { + local disk=$1 + + if [ "$disk" != "/dev/sda" ]; then + return 1 + fi + + echo "$expected_partuuids" + } + + local actual_partuuids + actual_partuuids="$(get_partuuids_of_disks_to_be_overwritten)" + + [ "$expected_partuuids" = "$actual_partuuids" ] +} + +@test "get partuuids for two disks to be overwritten" { + DISKS_TO_BE_OVERWRITTEN="/dev/sda /dev/sdb" + + function get_disk_partuuids() { + local disk=$1 + + if [ "$disk" = "/dev/sda" ]; then + echo "9bf08aed-f779-49fe-b310-9c21983289c1" + elif [ "$disk" = "/dev/sdb" ]; then + echo "368a5c5b-26bf-4a63-b9ae-64a92d79d085" + else + return 1 + fi + } + + local actual_partuuids + actual_partuuids="$(get_partuuids_of_disks_to_be_overwritten)" + + local expected_partuuids="368a5c5b-26bf-4a63-b9ae-64a92d79d085"$'\n'"9bf08aed-f779-49fe-b310-9c21983289c1" + + [ "$expected_partuuids" = "$actual_partuuids" ] +} + +@test "handle no disks to be overwritten" { + DISKS_TO_BE_OVERWRITTEN="" + + function get_disk_partuuids() { + : + } + + local partuuids + partuuids="$(get_partuuids_of_disks_to_be_overwritten)" + + [ -z "$partuuids" ] +} diff --git a/tests/COVE/001_remove_dangling_efi_entries.bats b/tests/COVE/001_remove_dangling_efi_entries.bats new file mode 100755 index 0000000000..026a2769d0 --- /dev/null +++ b/tests/COVE/001_remove_dangling_efi_entries.bats @@ -0,0 +1,87 @@ +setup_file() { + export REAR_SHARE_DIR="$(realpath "$BATS_TEST_DIRNAME/../../usr/share/rear")" + export USING_UEFI_BOOTLOADER="yes" + export COVE_TESTS="yes" +} + +setup() { + function is_cove_in_azure() { + false + } + + function LogPrint() { + echo "$@" + } + + # shellcheck disable=SC1091 + source "$REAR_SHARE_DIR/lib/global-functions.sh" + + # shellcheck disable=SC1091 + source "$REAR_SHARE_DIR/finalize/COVE/default/665_remove_dangling_efi_entries.sh" +} + +@test "no future dangling efi entries" { + FUTURE_DANGLING_EFI_ENTRIES="" + + run remove_dangling_efi_entries + + [ "$status" -eq 0 ] +} + +@test "one entry to remove" { + FUTURE_DANGLING_EFI_ENTRIES="0001" + + function remove_efi_entry() { + : + } + + local expected_output="Removing EFI Boot Manager entry with '0001' entry ID" + + run remove_dangling_efi_entries + + [ "$status" -eq 0 ] + [ "$output" = "$expected_output" ] +} + +@test "two entries to remove" { + FUTURE_DANGLING_EFI_ENTRIES="0001 0003" + + function remove_efi_entry() { + : + } + + local expected_output + expected_output="$(printf "%s\n%s" \ + "Removing EFI Boot Manager entry with '0001' entry ID" \ + "Removing EFI Boot Manager entry with '0003' entry ID")" + + run remove_dangling_efi_entries + + [ "$status" -eq 0 ] + [ "$output" = "$expected_output" ] +} + +@test "fail to remove efi entry" { + FUTURE_DANGLING_EFI_ENTRIES="0001 0003" + + function remove_efi_entry() { + local id="$1" + if [ "$id" = "0001" ]; then + return 0 + elif [ "$id" = "0003" ]; then + return 1 + fi + } + + local expected_output + expected_output="$(printf "%s\n%s\n%s" \ + "Removing EFI Boot Manager entry with '0001' entry ID" \ + "Removing EFI Boot Manager entry with '0003' entry ID" \ + "Failed to remove EFI Boot Manager entry with '0003' entry ID" + )" + + run remove_dangling_efi_entries + + [ "$status" -eq 0 ] + [ "$output" = "$expected_output" ] +} diff --git a/usr/sbin/rear b/usr/sbin/rear index 3b94e129da..b7a24eeea3 100755 --- a/usr/sbin/rear +++ b/usr/sbin/rear @@ -361,9 +361,15 @@ test "$WORKFLOW" = "udev" || readonly WORKFLOW # Make sure we have the necessary paths (eg. in cron), /sbin will be the first path to search. # some needed binaries are in /lib/udev or /usr/lib/udev +parent_command=$(ps -p $PPID -o comm= 2>/dev/null) +force_prepend=false +[ "$parent_command" = "BackupFP" ] && force_prepend=true + for path in /usr/bin /bin /usr/sbin /sbin ; do case ":$PATH:" in (*:"$path":*) + # For Cove only: always prepend paths + $force_prepend && PATH=$path:$PATH ;; (*) test -d "$path" && PATH=$path:$PATH diff --git a/usr/share/rear/conf/GNU/Linux.conf b/usr/share/rear/conf/GNU/Linux.conf index 9ebc3e70a4..fbfc6d39f3 100644 --- a/usr/share/rear/conf/GNU/Linux.conf +++ b/usr/share/rear/conf/GNU/Linux.conf @@ -15,6 +15,7 @@ partprobe fdisk cfdisk sfdisk +sgdisk ) # progs to take along @@ -22,7 +23,6 @@ PROGS+=( rpc.statd rpcbind mknod -blkid vol_id udev_volume_id portmap diff --git a/usr/share/rear/conf/default.conf b/usr/share/rear/conf/default.conf index 7fff90a9ee..945f8e8b38 100644 --- a/usr/share/rear/conf/default.conf +++ b/usr/share/rear/conf/default.conf @@ -1759,6 +1759,7 @@ REQUIRED_PROGS=( awk bash bc +blkid cat cmp cp @@ -4417,3 +4418,7 @@ DISABLE_DEPRECATION_ERRORS=() PYTHON_INTERPRETER=false # Set to true to include only the Python interpreter without any dist or site packages: PYTHON_MINIMAL=false + +#### +# Location of file created just before layout code is executed +LAYOUT_CODE_STARTED="$TMPDIR/cove_rear_layout_code_started" diff --git a/usr/share/rear/finalize/COVE/default/665_remove_dangling_efi_entries.sh b/usr/share/rear/finalize/COVE/default/665_remove_dangling_efi_entries.sh new file mode 100644 index 0000000000..c32f9b7b59 --- /dev/null +++ b/usr/share/rear/finalize/COVE/default/665_remove_dangling_efi_entries.sh @@ -0,0 +1,45 @@ +# +# Remove dangling EFI entries +# + +if ! is_true "$USING_UEFI_BOOTLOADER"; then + return 0 +fi + +if is_cove_in_azure; then + return 0 +fi + +if is_true "$EFI_STUB"; then + return 0 +fi + +# $1 - EFI entry id +function remove_efi_entry() { + if ! has_binary efibootmgr; then + return 1 + fi + + local id=$1 + + efibootmgr -b "$id" -B 2>/dev/null +} + +function remove_dangling_efi_entries() { + if [ -z "$FUTURE_DANGLING_EFI_ENTRIES" ]; then + return 0 + fi + + for id in $FUTURE_DANGLING_EFI_ENTRIES; do + LogPrint "Removing EFI Boot Manager entry with '$id' entry ID" + if ! remove_efi_entry "$id"; then + LogPrint "Failed to remove EFI Boot Manager entry with '$id' entry ID" + fi + done +} + +if is_true "$COVE_TESTS"; then + return 0 +fi + +remove_dangling_efi_entries diff --git a/usr/share/rear/finalize/Linux-i386/670_run_efibootmgr.sh b/usr/share/rear/finalize/Linux-i386/670_run_efibootmgr.sh index 71a4bab551..179cf96d7d 100644 --- a/usr/share/rear/finalize/Linux-i386/670_run_efibootmgr.sh +++ b/usr/share/rear/finalize/Linux-i386/670_run_efibootmgr.sh @@ -8,6 +8,15 @@ is_true $USING_UEFI_BOOTLOADER || return 0 # (cf. finalize/Linux-i386/610_EFISTUB_run_efibootmgr.sh): is_true $EFI_STUB && return +# Do not create EFI Boot Manager entries in Azure, because these are created +# for OS disks. Having incorrect EFI entries for the Cove Rescue Media OS disk +# can make it non-bootable for further use. +if is_cove_in_azure; then + LogPrint "Skip creating EFI Boot Manager entries in Azure" + NOBOOTLOADER="" + return 0 +fi + LogPrint "Creating EFI Boot Manager entries..." local esp_mountpoint esp_mountpoint_inside boot_efi_parts boot_efi_dev diff --git a/usr/share/rear/finalize/default/890_finish_checks.sh b/usr/share/rear/finalize/default/890_finish_checks.sh index 5d8c3ed720..3924ad00c3 100644 --- a/usr/share/rear/finalize/default/890_finish_checks.sh +++ b/usr/share/rear/finalize/default/890_finish_checks.sh @@ -4,6 +4,13 @@ if ls -l /sys/block/*/ | grep -q xen ; then # if some disks are xen related then assume this is a XEN PV VM which boots externally LogPrint "This looks like a XEN PV system, ignoring boot loader issues" elif test "$NOBOOTLOADER" ; then + if is_cove; then + text="For this system, the boot loader cannot be installed on the recovered system \ +because the installation code is either missing or malfunctioning." + cove_print_in_frame "ERROR" "$text" + Error "Bootloader failed to install." + fi + LogPrint "WARNING: For this system $OS_VENDOR_VERSION on $ARCH (based on $OS_MASTER_VENDOR_VERSION_ARCH) diff --git a/usr/share/rear/layout/prepare/COVE/default/020_umount_in_azure.sh b/usr/share/rear/layout/prepare/COVE/default/020_umount_in_azure.sh new file mode 100644 index 0000000000..4d97d5f26c --- /dev/null +++ b/usr/share/rear/layout/prepare/COVE/default/020_umount_in_azure.sh @@ -0,0 +1,6 @@ +# Unmount /mnt, which is mounted by cloud-init in the Azure environment + +if is_cove_in_azure; then + LogPrint "Unmounting '/mnt' in Azure" + umount_mountpoint /mnt +fi diff --git a/usr/share/rear/layout/prepare/GNU/Linux/100_include_partition_code.sh b/usr/share/rear/layout/prepare/GNU/Linux/100_include_partition_code.sh index 355db98608..08ad36cf70 100644 --- a/usr/share/rear/layout/prepare/GNU/Linux/100_include_partition_code.sh +++ b/usr/share/rear/layout/prepare/GNU/Linux/100_include_partition_code.sh @@ -161,7 +161,7 @@ EOF let last_number=0 local flags partition - while read part disk size pstart name flags partition junk; do + while read part disk size pstart name flags partition partuuid junk; do # Get the partition number from the name number=$( get_partition_number "$partition" ) @@ -332,6 +332,14 @@ EOF echo "my_udevsettle" ) >> $LAYOUT_CODE fi + + if [ "$label" = "gpt" ] && [ -n "$partuuid" ] && [ "$partuuid" != "no-partuuid" ]; then + ( + echo "my_udevsettle" + echo "set_partition_guid $device $number $partuuid || true" + echo "my_udevsettle" + ) >> "$LAYOUT_CODE" + fi done < <(grep "^part $device " $LAYOUT_FILE) # This will override all partition setup previously made, diff --git a/usr/share/rear/layout/prepare/default/250_compare_disks.sh b/usr/share/rear/layout/prepare/default/250_compare_disks.sh index 8fdb2b76cf..985ffcc65a 100644 --- a/usr/share/rear/layout/prepare/default/250_compare_disks.sh +++ b/usr/share/rear/layout/prepare/default/250_compare_disks.sh @@ -110,6 +110,9 @@ for current_device_path in /sys/block/* ; do test "$( < $current_device_path/removable )" = "1" && continue # Continue with next block device if the current one is designated as write-protected: is_write_protected $current_device_path && continue + # Continue with next block device if the current one is the COVE_RESCUE disk + device_name="$( get_device_name "$current_device_path" )" + is_cove_rescue_device "$device_name" && continue # Continue with next block device if no size can be read for the current one: test -r $current_device_path/size || continue current_size=$( get_disk_size $current_disk_name ) @@ -151,7 +154,11 @@ if ! is_true "$MIGRATION_MODE" ; then if test -e "/sys/block/$dev" ; then Log "Device /sys/block/$dev exists" newsize=$( get_disk_size $dev ) - if test "$newsize" -eq "$size" ; then + device_name="$( get_device_name "/sys/block/$dev" )" + if is_cove_rescue_device "$device_name" ; then + LogPrint "Device $dev is the COVE_RESCUE disk (needs manual configuration)" + MIGRATION_MODE='true' + elif test "$newsize" -eq "$size" ; then if is_write_protected "/sys/block/$dev"; then LogPrint "Device $dev is designated as write-protected (needs manual configuration)" MIGRATION_MODE='true' diff --git a/usr/share/rear/layout/prepare/default/300_map_disks.sh b/usr/share/rear/layout/prepare/default/300_map_disks.sh index c88477a151..6285770d4c 100644 --- a/usr/share/rear/layout/prepare/default/300_map_disks.sh +++ b/usr/share/rear/layout/prepare/default/300_map_disks.sh @@ -136,7 +136,7 @@ while read keyword orig_device orig_size junk ; do # its matching actual block device (e.g. /dev/sda) must be determined: preferred_target_device_name="$( get_device_name $current_device )" # Use the current one if it is of same size as the old one: - if has_mapping_hint "$orig_device" || test "$orig_size" -eq "$current_size" ; then + if ! is_cove_rescue_device "$preferred_target_device_name" && { has_mapping_hint "$orig_device" || test "$orig_size" -eq "$current_size"; }; then # Ensure the target device is really a block device on the replacement hardware. # Here the target device has same name as the original device which was a block device on the original hardware # but it might perhaps happen that this device name is not a block device on the replacement hardware: @@ -177,6 +177,11 @@ while read keyword orig_device orig_size junk ; do # The current_device_path (e.g. /sys/block/sdb) is not a block device so that # its matching actual block device (e.g. /dev/sdb) must be determined: preferred_target_device_name="$( get_device_name $current_device_path )" + # Continue with next block device if the current one is the COVE_RESCUE disk + if is_cove_rescue_device "$preferred_target_device_name" ; then + DebugPrint "Cannot use $preferred_target_device_name for recreating $orig_device ($preferred_target_device_name is COVE_RESCUE)" + continue + fi # Ensure the determined target device is really a block device (cf. above): test -b "$preferred_target_device_name" || continue # Continue with next block device if the current one is not of same size as the original: @@ -250,6 +255,11 @@ while read keyword orig_device orig_size junk ; do Log "$preferred_target_device_name excluded from device mapping choices (has no queue directory)" continue fi + # Continue with next block device if the current one is the COVE_RESCUE disk + if is_cove_rescue_device "$preferred_target_device_name" ; then + Log "$preferred_target_device_name excluded from device mapping choices (is COVE_RESCUE)" + continue + fi # Continue with next block device if the current one is already used as target in the mapping file: if is_mapping_target "$preferred_target_device_name" ; then Log "$preferred_target_device_name excluded from device mapping choices (is already used as mapping target)" diff --git a/usr/share/rear/layout/recreate/COVE/default/130_save_future_dangling_efi_entries.sh b/usr/share/rear/layout/recreate/COVE/default/130_save_future_dangling_efi_entries.sh new file mode 100644 index 0000000000..8ef9d3aa0e --- /dev/null +++ b/usr/share/rear/layout/recreate/COVE/default/130_save_future_dangling_efi_entries.sh @@ -0,0 +1,96 @@ +# +# Save future dangling EFI entries +# + +if ! is_true "$USING_UEFI_BOOTLOADER"; then + return 0 +fi + +if is_cove_in_azure; then + return 0 +fi + +if is_true "$EFI_STUB"; then + return 0 +fi + +# $1 - block device, e.g. /dev/sda or /dev/sdb +get_disk_partuuids() { + if ! has_binary blkid; then + return 1 + fi + + local disk=$1 + + if [ ! -b "$disk" ]; then + return 1 + fi + + disk="${disk}*" + + # shellcheck disable=SC2086 + blkid -s PARTUUID -o value $disk +} + +get_partuuids_of_disks_to_be_overwritten() { + if [ -z "$DISKS_TO_BE_OVERWRITTEN" ]; then + return 0 + fi + + local partuuids="" + for disk in $DISKS_TO_BE_OVERWRITTEN; do + local partuuids_for_disk + partuuids_for_disk="$(get_disk_partuuids "$disk")" || continue + partuuids+="$partuuids_for_disk"$'\n' + done + + if [ -n "$partuuids" ]; then + # Trim the result + partuuids="${partuuids::-1}" + fi + + echo "$partuuids" | sort -u +} + +get_efi_entries() { + if ! has_binary efibootmgr; then + return 1 + fi + + efibootmgr -v 2>/dev/null +} + +get_future_dangling_efi_entries() { + local partuuids + partuuids="$(get_partuuids_of_disks_to_be_overwritten)" || return 1 + + local boot_numbers + + local entry + while IFS= read -r entry; do + local partuuid + partuuid=$(echo "$entry" | grep -oP '(?<=GPT,|MBR,)[0-9a-fA-F-]+') || continue + if echo "$partuuids" | grep -q "^$partuuid$"; then + local boot_number + boot_number="$(echo "$entry" | awk '{print $1}' | grep -oP '(?<=Boot)[0-9a-fA-F]+')" || continue + boot_numbers+="$boot_number " + fi + done <<< "$(get_efi_entries)" + + if [ -n "$boot_numbers" ]; then + # Trim the result + boot_numbers="${boot_numbers::-1}" + fi + + echo "$boot_numbers" +} + +if is_true "$COVE_TESTS"; then + return 0 +fi + +if FUTURE_DANGLING_EFI_ENTRIES=$(get_future_dangling_efi_entries); then + LogPrint "Found future dangling EFI entries: $FUTURE_DANGLING_EFI_ENTRIES" +else + LogPrint "Failed to identify future dangling EFI entries" +fi diff --git a/usr/share/rear/layout/recreate/default/120_confirm_wipedisk_disks.sh b/usr/share/rear/layout/recreate/default/120_confirm_wipedisk_disks.sh index 75d0c31c2f..a3aa105188 100644 --- a/usr/share/rear/layout/recreate/default/120_confirm_wipedisk_disks.sh +++ b/usr/share/rear/layout/recreate/default/120_confirm_wipedisk_disks.sh @@ -145,6 +145,12 @@ fi DISKS_TO_BE_WIPED="$disks_to_be_wiped" # The DISKS_TO_BE_WIPED string is needed in the subsequent layout/recreate/default/150_wipe_disks.sh script +if is_cove; then + # DISKS_TO_BE_OVERWRITTEN is used to save future dangling EFI entries + # shellcheck disable=SC2034 + DISKS_TO_BE_OVERWRITTEN="$disks_to_be_wiped" +fi + # Show the user confirmation dialog in any case but when not in migration mode # automatically proceed with less timeout USER_INPUT_INTERRUPT_TIMEOUT (by default 30 seconds) # to avoid longer delays (USER_INPUT_TIMEOUT is by default 300 seconds) in case of unattended recovery: diff --git a/usr/share/rear/layout/recreate/default/200_run_layout_code.sh b/usr/share/rear/layout/recreate/default/200_run_layout_code.sh index 443b200000..f7fcf709ad 100644 --- a/usr/share/rear/layout/recreate/default/200_run_layout_code.sh +++ b/usr/share/rear/layout/recreate/default/200_run_layout_code.sh @@ -120,6 +120,8 @@ while true ; do # Run LAYOUT_CODE in a sub-shell because it sets 'set -e' # so that it exits the running shell in case of an error # but that exit must not exit this running bash here: + touch "${LAYOUT_CODE_STARTED}" + ( source $LAYOUT_CODE ) fi # One must explicitly test whether or not $? is zero in a separated bash command diff --git a/usr/share/rear/layout/save/GNU/Linux/100_create_layout_file.sh b/usr/share/rear/layout/save/GNU/Linux/100_create_layout_file.sh index 186c4233d1..a69164945b 100644 --- a/usr/share/rear/layout/save/GNU/Linux/100_create_layout_file.sh +++ b/usr/share/rear/layout/save/GNU/Linux/100_create_layout_file.sh @@ -34,7 +34,11 @@ echo "Disk layout dated $START_DATE_TIME_NUMBER (YYYYmmddHHMMSS)" >$DISKLAYOUT_F # lsblk -io NAME,KNAME,FSTYPE,LABEL,SIZE,MOUNTPOINT,UUID # and as fallback try 'lsblk -i' and finally try plain 'lsblk'. # When there is no 'lsblk' command there is no output (bad luck, no harm): -{ lsblk -ipo NAME,KNAME,PKNAME,TRAN,TYPE,FSTYPE,LABEL,SIZE,MOUNTPOINTS,UUID,WWN || lsblk -ipo NAME,KNAME,PKNAME,TRAN,TYPE,FSTYPE,LABEL,SIZE,MOUNTPOINT,UUID,WWN || lsblk -io NAME,KNAME,FSTYPE,LABEL,SIZE,MOUNTPOINT,UUID || lsblk -i || lsblk ; } >>$DISKLAYOUT_FILE +{ lsblk -ipo NAME,KNAME,PKNAME,TRAN,TYPE,FSTYPE,LABEL,SIZE,MOUNTPOINTS,UUID,PARTUUID,WWN || \ + lsblk -ipo NAME,KNAME,PKNAME,TRAN,TYPE,FSTYPE,LABEL,SIZE,MOUNTPOINT,UUID,PARTUUID,WWN || \ + lsblk -io NAME,KNAME,FSTYPE,LABEL,SIZE,MOUNTPOINT,UUID,PARTUUID || \ + lsblk -io NAME,KNAME,FSTYPE,LABEL,SIZE,MOUNTPOINT,UUID || \ + lsblk -i || lsblk ; } >>"$DISKLAYOUT_FILE" # Make all lines in disklayout.conf up to now as header comments: sed -i -e 's/^/# /' $DISKLAYOUT_FILE diff --git a/usr/share/rear/layout/save/GNU/Linux/200_partition_layout.sh b/usr/share/rear/layout/save/GNU/Linux/200_partition_layout.sh index f6b7fdd914..9614ce28db 100644 --- a/usr/share/rear/layout/save/GNU/Linux/200_partition_layout.sh +++ b/usr/share/rear/layout/save/GNU/Linux/200_partition_layout.sh @@ -39,9 +39,9 @@ extract_partitions() { ### Therefore we ignore these special partitions, see also ### https://github.com/rear/rear/issues/2087 for possible_sysfs_partition in "${sysfs_paths_unfiltered[@]}"; do - if [[ ! ( $possible_sysfs_partition = *'/mmcblk'+([0-9])'rpmb' || $possible_sysfs_partition = *'/mmcblk'+([0-9])'boot'+([0-9]) ) ]] ; then - sysfs_paths+=($possible_sysfs_partition) - fi + if [[ ! ( $possible_sysfs_partition = *'/mmcblk'+([0-9])'rpmb' || $possible_sysfs_partition = *'/mmcblk'+([0-9])'boot'+([0-9]) ) ]] ; then + sysfs_paths+=($possible_sysfs_partition) + fi done declare path sysfs_path @@ -352,8 +352,24 @@ extract_partitions() { done < $TMP_DIR/partitions-data fi + while read -r partition_nr size start type flags junk ; do + local partuuid + if [ "$disk_label" = "gpt" ]; then + local partition_name="${device%/*}/${partition_prefix#*/}$partition_nr" + local partition_device + partition_device="$(get_device_name "$partition_name")" + partuuid="$(get_partition_guid "$partition_device")" + fi + + if [ -z "$partuuid" ]; then + partuuid="no-partuuid" + fi + + sed -i /^"$partition_nr"\ /s/$/\ $partuuid/ "$TMP_DIR/partitions" + done < "$TMP_DIR/partitions-data" + ### Write to layout file - while read partition_nr size start type flags junk ; do + while read partition_nr size start type flags partuuid junk ; do # determine the name of the partition using the number # device=/dev/cciss/c0d0 ; partition_prefix=cciss/c0d0p # device=/dev/md127 ; partition_prefix=md127p @@ -388,7 +404,7 @@ extract_partitions() { fi fi # Some basic checks on the values happen in layout/save/default/950_verify_disklayout_file.sh - echo "part $device $size $start $type $flags $partition_device" + echo "part $device $size $start $type $flags $partition_device $partuuid" done < $TMP_DIR/partitions } @@ -449,7 +465,16 @@ Log "Saving disks and their partitions" continue fi disktype=$(parted -s $devname print | grep -E "Partition Table|Disk label" | cut -d ":" -f "2" | tr -d " ") - test $disktype || Error "Invalid 'disk $devname' entry (no partition table type for '$devname')" + if [ -z "$disktype" ]; then + err_msg="Invalid 'disk $devname' entry (no partition table type for '$devname')" + if is_true "$AUTOEXCLUDE_DISKS"; then + disktype="unknown" + LogPrintError "$err_msg" + else + Error "$err_msg" + fi + unset err_msg + fi if [ "$disktype" != "dasd" ]; then echo "# Disk $devname" echo "# Format: disk " @@ -483,7 +508,7 @@ Log "Saving disks and their partitions" Error "Invalid 'disk $devname' entry (DASD partition label on non-s390 arch $ARCH)" fi echo "# Partitions on $devname" - echo "# Format: part /dev/" + echo "# Format: part /dev/ " extract_partitions "$devname" fi fi diff --git a/usr/share/rear/layout/save/default/445_guess_bootloader.sh b/usr/share/rear/layout/save/default/445_guess_bootloader.sh index d06d26b56e..1f76400259 100644 --- a/usr/share/rear/layout/save/default/445_guess_bootloader.sh +++ b/usr/share/rear/layout/save/default/445_guess_bootloader.sh @@ -101,6 +101,21 @@ for block_device in /sys/block/* ; do else LogPrint "Using guessed bootloader '$known_bootloader' for 'rear recover' (found in first bytes on $disk_device)" fi + # In cloud providers such as Azure, VMs can be created from images + # that are dual-boot capable — GPT + EFI and GPT + BIOS. + if is_true "$USING_UEFI_BOOTLOADER"; then + case "$known_bootloader" in + (GRUB2) + known_bootloader="GRUB2-EFI" + ;; + (LILO) + known_bootloader="ELILO" + ;; + (*) + known_bootloader="EFI" + ;; + esac + fi echo "$known_bootloader" >$bootloader_file return fi diff --git a/usr/share/rear/layout/save/default/950_verify_disklayout_file.sh b/usr/share/rear/layout/save/default/950_verify_disklayout_file.sh index afe82ca911..ef04bf2b7e 100644 --- a/usr/share/rear/layout/save/default/950_verify_disklayout_file.sh +++ b/usr/share/rear/layout/save/default/950_verify_disklayout_file.sh @@ -244,7 +244,9 @@ for error_message in "${broken_part_errors[@]}" ; do disklayout_file_is_broken="yes" done # Non consecutive partitions are supported unless parted tells otherwise: -if is_false $FEATURE_PARTED_RESIZEPART && is_false $FEATURE_PARTED_RESIZE ; then +# Cove is an exception because Cove Rescue Media is used during recovery, +# which provides updated parted tool. +if is_false "$FEATURE_PARTED_RESIZEPART" && is_false "$FEATURE_PARTED_RESIZE" && ! is_cove; then for error_message in "${non_consecutive_part_errors[@]}" ; do contains_visible_char "$error_message" || continue LogPrintError "$error_message" diff --git a/usr/share/rear/lib/cove-functions.sh b/usr/share/rear/lib/cove-functions.sh index 9aaa3203da..f1d2598d6a 100644 --- a/usr/share/rear/lib/cove-functions.sh +++ b/usr/share/rear/lib/cove-functions.sh @@ -48,6 +48,12 @@ function is_cove() { [ "$BACKUP" = "COVE" ] } +function is_cove_in_azure() { + is_cove && grep -qw "cove_azure" /proc/cmdline && \ + curl -H "Metadata: true" "http://169.254.169.254/metadata/instance?api-version=2021-02-01" \ + --connect-timeout 3 1>/dev/null 2>&1 +} + # Since there is no reliable way of detecting whether it is running in a container or not, # it only tries to guess. function is_container() { @@ -88,3 +94,12 @@ function cove_error_if_container() { Error "The system is detected as ${container} container. System state is not supported for containers." } + +function is_cove_rescue_device() { + local device_name="$1" + + local label + label=$(blkid_label_of_device "$device_name") + + [ "$label" = "COVE_RESCUE" ] +} diff --git a/usr/share/rear/lib/layout-functions.sh b/usr/share/rear/lib/layout-functions.sh index 5782ea9f02..99d9991219 100644 --- a/usr/share/rear/lib/layout-functions.sh +++ b/usr/share/rear/lib/layout-functions.sh @@ -1531,4 +1531,33 @@ delete_dummy_partitions_and_resize_real_ones() { last_partition_number=0 } +get_partition_guid() { + local device=$1 + + local guid="" + if has_binary blkid; then + guid="$(blkid -s PARTUUID -o value "$device" 2>/dev/null)" + # Fallback for cases when blkid is not available + elif [ -d /dev/disk/by-partuuid ]; then + local disk + for disk in /dev/disk/by-partuuid/*; do + if [ -h "$disk" ] && [ "$(readlink -f "$disk")" = "$device" ]; then + guid="${disk#/dev/disk/by-partuuid/}" + break + fi + done + fi + echo "$guid" +} + +set_partition_guid() { + local device=$1 + local partnum=$2 + local guid=$3 + + if has_binary sgdisk; then + sgdisk --partition-guid="$partnum":"$guid" "$device" + fi +} + # vim: set et ts=4 sw=4: diff --git a/usr/share/rear/prep/systemstate/Linux-i386/330_set_efi_arch.sh b/usr/share/rear/prep/systemstate/Linux-i386/330_set_efi_arch.sh new file mode 120000 index 0000000000..afbb3fdc4f --- /dev/null +++ b/usr/share/rear/prep/systemstate/Linux-i386/330_set_efi_arch.sh @@ -0,0 +1 @@ +../../Linux-i386/330_set_efi_arch.sh \ No newline at end of file diff --git a/usr/share/rear/prep/systemstate/Linux-i386/370_detect_secure_boot.sh b/usr/share/rear/prep/systemstate/Linux-i386/370_detect_secure_boot.sh new file mode 120000 index 0000000000..0c98e54392 --- /dev/null +++ b/usr/share/rear/prep/systemstate/Linux-i386/370_detect_secure_boot.sh @@ -0,0 +1 @@ +../../Linux-i386/370_detect_secure_boot.sh \ No newline at end of file diff --git a/usr/share/rear/verify/COVE/default/400_check_if_reboot_required.sh b/usr/share/rear/verify/COVE/default/400_check_if_reboot_required.sh new file mode 100644 index 0000000000..0fb355948e --- /dev/null +++ b/usr/share/rear/verify/COVE/default/400_check_if_reboot_required.sh @@ -0,0 +1,11 @@ +# +# Check if layout code execution was not started +# + +if [ -e "${LAYOUT_CODE_STARTED}" ]; then + text="A new recovery attempt has been detected. \ +Some devices may have been mounted since the last attempt, \ +which could prevent the recovery from completing. \ +Please reboot the system and start the recovery again." + cove_print_in_frame "WARNING" "$text" +fi