Skip to content

_ovh

sc_crawler.vendors._ovh #

Functions:

Name Description
inventory_compliance_frameworks

Manual list of known compliance frameworks on OVHcloud.

inventory_regions

List all available OVHcloud Public Cloud regions.

inventory_zones

List all availability zones.

inventory_servers

List all server types (called "flavors" at OVHcloud).

inventory_server_prices

Fetch server pricing and regional availability.

inventory_server_prices_spot

There are no spot instances in OVHcloud Public Cloud.

inventory_storages

List all block storage offerings.

inventory_storage_prices

Extract storage prices from OVHCloud catalog.

inventory_traffic_prices

OVHcloud Public Cloud bandwidth pricing.

inventory_ipv4_prices

OVHcloud Public Cloud IPv4 pricing.

inventory_databases

List Public Cloud Databases for PostgreSQL compute SKUs.

inventory_database_prices

Fetch Public Cloud Databases for PostgreSQL on-demand compute prices.

inventory_database_storages

List extra disk for Public Cloud PostgreSQL.

inventory_database_storage_prices

Fetch extra PostgreSQL disk prices (GB-month) from the public catalog.

inventory_compliance_frameworks #

inventory_compliance_frameworks(vendor)

Manual list of known compliance frameworks on OVHcloud.

Data sources:

Source code in sc_crawler/vendors/_ovh.py
def inventory_compliance_frameworks(vendor):
    """Manual list of known compliance frameworks on OVHcloud.

    Data sources:

    - General information and list of supported compliance programs: <https://www.ovhcloud.com/en/compliance/>
    - ISO/IEC 27001/27017/27018: <https://www.ovhcloud.com/en/compliance/iso-27001-27017-27018/>
    - SOC 1, SOC 2, SOC 3 with SOC 2 Type 2 details: <https://www.ovhcloud.com/en/compliance/soc-1-2-3/>
    """
    # Additional OVHcloud compliance frameworks you may want to support later
    # (not yet present in lookup.py — listed here for reference only):
    #   - ISO/IEC 27017 (cloud security controls)
    #       URL: https://www.ovhcloud.com/en/compliance/iso-27001-27017-27018/
    #       Suggested ID: "iso27017"
    #   - ISO/IEC 27018 (protection of PII in public cloud)
    #       URL: https://www.ovhcloud.com/en/compliance/iso-27001-27017-27018/
    #       Suggested ID: "iso27018"
    #   - SOC 1 (SSAE18 Type 2)
    #       URL: https://www.ovhcloud.com/en/compliance/soc-1-2-3/
    #       Suggested ID: "soc1t2"
    #   - SOC 3 (general report)
    #       URL: https://www.ovhcloud.com/en/compliance/soc-1-2-3/
    #       Suggested ID: "soc3"
    #   - PCI DSS (payment card industry data security standard)
    #       URL: https://www.ovhcloud.com/en/compliance/pci-dss/
    #       Suggested ID: "pci-dss"
    #   - HDS (Hébergement de Données de Santé / Healthcare data hosting compliance)
    #       URL: https://www.ovhcloud.com/en/compliance/hds/
    #       Suggested ID: "hds"
    #   - ANSSI SecNumCloud (France national cloud security label)
    #       URL: https://www.ovhcloud.com/en/compliance/secnumcloud/
    #       Suggested ID: "secnumcloud"
    #   - ISO 14001 (environmental management systems — non-security but published by OVHcloud)
    #       URL: https://www.ovhcloud.com/en/compliance/iso-14001/
    #       Suggested ID: "iso14001"
    return map_compliance_frameworks_to_vendor(vendor.vendor_id, ["iso27001", "soc2t2"])

inventory_regions #

inventory_regions(vendor)

List all available OVHcloud Public Cloud regions.

Data sources:

