Skip to content

NIM air-gap cache processing script

This page provides a standalone Python script to stage a local NIM cache and optionally upload model profiles to S3-compatible storage after you complete the Air-gap configuration guide.

#!/usr/bin/env python3
"""
Uploads a local NIM cache directly to S3/MinIO without modifying the filesystem.

## Steps {: #steps }
1. Walk the NIM cache, resolving symlinks to real file paths.
2. Compute the URL-encoded NIM key for each file.
3. Upload directly to S3/MinIO using the computed key.

No files are moved, copied, or deleted on disk.

## Arguments {: #arguments }
--cache-dir            Path to the local NIM cache.        (required)
--bucket-name          Destination bucket name.            (required)
--endpoint-url         S3/MinIO endpoint URL.              (optional, omit for AWS S3)
--access-key           S3/MinIO access key.                (optional, falls back to env/IAM)
--secret-key           S3/MinIO secret key.                (optional, falls back to env/IAM)
--region               AWS region. Defaults to us-east-1.  (optional)
--insecure             Skip SSL certificate verification.  (optional)
--dry-run              Print keys without uploading.       (optional)

## Usage examples {: #usage-examples }
python process_nim_cache.py \\
    --cache-dir /opt/nim/.cache \\
    --bucket-name nim-models \\
    --endpoint-url https://minio.internal:9000 \\
    --access-key miniorootuser \\
    --secret-key secret \\
    --insecure

python process_nim_cache.py \\
    --cache-dir /opt/nim/.cache \\
    --bucket-name nim-models \\
    --endpoint-url https://minio.internal:9000 \\
    --access-key miniorootuser \\
    --secret-key secret \\
    --dry-run
"""
import os, argparse, urllib3, urllib.parse
from pathlib import Path


def iter_keys(cache_dir):
    """Yield (real_path, s3_key) for every file in the NIM cache."""
    cache = Path(cache_dir).expanduser() / "ngc/hub"

    if not cache.exists():
        raise SystemExit(f"NIM cache not found at {cache}")

    for snap in cache.glob("models--*/snapshots/*"):
        model = snap.parents[1].name[8:].replace("--", "/")
        sid = snap.name

        for f in filter(Path.is_file, snap.rglob("*")):
            real = f.resolve()
            # Pre-encode "/" in subdirectory paths so NIM can find them.
            # NIM double-encodes nested paths: "dir/file" → "dir%2Ffile" → "dir%252Ffile"
            file_path = str(f.relative_to(snap)).replace("/", "%2F")
            key = urllib.parse.quote(f"{model}:{sid}?file={file_path}", safe="")
            yield real, key


def s3_client(endpoint_url, access_key, secret_key, region, verify_ssl=True):
    import boto3
    from botocore.config import Config

    return boto3.client(
        "s3",
        endpoint_url=endpoint_url,
        aws_access_key_id=access_key,
        aws_secret_access_key=secret_key,
        region_name=region,
        verify=verify_ssl,
        config=Config(
            signature_version="s3v4",
            s3={"addressing_style": "path"},
        ),
    )


def upload(cache_dir, bucket, endpoint_url, access_key, secret_key,
           region="us-east-1", verify_ssl=True, dry_run=False):
    pairs = list(iter_keys(cache_dir))
    total = len(pairs)
    print(f"{'[DRY RUN] ' if dry_run else ''}Uploading {total} files to bucket '{bucket}'")

    # Each (real_path, key) pair is a unique upload — even if two profiles share
    # the same blob (same real_path), they have different keys and both must exist in S3.
    # We only skip exact duplicates where both real_path AND key are identical.
    seen = set()
    s3 = None if dry_run else s3_client(
        endpoint_url, access_key, secret_key, region, verify_ssl
    )

    skipped = 0
    for i, (real, key) in enumerate(pairs, 1):
        if (real, key) in seen:
            print(f"  [{i}/{total}] SKIP (exact duplicate): {key}")
            skipped += 1
            continue

        seen.add((real, key))
        print(f"  [{i}/{total}] {key}")

        if not dry_run:
            s3.upload_file(str(real), bucket, key)

    uploaded = total - skipped
    print(f"\nDone. Uploaded: {uploaded}, Skipped (exact duplicates): {skipped}")


if __name__ == "__main__":
    urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)

    p = argparse.ArgumentParser()
    p.add_argument("--cache-dir", required=True, help="Path to the local NIM cache")
    p.add_argument("--bucket-name", required=True, help="Destination S3/MinIO bucket name")
    p.add_argument("--endpoint-url", default=None, help="S3/MinIO endpoint URL (optional, omit for AWS S3)")
    p.add_argument("--access-key", default=None, help="S3/MinIO access key (optional, falls back to env/IAM)")
    p.add_argument("--secret-key", default=None, help="S3/MinIO secret key (optional, falls back to env/IAM)")
    p.add_argument("--region", default="us-east-1", help="AWS region (default: us-east-1)")
    p.add_argument("--insecure", action="store_true", help="Skip SSL certificate verification")
    p.add_argument("--dry-run", action="store_true", help="Print keys without uploading anything")
    args = p.parse_args()

    upload(
        cache_dir=args.cache_dir,
        bucket=args.bucket_name,
        endpoint_url=args.endpoint_url,
        access_key=args.access_key,
        secret_key=args.secret_key,
        region=args.region,
        verify_ssl=not args.insecure,
        dry_run=args.dry_run,
    )