Skip to content

Commit 3cc766c

Browse files
authored
Merge pull request #190 from knikolla/feature/ignore_hours
Support hours when providing outage intervals in billing
2 parents c61fd6e + 05d9739 commit 3cc766c

File tree

4 files changed

+73
-26
lines changed

4 files changed

+73
-26
lines changed

src/coldfront_plugin_cloud/management/commands/calculate_storage_gb_hours.py

+30-12
Original file line numberDiff line numberDiff line change
@@ -14,11 +14,25 @@
1414
from coldfront.core.allocation.models import Allocation, AllocationStatusChoice
1515
import pytz
1616

17-
from nerc_rates import load_from_url
1817

1918
logging.basicConfig(level=logging.INFO)
2019
logger = logging.getLogger(__name__)
2120

21+
_RATES = None
22+
23+
24+
def get_rates():
25+
# nerc-rates doesn't work with Python 3.9, which is what ColdFront is currently
26+
# using in Production. Lazily load the rates only when either of the storage rates
27+
# is not set via CLI arguments, so we can keep providing them via CLI until we upgrade
28+
# Python version.
29+
global _RATES
30+
31+
if _RATES is None:
32+
from nerc_rates import load_from_url
33+
_RATES = load_from_url()
34+
return _RATES
35+
2236

2337
@dataclasses.dataclass
2438
class InvoiceRow:
@@ -68,7 +82,7 @@ def get_values(self):
6882

6983

7084
def datetime_type(v):
71-
return pytz.utc.localize(datetime.strptime(v, '%Y-%m-%d'))
85+
return pytz.utc.localize(datetime.fromisoformat(v))
7286

7387

7488
class Command(BaseCommand):
@@ -87,19 +101,19 @@ def add_arguments(self, parser):
87101
)
88102
parser.add_argument('--output', type=str, default='invoices.csv',
89103
help='CSV file to write invoices to.')
90-
parser.add_argument('--openstack-gb-rate', type=Decimal, required=True,
104+
parser.add_argument('--openstack-gb-rate', type=Decimal, required=False,
91105
help='Rate for OpenStack Volume and Object GB/hour.')
92-
parser.add_argument('--openshift-gb-rate', type=Decimal, required=True,
106+
parser.add_argument('--openshift-gb-rate', type=Decimal, required=False,
93107
help='Rate for OpenShift GB/hour.')
94108
parser.add_argument('--s3-endpoint-url', type=str,
95109
default='https://s3.us-east-005.backblazeb2.com')
96110
parser.add_argument('--s3-bucket-name', type=str,
97111
default='nerc-invoicing')
98112
parser.add_argument('--upload-to-s3', default=False, action='store_true',
99113
help='Upload generated CSV invoice to S3 storage.')
100-
parser.add_argument('--excluded-date-ranges', type=str,
114+
parser.add_argument('--excluded-time-ranges', type=str,
101115
default=None, nargs='+',
102-
help='List of date ranges excluded from billing')
116+
help='List of time ranges excluded from billing, in ISO format.')
103117

104118
@staticmethod
105119
def default_start_argument():
@@ -176,9 +190,9 @@ def process_invoice_row(allocation, attrs, su_name, rate):
176190
logger.info(f'Processing invoices for {options["invoice_month"]}.')
177191
logger.info(f'Interval {options["start"] - options["end"]}.')
178192

179-
if options["excluded_date_ranges"]:
193+
if options["excluded_time_ranges"]:
180194
excluded_intervals_list = utils.load_excluded_intervals(
181-
options["excluded_date_ranges"]
195+
options["excluded_time_ranges"]
182196
)
183197
else:
184198
excluded_intervals_list = None
@@ -200,15 +214,19 @@ def process_invoice_row(allocation, attrs, su_name, rate):
200214
resources__in=openshift_resources
201215
)
202216

203-
rates = load_from_url()
204-
openstack_storage_rate = openshift_storage_rate = Decimal(
205-
rates.get_value_at('Storage GB Rate', options["invoice_month"]))
206-
207217
if options['openstack_gb_rate']:
208218
openstack_storage_rate = options['openstack_gb_rate']
219+
else:
220+
openstack_storage_rate = Decimal(
221+
get_rates().get_value_at('Storage GB Rate', options["invoice_month"])
222+
)
209223

210224
if options['openshift_gb_rate']:
211225
openshift_storage_rate = options['openshift_gb_rate']
226+
else:
227+
openshift_storage_rate = Decimal(
228+
get_rates().get_value_at('Storage GB Rate', options["invoice_month"])
229+
)
212230

213231
logger.info(f'Using storage rate {openstack_storage_rate} (Openstack) and '
214232
f'{openshift_storage_rate} (Openshift) for {options["invoice_month"]}')

src/coldfront_plugin_cloud/tests/unit/openshift/__init__.py

Whitespace-only changes.

src/coldfront_plugin_cloud/tests/unit/test_calculate_quota_unit_hours.py

+40-11
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import datetime
22
import unittest
33
import pytz
4+
import tempfile
45

56
import freezegun
67

@@ -9,6 +10,7 @@
910
from coldfront_plugin_cloud import utils
1011

1112
from coldfront.core.allocation import models as allocation_models
13+
from django.core.management import call_command
1214

1315

1416
SECONDS_IN_DAY = 3600 * 24
@@ -42,6 +44,32 @@ def test_new_allocation_quota(self):
4244
)
4345
self.assertEqual(value, 96)
4446

