Skip to content

Commit 5df72db

Browse files
committed
Add SHA256 checksum verification and robust URL validation for image downloads
1 parent 4a4994c commit 5df72db

File tree

2 files changed

+264
-12
lines changed

2 files changed

+264
-12
lines changed

cmd/image/qcow2ova/get-image.go

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@
1515
package qcow2ova
1616

1717
import (
18+
"crypto/sha256"
19+
"encoding/hex"
1820
"fmt"
1921
"io"
2022
"net/http"
@@ -30,8 +32,34 @@ const (
3032
DefaultGetTimeout = 30 * time.Minute
3133
)
3234

35+
// verifyCheckSum validates SHA256 of a downloaded file
36+
func verifyCheckSum(filePath, expected string) error {
37+
if expected == "" {
38+
klog.V(1).Infof("No checksum provided for %s, skipping verification", filePath)
39+
return nil
40+
}
41+
f, err := os.Open(filePath)
42+
if err != nil {
43+
return fmt.Errorf("failed to open file for checksum: %v", err)
44+
}
45+
defer f.Close()
46+
47+
h := sha256.New()
48+
if _, err := io.Copy(h, f); err != nil {
49+
return fmt.Errorf("failed to calculate checksum: %v", err)
50+
}
51+
52+
actual := hex.EncodeToString(h.Sum(nil))
53+
if actual != expected {
54+
return fmt.Errorf("checksum mismatch for %s:\n expected: %s\n actual: %s", filePath, expected, actual)
55+
}
56+
klog.V(1).Infof("Checksum verification PASSED FOR %s", filePath)
57+
return nil
58+
}
59+
3360
// Downloads or copy the image into the target dir mentioned
34-
func getImage(downloadDir string, srcUrl string, timeout time.Duration) (string, error) {
61+
// Added checksum verification (optional)
62+
func getImage(downloadDir string, srcUrl string, timeout time.Duration, expectedSha string) (string, error) {
3563
if timeout == 0 {
3664
timeout = DefaultGetTimeout
3765
}
@@ -71,6 +99,11 @@ func getImage(downloadDir string, srcUrl string, timeout time.Duration) (string,
7199
}
72100
klog.V(1).Info("Download Completed!")
73101
}
102+
// Verify checksum if provided
103+
if err := verifyCheckSum(dest, expectedSha); err != nil {
104+
return "", err
105+
}
106+
74107
return dest, nil
75108
}
76109

samples/convert-upload-images-powervs/convert-upload-images-powervs

Lines changed: 230 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,11 @@ limitations under the License.
1515
set -e
1616
#set -x
1717

18+
error() { echo "ERROR: $*" >&2; }
19+
warn() { echo "WARN: $*"; }
20+
success() { echo "SUCCESS: $*"; }
21+
log() { echo "LOG: $*"; }
22+
1823
source <(curl -L https://raw.githubusercontent.com/ocp-power-automation/openshift-install-power/92996305e1a8bef69fbe613b912d5561cc753172/openshift-install-powervs 2> /dev/null | sed 's/main "$@"//g')
1924

2025
function help {
@@ -36,7 +41,13 @@ Args:
3641
--cos-access-key string Cloud Storage access key(optional)
3742
--cos-secret-key string Cloud Storage secret key(optional)
3843
--skip-os-password Skip the root user password (optional)
44+
--rhel-sha256 string Expected SHA256 checksum for RHEL image(optional)
45+
--rhcos-sha256 string Expected SHA256 checksum for RHCOS image(optional)
3946
--help help for upload
47+
Environment Variables:
48+
DOWNLOAD_MAX_RETRIES Maximum number of retry attempts if a download fails or the checksum validation fails (default: 3)
49+
DOWNLOAD_RETRY_DELAY Delay between retries in seconds (default: 5)
50+
4051
4152
EOF
4253
exit 0
@@ -62,6 +73,11 @@ PVSADM_VERSION="v0.1.11"
6273
IMAGE_SIZE="11"
6374
TARGET_DISK_SIZE="120"
6475

76+
# Download retry configuration
77+
DOWNLOAD_MAX_RETRIES=${DOWNLOAD_MAX_RETRIES:-"3"}
78+
DOWNLOAD_RETRY_DELAY=${DOWNLOAD_RETRY_DELAY:-"5"}
79+
DOWNLOAD_TIMEOUT=300
80+
6581
# Default Centos image name
6682
CENTOS_VM_IMAGE_NAME='CentOS-Stream-8'
6783

@@ -612,16 +628,156 @@ function copy_image_file {
612628
}
613629

614630
function download_url() {
615-
local url=$1
631+
local url="$1"
632+
local expected_sha256="$2"
616633
local image_name=${url##*/}
617-
rm -rf $image_name
618-
retry "curl -fsSL $url -o ./$image_name"
619-
if [[ $? -eq 0 ]]; then
620-
#IMAGE_PATH=$(realpath ./$image_name)
621-
IMAGE_PATH=./$image_name
622-
DOWNLOAD_IMAGE_NAME=$image_name
634+
local retry_count=0
635+
local download_success=false
636+
637+
log "========================================="
638+
log "Starting download: $(basename "$image_name")"
639+
log "Source URL: $url"
640+
log "========================================="
641+
642+
# Validate URL before attempting download
643+
validate_url "$url"
644+
645+
# Remove any existing file
646+
rm -f "$image_name"
647+
648+
# Retry loop: focus purely on download
649+
while [ $retry_count -lt $DOWNLOAD_MAX_RETRIES ]; do
650+
if [ $retry_count -gt 0 ]; then
651+
warn "Retry attempt $retry_count of $DOWNLOAD_MAX_RETRIES"
652+
sleep $DOWNLOAD_RETRY_DELAY
653+
else
654+
log "Download attempt $((retry_count + 1)) of $DOWNLOAD_MAX_RETRIES"
655+
fi
656+
657+
log "Downloading $(basename "$image_name")..."
658+
if curl -fLSs --retry 2 --retry-delay 2 --connect-timeout 60 \
659+
--max-time $DOWNLOAD_TIMEOUT "$url" -o "./$image_name" 2>&1; then
660+
download_success=true
661+
break
662+
else
663+
local curl_exit=$?
664+
error "Download failed (curl exit code: $curl_exit)"
665+
case $curl_exit in
666+
1) error " Could not resolve host (DNS failure)" ;;
667+
2) error " Failed to connect to host" ;;
668+
3) error " Partial file transfer" ;;
669+
4) error " HTTP error (404/403/etc.)" ;;
670+
5) error " Operation timeout" ;;
671+
6) error " SSL connection error" ;;
672+
*) error " See curl manual for exit code $curl_exit" ;;
673+
esac
674+
rm -f "./$image_name"
675+
retry_count=$((retry_count + 1))
676+
fi
677+
done
678+
679+
# Verify file existence and content after all download attempts
680+
if [ ! -f "./$image_name" ] || [ ! -s "./$image_name" ]; then
681+
error "Downloaded file is missing or empty after $DOWNLOAD_MAX_RETRIES attempts."
682+
return 1
683+
fi
684+
685+
# Perform verification once, after successful download
686+
log "Download completed — running one-time verification checks..."
687+
688+
if ! verify_file_size "./$image_name" "$url"; then
689+
warn "File size verification failed; please verify manually."
690+
fi
691+
692+
if ! verify_sha256 "./$image_name" "$expected_sha256"; then
693+
error "Checksum verification failed; downloaded file may be corrupted."
694+
return 1
695+
fi
696+
697+
IMAGE_PATH="./$image_name"
698+
DOWNLOAD_IMAGE_NAME="$image_name"
699+
700+
success "========================================="
701+
success "✓ Download and verification completed successfully!"
702+
success " File: $(basename "$image_name")"
703+
success " Location: $IMAGE_PATH"
704+
success "========================================="
705+
return 0
706+
}
707+
708+
709+
# All retries failed
710+
if [ "$download_success" = false ]; then
711+
error "========================================="
712+
error "✗ Failed to download after $DOWNLOAD_MAX_RETRIES attempts"
713+
error "========================================="
714+
error "Troubleshooting steps:"
715+
error " 1. Check your internet connection"
716+
error " 2. Verify the URL is correct and accessible:"
717+
error " $url"
718+
error " 3. Ensure special characters in URL are properly escaped"
719+
error " 4. Check if the checksum value is correct"
720+
error " 5. Try downloading manually to diagnose:"
721+
error " curl -LO \"$url\""
722+
error " 6. Increase retry attempts: export DOWNLOAD_MAX_RETRIES=5"
723+
return 1
724+
fi
725+
}
726+
727+
#-------------------------------------------------------------------------
728+
# Verify file size matches expected size from HTTP headers
729+
#-------------------------------------------------------------------------
730+
function verify_file_size() {
731+
local file="$1"
732+
local url="$2"
733+
734+
log "Verifying file size for $(basename "$file")..."
735+
736+
# Get expected size from HTTP headers
737+
local expected_size=$(curl -sI "$url" | grep -i "^content-length:" | awk '{print $2}' | tr -d '\r\n')
738+
739+
if [ -z "$expected_size" ] || [ "$expected_size" = "0" ]; then
740+
warn "Unable to determine expected file size from server, skipping size verification"
741+
return 0
742+
fi
743+
744+
# Get actual file size
745+
local actual_size=$(stat -c%s "$file" 2>/dev/null || stat -f%z "$file" 2>/dev/null)
746+
747+
log "Expected size: $(numfmt --to=iec-i --suffix=B $expected_size 2>/dev/null || echo "$expected_size bytes")"
748+
log "Actual size: $(numfmt --to=iec-i --suffix=B $actual_size 2>/dev/null || echo "$actual_size bytes")"
749+
750+
# Allow 1% difference for potential metadata differences
751+
local size_diff=$((expected_size - actual_size))
752+
local size_diff_abs=${size_diff#-}
753+
local threshold=$((expected_size / 100))
754+
755+
if [ "$size_diff_abs" -le "$threshold" ]; then
756+
success "✓ File size verification PASSED"
757+
return 0
623758
else
624-
error "Unable to fetch the url"
759+
error "✗ File size verification FAILED (difference: $size_diff_abs bytes)"
760+
return 1
761+
fi
762+
}
763+
764+
#-------------------------------------------------------------------------
765+
# Validate URL for common issues
766+
#-------------------------------------------------------------------------
767+
function validate_url() {
768+
local url="$1"
769+
770+
# Check for unescaped ampersands
771+
if [[ "$url" =~ [^\\]\&[^\ ] ]]; then
772+
warn "⚠ Warning: URL contains unescaped & characters"
773+
warn " This may cause download issues. Consider escaping with \\& or using quotes"
774+
warn " URL: $url"
775+
fi
776+
777+
# Check if URL is accessible
778+
if ! curl -sf --head "$url" >/dev/null 2>&1; then
779+
warn "⚠ Warning: Unable to verify URL accessibility"
780+
warn " This might indicate network issues or incorrect URL"
625781
fi
626782
}
627783

@@ -632,15 +788,15 @@ function download_image {
632788
if [[ "$1" == "rhel" ]];then
633789
if echo $RHEL_URL | grep -q -i 'access.cdn.redhat.com' ; then
634790
log "downloading rhel image"
635-
download_url $RHEL_URL
791+
download_url "$RHEL_URL" "$RHEL_SHA256"
636792
RHEL_IMAGE=$IMAGE_PATH
637793
RHEL_DOWNLOADED_IMAGE_NAME=$DOWNLOAD_IMAGE_NAME
638794
RHEL_NEW_IMAGE_PATH=$IMAGE_NEW_PATH
639795
COPY_RHEL_IMAGE=1
640796
fi
641797
elif [[ "$1" == "rhcos" ]];then
642-
download_url $RHCOS_URL
643-
RHCOS_IMAGE=IMAGE_PATH
798+
download_url "$RHCOS_URL" "$RHCOS_SHA256"
799+
RHCOS_IMAGE=$IMAGE_PATH
644800
RHCOS_DOWNLOAD_IMAGE_NAME=$DOWNLOAD_IMAGE_NAME
645801
copy_image_file $RHCOS_IMAGE $RHCOS_OBJECT_NAME
646802
RHCOS_NEW_IMAGE_PATH=$IMAGE_NEW_PATH
@@ -649,6 +805,61 @@ function download_image {
649805
warn "Unknown image"
650806
fi
651807
}
808+
function calc_sha256() {
809+
local f="$1"
810+
if command -v sha256sum >/dev/null 2>&1; then
811+
sha256sum "$f" | awk '{print $1}'
812+
elif command -v shasum >/dev/null 2>&1; then
813+
shasum -a 256 "$f" | awk '{print $1}'
814+
elif command -v openssl >/dev/null 2>&1; then
815+
openssl dgst -sha256 "$f" | awk '{print $NF}'
816+
else
817+
error "No SHA-256 tool available (need sha256sum, shasum, or openssl)"
818+
fi
819+
}
820+
821+
822+
function verify_sha256() {
823+
local f="$1"
824+
local expected="$2"
825+
826+
if [ -z "$expected" ]; then
827+
warn "No checksum provided for $(basename "$f"), skipping verification"
828+
return 0
829+
fi
830+
831+
log "Verifying SHA256 checksum for $(basename "$f")..."
832+
833+
local actual
834+
actual="$(calc_sha256 "$f")"
835+
836+
if [ -z "$actual" ]; then
837+
error "Failed to calculate checksum for $f"
838+
return 1
839+
fi
840+
841+
local actual_lc=$(echo "$actual" | tr '[:upper:]' '[:lower:]')
842+
local expected_lc=$(echo "$expected" | tr '[:upper:]' '[:lower:]')
843+
844+
log "Expected: $expected_lc"
845+
log "Actual: $actual_lc"
846+
847+
if [[ "$actual_lc" != "$expected_lc" ]]; then
848+
error "SHA-256 checksum mismatch for $(basename "$f")"
849+
error " Expected: $expected_lc"
850+
error " Actual: $actual_lc"
851+
error "Possible causes:"
852+
error " - Incomplete download (network interruption)"
853+
error " - Corrupted file during transfer"
854+
error " - Incorrect URL (check for unescaped special characters like &)"
855+
error " - Wrong checksum value provided"
856+
return 1
857+
fi
858+
859+
success "✓ Checksum verification PASSED for $(basename "$f")"
860+
return 0
861+
}
862+
652863

653864
function main {
654865
mkdir -p ./logs
@@ -702,6 +913,14 @@ function main {
702913
"--skip-os-password")
703914
SKIP_OS_PASSWORD="--skip-os-password"
704915
;;
916+
"--rhel-sha256")
917+
shift
918+
RHEL_SHA256="$1"
919+
;;
920+
"--rhcos-sha256")
921+
shift
922+
RHCOS_SHA256="$1"
923+
;;
705924
"--help")
706925
help
707926
;;

0 commit comments

Comments
 (0)