Source code in sc_crawler/vendors/_ovh.py
def inventory_regions(vendor) -> list[dict]:
    """List all available OVHcloud Public Cloud regions.

    Data sources:

    - <https://www.ovhcloud.com/en/public-cloud/regions-availability/>
    - <https://www.ovhcloud.com/en/about-us/global-infrastructure/expansion-regions-az/>
    - <https://vms.status-ovhcloud.com/>
    - Google Maps search for the datacenter location or city-level coordinates
    """
    items = []
    regions = _get_regions()
    datacenters = {
        # Europe (EMEA)
        "SBG": {
            "country_id": "FR",
            "city": "Strasbourg",
            "address_line": "9 Rue du Bass. de l'Industrie",
            "zip_code": "67000",
            "lat": 48.5854388,
            "lon": 7.7974307,
        },
        "GRA": {
            "country_id": "FR",
            "city": "Gravelines",
            "address_line": "1 Rte de la Frm Masson",
            "zip_code": "59820",
            "lat": 51.0166852,
            "lon": 2.1551437,
        },
        "RBX": {
            "country_id": "FR",
            "city": "Roubaix",
            "address_line": "2 Rue Kellermann",
            "zip_code": "59100",
            "lat": 50.691834,
            "lon": 3.2003148,
        },
        "PAR": {
            "country_id": "FR",
            "city": "Paris",
            "address_line": "12 Rue Riquet",
            "zip_code": "75019",
            "lat": 48.8885363,
            "lon": 2.3755977,
        },
        "UK": {
            "country_id": "GB",
            "city": "London",
            "address_line": "8 Viking Way",
            "zip_code": "DA8 1EW",
            "lat": 51.4915264,
            "lon": 0.1668186,
        },
        "DE": {
            "country_id": "DE",
            "city": "Frankfurt",
            # city-level coordinates from Google Maps
            "lat": 50.1109221,
            "lon": 8.6821267,
        },
        "WAW": {
            "country_id": "PL",
            "city": "Warsaw",
            "address_line": "Kazimierza Kamińskiego 6",
            "zip_code": "05-850",
            "lat": 52.2077264,
            "lon": 20.8080621,
        },
        "MIL": {
            "country_id": "IT",
            "city": "Milan",
            # OVH office in Milan, Italy
            "lat": 45.4992183,
            "lon": 9.1832528,
        },
        # North America
        "BHS": {
            "country_id": "CA",
            "city": "Beauharnois",
            "address_line": "50 Rue de l'Aluminerie",
            "state": "Quebec",
            "zip_code": "J6N 0C2",
            "lat": 45.3093037,
            "lon": -73.8965535,
        },
        "TOR": {
            "country_id": "CA",
            "city": "Toronto",
            "address_line": "17 Vondrau Dr",
            "state": "Ontario",
            "zip_code": "N3E 1B8",
            "lat": 43.4273216,
            "lon": -80.3726843,
        },
        "HIL": {
            "country_id": "US",
            "city": "Hillsboro",
            "state": "Oregon",
            # city-level coordinates from Google Maps
            "lat": 45.520137,
            "lon": -122.9898308,
        },
        "VIN": {
            "country_id": "US",
            "city": "Vint Hill",
            "address_line": "6872 Watson Ct",
            "state": "North Virginia",
            "zip_code": "20187",
            "lat": 38.7474561,
            "lon": -77.6744531,
        },
        # Asia-Pacific
        "SGP": {
            "country_id": "SG",
            "city": "Singapore",
            "address_line": "1 Paya Lebar Link",
            "zip_code": "408533",
            "lat": 1.3177101,
            "lon": 103.893902,
        },
        "SYD": {
            "country_id": "AU",
            "city": "Sydney",
            # city-level coordinates from Google Maps
            "lat": -33.8727409,
            "lon": 151.2057136,
        },
        "YNM": {
            "country_id": "IN",
            "city": "Mumbai",
            # city-level coordinates from Google Maps
            "lat": 19.0824822,
            "lon": 72.7141328,
        },
    }

    vendor.progress_tracker.start_task(name="Fetching regions", total=len(regions))
    for region in regions:
        with sentry_capture_or_raise(vendor=vendor):
            datacenter = _get_region(region)["datacenterLocation"]
            lookup = datacenters[datacenter]
            items.append(
                {
                    "vendor_id": vendor.vendor_id,
                    "region_id": region,
                    "name": region,
                    "api_reference": region,
                    "display_name": f"{region} ({lookup['country_id']})",
                    "aliases": [],
                    "country_id": lookup["country_id"],
                    "state": lookup.get("state"),
                    "city": lookup.get("city"),
                    "address_line": lookup.get("address_line"),
                    "zip_code": lookup.get("zip_code"),
                    "lon": lookup.get("lon"),
                    "lat": lookup.get("lat"),
                    # OVHcloud region page confirms Frankfurt (Limburg), Warsaw (Ożarów), London (Erith)
                    # as specific datacenter locations opened in 2016
                    "founding_year": 2016
                    if datacenter in ["DE", "WAW", "UK"]
                    else None,
                    "green_energy": None,
                }
            )
        vendor.progress_tracker.advance_task()
    vendor.progress_tracker.hide_task()

    return items

inventory_zones #

inventory_zones(vendor)

List all availability zones.

Data sources:

Source code in sc_crawler/vendors/_ovh.py
def inventory_zones(vendor) -> list[dict]:
    """List all availability zones.

    Data sources:

    - `/cloud/project/{serviceName}/region/{regionName}` API endpoint provides AZs for 3AZ regions
    - 1AZ zones have a standard "a" suffix as per <https://www.ovhcloud.com/en/about-us/global-infrastructure/expansion-regions-az/>
    """

    items = []
    regions = _get_regions()
    vendor.progress_tracker.start_task(name="Fetching zones", total=len(regions))
    for region in regions:
        zones = _get_region(region, _get_project_id())["availabilityZones"]
        if not zones:
            # single zone regions have a standard "a" suffix
            zones = [region.lower() + "-a"]
        for zone in zones:
            items.append(
                {
                    "vendor_id": vendor.vendor_id,
                    "region_id": region,
                    "zone_id": zone,
                    "name": zone,
                    "api_reference": zone,
                    "display_name": zone,
                }
            )
        vendor.progress_tracker.advance_task()
    vendor.progress_tracker.hide_task()
    return items

inventory_servers #

inventory_servers(vendor)

List all server types (called "flavors" at OVHcloud).

Lifecycle (catalog plan blobs.tags): active tag -> ACTIVE, otherwise INACTIVE. The legacy tag is not treated as retirement.

