diff --git a/openshift_metrics/invoice.py b/openshift_metrics/invoice.py index 34de449..d1f194b 100644 --- a/openshift_metrics/invoice.py +++ b/openshift_metrics/invoice.py @@ -1,7 +1,7 @@ import math from dataclasses import dataclass, field from collections import namedtuple -from typing import List +from typing import List, Tuple from decimal import Decimal, ROUND_HALF_UP import datetime @@ -112,9 +112,27 @@ def get_service_unit(self) -> ServiceUnit: return ServiceUnit(su_type, su_count, determining_resource) - def get_runtime(self) -> Decimal: + def get_runtime( + self, ignore_times: List[Tuple[datetime.datetime, datetime.datetime]] = None + ) -> Decimal: """Return runtime eligible for billing in hours""" - return Decimal(self.duration) / 3600 + + total_runtime = self.duration + end_time = self.start_time + self.duration + + if ignore_times: + for ignore_start_date, ignore_end_date in ignore_times: + ignore_start = int(ignore_start_date.timestamp()) + ignore_end = int(ignore_end_date.timestamp()) + if ignore_end <= self.start_time or ignore_start >= end_time: + continue + overlap_start = max(self.start_time, ignore_start) + overlap_end = min(end_time, ignore_end) + + overlap_duration = max(0, overlap_end - overlap_start) + total_runtime = max(0, total_runtime - overlap_duration) + + return Decimal(total_runtime) / 3600 @property def end_time(self) -> int: diff --git a/openshift_metrics/tests/test_invoice.py b/openshift_metrics/tests/test_invoice.py new file mode 100644 index 0000000..16af81c --- /dev/null +++ b/openshift_metrics/tests/test_invoice.py @@ -0,0 +1,69 @@ +from unittest import TestCase +from datetime import datetime +from decimal import Decimal + +from openshift_metrics import invoice + + +class TestPodGetRuntime(TestCase): + def setUp(self): + """Gives us a pod that starts at 2024-10-11 12:00 UTC and ends at 2024-10-11 20:00 UTC""" + self.pod = invoice.Pod( + pod_name="test-pod", + namespace="test-namespace", + start_time=int(datetime(2024, 10, 11, 12, 0).timestamp()), + duration=3600 * 8, + cpu_request=Decimal("1.0"), + gpu_request=Decimal(0), + memory_request=Decimal("4.0"), + gpu_type=None, + gpu_resource=None, + node_hostname="node-1", + node_model=None, + ) + + def test_no_ignore_times(self): + runtime = self.pod.get_runtime() + self.assertEqual(runtime, Decimal("8.0")) + + def test_one_ignore_range(self): + ignore_range = [(datetime(2024, 10, 11, 13, 0), datetime(2024, 10, 11, 14, 0))] + self.assertEqual(self.pod.get_runtime(ignore_range), Decimal(7.0)) + + def test_multiple_ignore_times(self): + ignore_times = [ + (datetime(2024, 10, 11, 13, 0), datetime(2024, 10, 11, 14, 0)), + (datetime(2024, 10, 11, 14, 0), datetime(2024, 10, 11, 15, 0)), + (datetime(2024, 10, 11, 19, 0), datetime(2024, 10, 11, 20, 0)), + ] + self.assertEqual(self.pod.get_runtime(ignore_times), Decimal(5.0)) + + def test_ignore_times_outside_runtime(self): + ignore_times = [ + ( + datetime(2024, 10, 11, 10, 0), + datetime(2024, 10, 11, 11, 0), + ), # before start + (datetime(2024, 10, 11, 20, 0), datetime(2024, 10, 11, 22, 0)), # after end + ] + self.assertEqual(self.pod.get_runtime(ignore_times), Decimal(8.0)) + + def test_partial_overlap_ignore_range(self): + ignore_range = [ + (datetime(2024, 10, 11, 10, 30), datetime(2024, 10, 11, 14, 30)) + ] + self.assertEqual(self.pod.get_runtime(ignore_range), Decimal(5.5)) + + def test_ignore_range_greater_than_pod_runtime(self): + ignore_range = [ + (datetime(2024, 10, 11, 11, 00), datetime(2024, 10, 11, 21, 00)) + ] + self.assertEqual(self.pod.get_runtime(ignore_range), Decimal(0)) + + def test_runtime_is_never_negative(self): + ignore_times = [ + (datetime(2024, 10, 11, 13, 0), datetime(2024, 10, 11, 17, 0)), + (datetime(2024, 10, 11, 13, 0), datetime(2024, 10, 11, 17, 0)), + (datetime(2024, 10, 11, 10, 0), datetime(2024, 10, 11, 22, 0)), + ] + self.assertEqual(self.pod.get_runtime(ignore_times), Decimal(0.0))