|
| 1 | +#!/bin/bash |
| 2 | +# Step 05: Restore the standalone VMDisk in-place from its backup. |
| 3 | +set -euo pipefail |
| 4 | + |
| 5 | +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" |
| 6 | +source "$SCRIPT_DIR/00-helpers.sh" |
| 7 | + |
| 8 | +print_header "Step 5: Restore VMDisk In-Place" |
| 9 | + |
| 10 | +PVC=vm-disk-backup-src |
| 11 | +CONSUMER=vm-disk-backup-src-restore-consumer |
| 12 | + |
| 13 | +orig_uid=$(kubectl -n "$NAMESPACE" get pvc "$PVC" -o jsonpath='{.metadata.uid}') |
| 14 | +[[ -n "$orig_uid" ]] || { log_error "PVC $PVC has no uid before the restore"; exit 1; } |
| 15 | +log_info "PVC $PVC before restore: uid $orig_uid" |
| 16 | + |
| 17 | +log_step "Creating RestoreJob 'vmdisk-restore-in-place' in namespace $NAMESPACE..." |
| 18 | +log_info "In-place restore: the original PVC is renamed and the disk is restored from backup" |
| 19 | +log_command "kubectl apply -f - (RestoreJob: vmdisk-restore-in-place)" |
| 20 | + |
| 21 | +# The controller suspends the vm-disk-<name> HelmRelease, deletes its DataVolume |
| 22 | +# and (keepOriginalPVC) renames the live PVC to <name>-orig-<hash>; Velero then |
| 23 | +# creates a fresh PVC under the original name for the data mover to populate. |
| 24 | +kubectl apply -f - <<EOF |
| 25 | +apiVersion: backups.cozystack.io/v1alpha1 |
| 26 | +kind: RestoreJob |
| 27 | +metadata: |
| 28 | + name: vmdisk-restore-in-place |
| 29 | + namespace: ${NAMESPACE} |
| 30 | +spec: |
| 31 | + backupRef: |
| 32 | + name: vmdisk-backup |
| 33 | + targetApplicationRef: |
| 34 | + apiGroup: apps.cozystack.io |
| 35 | + kind: VMDisk |
| 36 | + name: backup-src |
| 37 | + options: |
| 38 | + keepOriginalPVC: true # rename the original PVC to <name>-orig-<hash> before restoring |
| 39 | +EOF |
| 40 | + |
| 41 | +log_success "RestoreJob created" |
| 42 | + |
| 43 | +# On a WaitForFirstConsumer StorageClass the Velero data mover does not start |
| 44 | +# until a scheduled Pod has picked a node for the restored PVC (Velero's |
| 45 | +# WaitPVCConsumed). A VMInstance restore gets that from the virt-launcher Pod; a |
| 46 | +# detached disk has no consumer, so stand in for the VM that would attach it. |
| 47 | +# Only once the new PVC exists: a Pod mounting the original would pin it with |
| 48 | +# pvc-protection and block the rename. |
| 49 | +start_consumer() { |
| 50 | + local mode volume mount |
| 51 | + mode=$(kubectl -n "$NAMESPACE" get pvc "$PVC" -o jsonpath='{.spec.volumeMode}') |
| 52 | + if [[ "$mode" == "Block" ]]; then |
| 53 | + mount='volumeDevices: [{name: disk, devicePath: /dev/disk0}]' |
| 54 | + else |
| 55 | + mount='volumeMounts: [{name: disk, mountPath: /disk}]' |
| 56 | + fi |
| 57 | + volume="{name: disk, persistentVolumeClaim: {claimName: $PVC}}" |
| 58 | + log_info "Starting consumer Pod $CONSUMER for the restored PVC ($mode)" |
| 59 | + kubectl apply -f - <<EOF |
| 60 | +apiVersion: v1 |
| 61 | +kind: Pod |
| 62 | +metadata: |
| 63 | + name: ${CONSUMER} |
| 64 | + namespace: ${NAMESPACE} |
| 65 | +spec: |
| 66 | + securityContext: |
| 67 | + runAsNonRoot: true |
| 68 | + runAsUser: 65532 |
| 69 | + seccompProfile: |
| 70 | + type: RuntimeDefault |
| 71 | + containers: |
| 72 | + - name: pause |
| 73 | + image: registry.k8s.io/pause:3.9 |
| 74 | + securityContext: |
| 75 | + allowPrivilegeEscalation: false |
| 76 | + readOnlyRootFilesystem: true |
| 77 | + capabilities: |
| 78 | + drop: ["ALL"] |
| 79 | + ${mount} |
| 80 | + volumes: |
| 81 | + - ${volume} |
| 82 | +EOF |
| 83 | +} |
| 84 | + |
| 85 | +log_step "Waiting for RestoreJob to complete..." |
| 86 | +log_command "waiting for restorejob/vmdisk-restore-in-place {.status.phase} == 'Succeeded'" |
| 87 | +consumer_started=false |
| 88 | +elapsed=0 |
| 89 | +timeout="${RESTORE_WAIT:-1800}" |
| 90 | +while true; do |
| 91 | + phase=$(kubectl -n "$NAMESPACE" get restorejob vmdisk-restore-in-place -o jsonpath='{.status.phase}' || true) |
| 92 | + [[ "$phase" == "Succeeded" ]] && break |
| 93 | + if [[ "$phase" == "Failed" ]]; then |
| 94 | + log_error "restorejob/vmdisk-restore-in-place reached terminal failure state 'Failed'" |
| 95 | + exit 1 |
| 96 | + fi |
| 97 | + if [[ "$consumer_started" == false ]]; then |
| 98 | + # A PVC CDI recreated in the rename window has a new uid too but no |
| 99 | + # restore label; mounting it would pin it while the controller clears it. |
| 100 | + read -r uid restored < <(kubectl -n "$NAMESPACE" get pvc "$PVC" --ignore-not-found \ |
| 101 | + -o jsonpath='{.metadata.uid} {.metadata.labels.velero\.io/restore-name}' || true) || true |
| 102 | + if [[ -n "${uid:-}" && "$uid" != "$orig_uid" && -n "${restored:-}" ]]; then |
| 103 | + start_consumer |
| 104 | + consumer_started=true |
| 105 | + fi |
| 106 | + fi |
| 107 | + if [[ $elapsed -ge $timeout ]]; then |
| 108 | + log_error "Timeout waiting for restorejob/vmdisk-restore-in-place (current: '$phase', expected: 'Succeeded', consumer started: $consumer_started)" |
| 109 | + exit 1 |
| 110 | + fi |
| 111 | + sleep 5 |
| 112 | + elapsed=$((elapsed + 5)) |
| 113 | + echo -n "." >&2 |
| 114 | +done |
| 115 | +log_success "restorejob/vmdisk-restore-in-place reached 'Succeeded'" |
| 116 | + |
| 117 | +kubectl -n "$NAMESPACE" delete pod "$CONSUMER" --ignore-not-found --timeout=120s |
| 118 | + |
| 119 | +# Neither the restore-name label nor a new uid proves the data moved: Velero's |
| 120 | +# existingResourcePolicy: update stamps the label on a PVC it only patched, and |
| 121 | +# CDI recreating the disk would also give it a new uid. The DataDownload check |
| 122 | +# below does; the uid and the -orig- PVC show the original was kept aside. |
| 123 | +log_step "Verifying the PVC was recreated from the backup..." |
| 124 | +wait_for_field pvc "$PVC" '{.status.phase}' Bound "$NAMESPACE" 120 |
| 125 | +new_uid=$(kubectl -n "$NAMESPACE" get pvc "$PVC" -o jsonpath='{.metadata.uid}') |
| 126 | +[[ "$new_uid" != "$orig_uid" ]] || { |
| 127 | + log_error "PVC $PVC still has its pre-restore uid $orig_uid — the restore did not recreate it" |
| 128 | + exit 1 |
| 129 | +} |
| 130 | +restore_label=$(kubectl -n "$NAMESPACE" get pvc "$PVC" \ |
| 131 | + -o "jsonpath={.metadata.labels.velero\.io/restore-name}") |
| 132 | +[[ -n "$restore_label" ]] || { |
| 133 | + log_error "PVC $PVC carries no velero.io/restore-name label — Velero did not restore it" |
| 134 | + exit 1 |
| 135 | +} |
| 136 | +all_pvcs=$(kubectl -n "$NAMESPACE" get pvc -o name) |
| 137 | +[[ "$all_pvcs" == *"persistentvolumeclaim/${PVC}-orig-"* ]] || { |
| 138 | + log_error "no ${PVC}-orig-* PVC — keepOriginalPVC did not rename the original disk" |
| 139 | + exit 1 |
| 140 | +} |
| 141 | +# The DataDownload carries the same restore-name label as the PVC; one that |
| 142 | +# Completed having written bytes is what shows the data mover filled the disk. |
| 143 | +downloads=$(kubectl -n cozy-velero get datadownloads.velero.io -l "velero.io/restore-name=${restore_label}" \ |
| 144 | + -o jsonpath='{range .items[*]}{.status.phase} {.status.progress.bytesDone}{"\n"}{end}') |
| 145 | +grep -Eq '^Completed [1-9][0-9]*$' <<<"$downloads" || { |
| 146 | + log_error "no Completed DataDownload with data for restore ${restore_label} — the data mover did not restore $PVC (got: ${downloads:-none})" |
| 147 | + exit 1 |
| 148 | +} |
| 149 | + |
| 150 | +log_step "Waiting for the restored VMDisk to become Ready again..." |
| 151 | +# The HelmRelease was Ready before the controller suspended it, and that |
| 152 | +# condition outlives the restore; only once Flux has reconciled the restored |
| 153 | +# spec does Ready describe the restored release. |
| 154 | +hr_generation=$(kubectl -n "$NAMESPACE" get hr vm-disk-backup-src -o jsonpath='{.metadata.generation}') |
| 155 | +wait_for_field hr vm-disk-backup-src '{.status.observedGeneration}' "$hr_generation" "$NAMESPACE" 120 |
| 156 | +wait_hr_ready vm-disk-backup-src 180 |
| 157 | + |
| 158 | +kubectl get restorejob vmdisk-restore-in-place -n "$NAMESPACE" -o wide >&2 || true |
| 159 | +log_success "Restore completed; VMDisk 'backup-src' recreated from backup (${restore_label}, uid ${new_uid}), Bound and Ready" |
0 commit comments