Skip to content

Commit 4477f39

Browse files
committed
feat: add budget year field and modify budget update scheduler
Signed-off-by: ImMin5 <[email protected]>
1 parent a939a16 commit 4477f39

File tree

7 files changed

+149
-101
lines changed

7 files changed

+149
-101
lines changed

src/spaceone/cost_analysis/error/budget.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ class ERROR_PROVIDER_FILTER_IS_EMPTY(ERROR_INVALID_ARGUMENT):
5252

5353

5454
class ERROR_BUDGET_ALREADY_EXIST(ERROR_INVALID_ARGUMENT):
55-
_message = "Budget already exist. (service_account_id = {service_account_id}, workspace_id= {workspace_id}, target = {target})"
55+
_message = "Budget already exist. (budget_year = {budget_year}, target ={budget_target}, workspace_id= {workspace_id})"
5656

5757

5858
class ERROR_NOTIFICATION_IS_NOT_SUPPORTED_IN_PROJECT(ERROR_INVALID_ARGUMENT):

src/spaceone/cost_analysis/manager/budget_usage_manager.py

+81-78
Original file line numberDiff line numberDiff line change
@@ -21,11 +21,11 @@
2121
class BudgetUsageManager(BaseManager):
2222
def __init__(self, *args, **kwargs):
2323
super().__init__(*args, **kwargs)
24+
2425
self.budget_mgr = BudgetManager()
25-
self.notification_mgr: NotificationManager = self.locator.get_manager(
26-
"NotificationManager"
27-
)
26+
self.notification_mgr = NotificationManager()
2827
self.email_mgr = None
28+
2929
self.budget_usage_model = BudgetUsage
3030

3131
def create_budget_usages(self, budget_vo: Budget) -> None:
@@ -113,95 +113,98 @@ def notify_budget_usage(self, budget_vo: Budget) -> None:
113113
plans = notification.plans or []
114114

115115
for plan in plans:
116-
if current_month not in budget_vo.notified_months:
117-
unit = plan.unit
118-
threshold = plan.threshold
119-
is_notify = False
120-
121-
if budget_vo.time_unit == "TOTAL":
122-
budget_usage_vos = self.filter_budget_usages(
123-
budget_id=budget_id,
124-
workspace_id=workspace_id,
125-
domain_id=domain_id,
126-
)
127-
total_budget_usage = sum(
128-
[budget_usage_vo.cost for budget_usage_vo in budget_usage_vos]
129-
)
130-
budget_limit = budget_vo.limit
131-
else:
132-
budget_usage_vos = self.filter_budget_usages(
133-
budget_id=budget_id,
134-
workspace_id=workspace_id,
135-
domain_id=domain_id,
136-
date=current_month,
137-
)
138116

139-
if budget_usage_vos.count() == 0:
140-
_LOGGER.debug(
141-
f"[notify_budget_usage] budget_usage_vos is empty: {budget_id}"
142-
)
143-
continue
117+
if plan.notified:
118+
_LOGGER.debug(
119+
f"[notify_budget_usage] skip notification: already notified {budget_id} (usage percent: {plan.threshold}%, threshold: {plan.threshold}%)"
120+
)
121+
continue
122+
123+
unit = plan.unit
124+
threshold = plan.threshold
125+
is_notify = False
144126

145-
total_budget_usage = budget_usage_vos[0].cost
146-
budget_limit = budget_usage_vos[0].limit
127+
if budget_vo.time_unit == "TOTAL":
128+
129+
budget_usage_vos = self.filter_budget_usages(
130+
budget_id=budget_id,
131+
workspace_id=workspace_id,
132+
domain_id=domain_id,
133+
)
134+
total_budget_usage = sum(
135+
[budget_usage_vo.cost for budget_usage_vo in budget_usage_vos]
136+
)
137+
budget_limit = budget_vo.limit
138+
else:
139+
budget_usage_vos = self.filter_budget_usages(
140+
budget_id=budget_id,
141+
workspace_id=workspace_id,
142+
domain_id=domain_id,
143+
date=current_month,
144+
)
147145