Source code in sc_crawler/vendors/_ovh.py
def inventory_servers(vendor) -> list[dict]:
    """List all server types (called "flavors" at OVHcloud).

    Lifecycle (catalog plan `blobs.tags`): `active` tag -> ACTIVE, otherwise
    INACTIVE. The `legacy` tag is not treated as retirement.
    """
    items = []

    server_plans = [
        s
        for s in _get_catalog()["addons"]
        if (
            s.get("product") == "publiccloud-instance"
            and s.get("blobs")
            and s["blobs"].get("technical")
            # TODO list Windows machines later
            and s["blobs"].get("technical").get("os", {}).get("family") == "linux"
            # filter for hourly rates for now
            and s.get("planCode", "").endswith(".consumption")
        )
    ]
    # dedupe just in case
    servers = {}
    for server_plan in server_plans:
        servers[server_plan["invoiceName"]] = server_plan

    for server_id, server in servers.items():
        blobs = server.get("blobs", {})
        if not blobs:
            continue

        commercial = blobs.get("commercial", {})
        technical = blobs.get("technical", {})
        name = commercial.get("name", server_id)
        family = _get_server_family(server_id)

        # all resources are dedicated expect for the Discovery series
        vcpus = technical.get("cpu", {}).get("cores", 0)
        cpu_allocation = (
            CpuAllocation.SHARED
            if commercial.get("brickSubtype") == "discovery"
            else CpuAllocation.DEDICATED
        )

        memory = technical.get("memory", {})
        memory_size_gb = memory.get("size", None)
        memory_size = memory_size_gb * _MIB_PER_GIB if memory_size_gb else None

        _gpu_count, _gpu_memory_total, gpu_manufacturer, gpu_family, _gpu_model = (
            _get_gpu_info(server_id)
        )
        gpu = technical.get("gpu", {})
        gpu_count = _gpu_count or gpu.get("number", 0)
        gpu_memory_per_gpu = (
            gpu.get("memory").get("size", 0) * _MIB_PER_GIB if gpu.get("memory") else 0
        )
        gpu_memory_total = _gpu_memory_total or (
            gpu_memory_per_gpu * gpu_count if gpu_memory_per_gpu and gpu_count else 0
        )
        gpu_model = _gpu_model or gpu.get("model", None)

        storages: List[Disk] = []
        for disk in technical.get("nvme", {}).get("disks", []):
            capacity = disk.get("capacity", 0)
            if not capacity:
                continue
            for _ in range(disk.get("number", 1)):
                storages.append(
                    Disk(
                        size=capacity,
                        storage_type=StorageType.NVME_SSD,
                    )
                )
        for disk in technical.get("storage", {}).get("disks", []):
            capacity = disk.get("capacity", 0)
            if not capacity:
                continue
            # no number field here
            is_nvme = (
                "nvme"
                in disk.get("technology", "").lower()
                + disk.get("interface", "").lower()
            )
            storage_type = StorageType.NVME_SSD if is_nvme else StorageType.SSD
            storages.append(
                Disk(
                    size=capacity,
                    storage_type=storage_type,
                )
            )
        storage_type = storages[0].storage_type if storages else None
        storage_size = sum(disk.size for disk in storages)
        nvme_size = sum(
            disk.size for disk in storages if disk.storage_type == StorageType.NVME_SSD
        )
        ssd_size = sum(
            disk.size for disk in storages if disk.storage_type == StorageType.SSD
        )

        description_parts = [
            f"{vcpus} vCPUs",
            f"{memory_size_gb} GiB RAM",
            f"{nvme_size} GB NVMe" if nvme_size else None,
            f"{ssd_size} GB SSD" if ssd_size else None,
            (
                f"{gpu_count}x{gpu_model} {int(gpu_memory_per_gpu / _MIB_PER_GIB)} GiB VRAM"
                if gpu_count and gpu_model
                else None
            ),
        ]
        description_parts_str = ", ".join(filter(None, description_parts))
        description = (
            f"{family} ({description_parts_str})" if family else description_parts_str
        )

        network_speed = technical.get("bandwidth", {}).get("level", None)
        network_speed_gbps = network_speed / 1000 if network_speed else None
        network_speed_max = technical.get("vrack", {}).get("level", None)
        network_speed_max_gbps = network_speed_max / 1000 if network_speed_max else None

        items.append(
            {
                "vendor_id": vendor.vendor_id,
                "server_id": server_id,
                "name": server_id,
                "api_reference": server_id,
                "display_name": name,
                "description": description,
                "family": family,
                "vcpus": vcpus,
                # as per OVH FAQ: https://www.ovhcloud.com/en/public-cloud/virtual-instances/
                # alsoverified from lscpu on B3-8 instance (2025-11-17)
                "hypervisor": "KVM",
                "cpu_allocation": cpu_allocation,
                "cpu_cores": None,
                "cpu_speed": technical.get("cpu", {}).get("frequency"),
                "cpu_architecture": CpuArchitecture.X86_64,  # All OVHcloud instances use x86_64
                "cpu_manufacturer": None,
                "cpu_family": None,
                "cpu_model": None,
                "cpu_flags": [],
                "cpus": [],
                "memory_amount": memory_size,
                "memory_generation": None,
                "memory_speed": None,
                "memory_ecc": None,
                "gpu_count": gpu_count,
                "gpu_memory_min": gpu_memory_per_gpu,
                "gpu_memory_total": gpu_memory_total,
                "gpu_manufacturer": gpu_manufacturer,
                "gpu_family": gpu_family,
                "gpu_model": gpu_model,
                "gpus": [],  # TODO fill this array
                "storage_size": storage_size,
                "storage_type": storage_type,
                "storages": storages,
                "network_speed_baseline": network_speed_gbps,
                "network_speed_max": network_speed_max_gbps,
                # TODO: have to implement manual mapping for network_storage_speed related fields
                "network_storage_speed_baseline": None,
                "network_storage_speed_max": None,
                # no bundled free traffic as all traffic is unmetered
                # https://www.ovhcloud.com/en-ie/public-cloud/prices/
                "inbound_traffic": 0,
                "outbound_traffic": 0,
                "ipv4": 1,  # each instance gets one IPv4
                "status": (
                    Status.ACTIVE
                    if "active" in (blobs.get("tags") or [])
                    else Status.INACTIVE
                ),
            }
        )

    return items

inventory_server_prices #

inventory_server_prices(vendor)

Fetch server pricing and regional availability.

