diff --git a/CHANGELOG.md b/CHANGELOG.md index 9cb1a8a..adfff92 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,32 @@ All notable changes to the Azure Pricing MCP Server will be documented in this f The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [3.4.0] - 2026-03-03 + +### Added + +- **Azure Databricks DBU Pricing Tools** (contributed by PR #28) + - `databricks_dbu_pricing` - Search and list Azure Databricks DBU rates by workload type, tier, and region + - `databricks_cost_estimate` - Estimate monthly and annual Databricks costs based on DBU consumption + - `databricks_compare_workloads` - Compare DBU costs across workload types or regions + - Supports 14 workload types with fuzzy alias matching (e.g., 'etl' -> 'jobs', 'warehouse' -> 'serverless sql') + - Real-time pricing from Azure Retail Prices API — no authentication required + - Photon pricing comparison included automatically + +### Changed + +- **Orphaned Resource Detection** expanded from 5 to 11 resource types (contributed by [@iditbnaya](https://github.com/iditbnaya), PR #30) + - Removed NICs and NSGs (no cost impact — not billable resources) + - Added: SQL Elastic Pools, Application Gateways, NAT Gateways, Load Balancers, Private DNS Zones, Private Endpoints, Virtual Network Gateways, DDoS Protection Plans + - Fixed SQL Elastic Pools query to correctly filter for pools with no databases (leftanti join) + - Fixed Private Endpoints query to check both auto-approved and manual-approval connections + - Updated all documentation (FEATURES.md, ORPHANED_RESOURCES.md, TOOLS.md, USAGE_EXAMPLES.md) + +### Documentation + +- Added Databricks DBU pricing tools to TOOLS.md +- Updated orphaned resource documentation across all docs + ## [3.3.0] - 2026-02-12 ### Added @@ -36,7 +62,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - **Orphaned Resource Detection Tool** (contributed by [@iditbnaya](https://github.com/iditbnaya)) - `find_orphaned_resources` - Detect orphaned Azure resources and compute wasted costs - - Scans for unattached managed disks, orphaned NICs, public IPs, NSGs, and empty App Service Plans + - Initial release: scans for unattached managed disks, orphaned NICs, public IPs, NSGs, and empty App Service Plans - Integrates with Azure Cost Management API for historical cost lookup - Groups results by resource type with per-type summary tables - Configurable lookback period (default: 60 days) diff --git a/docs/USAGE_EXAMPLES.md b/docs/USAGE_EXAMPLES.md index 0add2f7..626b487 100644 --- a/docs/USAGE_EXAMPLES.md +++ b/docs/USAGE_EXAMPLES.md @@ -373,7 +373,7 @@ Subscriptions scanned: 3 |---------------|-------|-----------| | Unattached Disk | 2 | $85.00 | | Orphaned Public IP | 2 | $42.50 | -| Orphaned NSG | 1 | $0.00 | +| Orphaned Load Balancer | 1 | $18.25 | ``` ### Custom Lookback Period @@ -395,10 +395,16 @@ The tool detects these orphaned resource types: | Resource Type | Detection Criteria | |---------------|--------------------| | **Unattached Disk** | Managed disks with no `managedBy` reference | -| **Orphaned NIC** | Network interfaces not attached to a VM or private endpoint | | **Orphaned Public IP** | Public IPs not associated with any resource | -| **Orphaned NSG** | Network security groups not attached to any NIC or subnet | | **Empty App Service Plan** | App Service Plans with zero hosted apps | +| **Orphaned SQL Elastic Pool** | SQL Elastic Pools with no databases in the pool | +| **Orphaned Application Gateway** | Application gateways with no backend address pools or targets | +| **Orphaned NAT Gateway** | NAT gateways not associated with any subnet | +| **Orphaned Load Balancer** | Load balancers with no backend address pools | +| **Orphaned Private DNS Zone** | Private DNS zones with no virtual network links | +| **Orphaned Private Endpoint** | Private endpoints with no connections or unapproved connections | +| **Orphaned Virtual Network Gateway** | Virtual network gateways with no IP configurations | +| **Orphaned DDoS Protection Plan** | DDoS protection plans with no associated virtual networks | ### Cost Analysis diff --git a/src/azure_pricing_mcp/formatters.py b/src/azure_pricing_mcp/formatters.py index b73b8e4..7734110 100644 --- a/src/azure_pricing_mcp/formatters.py +++ b/src/azure_pricing_mcp/formatters.py @@ -645,7 +645,8 @@ def format_orphaned_resources_response(result: dict[str, Any]) -> str: f"Scanned {len(subscriptions)} subscription(s) — " "no orphaned disks, public IPs, App Service Plans, SQL Elastic Pools, " "Application Gateways, NAT Gateways, Load Balancers, Private DNS Zones, " - "Private Endpoints, Virtual Network Gateways, or DDoS Protection Plans detected." + "Private Endpoints, Virtual Network Gateways, " + "or DDoS Protection Plans detected." ) # Collect all orphaned resources across subscriptions @@ -697,9 +698,9 @@ def format_orphaned_resources_response(result: dict[str, Any]) -> str: return "\n".join(response_lines) -# --------------------------------------------------------------------------- +# ============================================================================= # PTU Sizing + Cost Planner -# --------------------------------------------------------------------------- +# ============================================================================= def format_ptu_sizing_response(result: dict[str, Any]) -> str: diff --git a/src/azure_pricing_mcp/services/orphaned_resources.py b/src/azure_pricing_mcp/services/orphaned_resources.py index 316c81c..0840a93 100644 --- a/src/azure_pricing_mcp/services/orphaned_resources.py +++ b/src/azure_pricing_mcp/services/orphaned_resources.py @@ -69,9 +69,17 @@ """ # Resource Graph query for orphaned SQL Elastic Pools (no databases) +# Uses leftanti join: keep only pools that have NO matching database. ORPHANED_SQL_ELASTIC_POOLS_QUERY = """ Resources | where type =~ 'microsoft.sql/servers/elasticpools' +| extend poolId = tolower(id) +| join kind=leftanti ( + Resources + | where type =~ 'microsoft.sql/servers/databases' + | where isnotempty(properties.elasticPoolId) + | extend poolId = tolower(properties.elasticPoolId) +) on poolId | project id, name, type, location, resourceGroup, subscriptionId, sku = tostring(sku.name), @@ -123,12 +131,18 @@ """ # Resource Graph query for orphaned Private Endpoints (no connections or not approved) +# Checks both auto-approved (privateLinkServiceConnections) and manual-approval +# (manualPrivateLinkServiceConnections) arrays to avoid false positives. ORPHANED_PRIVATE_ENDPOINTS_QUERY = """ Resources | where type =~ 'microsoft.network/privateendpoints' -| where isnull(properties.privateLinkServiceConnections) - or array_length(properties.privateLinkServiceConnections) == 0 - or properties.privateLinkServiceConnections[0].properties.privateLinkServiceConnectionState.status != 'Approved' +| extend autoConns = array_length(properties.privateLinkServiceConnections) +| extend manualConns = array_length(properties.manualPrivateLinkServiceConnections) +| extend autoStatus = tostring(properties.privateLinkServiceConnections[0].properties.privateLinkServiceConnectionState.status) +| extend manualStatus = tostring(properties.manualPrivateLinkServiceConnections[0].properties.privateLinkServiceConnectionState.status) +| where (isnull(autoConns) or autoConns == 0) and (isnull(manualConns) or manualConns == 0) + or ((isnull(autoConns) or autoConns == 0 or autoStatus != 'Approved') + and (isnull(manualConns) or manualConns == 0 or manualStatus != 'Approved')) | project id, name, type, location, resourceGroup, subscriptionId """