148-
if budget_limit == 0:
146+
if budget_usage_vos.count() == 0:
149147
_LOGGER.debug(
150-
f"[notify_budget_usage] budget_limit is 0: {budget_id}"
148+
f"[notify_budget_usage] budget_usage_vos is empty: {budget_id}"
151149
)
152150
continue
153151

154-
budget_percentage = round(total_budget_usage / budget_limit * 100, 2)
152+
total_budget_usage = budget_usage_vos[0].cost
153+
budget_limit = budget_usage_vos[0].limit
155154

155+
if budget_limit == 0:
156+
_LOGGER.debug(f"[notify_budget_usage] budget_limit is 0: {budget_id}")
157+
continue
158+
159+
budget_percentage = budget_vo.utilization_rate
160+
161+
if unit == "PERCENT":
162+
if budget_percentage > threshold:
163+
is_notify = True
164+
is_changed = True
165+
if is_notify:
166+
_LOGGER.debug(
167+
f"[notify_budget_usage] notify event: {budget_id}, current month: {current_month} (plan: {plan.to_dict()})"
168+
)
169+
170+
try:
171+
self._notify_message(
172+
budget_vo,
173+
total_budget_usage,
174+
budget_percentage,
175+
threshold,
176+
)
177+
178+
updated_plans.append(
179+
{
180+
"threshold": threshold,
181+
"unit": unit,
182+
"notified": True,
183+
}
184+
)
185+
186+
except Exception as e:
187+
_LOGGER.error(
188+
f"[notify_budget_usage] Failed to notify message ({budget_id}): {e}",
189+
exc_info=True,
190+
)
191+
else:
156192
if unit == "PERCENT":
157-
if budget_percentage > threshold:
158-
is_notify = True
159-
is_changed = True
160-
if is_notify:
161193
_LOGGER.debug(
162-
f"[notify_budget_usage] notify event: {budget_id}, current month: {current_month} (plan: {plan.to_dict()})"
194+
f"[notify_budget_usage] skip notification: {budget_id} "
195+
f"(usage percent: {budget_percentage}%, threshold: {threshold}%)"
163196
)
164-
try:
165-
self._notify_message(
166-
budget_vo,
167-
total_budget_usage,
168-
budget_percentage,
169-
threshold,
170-
)
171-
172-
updated_plans.append(
173-
{
174-
"threshold": threshold,
175-
"unit": unit,
176-
"notified_months": plan.notified_months
177-
+ [current_month],
178-
}
179-
)
180-
except Exception as e:
181-
_LOGGER.error(
182-
f"[notify_budget_usage] Failed to notify message ({budget_id}): {e}",
183-
exc_info=True,
184-
)
185197
else:
186-
if unit == "PERCENT":
187-
_LOGGER.debug(
188-
f"[notify_budget_usage] skip notification: {budget_id} "
189-
f"(usage percent: {budget_percentage}%, threshold: {threshold}%)"
190-
)
191-
else:
192-
_LOGGER.debug(
193-
f"[notify_budget_usage] skip notification: {budget_id} "
194-
f"(usage cost: {total_budget_usage}, threshold: {threshold})"
195-
)
196-
197-
updated_plans.append(plan.to_dict())
198+
_LOGGER.debug(
199+
f"[notify_budget_usage] skip notification: {budget_id} "
200+
f"(usage cost: {total_budget_usage}, threshold: {threshold})"
201+
)
198202

199-
else:
200203
updated_plans.append(plan.to_dict())
201204

202205
if is_changed:
203206
notification.plans = updated_plans
204-
budget_vo.update({"notifications": notification.to_dict()})
207+
budget_vo.update({"notification": notification.to_dict()})
205208