Region availability information is fetched from the /cloud/project/{serviceName}/flavor API endpoint, then the related prices are fetched from the /order/catalog/public/cloud API endpoint.

Source code in sc_crawler/vendors/_ovh.py
def inventory_server_prices(vendor) -> list[dict]:
    """Fetch server pricing and regional availability.

    Region availability information is fetched from the
    `/cloud/project/{serviceName}/flavor` API endpoint, then the related prices
    are fetched from the `/order/catalog/public/cloud` API endpoint.
    """
    regions = scmodels_to_dict(vendor.regions, keys=["api_reference"])
    # list all addon prices and convert to a lookup dict
    catalog = _get_catalog()
    addons = {addon["planCode"]: addon for addon in catalog["addons"]}
    # list all server <> region offers with a link to the price list
    offers = _client().get(f"/cloud/project/{_get_project_id()}/flavor")
    offers = [o for o in offers if o["osType"] == "linux"]
    items = []
    vendor.progress_tracker.start_task(name="Fetching server offers", total=len(offers))
    for offer in offers:
        region = regions.get(offer["region"])
        addon = addons[offer["planCodes"]["hourly"]]
        if region is None:
            vendor.log(
                f"Excluding offer for {addon['invoiceName']} from unknown region: {offer['region']}"
            )
            continue
        # TODO check if server id is known for this vendor?
        for zone in region.zones:
            if zone.status != Status.ACTIVE:
                continue
            items.append(
                {
                    "vendor_id": vendor.vendor_id,
                    "region_id": region.region_id,
                    "zone_id": zone.zone_id,
                    "server_id": addon["invoiceName"],
                    "operating_system": addon["blobs"]["technical"]["os"]["family"],
                    "allocation": Allocation.ONDEMAND,
                    # we already filtered for hourly plan
                    "unit": PriceUnit.HOUR,
                    "price": (
                        addon["pricings"][0]["price"] / _MICROCENTS_PER_CURRENCY_UNIT
                    ),
                    "price_upfront": 0,
                    "price_tiered": [],
                    "currency": catalog["locale"]["currencyCode"],
                    "status": Status.ACTIVE,
                }
            )
        vendor.progress_tracker.advance_task()
    vendor.progress_tracker.hide_task()
    return items

inventory_server_prices_spot #

inventory_server_prices_spot(vendor)

There are no spot instances in OVHcloud Public Cloud.

Source code in sc_crawler/vendors/_ovh.py
def inventory_server_prices_spot(vendor) -> list[dict]:
    """There are no spot instances in OVHcloud Public Cloud."""
    return []

inventory_storages #

inventory_storages(vendor)

List all block storage offerings.

Data sources:

Sizes are in GB (decimal) and throughput values are in MB/s as per the documentation. Maximum volume size is 12 TB (12,000 GB) as stated on the product page.

Source code in sc_crawler/vendors/_ovh.py
def inventory_storages(vendor) -> list[dict]:
    """List all block storage offerings.

    Data sources:

    - <https://www.ovhcloud.com/en-ie/public-cloud/block-storage/>
    - <https://www.ovhcloud.com/en-ie/public-cloud/prices/>
    - API endpoint: `/order/catalog/public/cloud` with `publiccloud-volume-classic` product name

    Sizes are in GB (decimal) and throughput values are in MB/s as per the documentation.
    Maximum volume size is 12 TB (12,000 GB) as stated on the product page.
    """
    items = [
        {
            "storage_id": "classic",
            "vendor_id": vendor.vendor_id,
            "name": "Classic Volume",
            # quote from homepage
            "description": "Perfect for the daily application needs of databases, virtual machines, and backups.",
            "storage_type": StorageType.NETWORK,
            "max_iops": 500,
            "max_throughput": 64,
            "min_size": 10,
            "max_size": 12_000,
        },
        {
            "storage_id": "high-speed",
            "vendor_id": vendor.vendor_id,
            "name": "High Speed Volume Gen 1",
            # quote from homepage
            "description": "Offers optimised and scalable performance, and is recommended for intensive workloads.",
            "storage_type": StorageType.NETWORK,
            "max_iops": 3_000,
            "max_throughput": 128,
            "min_size": 10,
            "max_size": 12_000,
        },
        {
            "storage_id": "high-speed-gen2",
            "vendor_id": vendor.vendor_id,
            "name": "High Speed Volume Gen 2",
            # quote from homepage
            "description": "Offers optimised and scalable performance, and is recommended for intensive workloads.",
            "storage_type": StorageType.NETWORK,
            "max_iops": 20_000,
            "max_throughput": 320,
            "min_size": 10,
            "max_size": 12_000,
        },
    ]
    return items

inventory_storage_prices #

inventory_storage_prices(vendor)

Extract storage prices from OVHCloud catalog.

The catalog does not provide a detailed price list, only differentiates the prices of the known 3 storage types between regions with a single or three zones, so we assume all storage types are available in all regions.

Source code in sc_crawler/vendors/_ovh.py
def inventory_storage_prices(vendor) -> list[dict]:
    """Extract storage prices from OVHCloud catalog.

    The catalog does not provide a detailed price list, only differentiates the
    prices of the known 3 storage types between regions with a single or three
    zones, so we assume all storage types are available in all regions.
    """
    catalog = _get_catalog()
    addons = {addon["planCode"]: addon for addon in catalog["addons"]}

    items = []
    for storage in vendor.storages:
        for region in vendor.regions:
            addon_name = f"volume.{storage.storage_id}.consumption"
            if len(region.zones) > 1:
                addon_name += ".3AZ"
            addon = addons[addon_name]
            price = addon["pricings"][0]["price"] / _MICROCENTS_PER_CURRENCY_UNIT
            items.append(
                {
                    "vendor_id": vendor.vendor_id,
                    "region_id": region.region_id,
                    "storage_id": storage.storage_id,
                    "unit": PriceUnit.GB_MONTH,
                    "price": price * _HOURS_PER_MONTH,
                    "currency": catalog["locale"]["currencyCode"],
                }
            )
    return items

