Skip to content

Commit 3a9c359

Browse files
authored
Merge pull request #31 from msftnadavbh/fix/orphaned-resources-cleanup
fix: orphaned resources query bugs + doc cleanup
2 parents 6e98093 + 3cb494e commit 3a9c359

File tree

4 files changed

+57
-10
lines changed

4 files changed

+57
-10
lines changed

CHANGELOG.md

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,32 @@ All notable changes to the Azure Pricing MCP Server will be documented in this f
55
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
66
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
77

8+
## [3.4.0] - 2026-03-03
9+
10+
### Added
11+
12+
- **Azure Databricks DBU Pricing Tools** (contributed by PR #28)
13+
- `databricks_dbu_pricing` - Search and list Azure Databricks DBU rates by workload type, tier, and region
14+
- `databricks_cost_estimate` - Estimate monthly and annual Databricks costs based on DBU consumption
15+
- `databricks_compare_workloads` - Compare DBU costs across workload types or regions
16+
- Supports 14 workload types with fuzzy alias matching (e.g., 'etl' -> 'jobs', 'warehouse' -> 'serverless sql')
17+
- Real-time pricing from Azure Retail Prices API — no authentication required
18+
- Photon pricing comparison included automatically
19+
20+
### Changed
21+
22+
- **Orphaned Resource Detection** expanded from 5 to 11 resource types (contributed by [@iditbnaya](https://github.com/iditbnaya), PR #30)
23+
- Removed NICs and NSGs (no cost impact — not billable resources)
24+
- Added: SQL Elastic Pools, Application Gateways, NAT Gateways, Load Balancers, Private DNS Zones, Private Endpoints, Virtual Network Gateways, DDoS Protection Plans
25+
- Fixed SQL Elastic Pools query to correctly filter for pools with no databases (leftanti join)
26+
- Fixed Private Endpoints query to check both auto-approved and manual-approval connections
27+
- Updated all documentation (FEATURES.md, ORPHANED_RESOURCES.md, TOOLS.md, USAGE_EXAMPLES.md)
28+
29+
### Documentation
30+
31+
- Added Databricks DBU pricing tools to TOOLS.md
32+
- Updated orphaned resource documentation across all docs
33+
834
## [3.3.0] - 2026-02-12
935

1036
### Added
@@ -36,7 +62,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
3662

3763
- **Orphaned Resource Detection Tool** (contributed by [@iditbnaya](https://github.com/iditbnaya))
3864
- `find_orphaned_resources` - Detect orphaned Azure resources and compute wasted costs
39-
- Scans for unattached managed disks, orphaned NICs, public IPs, NSGs, and empty App Service Plans
65+
- Initial release: scans for unattached managed disks, orphaned NICs, public IPs, NSGs, and empty App Service Plans
4066
- Integrates with Azure Cost Management API for historical cost lookup
4167
- Groups results by resource type with per-type summary tables
4268
- Configurable lookback period (default: 60 days)

docs/USAGE_EXAMPLES.md

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -373,7 +373,7 @@ Subscriptions scanned: 3
373373
|---------------|-------|-----------|
374374
| Unattached Disk | 2 | $85.00 |
375375
| Orphaned Public IP | 2 | $42.50 |
376-
| Orphaned NSG | 1 | $0.00 |
376+
| Orphaned Load Balancer | 1 | $18.25 |
377377
```
378378

379379
### Custom Lookback Period
@@ -395,10 +395,16 @@ The tool detects these orphaned resource types:
395395
| Resource Type | Detection Criteria |
396396
|---------------|--------------------|
397397
| **Unattached Disk** | Managed disks with no `managedBy` reference |
398-
| **Orphaned NIC** | Network interfaces not attached to a VM or private endpoint |
399398
| **Orphaned Public IP** | Public IPs not associated with any resource |
400-
| **Orphaned NSG** | Network security groups not attached to any NIC or subnet |
401399
| **Empty App Service Plan** | App Service Plans with zero hosted apps |
400+
| **Orphaned SQL Elastic Pool** | SQL Elastic Pools with no databases in the pool |
401+
| **Orphaned Application Gateway** | Application gateways with no backend address pools or targets |
402+
| **Orphaned NAT Gateway** | NAT gateways not associated with any subnet |
403+
| **Orphaned Load Balancer** | Load balancers with no backend address pools |
404+
| **Orphaned Private DNS Zone** | Private DNS zones with no virtual network links |
405+
| **Orphaned Private Endpoint** | Private endpoints with no connections or unapproved connections |
406+
| **Orphaned Virtual Network Gateway** | Virtual network gateways with no IP configurations |
407+
| **Orphaned DDoS Protection Plan** | DDoS protection plans with no associated virtual networks |
402408

403409
### Cost Analysis
404410

src/azure_pricing_mcp/formatters.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -645,7 +645,8 @@ def format_orphaned_resources_response(result: dict[str, Any]) -> str:
645645
f"Scanned {len(subscriptions)} subscription(s) — "
646646
"no orphaned disks, public IPs, App Service Plans, SQL Elastic Pools, "
647647
"Application Gateways, NAT Gateways, Load Balancers, Private DNS Zones, "
648-
"Private Endpoints, Virtual Network Gateways, or DDoS Protection Plans detected."
648+
"Private Endpoints, Virtual Network Gateways, "
649+
"or DDoS Protection Plans detected."
649650
)
650651

651652
# Collect all orphaned resources across subscriptions
@@ -697,9 +698,9 @@ def format_orphaned_resources_response(result: dict[str, Any]) -> str:
697698
return "\n".join(response_lines)
698699

699700

700-
# ---------------------------------------------------------------------------
701+
# =============================================================================
701702
# PTU Sizing + Cost Planner
702-
# ---------------------------------------------------------------------------
703+
# =============================================================================
703704

704705

705706
def format_ptu_sizing_response(result: dict[str, Any]) -> str:

src/azure_pricing_mcp/services/orphaned_resources.py

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -69,9 +69,17 @@
6969
"""
7070

7171
# Resource Graph query for orphaned SQL Elastic Pools (no databases)
72+
# Uses leftanti join: keep only pools that have NO matching database.
7273
ORPHANED_SQL_ELASTIC_POOLS_QUERY = """
7374
Resources
7475
| where type =~ 'microsoft.sql/servers/elasticpools'
76+
| extend poolId = tolower(id)
77+
| join kind=leftanti (
78+
Resources
79+
| where type =~ 'microsoft.sql/servers/databases'
80+
| where isnotempty(properties.elasticPoolId)
81+
| extend poolId = tolower(properties.elasticPoolId)
82+
) on poolId
7583
| project id, name, type, location, resourceGroup,
7684
subscriptionId,
7785
sku = tostring(sku.name),
@@ -123,12 +131,18 @@
123131
"""
124132

125133
# Resource Graph query for orphaned Private Endpoints (no connections or not approved)
134+
# Checks both auto-approved (privateLinkServiceConnections) and manual-approval
135+
# (manualPrivateLinkServiceConnections) arrays to avoid false positives.
126136
ORPHANED_PRIVATE_ENDPOINTS_QUERY = """
127137
Resources
128138
| where type =~ 'microsoft.network/privateendpoints'
129-
| where isnull(properties.privateLinkServiceConnections)
130-
or array_length(properties.privateLinkServiceConnections) == 0
131-
or properties.privateLinkServiceConnections[0].properties.privateLinkServiceConnectionState.status != 'Approved'
139+
| extend autoConns = array_length(properties.privateLinkServiceConnections)
140+
| extend manualConns = array_length(properties.manualPrivateLinkServiceConnections)
141+
| extend autoStatus = tostring(properties.privateLinkServiceConnections[0].properties.privateLinkServiceConnectionState.status)
142+
| extend manualStatus = tostring(properties.manualPrivateLinkServiceConnections[0].properties.privateLinkServiceConnectionState.status)
143+
| where (isnull(autoConns) or autoConns == 0) and (isnull(manualConns) or manualConns == 0)
144+
or ((isnull(autoConns) or autoConns == 0 or autoStatus != 'Approved')
145+
and (isnull(manualConns) or manualConns == 0 or manualStatus != 'Approved'))
132146
| project id, name, type, location, resourceGroup,
133147
subscriptionId
134148
"""

0 commit comments

Comments
 (0)