Find unattached volumes from a mocked cloud API response
Problem statement
Block storage volumes keep costing money after the instance they belonged to is gone. You have saved the output of a "describe volumes" call to volumes.json. The shape is simplified from an AWS EC2 DescribeVolumes response and keeps its well-known field names (VolumeId, Size in GiB, VolumeType, State, Attachments, Tags, CreateTime).
Print every volume that is available (not attached to anything), with an estimated monthly cost, largest cost first (ties by volume ID), then a summary line. Use these made-up prices per GiB-month: gp2 0.10, gp3 0.08, io2 0.125. A volume type with no price is still listed, with ? as its cost, and named separately so it is not silently left out of the total.
Skip volumes in any other state (in-use, creating, deleting...). Tags may be missing. Use Decimal, not float, for money.
volumes.json
{ "Volumes": [ {"VolumeId": "vol-0a1", "Size": 100, "VolumeType": "gp3", "State": "in-use", "Attachments": [{"InstanceId": "i-01", "State": "attached"}], "Tags": [{"Key": "Name", "Value": "web-1-root"}]}, {"VolumeId": "vol-0b2", "Size": 500, "VolumeType": "gp2", "State": "available", "Attachments": [], "CreateTime": "2026-03-02T10:15:00Z", "Tags": [{"Key": "Name", "Value": "old-elasticsearch-data"}]}, {"VolumeId": "vol-0c3", "Size": 20, "VolumeType": "gp3", "State": "available", "Attachments": [], "CreateTime": "2026-09-20T08:00:00Z"}, {"VolumeId": "vol-0d4", "Size": 1000, "VolumeType": "io2", "State": "available", "Attachments": [], "CreateTime": "2026-06-11T12:00:00Z", "Tags": [{"Key": "Name", "Value": "db-migration-scratch"}, {"Key": "owner", "Value": "dba"}]}, {"VolumeId": "vol-0e5", "Size": 50, "VolumeType": "gp3", "State": "creating", "Attachments": []}, {"VolumeId": "vol-0f6", "Size": 8, "VolumeType": "standard", "State": "available", "Attachments": [], "CreateTime": "2025-11-30T00:00:00Z"} ]}Examples
Example 1
Input: python solution.py volumes.json
Output: VOLUME TYPE GiB $/MONTH CREATED NAME
vol-0d4 io2 1000 125.00 2026-06-11 db-migration-scratch
vol-0b2 gp2 500 50.00 2026-03-02 old-elasticsearch-data
vol-0c3 gp3 20 1.60 2026-09-20 -
vol-0f6 standard 8 ? 2025-11-30 -
4 unattached volume(s), 1528 GiB, $176.60/month
no price for: vol-0f6 (not included in the total)
Explanation: vol-0e5 is unattached but still creating, so it is skipped. vol-0f6 is kept in the list even though its type has no price.
Hints
Approach
Optimal
- Filter.
unattachedyields volumes whoseStateisavailableand whoseAttachmentslist is empty. Checking the state matters: a volume that iscreatinghas no attachments yet but is about to be used. - Price. Look up the type in a price table. A missing type produces
Nonerather than a crash or a silent zero, and the volume ID is remembered for the final warning. UsingDecimalkeeps0.08 * 20at exactly1.60. - Sort. The key
(cost is None, -cost, VolumeId)puts priced volumes first, most expensive at the top, with a stable tie-break, and unpriced volumes at the end. - Print.
tag()scans theTagslist (which may be missing ornull) forName.CreateTime[:10]shows the date part, so a reviewer can see that a volume has been idle since March. - Summary. Count, total size and total monthly cost, plus an explicit line for volumes that could not be priced. A report that quietly omits things is worse than one that says what it could not do.
v is the number of volumes.
O(v log v)Space O(v)import jsonimport sysfrom decimal import Decimal # Made-up prices per GiB-month for this exercise (not real pricing).PRICE_PER_GIB = {"gp2": Decimal("0.10"), "gp3": Decimal("0.08"), "io2": Decimal("0.125")} def tag(volume, key, default="-"): for t in volume.get("Tags") or []: # Tags may be absent or null if t.get("Key") == key: return t.get("Value", default) return default def unattached(response): for v in response.get("Volumes", []): # "available" = exists and is not attached. Skip creating/deleting/error states. if v.get("State") == "available" and not v.get("Attachments"): yield v def main(path): with open(path, encoding="utf-8") as f: response = json.load(f) rows, unknown = [], [] for v in unattached(response): price = PRICE_PER_GIB.get(v.get("VolumeType")) if price is None: unknown.append(v["VolumeId"]) cost = None else: cost = price * v["Size"] rows.append((cost, v)) rows.sort(key=lambda r: (r[0] is None, -(r[0] or 0), r[1]["VolumeId"])) print(f"{'VOLUME':<9}{'TYPE':<10}{'GiB':>5} {'$/MONTH':>8} {'CREATED':<11}NAME") for cost, v in rows: shown = f"{cost:.2f}" if cost is not None else "?" created = v.get("CreateTime", "")[:10] or "-" print(f"{v['VolumeId']:<9}{v['VolumeType']:<10}{v['Size']:>5} {shown:>8} {created:<11}{tag(v, 'Name')}") total = sum(c for c, _ in rows if c is not None) gib = sum(v["Size"] for _, v in rows) print(f"{len(rows)} unattached volume(s), {gib} GiB, ${total:.2f}/month") if unknown: print(f"no price for: {', '.join(unknown)} (not included in the total)") if __name__ == "__main__": main(sys.argv[1] if len(sys.argv) > 1 else "volumes.json")Follow-up questions
- Only report volumes that have been unattached for more than 14 days. What data would you need, given
CreateTimeis not the detach time? - Run it across every region and account, and merge the results into one report.
- Snapshot each volume before deleting it, and record the snapshot ID in a tag.
Frequently asked questions
Orphaned volumes are one of the most common sources of cloud waste, and finding them is a typical first automation task for cloud and FinOps work. The interview version checks that you read nested API data carefully and handle missing fields.
Not by default. An unattached volume may be a deliberate backup or waiting for a migration. The usual flow is report first, tag or snapshot, notify the owner, and delete only after a grace period, ideally behind an explicit --delete flag.
With boto3 you would call the EC2 client's describe_volumes with a filter on status=available and use a paginator, because real accounts return results in pages. The filtering and reporting code stays the same; only the data source changes. Check the SDK documentation for the exact response fields.