inventory_traffic_prices #

inventory_traffic_prices(vendor)

OVHcloud Public Cloud bandwidth pricing.

As per https://www.ovhcloud.com/en/public-cloud/prices:

Outbound public network traffic is included in the price of instances on all locations, except the Asia-Pacific region (Singapore, Sydney and Mumbai). In the tree regions, 1 TB/month of outbound public traffic is included for each Public Cloud project. Beyond this quota, each additional GB of traffic is charged. Inbound network traffic from the public network is included in all cases and in all regions.

The overage traffic priced at €0.01 ex. VAT/GB (see on the homepage after selecting one of the Asia-Pacific regions).

Source code in sc_crawler/vendors/_ovh.py
def inventory_traffic_prices(vendor) -> list[dict]:
    """OVHcloud Public Cloud bandwidth pricing.

    As per <https://www.ovhcloud.com/en/public-cloud/prices>:

    > Outbound public network traffic is included in the price of instances on
    > all locations, except the Asia-Pacific region (Singapore, Sydney and
    > Mumbai). In the tree regions, 1 TB/month of outbound public traffic is
    > included for each Public Cloud project. Beyond this quota, each additional
    > GB of traffic is charged. Inbound network traffic from the public network
    > is included in all cases and in all regions.

    The overage traffic priced at €0.01 ex. VAT/GB (see on the homepage after
    selecting one of the Asia-Pacific regions).
    """
    # Outbound traffic pricing for Asia-Pacific regions (Singapore, Sydney, Mumbai)
    # Tier 1: 1-1024 GiB = Free (included in project quota)
    # Tier 2: 1025+ GiB = $0.0109/GB
    outbound_SYD_SGP_MUM_tiers = [
        {
            "lower": 1,
            "upper": 1024,
            "price": 0,
        },
        {
            "lower": 1025,
            "upper": "Infinity",
            "price": 0.01,
        },
    ]

    items = []
    for region in vendor.regions:
        # Incoming public traffic
        items.append(
            {
                "vendor_id": vendor.vendor_id,
                "region_id": region.region_id,
                "price": 0,
                "price_tiered": [],
                "currency": "EUR",
                "unit": PriceUnit.GB_MONTH,
                "direction": TrafficDirection.IN,
            }
        )
        # Outgoing public traffic
        datacenter = _get_region(region.region_id)["datacenterLocation"]
        # Asia-Pacific region (Singapore, Sydney and Mumbai)
        is_apac = datacenter in ["SGP", "SYD", "YNM"]
        items.append(
            {
                "vendor_id": vendor.vendor_id,
                "region_id": region.region_id,
                "price": 0.01 if is_apac else 0,  # max tier price
                "price_tiered": outbound_SYD_SGP_MUM_tiers if is_apac else [],
                "currency": "EUR",
                "unit": PriceUnit.GB_MONTH,
                "direction": TrafficDirection.OUT,
            }
        )
    return items

inventory_ipv4_prices #

inventory_ipv4_prices(vendor)

OVHcloud Public Cloud IPv4 pricing.

Note that the API catalog endpoint states the "publiccloud-publicip-ip" product to be free on the single-AZ and 3AZ regions, but it's listed at 1.5 EUR/month in the control panel for any additional public IPv4 address, so we use that value for now.

Data sources:

Source code in sc_crawler/vendors/_ovh.py
def inventory_ipv4_prices(vendor) -> list[dict]:
    """OVHcloud Public Cloud IPv4 pricing.

    Note that the API catalog endpoint states the "publiccloud-publicip-ip"
    product to be free on the single-AZ and 3AZ regions, but it's listed at 1.5
    EUR/month in the control panel for any additional public IPv4 address, so we
    use that value for now.

    Data sources:

    - <https://www.ovhcloud.com/en/public-cloud/prices>
    - OVH Control Manager
    """
    items = []
    for region in vendor.regions:
        # NOTE local zone prices are different, but these are skipped for now
        items.append(
            {
                "vendor_id": vendor.vendor_id,
                "region_id": region.region_id,
                "price": 1.5,
                "currency": "EUR",
                "unit": PriceUnit.MONTH,
            }
        )
    return items

inventory_databases #

inventory_databases(vendor)

List Public Cloud Databases for PostgreSQL compute SKUs.

The availability API returns one row per region, version, and network mode. Rows sharing the same engine/plan/flavor are collapsed into a single SKU.

Lifecycle (availability[].lifecycle.status): END_OF_LIFE -> RETIRED, STABLE/BETA -> ACTIVE, otherwise INACTIVE.

Data sources:

  • GET /cloud/project/{serviceName}/database/availability
  • GET /cloud/project/{serviceName}/database/capabilities
  • https://docs.ovhcloud.com/en/guides/public-cloud/databases/postgresql-capabilities
  • https://docs.ovhcloud.com/en/guides/public-cloud/databases/postgresql-concept-high-availability