47+
with tempfile.NamedTemporaryFile() as fp:
48+
call_command(
49+
'calculate_storage_gb_hours',
50+
'--output', fp.name,
51+
'--start', '2020-03-01',
52+
'--end', '2020-03-31',
53+
'--openstack-gb-rate','0.0000087890625',
54+
'--openshift-gb-rate','0.0000087890625',
55+
'--invoice-month','2020-03'
56+
)
57+
58+
# Let's test a complete CLI call including excluded time, while we're at it. This is not for testing
59+
# the validity but just the unerrored execution of the complete pipeline.
60+
# Tests that verify the correct output are further down in the test file.
61+
with tempfile.NamedTemporaryFile() as fp:
62+
call_command(
63+
'calculate_storage_gb_hours',
64+
'--output', fp.name,
65+
'--start', '2020-03-01',
66+
'--end', '2020-03-31',
67+
'--openstack-gb-rate','0.0000087890625',
68+
'--openshift-gb-rate','0.0000087890625',
69+
'--invoice-month','2020-03',
70+
'--excluded-time-ranges', '2020-03-02 00:00:00,2020-03-03 05:00:00'
71+
)
72+
4573

4674
def test_new_allocation_quota_expired(self):
4775
"""Test that expiration doesn't affect invoicing."""
@@ -391,16 +419,17 @@ def get_excluded_interval_datetime_list(excluded_interval_list):
391419
]
392420

393421
# Single interval within active period
394-
excluded_intervals = get_excluded_interval_datetime_list(
395-
(((2020, 3, 15), (2020, 3, 16)),)
396-
)
422+
excluded_intervals = [
423+
(datetime.datetime(2020, 3, 15, 9, 30, 0),
424+
datetime.datetime(2020, 3, 16, 10, 30, 0)),
425+
]
397426

398427
value = utils.get_included_duration(
399428
datetime.datetime(2020, 3, 15, 0, 0, 0),
400429
datetime.datetime(2020, 3, 17, 0, 0, 0),
401430
excluded_intervals
402431
)
403-
self.assertEqual(value, SECONDS_IN_DAY * 1)
432+
self.assertEqual(value, SECONDS_IN_DAY * 1 - 3600)
404433

405434
# Interval starts before active period
406435
excluded_intervals = get_excluded_interval_datetime_list(
@@ -470,21 +499,21 @@ def test_load_excluded_intervals(self):
470499
]
471500
output = utils.load_excluded_intervals(interval_list)
472501
self.assertEqual(output, [
473-
[datetime.datetime(2023, 1, 1, 0, 0, 0),
474-
datetime.datetime(2023, 1, 2, 0, 0, 0)]
502+
[pytz.utc.localize(datetime.datetime(2023, 1, 1, 0, 0, 0)),
503+
pytz.utc.localize(datetime.datetime(2023, 1, 2, 0, 0, 0))]
475504
])
476505

477506
# More than 1 interval
478507
interval_list = [
479508
"2023-01-01,2023-01-02",
480-
"2023-01-04,2023-01-15",
509+
"2023-01-04 09:00:00,2023-01-15 10:00:00",
481510
]
482511
output = utils.load_excluded_intervals(interval_list)
483512
self.assertEqual(output, [
484-
[datetime.datetime(2023, 1, 1, 0, 0, 0),
485-
datetime.datetime(2023, 1, 2, 0, 0, 0)],
486-
[datetime.datetime(2023, 1, 4, 0, 0, 0),
487-
datetime.datetime(2023, 1, 15, 0, 0, 0)]
513+
[pytz.utc.localize(datetime.datetime(2023, 1, 1, 0, 0, 0)),
514+
pytz.utc.localize(datetime.datetime(2023, 1, 2, 0, 0, 0))],
515+
[pytz.utc.localize(datetime.datetime(2023, 1, 4, 9, 0, 0)),
516+
pytz.utc.localize(datetime.datetime(2023, 1, 15, 10, 0, 0))]
488517
])
489518

490519
def test_load_excluded_intervals_invalid(self):

src/coldfront_plugin_cloud/utils.py

+3-3
Original file line numberDiff line numberDiff line change
@@ -185,12 +185,12 @@ def check_overlapping_intervals(excluded_intervals_list):
185185
excluded_intervals_list = list()
186186
for interval in excluded_interval_arglist:
187187
start, end = interval.strip().split(",")
188-
start_dt, end_dt = [datetime.datetime.strptime(i, "%Y-%m-%d") for i in [start, end]]
188+
start_dt, end_dt = [datetime.datetime.fromisoformat(i) for i in [start, end]]
189189
assert end_dt > start_dt, f"Interval end date ({end}) is before start date ({start})!"
190190
excluded_intervals_list.append(
191191
[
192-
datetime.datetime.strptime(start, "%Y-%m-%d"),
193-
datetime.datetime.strptime(end, "%Y-%m-%d")
192+
pytz.utc.localize(datetime.datetime.fromisoformat(start)),
193+
pytz.utc.localize(datetime.datetime.fromisoformat(end))
194194
]
195195
)
196196

0 commit comments

Comments
 (0)