diff --git a/ramalama/cli.py b/ramalama/cli.py index e6afd28e..69f404d0 100644 --- a/ramalama/cli.py +++ b/ramalama/cli.py @@ -663,6 +663,8 @@ def _get_source(args): if smodel.type == "OCI": return src else: + if not smodel.exists(args): + return smodel.pull(args) return smodel.path(args) diff --git a/ramalama/common.py b/ramalama/common.py index 7f4f2e4e..fdfa1aad 100644 --- a/ramalama/common.py +++ b/ramalama/common.py @@ -1,19 +1,29 @@ """ramalama common module.""" - import glob import hashlib import os import random +import logging import shutil import string import subprocess +import time import sys import urllib.request import urllib.error +import ramalama.console as console + from ramalama.http_client import HttpClient + +logging.basicConfig( + level=logging.WARNING, + format="%(asctime)s - %(levelname)s - %(message)s" +) + MNT_DIR = "/mnt/models" MNT_FILE = f"{MNT_DIR}/model.file" +HTTP_RANGE_NOT_SATISFIABLE = 416 def container_manager(): @@ -165,25 +175,61 @@ def download_file(url, dest_path, headers=None, show_progress=True): headers (dict): Optional headers to include in the request. show_progress (bool): Whether to show a progress bar during download. - Returns: - None + Raises: + RuntimeError: If the download fails after multiple attempts. """ - http_client = HttpClient() - headers = headers or {} - # if we are not a tty, don't show progress, can pollute CI output and such + # If not running in a TTY, disable progress to prevent CI pollution if not sys.stdout.isatty(): show_progress = False - try: - http_client.init(url=url, headers=headers, output_file=dest_path, progress=show_progress) - except urllib.error.HTTPError as e: - if e.code == 416: # Range not satisfiable - if show_progress: - print(f"File {url} already fully downloaded.") - else: - raise e + http_client = HttpClient() + max_retries = 5 # Stop after 5 failures + retries = 0 + + while retries < max_retries: + try: + # Initialize HTTP client for the request + http_client.init(url=url, headers=headers, output_file=dest_path, progress=show_progress) + return # Exit function if successful + + except urllib.error.HTTPError as e: + if e.code == HTTP_RANGE_NOT_SATISFIABLE: # "Range Not Satisfiable" error (file already downloaded) + return # No need to retry + + except urllib.error.URLError as e: + console.error(f"Network Error: {e.reason}") + retries += 1 + + except TimeoutError: + retries += 1 + console.warning(f"TimeoutError: The server took too long to respond. Retrying {retries}/{max_retries}...") + + except RuntimeError as e: # Catch network-related errors from HttpClient + retries += 1 + console.warning(f"{e}. Retrying {retries}/{max_retries}...") + + except IOError as e: + retries += 1 + console.warning(f"I/O Error: {e}. Retrying {retries}/{max_retries}...") + + except Exception as e: + console.error(f"Unexpected error: {str(e)}") + raise + + if retries >= max_retries: + error_message = ( + "\nDownload failed after multiple attempts.\n" + "Possible causes:\n" + "- Internet connection issue\n" + "- Server is down or unresponsive\n" + "- Firewall or proxy blocking the request\n" + ) + console.error(error_message) + sys.exit(1) + + time.sleep(2 ** retries * 0.1) # Exponential backoff (0.1s, 0.2s, 0.4s...) def engine_version(engine): diff --git a/ramalama/console.py b/ramalama/console.py new file mode 100644 index 00000000..f7980c7a --- /dev/null +++ b/ramalama/console.py @@ -0,0 +1,35 @@ +import os +import sys +import locale +import logging + +def is_locale_utf8(): + """Check if the system locale is UTF-8.""" + return 'UTF-8' in os.getenv('LC_CTYPE', '') or 'UTF-8' in os.getenv('LANG', '') + +def supports_emoji(): + """Detect if the terminal supports emoji output.""" + term = os.getenv("TERM") + if not term or term in ("dumb", "linux"): + return False + + return is_locale_utf8() + +# Allow users to override emoji support via an environment variable +# If RAMALAMA_FORCE_EMOJI is not set, it defaults to checking supports_emoji() +RAMALAMA_FORCE_EMOJI = os.getenv("RAMALAMA_FORCE_EMOJI") +FORCE_EMOJI = RAMALAMA_FORCE_EMOJI.lower() == "true" if RAMALAMA_FORCE_EMOJI else None +EMOJI = FORCE_EMOJI if FORCE_EMOJI is not None else supports_emoji() + +# Define emoji-aware logging messages +def error(msg): + formatted_msg = f"❌ {msg}" if EMOJI else f"[ERROR] {msg}" + logging.error(formatted_msg) + +def warning(msg): + formatted_msg = f"⚠️ {msg}" if EMOJI else f"[WARNING] {msg}" + logging.warning(formatted_msg) + +def info(msg): + formatted_msg = f"ℹ️ {msg}" if EMOJI else f"[INFO] {msg}" + logging.info(formatted_msg) diff --git a/ramalama/ollama.py b/ramalama/ollama.py index 32aa4565..4432cb4d 100644 --- a/ramalama/ollama.py +++ b/ramalama/ollama.py @@ -107,6 +107,8 @@ def pull(self, args): try: return init_pull(repos, accept, registry_head, model_name, model_tag, models, model_path, self.model) except urllib.error.HTTPError as e: + if "Not Found" in e.reason: + raise KeyError(f"{self.model} was not found in the Ollama registry") raise KeyError(f"failed to pull {registry_head}: " + str(e).strip("'")) def model_path(self, args): diff --git a/test/system/040-serve.bats b/test/system/040-serve.bats index f3680703..311e9c26 100755 --- a/test/system/040-serve.bats +++ b/test/system/040-serve.bats @@ -45,7 +45,7 @@ verify_begin=".*run --rm -i --label RAMALAMA --security-opt=label=disable --name fi run_ramalama 1 serve MODEL - is "$output" ".*Error: failed to pull .*MODEL" "failed to pull model" + is "$output" "Error: MODEL was not found in the Ollama registry" } @test "ramalama --detach serve" { diff --git a/test/system/050-pull.bats b/test/system/050-pull.bats index 30299780..8559f32c 100644 --- a/test/system/050-pull.bats +++ b/test/system/050-pull.bats @@ -26,7 +26,7 @@ load setup_suite random_image_name=i_$(safename) run_ramalama 1 pull ${random_image_name} - is "$output" "Error: failed to pull https://registry.ollama.ai/v2/library/${random_image_name}: HTTP Error 404: Not Found" "image does not exist" + is "$output" "Error: ${random_image_name} was not found in the Ollama registry" } # bats test_tags=distro-integration diff --git a/test/system/055-convert.bats b/test/system/055-convert.bats index c6afdcab..7a331b20 100644 --- a/test/system/055-convert.bats +++ b/test/system/055-convert.bats @@ -10,7 +10,7 @@ load helpers run_ramalama 2 convert tiny is "$output" ".*ramalama convert: error: the following arguments are required: TARGET" run_ramalama 1 convert bogus foobar - is "$output" "Error: bogus does not exist" + is "$output" "Error: bogus was not found in the Ollama registry" } @test "ramalama convert file to image" {