Source code in sc_crawler/vendors/_ovh.py
def inventory_databases(vendor):
    """List Public Cloud Databases for PostgreSQL compute SKUs.

    The availability API returns one row per region, version, and network mode.
    Rows sharing the same engine/plan/flavor are collapsed into a single SKU.

    Lifecycle (`availability[].lifecycle.status`): `END_OF_LIFE` -> RETIRED,
    `STABLE`/`BETA` -> ACTIVE, otherwise INACTIVE.

    Data sources:

    - `GET /cloud/project/{serviceName}/database/availability`
    - `GET /cloud/project/{serviceName}/database/capabilities`
    - https://docs.ovhcloud.com/en/guides/public-cloud/databases/postgresql-capabilities
    - https://docs.ovhcloud.com/en/guides/public-cloud/databases/postgresql-concept-high-availability
    """
    vendor.progress_tracker.start_task(name="Fetching database catalogs", total=None)
    availability = _get_database_availability()
    capabilities = _get_database_capabilities()
    vendor.progress_tracker.hide_task()

    flavors = {f["name"]: f for f in capabilities.get("flavors", [])}
    regions = scmodels_to_dict(vendor.regions, keys=["api_reference"])
    server_ids = {server.server_id for server in vendor.servers}

    offers_by_id: dict[str, list[dict]] = {}
    for offer in availability:
        if offer.get("engine") != "postgresql":
            continue
        flavor_name = offer.get("specifications", {}).get("flavor")
        if not flavor_name:
            continue
        database_id = f"{offer['engine']}-{offer['plan']}-{flavor_name}"
        offers_by_id.setdefault(database_id, []).append(offer)

    items = []
    for database_id, offers in offers_by_id.items():
        sample = offers[0]
        engine = sample["engine"]
        plan = sample["plan"]
        flavor_name = sample["specifications"]["flavor"]
        flavor = flavors.get(flavor_name)
        if flavor is None:
            continue

        flavor_specs = flavor.get("specifications", {})
        vcpus = flavor_specs.get("core")
        memory = flavor_specs.get("memory", {}).get("value")
        storage_size = flavor_specs.get("storage", {}).get("value")
        plan_label = plan.replace("-", " ").title()
        spec_parts = [
            f"{vcpus} vCPU{'s' if vcpus != 1 else ''}" if vcpus else None,
            f"{memory} GB RAM" if memory else None,
            f"{storage_size} GB SSD" if storage_size else None,
        ]

        engine_versions: set[str] = set()
        ha: set[DatabaseHaLevel] = set()
        ha_strategy: set[DatabaseHaStrategy] = set()
        max_read_replicas = 0
        storage_extra_max = None
        scheduled_backups = False
        continuous_backups = 0
        sla = None
        offer_statuses: set[Status] = set()

        for offer in offers:
            lifecycle = offer.get("lifecycle", {}).get("status")
            if lifecycle == "END_OF_LIFE":
                offer_status = Status.RETIRED
            elif lifecycle in {"STABLE", "BETA"}:
                offer_status = Status.ACTIVE
            else:
                offer_status = Status.INACTIVE
            offer_statuses.add(offer_status)

            region = regions.get(offer.get("region"))
            multi_az = region is not None and len(region.zones) > 1
            nodes = offer.get("specifications", {}).get("nodes", {})
            min_nodes = nodes.get("minimum", 1)
            max_nodes = nodes.get("maximum", min_nodes)
            storage = offer.get("specifications", {}).get("storage", {})
            storage_min = storage.get("minimum", {}).get("value")
            storage_max = storage.get("maximum", {}).get("value")
            backups = offer.get("backups", {})
            scheduled = backups.get("available")
            retention = backups.get("retentionDays", 0)

            engine_versions.add(offer["version"])
            max_read_replicas = max(max_read_replicas, max_nodes - 1)
            scheduled_backups = scheduled_backups or bool(scheduled)
            continuous_backups = max(continuous_backups, retention)
            if (
                storage_min is not None
                and storage_max is not None
                and storage_max > storage_min
            ):
                headroom = storage_max - storage_min
                storage_extra_max = (
                    headroom
                    if storage_extra_max is None
                    else max(storage_extra_max, headroom)
                )

            # Node count is fixed per plan (`minimum` == `maximum` for PG today).
            # https://docs.ovhcloud.com/en/guides/public-cloud/databases/postgresql-concept-high-availability
            if min_nodes == 1 or min_nodes < max_nodes:
                ha.add(DatabaseHaLevel.NONE)
                ha_strategy.add(DatabaseHaStrategy.NONE)
            if min_nodes > 1:
                ha.add(
                    DatabaseHaLevel.MULTI_ZONE
                    if multi_az
                    else DatabaseHaLevel.SINGLE_ZONE
                )
                ha_strategy.add(DatabaseHaStrategy.READABLE_CLUSTER)

            if plan in {"enterprise", "advanced"}:
                offer_sla = 99.99 if multi_az else 99.95
            elif plan in {"business", "production"}:
                offer_sla = 99.95 if multi_az else 99.9
            else:
                offer_sla = None
            if offer_sla is not None:
                sla = offer_sla if sla is None else max(sla, offer_sla)

        status = Status.best(offer_statuses) if offer_statuses else Status.INACTIVE

        items.append(
            {
                "vendor_id": vendor.vendor_id,
                "database_id": database_id,
                "name": database_id,
                "api_reference": database_id,
                # https://www.pulumi.com/registry/packages/ovh/api-docs/cloudproject/database/
                "api_reference_object": {
                    "engine": engine,
                    "plan": plan,
                    "flavor": flavor_name,
                },
                "display_name": f"{plan_label} {flavor_name}",
                "description": f"PostgreSQL {plan_label} ({', '.join(filter(None, spec_parts))})",
                "server_id": flavor_name if flavor_name in server_ids else None,
                "engine": DatabaseEngine.POSTGRESQL,
                "wire_protocol": DatabaseWireProtocol.POSTGRESQL,
                "engine_versions": sorted(engine_versions),
                # Service plan tier (Essential, Production, Advanced, ...).
                "family": plan_label,
                "vcpus": vcpus,
                "memory_amount": memory * _MIB_PER_GIB if memory is not None else None,
                "storage_size": storage_size,
                "storage_extra_min": 0 if storage_extra_max is not None else None,
                "storage_extra_max": storage_extra_max,
                # Flexible storage is increased manually; disk-full goes read-only until storage is provisioned.
                # https://docs.ovhcloud.com/en/guides/public-cloud/databases/postgresql-capabilities/
                "storage_extra_autosize": False,
                "ha": DatabaseHaLevel.ordered(ha),
                "ha_strategy": DatabaseHaStrategy.ordered(ha_strategy),
                "max_read_replicas": max_read_replicas,
                # https://docs.ovhcloud.com/en/guides/public-cloud/databases/advanced-configuration
                "custom_config": True,
                # https://docs.ovhcloud.com/en/guides/public-cloud/databases/postgresql-extensions
                "custom_extensions": True,
                # https://docs.ovhcloud.com/en/guides/public-cloud/databases/concepts-security-overview
                "disk_encryption": True,
                # https://docs.ovhcloud.com/en/guides/public-cloud/databases/capabilities
                "auto_upgrade_versions": True,
                "scheduled_backups": scheduled_backups,
                "continuous_backups": continuous_backups,
                # https://docs.ovhcloud.com/en/guides/public-cloud/databases/postgresql-pool
                "connection_pool": True,
                # https://docs.ovhcloud.com/en/guides/public-cloud/databases/postgresql-capabilities
                "system_monitoring": True,
                "database_monitoring": True,
                "autotuning_advice": None,
                "autotuning_apply": None,
                "sla": sla,
                "status": status,
                # https://docs.ovhcloud.com/en/guides/public-cloud/databases/concepts-security-overview
                "security_features": [
                    DatabaseSecurityFeature.IP_FILTERING,
                    DatabaseSecurityFeature.PRIVATE_NETWORK,
                    DatabaseSecurityFeature.ENFORCED_TLS,
                    DatabaseSecurityFeature.AUDIT_LOGGING,
                ],
            }
        )
    return items