206209
def delete_budget_usage_by_budget_vo(self, budget_vo: Budget) -> None:
207210
budget_usage_vos = self.filter_budget_usages(
@@ -324,15 +327,15 @@ def _update_monthly_budget_usage(
324327
budget_usage_vo.update({"cost": 0})
325328

326329
if budget_vo.time_unit == "TOTAL":
327-
budget_utilization_rate = total_usage_cost / budget_vo.limit * 100
330+
budget_utilization_rate = round(total_usage_cost / budget_vo.limit * 100, 2)
328331
self.budget_mgr.update_budget_by_vo(
329332
{"utilization_rate": budget_utilization_rate}, budget_vo
330333
)
331334
else:
332335
for budget_usage_vo in budget_usage_vos:
333336
if budget_usage_vo.date == current_month:
334-
budget_utilization_rate = (
335-
budget_usage_vo.cost / budget_usage_vo.limit * 100
337+
budget_utilization_rate = round(
338+
budget_usage_vo.cost / budget_usage_vo.limit * 100, 2
336339
)
337340
self.budget_mgr.update_budget_by_vo(
338341
{"utilization_rate": budget_utilization_rate}, budget_vo

src/spaceone/cost_analysis/model/budget/database.py

+3-2
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ class PlannedLimit(EmbeddedDocument):
1111
class Plan(EmbeddedDocument):
1212
threshold = FloatField(required=True)
1313
unit = StringField(max_length=20, required=True, choices=["PERCENT", "ACTUAL_COST"])
14+
notified = BooleanField(default=False)
1415

1516
def to_dict(self):
1617
return dict(self.to_mongo())
@@ -41,8 +42,8 @@ class Budget(MongoModel):
4142
time_unit = StringField(max_length=20, choices=["TOTAL", "MONTHLY"])
4243
start = StringField(required=True, max_length=7)
4344
end = StringField(required=True, max_length=7)
45+
budget_year = StringField(max_length=4, required=True)
4446
notification = EmbeddedDocumentField(Notification)
45-
notified_months = ListField(StringField(max_length=10))
4647
utilization_rate = FloatField(null=True, default=0)
4748
tags = DictField(default={})
4849
resource_group = StringField(
@@ -64,8 +65,8 @@ class Budget(MongoModel):
6465
"planned_limits",
6566
"start",
6667
"end",
68+
"budget_year",
6769
"notification",
68-
"notified_months",
6970
"utilization_rate",
7071
"tags",
7172
"budget_manager_id",

src/spaceone/cost_analysis/model/budget/request.py

+2-1
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,11 @@ class BudgetCreateRequest(BaseModel):
1010
name: str
1111
limit: Union[float, None] = None
1212
planned_limits: Union[list, None] = None
13-
currency: Union[str, None] = None
13+
currency: str
1414
time_unit: TimeUnit
1515
start: str
1616
end: str
17+
budget_year: str
1718
notification: Union[dict, None] = None
1819
tags: Union[dict, None] = None
1920
resource_group: ResourceGroup

src/spaceone/cost_analysis/model/budget/response.py

+1
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ class BudgetResponse(BaseModel):
3232
time_unit: Union[str, None] = None
3333
start: Union[str, None] = None
3434
end: Union[str, None] = None
35+
budget_year: Union[str, None] = None
3536
notification: Union[Notification, dict] = None
3637
utilization_rate: Union[float, None] = None
3738
tags: Union[dict, None] = None

src/spaceone/cost_analysis/model/budget_usage/database.py

+4-4
Original file line numberDiff line numberDiff line change
@@ -23,12 +23,12 @@ class BudgetUsage(MongoModel):
2323
"change_query_keys": {"user_projects": "project_id"},
2424
"ordering": ["budget_id", "date"],
2525
"indexes": [
26+
"domain_id",
27+
"workspace_id",
28+
"project_id",
29+
"resource_group",
2630
"budget_id",
2731
"name",
2832
"date",
29-
"resource_group",
30-
"project_id",
31-
"workspace_id",
32-
"domain_id",
3333
],
3434
}

0 commit comments

Comments
 (0)