Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

M kovalsky/reservations #339

Open
wants to merge 4 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions src/sempy_labs/_authentication.py
Original file line number Diff line number Diff line change
Expand Up @@ -106,3 +106,22 @@ def __call__(self, audience: Literal["pbi", "storage"] = "pbi") -> str:
return self.credential.get_token("https://storage.azure.com/.default").token
else:
raise NotImplementedError


def _get_headers(
token_provider: str, audience: Literal["pbi", "storage", "azure", "graph"] = "azure"
):
"""
Generates headers for an API request.
"""

token = token_provider(audience=audience)

headers = {"Authorization": f"Bearer {token}"}

if audience == "graph":
headers["ConsistencyLevel"] = "eventual"
else:
headers["Content-Type"] = "application/json"

return headers
196 changes: 196 additions & 0 deletions src/sempy_labs/_capacities.py
Original file line number Diff line number Diff line change
Expand Up @@ -688,3 +688,199 @@ def create_resource_group(
print(
f"{icons.green_dot} The '{resource_group}' resource group has been created within the '{region}' region within the '{azure_subscription_id}' Azure subscription."
)


def list_reservation_orders(token_provider: str) -> pd.DataFrame:

# https://learn.microsoft.com/rest/api/reserved-vm-instances/reservation-order/list?view=rest-reserved-vm-instances-2022-11-01

url = "https://management.azure.com/providers/Microsoft.Capacity/reservationOrders?api-version=2022-11-01"

headers = {
"Authorization": f"Bearer {token}", # How to generate headers here?
"Content-Type": "application/json",
}

response = requests.get(url, headers=headers)

if response.status_code != 200:
raise FabricHTTPException(response)

df = pd.DataFrame(
columns=[
"Order Id",
"Order Type",
"Order Name",
"Etag",
"Display Name",
"Request Date Time",
"Created Date Time",
"Benefit Start Time",
"Expiry Date",
"Expiry Date Time",
"Term",
"Billing Plan",
"Provisioning State",
"Reservations",
"Original Quantity",
]
)

for v in response.json().get("value"):
p = v.get("properties", {})
new_data = {
"Order Id": v.get("id"),
"Order Type": v.get("type"),
"Order Name": v.get("name"),
"Etag": v.get("etag"),
"Display Name": p.get("displayName"),
"Request Date Time": p.get("requestDateTime"),
"Created Date Time": p.get("createdDateTime"),
"Benefit Start Time": p.get("benefitStartTime"),
"Expiry Date": p.get("expiryDate"),
"Expiry Date Time": p.get("expiryDateTime"),
"Term": p.get("term"),
"Billing Plan": p.get("billingPlan"),
"Provisioning State": p.get("provisioningState"),
"Reservations": [
reservation.get("id") for reservation in p.get("reservations", [])
],
"Original Quantity": p.get("originalQuanitity"),
}

df = pd.concat([df, pd.DataFrame(new_data, index=[0])], ignore_index=True)

df["Request Date Time"] = pd.to_datetime(df["Request Date Time"])
df["Created Date Time"] = pd.to_datetime(df["Created Date Time"])
df["Benefit Start Time"] = pd.to_datetime(df["Benefit Start Time"])
df["Expiry Date Time"] = pd.to_datetime(df["Expiry Date Time"])

return df


def list_reservation_transactions(billing_account_id: str) -> pd.DataFrame:

# https://learn.microsoft.com/rest/api/consumption/reservation-transactions/list?view=rest-consumption-2024-08-01

url = f"https://management.azure.com/providers/Microsoft.Billing/billingAccounts/{billing_account_id}/providers/Microsoft.Consumption/reservationTransactions?api-version=2024-08-01"

response = requests.get(url, headers=headers)

if response.status_code != 200:
raise FabricHTTPException(response)


def list_reservations(
reservation_id: str,
reservation_order_id: str,
grain="monthly",
filter: Optional[str] = None,
) -> pd.DataFrame:

# https://learn.microsoft.com/rest/api/consumption/reservations-summaries/list-by-reservation-order-and-reservation?view=rest-consumption-2024-08-01

grain_options = ["monthly", "daily"]
if grain not in grain_options:
raise ValueError(
f"{icons.red_dot} Invalid grain. Valid options: {grain_options}."
)

if filter is None and grain == "daily":
raise ValueError(
f"{icons.red_dot} The 'filter' parameter is required for daily grain."
)

url = f"https://management.azure.com/providers/Microsoft.Capacity/reservationorders/{reservation_order_id}/reservations/{reservation_id}/providers/Microsoft.Consumption/reservationSummaries?grain={grain}"

if filter is not None:
url += f"&$filter={filter}"

url += "&api-version=2024-08-01"

df = pd.DataFrame(columns=[])

response = requests.get(url, headers=headers)

if response.status_code != 200:
raise FabricHTTPException(response)

for v in response.json().get("value", []):
p = v.get("properties", {})
new_data = {
"Reservation Summary Id": v.get("id"),
"Reservation Summary Name": v.get("name"),
"Type": v.get("type"),
"Tags": v.get("tags"),
"Reservation Order Id": p.get("reservationOrderId"),
"Reservation Id": p.get("reservationId"),
"Sku Name": p.get("skuName"),
"Kind": p.get("kind"),
"Reserved Hours": p.get("reservedHours"),
"Usage Date": p.get("usageDate"),
"Used Hours": p.get("usedHours"),
"Min Utilization Percentage": p.get("minUtilizationPercentage"),
"Avg Utilization Percentage": p.get("avgUtilizationPercentage"),
"Max Utilization Percentage": p.get("maxUtilizationPercentage"),
"Purchased Quantity": p.get("purchasedQuantity"),
"Remaining Quantity": p.get("remainingQuantity"),
"Total Reserved Quantity": p.get("totalReservedQuantity"),
"Used Quantity": p.get("usedQuantity"),
"Utilized Percentage": p.get("utilizedPercentage"),
}

df = pd.concat([df, pd.DataFrame(new_data, index=[0])], ignore_index=True)

int_cols = [
"Reserved Hours",
"Used Hours",
"Min Utilization Percentage",
"Avg Utilization Percentage",
"Max Utilization Percentage",
"Purchased Quantity",
"Remaining Quanity",
"Total Reserved Quantity",
"Used Quantity",
"Utilized Percentage",
]

df[int_cols] = df[int_cols].astype(int)


def buy_reservation(
reservation_order_id: str,
name: str,
sku: str,
region: str,
billing_scope_id: str,
term: str,
billing_plan: str,
quantity: int,
):

# https://learn.microsoft.com/en-us/rest/api/reserved-vm-instances/reservation-order/purchase?view=rest-reserved-vm-instances-2022-11-01&tabs=HTTP

url = f"https://management.azure.com/providers/Microsoft.Capacity/reservationOrders/{reservation_order_id}?api-version=2022-11-01"

payload = {
"sku": {
"name": sku,
},
"location": region,
"properties": {
"reservedResourceType": "VirtualMachines",
"billingScopeId": billing_scope_id,
"term": term,
"billingPlan": billing_plan,
"quantity": quantity,
"displayName": name,
"appliedScopes": None,
"appliedScopeType": "Shared",
"reservedResourceProperties": {"instanceFlexibility": "On"},
"renew": False,
},
}

response = requests.put(url, headers=headers, json=payload)

if response.status_code not in [200, 202]:
raise FabricHTTPException(response)
Loading
Loading