inventory_database_prices #

inventory_database_prices(vendor)

Fetch Public Cloud Databases for PostgreSQL on-demand compute prices.

Hourly consumption addons come from /order/catalog/public/cloud. Catalog lookup uses databases.{planCode} with .apac / .3az suffixes. Price is the full cluster amount (per-node catalog rate * included nodes).

Source code in sc_crawler/vendors/_ovh.py
def inventory_database_prices(vendor):
    """Fetch Public Cloud Databases for PostgreSQL on-demand compute prices.

    Hourly consumption addons come from `/order/catalog/public/cloud`.
    Catalog lookup uses `databases.{planCode}` with `.apac` / `.3az` suffixes.
    Price is the full cluster amount (per-node catalog rate * included nodes).
    """
    regions = scmodels_to_dict(vendor.regions, keys=["api_reference"])
    databases = {database.database_id: database for database in vendor.databases}
    vendor.progress_tracker.start_task(name="Fetching database prices", total=None)
    catalog = _get_catalog()
    availability = _get_database_availability()
    vendor.progress_tracker.hide_task()

    addons = {addon["planCode"]: addon for addon in catalog.get("addons", [])}
    currency = catalog.get("locale", {}).get("currencyCode", "EUR")
    items = []
    seen: set[tuple[str, str, DatabaseHaLevel, DatabaseHaStrategy]] = set()
    for offer in availability:
        if offer.get("engine") != "postgresql":
            continue
        if offer.get("lifecycle", {}).get("status") in {"UNAVAILABLE", "END_OF_LIFE"}:
            continue
        region = regions.get(offer.get("region"))
        if region is None:
            vendor.log(
                f"Excluding database offer from unknown region: {offer['region']}"
            )
            continue
        specs = offer.get("specifications", {})
        flavor_name = specs.get("flavor")
        plan = offer["plan"]
        database_id = f"{offer['engine']}-{plan}-{flavor_name}"
        if databases and database_id not in databases:
            continue
        multi_az = len(region.zones) > 1
        suffixes = _catalog_suffixes(region)
        plan_code = offer.get("planCode", "")
        catalog_key = (
            plan_code
            if plan_code.startswith("databases.")
            else f"databases.{plan_code}"
        )
        addon = None
        for suffix in suffixes:
            addon = addons.get(catalog_key + suffix)
            if addon:
                break
        if addon is None:
            vendor.log(
                f"Excluding {database_id} in {region.region_id}: no catalog addon"
            )
            continue
        node_price = None
        for pricing in addon.get("pricings", []):
            if pricing.get("interval") == 0 and "consumption" in pricing.get(
                "capacities", []
            ):
                node_price = pricing["price"] / _MICROCENTS_PER_CURRENCY_UNIT
                break
        if node_price is None:
            continue
        min_nodes = specs.get("nodes", {}).get("minimum", 1)
        if min_nodes > 1:
            ha = DatabaseHaLevel.MULTI_ZONE if multi_az else DatabaseHaLevel.SINGLE_ZONE
            ha_strategy = DatabaseHaStrategy.READABLE_CLUSTER
        else:
            ha = DatabaseHaLevel.NONE
            ha_strategy = DatabaseHaStrategy.NONE
        key = (region.region_id, database_id, ha, ha_strategy)
        if key in seen:
            continue
        seen.add(key)
        items.append(
            {
                "vendor_id": vendor.vendor_id,
                "region_id": region.region_id,
                "database_id": database_id,
                "allocation": Allocation.ONDEMAND,
                "ha": ha,
                "ha_strategy": ha_strategy,
                "unit": PriceUnit.HOUR,
                "price": node_price * min_nodes,
                "price_upfront": 0,
                "price_tiered": [],
                "currency": currency,
            }
        )
    return items

