Skip to content

Commit

Permalink
Merge pull request #68 from knikolla/outages
Browse files Browse the repository at this point in the history
Add support for not billing during outages
  • Loading branch information
knikolla authored May 31, 2024
2 parents 6b30a5e + e193174 commit c7bd4aa
Show file tree
Hide file tree
Showing 8 changed files with 105 additions and 23 deletions.
40 changes: 37 additions & 3 deletions src/openstack_billing_db/billing.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,18 @@
import os

from openstack_billing_db import model
from openstack_billing_db import utils

import boto3

logger = logging.getLogger(__name__)

# FIXME(knikolla): Temporarily hardcoding the days of outage here
# until we have formalized how to store them in the nerc-rates repo.
# Usage during these intervals is subtracted from the usage during
# the month.
OUTAGES_FOR_MONTH = {"2024-05": [("2024-05-22", "2024-05-29")]}


@dataclass()
class Rates(object):
Expand Down Expand Up @@ -81,7 +88,26 @@ def gpu_a2_su_cost(self) -> Decimal:
return self.rates.gpu_a2 * self.gpu_a2_su_hours


def collect_invoice_data_from_openstack(database, billing_start, billing_end, rates):
def get_runtime_for_instance(
instance: model.Instance,
start: datetime,
end: datetime,
excluded_intervals: list[(str, str)],
):
runtime = instance.get_runtime_during(start, end)
for interval in excluded_intervals:
excluded_runtime = instance.get_runtime_during(
start_time=utils.parse_time_from_string(interval[0]),
end_time=utils.parse_time_from_string(interval[1]),
)
runtime = runtime - excluded_runtime

return runtime


def collect_invoice_data_from_openstack(
database, billing_start, billing_end, rates, invoice_month=None
):
invoices = []
for project in database.projects:
invoice = ProjectInvoice(
Expand All @@ -94,8 +120,14 @@ def collect_invoice_data_from_openstack(database, billing_start, billing_end, ra
rates=rates,
)

excluded_intervals = []
if invoice_month:
excluded_intervals = OUTAGES_FOR_MONTH.get(invoice_month, [])

for i in project.instances: # type: model.Instance
runtime = i.get_runtime_during(billing_start, billing_end)
runtime = get_runtime_for_instance(
i, billing_start, billing_end, excluded_intervals
)
runtime_seconds = runtime.total_seconds_running
if rates.include_stopped_runtime:
runtime_seconds += runtime.total_seconds_stopped
Expand Down Expand Up @@ -221,7 +253,9 @@ def generate_billing(
):
database = model.Database(start, sql_dump_file)

invoices = collect_invoice_data_from_openstack(database, start, end, rates)
invoices = collect_invoice_data_from_openstack(
database, start, end, rates, invoice_month=invoice_month
)
if coldfront_data_file:
merge_coldfront_data(invoices, coldfront_data_file)
write(invoices, output, invoice_month)
Expand Down
4 changes: 2 additions & 2 deletions src/openstack_billing_db/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,15 @@
import argparse
import logging

from openstack_billing_db import billing, fetch
from openstack_billing_db import billing, fetch, utils

logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)


def parse_time_argument(arg):
if isinstance(arg, str):
return datetime.strptime(arg, "%Y-%m-%d")
return utils.parse_time_from_string(arg)
return arg


Expand Down
19 changes: 10 additions & 9 deletions src/openstack_billing_db/model.py
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,12 @@ class InstanceRuntime(object):
total_seconds_running: int = 0
total_seconds_stopped: int = 0

def __sub__(self, other):
return InstanceRuntime(
self.total_seconds_running - other.total_seconds_running,
self.total_seconds_stopped - other.total_seconds_stopped,
)


@dataclass
class Instance(object):
Expand Down Expand Up @@ -100,20 +106,15 @@ def get_runtime_during(self, start_time, end_time):
in_error_state = False
delete_action_found = False

# If the instance as a deleted_at time, clamp it to within
# the invoicing period.
if self.deleted_at:
self.deleted_at = self._clamp_time(self.deleted_at, start_time, end_time)

for event in self.events:
event.time = self._clamp_time(event.time, start_time, end_time)
event_time = self._clamp_time(event.time, start_time, end_time)

if event.message == "Error":
in_error_state = True
continue

if event.name in ["create", "start"]:
last_start = event.time
last_start = event_time
in_error_state = False

