Skip to content

Commit 1a23254

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

File tree

2 files changed

+272
-12
lines changed

2 files changed

+272
-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: 238 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,164 @@ 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 -rf "$image_name"
647+
648+
# Retry loop
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+
# Download with timeout and progress
658+
log "Downloading $(basename "$image_name")..."
659+
if curl -fLSs --retry 2 --retry-delay 2 --connect-timeout 60 --max-time $DOWNLOAD_TIMEOUT "$url" -o "./$image_name" 2>&1; then
660+
661+
# Verify file exists and has content
662+
if [ ! -f "./$image_name" ] || [ ! -s "./$image_name" ]; then
663+
error "Downloaded file is missing or empty"
664+
retry_count=$((retry_count + 1))
665+
continue
666+
fi
667+
668+
log "Download completed, verifying integrity..."
669+
670+
# Verify file size
671+
if ! verify_file_size "./$image_name" "$url"; then
672+
warn "File size verification failed, retrying..."
673+
rm -f "./$image_name"
674+
retry_count=$((retry_count + 1))
675+
continue
676+
fi
677+
678+
# Verify SHA256 checksum
679+
if ! verify_sha256 "./$image_name" "$expected_sha256"; then
680+
error "Checksum verification failed, retrying..."
681+
rm -f "./$image_name"
682+
retry_count=$((retry_count + 1))
683+
continue
684+
fi
685+
686+
# All verifications passed
687+
IMAGE_PATH="./$image_name"
688+
DOWNLOAD_IMAGE_NAME="$image_name"
689+
download_success=true
690+
691+
success "========================================="
692+
success "✓ Download and verification completed successfully!"
693+
success " File: $(basename "$image_name")"
694+
success " Location: $IMAGE_PATH"
695+
success "========================================="
696+
return 0
697+
else
698+
local curl_exit=$?
699+
error "Download failed with curl error code: $curl_exit"
700+
701+
# Provide specific error messages for common curl exit codes
702+
case $curl_exit in
703+
1) error " Reason: Could not resolve host (DNS failure)" ;;
704+
2) error " Reason: Failed to connect to host" ;;
705+
3) error " Reason: Partial file transfer" ;;
706+
4) error " Reason: HTTP error (404, 403, etc.)" ;;
707+
5) error " Reason: Operation timeout" ;;
708+
6) error " Reason: SSL connection error" ;;
709+
*) error " Reason: See curl manual for error code $curl_exit" ;;
710+
esac
711+
712+
rm -f "./$image_name"
713+
retry_count=$((retry_count + 1))
714+
fi
715+
done
716+
717+
# All retries failed
718+
if [ "$download_success" = false ]; then
719+
error "========================================="
720+
error "✗ Failed to download after $DOWNLOAD_MAX_RETRIES attempts"
721+
error "========================================="
722+
error "Troubleshooting steps:"
723+
error " 1. Check your internet connection"
724+
error " 2. Verify the URL is correct and accessible:"
725+
error " $url"
726+
error " 3. Ensure special characters in URL are properly escaped"
727+
error " 4. Check if the checksum value is correct"
728+
error " 5. Try downloading manually to diagnose:"
729+
error " curl -LO \"$url\""
730+
error " 6. Increase retry attempts: export DOWNLOAD_MAX_RETRIES=5"
731+
return 1
732+
fi
733+
}
734+
735+
#-------------------------------------------------------------------------
736+
# Verify file size matches expected size from HTTP headers
737+
#-------------------------------------------------------------------------
738+
function verify_file_size() {
739+
local file="$1"
740+
local url="$2"
741+
742+
log "Verifying file size for $(basename "$file")..."
743+
744+
# Get expected size from HTTP headers
745+
local expected_size=$(curl -sI "$url" | grep -i "^content-length:" | awk '{print $2}' | tr -d '\r\n')
746+
747+
if [ -z "$expected_size" ] || [ "$expected_size" = "0" ]; then
748+
warn "Unable to determine expected file size from server, skipping size verification"
749+
return 0
750+
fi
751+
752+
# Get actual file size
753+
local actual_size=$(stat -c%s "$file" 2>/dev/null || stat -f%z "$file" 2>/dev/null)
754+
755+
log "Expected size: $(numfmt --to=iec-i --suffix=B $expected_size 2>/dev/null || echo "$expected_size bytes")"
756+
log "Actual size: $(numfmt --to=iec-i --suffix=B $actual_size 2>/dev/null || echo "$actual_size bytes")"
757+
758+
# Allow 1% difference for potential metadata differences
759+
local size_diff=$((expected_size - actual_size))
760+
local size_diff_abs=${size_diff#-}
761+
local threshold=$((expected_size / 100))
762+
763+
if [ "$size_diff_abs" -le "$threshold" ]; then
764+
success "✓ File size verification PASSED"
765+
return 0
623766
else
624-
error "Unable to fetch the url"
767+
error "✗ File size verification FAILED (difference: $size_diff_abs bytes)"
768+
return 1
769+
fi
770+
}
771+
772+
#-------------------------------------------------------------------------
773+
# Validate URL for common issues
774+
#-------------------------------------------------------------------------
775+
function validate_url() {
776+
local url="$1"
777+
778+
# Check for unescaped ampersands
779+
if [[ "$url" =~ [^\\]\&[^\ ] ]]; then
780+
warn "⚠ Warning: URL contains unescaped & characters"
781+
warn " This may cause download issues. Consider escaping with \\& or using quotes"
782+
warn " URL: $url"
783+
fi
784+
785+
# Check if URL is accessible
786+
if ! curl -sf --head "$url" >/dev/null 2>&1; then
787+
warn "⚠ Warning: Unable to verify URL accessibility"
788+
warn " This might indicate network issues or incorrect URL"
625789
fi
626790
}
627791

@@ -632,15 +796,15 @@ function download_image {
632796
if [[ "$1" == "rhel" ]];then
633797
if echo $RHEL_URL | grep -q -i 'access.cdn.redhat.com' ; then
634798
log "downloading rhel image"
635-
download_url $RHEL_URL
799+
download_url "$RHEL_URL" "$RHEL_SHA256"
636800
RHEL_IMAGE=$IMAGE_PATH
637801
RHEL_DOWNLOADED_IMAGE_NAME=$DOWNLOAD_IMAGE_NAME
638802
RHEL_NEW_IMAGE_PATH=$IMAGE_NEW_PATH
639803
COPY_RHEL_IMAGE=1
640804
fi
641805
elif [[ "$1" == "rhcos" ]];then
642-
download_url $RHCOS_URL
643-
RHCOS_IMAGE=IMAGE_PATH
806+
download_url "$RHCOS_URL" "$RHCOS_SHA256"
807+
RHCOS_IMAGE=$IMAGE_PATH
644808
RHCOS_DOWNLOAD_IMAGE_NAME=$DOWNLOAD_IMAGE_NAME
645809
copy_image_file $RHCOS_IMAGE $RHCOS_OBJECT_NAME
646810
RHCOS_NEW_IMAGE_PATH=$IMAGE_NEW_PATH
@@ -649,6 +813,61 @@ function download_image {
649813
warn "Unknown image"
650814
fi
651815
}
816+
function calc_sha256() {
817+
local f="$1"
818+
if command -v sha256sum >/dev/null 2>&1; then
819+
sha256sum "$f" | awk '{print $1}'
820+
elif command -v shasum >/dev/null 2>&1; then
821+
shasum -a 256 "$f" | awk '{print $1}'
822+
elif command -v openssl >/dev/null 2>&1; then
823+
openssl dgst -sha256 "$f" | awk '{print $NF}'
824+
else
825+
error "No SHA-256 tool available (need sha256sum, shasum, or openssl)"
826+
fi
827+
}
828+
829+
830+
function verify_sha256() {
831+
local f="$1"
832+
local expected="$2"
833+
834+
if [ -z "$expected" ]; then
835+
warn "No checksum provided for $(basename "$f"), skipping verification"
836+
return 0
837+
fi
838+
839+
log "Verifying SHA256 checksum for $(basename "$f")..."
840+
841+
local actual
842+
actual="$(calc_sha256 "$f")"
843+
844+
if [ -z "$actual" ]; then
845+
error "Failed to calculate checksum for $f"
846+
return 1
847+
fi
848+
849+
local actual_lc=$(echo "$actual" | tr '[:upper:]' '[:lower:]')
850+
local expected_lc=$(echo "$expected" | tr '[:upper:]' '[:lower:]')
851+
852+
log "Expected: $expected_lc"
853+
log "Actual: $actual_lc"
854+
855+
if [[ "$actual_lc" != "$expected_lc" ]]; then
856+
error "SHA-256 checksum mismatch for $(basename "$f")"
857+
error " Expected: $expected_lc"
858+
error " Actual: $actual_lc"
859+
error "Possible causes:"
860+
error " - Incomplete download (network interruption)"
861+
error " - Corrupted file during transfer"
862+
error " - Incorrect URL (check for unescaped special characters like &)"
863+
error " - Wrong checksum value provided"
864+
return 1
865+
fi
866+
867+
success "✓ Checksum verification PASSED for $(basename "$f")"
868+
return 0
869+
}
870+
652871

653872
function main {
654873
mkdir -p ./logs
@@ -702,6 +921,14 @@ function main {
702921
"--skip-os-password")
703922
SKIP_OS_PASSWORD="--skip-os-password"
704923
;;
924+
"--rhel-sha256")
925+
shift
926+
RHEL_SHA256="$1"
927+
;;
928+
"--rhcos-sha256")
929+
shift
930+
RHCOS_SHA256="$1"
931+
;;
705932
"--help")
706933
help
707934
;;

0 commit comments

Comments
 (0)