inventory_database_storages #

inventory_database_storages(vendor)

List extra disk for Public Cloud PostgreSQL.

Each flavor includes a base volume; extra GB can be attached up to the availability maximum and is billed separately per plan. https://docs.ovhcloud.com/en/guides/public-cloud/databases/resize-cluster-storage https://docs.ovhcloud.com/en/guides/public-cloud/databases/pricing

Source code in sc_crawler/vendors/_ovh.py
def inventory_database_storages(vendor):
    """List extra disk for Public Cloud PostgreSQL.

    Each flavor includes a base volume; extra GB can be attached up to the
    availability maximum and is billed separately per plan.
    https://docs.ovhcloud.com/en/guides/public-cloud/databases/resize-cluster-storage
    https://docs.ovhcloud.com/en/guides/public-cloud/databases/pricing
    """
    availability = _get_database_availability()
    caps = _get_database_capabilities()
    disk_types = caps.get("diskTypes", [])
    iops_values = [
        disk_type.get("specs", {}).get("iops", {}).get("max")
        for disk_type in disk_types
        if isinstance(disk_type.get("specs", {}).get("iops", {}).get("max"), int)
    ]
    throughput_values = []
    for disk_type in disk_types:
        throughput_max = disk_type.get("specs", {}).get("throughput", {}).get("max")
        if throughput_max is None:
            throughput_values = []
            break
        if isinstance(throughput_max, int):
            throughput_values.append(throughput_max)
    max_iops = min(iops_values) if iops_values else None
    max_throughput = min(throughput_values) if throughput_values else None

    merged: dict[str, dict] = {}
    for offer in availability:
        if offer.get("engine") != "postgresql":
            continue
        if offer.get("lifecycle", {}).get("status") in {"UNAVAILABLE", "END_OF_LIFE"}:
            continue
        storage = offer.get("specifications", {}).get("storage", {})
        storage_min = storage.get("minimum", {}).get("value")
        storage_max = storage.get("maximum", {}).get("value")
        if storage_min is None or storage_max is None:
            continue
        additional_max = storage_max - storage_min
        if additional_max <= 0:
            continue
        plan = offer["plan"]
        storage_id = f"postgresql-{plan}-additional"
        row = merged.get(storage_id)
        if row is None:
            plan_label = plan.replace("-", " ").title()
            merged[storage_id] = {
                "vendor_id": vendor.vendor_id,
                "database_storage_id": storage_id,
                "name": storage_id,
                "description": f"PostgreSQL {plan_label} additional storage",
                "scope": DatabaseStorageScope.DATA,
                "min_size": 0,
                "max_size": additional_max,
                "max_iops": max_iops,
                "max_throughput": max_throughput,
            }
        else:
            row["max_size"] = max(row["max_size"], additional_max)
    return list(merged.values())

inventory_database_storage_prices #

inventory_database_storage_prices(vendor)

Fetch extra PostgreSQL disk prices (GB-month) from the public catalog.

Source code in sc_crawler/vendors/_ovh.py
def inventory_database_storage_prices(vendor):
    """Fetch extra PostgreSQL disk prices (GB-month) from the public catalog."""
    regions = scmodels_to_dict(vendor.regions, keys=["api_reference"])
    storages = {storage.database_storage_id for storage in vendor.database_storages}
    catalog = _get_catalog()
    availability = _get_database_availability()
    addons = {addon["planCode"]: addon for addon in catalog.get("addons", [])}
    currency = catalog.get("locale", {}).get("currencyCode", "EUR")
    items = []
    seen: set[tuple[str, str]] = set()
    for offer in availability:
        if offer.get("engine") != "postgresql":
            continue
        if offer.get("lifecycle", {}).get("status") in {"UNAVAILABLE", "END_OF_LIFE"}:
            continue
        region = regions.get(offer.get("region"))
        if region is None:
            vendor.log(
                f"Excluding database storage from unknown region: {offer['region']}"
            )
            continue
        plan = offer["plan"]
        storage_id = f"postgresql-{plan}-additional"
        if storages and storage_id not in storages:
            continue
        suffixes = _catalog_suffixes(region)
        catalog_key = (
            f"databases.postgresql-{plan}-additionnal-storage-gb.hour.consumption"
        )
        addon = None
        for suffix in suffixes:
            addon = addons.get(catalog_key + suffix)
            if addon:
                break
        if addon is None:
            vendor.log(
                f"Excluding {storage_id} in {region.region_id}: no catalog addon"
            )
            continue
        gb_hour = None
        for pricing in addon.get("pricings", []):
            if pricing.get("interval") == 0 and "consumption" in pricing.get(
                "capacities", []
            ):
                gb_hour = pricing["price"] / _MICROCENTS_PER_CURRENCY_UNIT
                break
        if gb_hour is None:
            continue
        key = (region.region_id, storage_id)
        if key in seen:
            continue
        seen.add(key)
        items.append(
            {
                "vendor_id": vendor.vendor_id,
                "region_id": region.region_id,
                "database_storage_id": storage_id,
                "unit": PriceUnit.GB_MONTH,
                "price": gb_hour * _HOURS_PER_MONTH,
                "price_upfront": 0,
                "price_tiered": [],
                "currency": currency,
            }
        )
    return items