# Count stopped time from last known stop.
Expand All @@ -129,7 +130,7 @@ def get_runtime_during(self, start_time, end_time):
delete_action_found = True

if event.name in ["delete", "stop"]:
last_stop = event.time
last_stop = event_time

# Count running time from last known start.
if last_start:
Expand All @@ -146,7 +147,7 @@ def get_runtime_during(self, start_time, end_time):

if self.deleted_at and not delete_action_found:
self.no_delete_action = True
end_time = self.deleted_at
end_time = self._clamp_time(self.deleted_at, start_time, end_time)

# Handle the time since the last event.
if last_start:
Expand Down
29 changes: 29 additions & 0 deletions src/openstack_billing_db/tests/unit/test_billing.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import uuid
from datetime import datetime, timedelta

from openstack_billing_db import billing
from openstack_billing_db.model import Instance, InstanceEvent
from openstack_billing_db.tests.unit.utils import FLAVORS, MINUTE, HOUR, DAY, MONTH


def test_instance_simple_runtime():
time = datetime(year=2000, month=1, day=1, hour=0, minute=0, second=0)
events = [
InstanceEvent(time=time, name="create", message=""),
InstanceEvent(time=time + timedelta(days=15), name="delete", message=""),
]
i = Instance(
uuid=uuid.uuid4().hex, name=uuid.uuid4().hex, flavor=FLAVORS[1], events=events
)

r = billing.get_runtime_for_instance(
i,
datetime(year=2000, month=1, day=1, hour=0, minute=0, second=0),
datetime(year=2000, month=2, day=1, hour=0, minute=0, second=0),
excluded_intervals=[
["2000-01-07", "2000-01-08"],
["2000-01-01", "2000-01-02"],
],
)
assert r.total_seconds_running == (15 * DAY) - (DAY * 2)
assert r.total_seconds_stopped == 0
12 changes: 3 additions & 9 deletions src/openstack_billing_db/tests/unit/test_instance.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,8 @@
import uuid
from datetime import datetime, timedelta

from openstack_billing_db.model import Instance, InstanceEvent, Flavor

FLAVORS = {1: Flavor(id=1, name="TestFlavor", vcpus=1, memory=4096, storage=10)}

MINUTE = 60
HOUR = 60 * MINUTE
DAY = HOUR * 24
MONTH = 31 * DAY
from openstack_billing_db.model import Instance, InstanceEvent
from openstack_billing_db.tests.unit.utils import FLAVORS, MINUTE, HOUR, DAY, MONTH


def test_instance_simple_runtime():
Expand Down Expand Up @@ -115,7 +109,7 @@ def test_instance_no_delete_action():

r = i.get_runtime_during(
datetime(year=1999, month=11, day=1, hour=0, minute=0, second=0),
datetime(year=2000, month=12, day=1, hour=0, minute=0, second=0),
datetime(year=1999, month=12, day=1, hour=0, minute=0, second=0),
)
assert r.total_seconds_running == 0
assert r.total_seconds_stopped == 0
Expand Down
11 changes: 11 additions & 0 deletions src/openstack_billing_db/tests/unit/test_instance_runtime.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
from openstack_billing_db.model import InstanceRuntime


def test_instance_runtime_subtract():
a = InstanceRuntime(total_seconds_running=1000, total_seconds_stopped=1000)
b = InstanceRuntime(total_seconds_running=100, total_seconds_stopped=200)
c = a - b
assert c.total_seconds_running == 900
assert c.total_seconds_running == a.total_seconds_running - b.total_seconds_running
assert c.total_seconds_stopped == 800
assert c.total_seconds_stopped == a.total_seconds_stopped - b.total_seconds_stopped
8 changes: 8 additions & 0 deletions src/openstack_billing_db/tests/unit/utils.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
from openstack_billing_db import model

FLAVORS = {1: model.Flavor(id=1, name="TestFlavor", vcpus=1, memory=4096, storage=10)}

MINUTE = 60
HOUR = 60 * MINUTE
DAY = HOUR * 24
MONTH = 31 * DAY
5 changes: 5 additions & 0 deletions src/openstack_billing_db/utils.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
from datetime import datetime


def parse_time_from_string(time_str: str) -> datetime:
return datetime.strptime(time_str, "%Y-%m-%d")

0 comments on commit c7bd4aa

Please sign in to comment.