From 9df64f163c71bc11c88679ef9fb9f591376f08b5 Mon Sep 17 00:00:00 2001 From: Joachim Wiberg Date: Tue, 4 Nov 2025 15:07:20 +0100 Subject: [PATCH] utils: improve bump-gns3.py error handling and usage text Split download and MD5 computation into separate functions for clarity. Add proper error handling for HTTP 404 and network errors. Improve help text to mention appliances/ directory location. [skip ci] Signed-off-by: Joachim Wiberg --- utils/bump-gns3.py | 65 +++++++++++++++++++++++++++++++++++++++------- 1 file changed, 55 insertions(+), 10 deletions(-) diff --git a/utils/bump-gns3.py b/utils/bump-gns3.py index 72c02b20..5a9d4121 100755 --- a/utils/bump-gns3.py +++ b/utils/bump-gns3.py @@ -5,6 +5,7 @@ import json import re import shutil import sys +import urllib.error import urllib.request from datetime import datetime @@ -16,30 +17,63 @@ def check_version_format(ver): sys.exit("Error: version must be full form like 25.08.0") return ver -def compute_md5_and_size(url): - md5 = hashlib.md5() - size = 0 +def download_file(url): + """Download file from URL, yielding chunks. + + Args: + url: URL of the file to download + + Yields: + bytes: 1MB chunks of the file + + Raises: + urllib.error.HTTPError: If the URL returns an error (e.g., 404) + urllib.error.URLError: If there's a network error + """ with urllib.request.urlopen(url) as resp: while True: chunk = resp.read(1024 * 1024) if not chunk: break - md5.update(chunk) - size += len(chunk) + yield chunk + +def compute_md5_and_size(chunks): + """Compute MD5 hash and total size from data chunks. + + Args: + chunks: Iterable of byte chunks + + Returns: + tuple: (md5_hexdigest, size_in_bytes) + """ + md5 = hashlib.md5() + size = 0 + for chunk in chunks: + md5.update(chunk) + size += len(chunk) return md5.hexdigest(), size def main(): - parser = argparse.ArgumentParser(description="Add a new Infix version to a GNS3 appliance file") + parser = argparse.ArgumentParser( + description="Add a new Infix version to a GNS3 appliance file", + epilog="The .gns3a appliance files are typically found in the appliances/ " + "directory of the gns3-registry project." + ) parser.add_argument("version", help="Infix version (e.g. 25.08.0)") - parser.add_argument("appliance", help="Path to appliance JSON (.gns3a)") + parser.add_argument("appliance", help="Path to appliance JSON file (.gns3a)") args = parser.parse_args() version = check_version_format(args.version) appliance_path = args.appliance # Load JSON - with open(appliance_path, "r", encoding="utf-8") as f: - data = json.load(f) + try: + with open(appliance_path, "r", encoding="utf-8") as f: + data = json.load(f) + except FileNotFoundError: + sys.exit(f"Error: Appliance file not found: {appliance_path}") + except json.JSONDecodeError as e: + sys.exit(f"Error: Invalid JSON in appliance file: {e}") # Skip if version already exists for v in data.get("versions", []): @@ -52,7 +86,18 @@ def main(): url = f"{REPO}/releases/download/v{version}/{filename}" print(f"Downloading {url} to compute MD5 and size...") - md5sum, size = compute_md5_and_size(url) + try: + chunks = download_file(url) + md5sum, size = compute_md5_and_size(chunks) + except urllib.error.HTTPError as e: + if e.code == 404: + sys.exit(f"Error: Version {version} not found. " + f"The release v{version} does not exist or the disk image is not available.\n" + f"URL: {url}") + else: + sys.exit(f"Error: HTTP {e.code} when downloading: {e.reason}\nURL: {url}") + except urllib.error.URLError as e: + sys.exit(f"Error: Network error while downloading: {e.reason}\nURL: {url}") # Add image entry image_entry = {