diff --git a/coral_credits/api/business_objects.py b/coral_credits/api/business_objects.py index 77c8438..27001c7 100644 --- a/coral_credits/api/business_objects.py +++ b/coral_credits/api/business_objects.py @@ -42,7 +42,7 @@ class PhysicalReservation(BaseReservation): class FlavorReservation(BaseReservation): amount: int flavor_id: str - affinity: str = "None" + affinity: Optional[str] = None @dataclass(kw_only=True) @@ -51,7 +51,7 @@ class VirtualReservation(BaseReservation): vcpus: int memory_mb: int disk_gb: int - affinity: str = "None" + affinity: Optional[str] = None resource_properties: Optional[str] = None diff --git a/coral_credits/api/db_utils.py b/coral_credits/api/db_utils.py index edac680..4111cab 100644 --- a/coral_credits/api/db_utils.py +++ b/coral_credits/api/db_utils.py @@ -313,9 +313,10 @@ def spend_credits( ) # Subtract expenditure from CreditAllocationResource # Or add, if the update delta is < 0 - credit_allocations[resource_class].resource_hours = ( + credit_allocations[resource_class].resource_hours = round( credit_allocations[resource_class].resource_hours - - resource_requests[resource_class] + - resource_requests[resource_class], + 0, ) credit_allocations[resource_class].save() diff --git a/coral_credits/api/serializers.py b/coral_credits/api/serializers.py index 8f38fb7..53e8603 100644 --- a/coral_credits/api/serializers.py +++ b/coral_credits/api/serializers.py @@ -130,7 +130,7 @@ class PhysicalReservationSerializer(BaseReservationSerializer): class FlavorReservationSerializer(BaseReservationSerializer): amount = serializers.IntegerField() flavor_id = serializers.CharField() - affinity = serializers.CharField(required=False, default="None") + affinity = serializers.CharField(required=False, default=None, allow_null=True) class VirtualReservationSerializer(BaseReservationSerializer): @@ -138,8 +138,8 @@ class VirtualReservationSerializer(BaseReservationSerializer): vcpus = serializers.IntegerField() memory_mb = serializers.IntegerField() disk_gb = serializers.IntegerField() - affinity = serializers.CharField(required=False, default="None") - resource_properties = serializers.CharField(required=False, allow_blank=True) + affinity = serializers.CharField(required=False, default=None, allow_blank=True) + resource_properties = serializers.CharField(required=False, allow_null=True) class ReservationFactory: diff --git a/coral_credits/api/tests/conftest.py b/coral_credits/api/tests/conftest.py index 2c30ce1..b316f7a 100644 --- a/coral_credits/api/tests/conftest.py +++ b/coral_credits/api/tests/conftest.py @@ -20,6 +20,7 @@ def pytest_configure(config): config.END_DATE = config.START_DATE + timedelta(days=1) config.END_EARLY_DATE = config.START_DATE + timedelta(days=0.75) config.END_LATE_DATE = config.START_DATE + timedelta(days=1.5) + config.START_EARLY_DATE = config.START_DATE + timedelta(days=-0.75) # Get auth token @@ -69,6 +70,16 @@ def credit_allocation(account, request): ) +@pytest.fixture +def early_credit_allocation(account, request): + return models.CreditAllocation.objects.create( + account=account, + name="test", + start=request.config.START_EARLY_DATE, + end=request.config.END_DATE, + ) + + @pytest.fixture def api_client(token): client = APIClient() @@ -162,7 +173,7 @@ def flavor_request_data(base_request_data): "amount": 2, "flavor_id": "e26a4241-b83d-4516-8e0e-8ce2665d1966", "resource_type": "flavor:instance", - "affinity": "None", + "affinity": None, "allocations": [], } ], @@ -196,6 +207,19 @@ def flavor_shorten_current_request_data(flavor_request_data, request): return deep_merge(flavor_request_data, shorten_request_data) +@pytest.fixture +def start_early_request_data(flavor_request_data, request): + zero_hours_request_data = { + "lease": { + "start_date": request.config.START_EARLY_DATE.isoformat(), + "end_date": ( + request.config.START_EARLY_DATE + timedelta(days=1) + ).isoformat(), + } + } + return deep_merge(flavor_request_data, zero_hours_request_data) + + @pytest.fixture def zero_hours_request_data(flavor_request_data, request): zero_hours_request_data = { @@ -244,7 +268,7 @@ def virtual_request_data(base_request_data): "vcpus": 1, "memory_mb": 1, "disk_gb": 0, - "affinity": "None", + "affinity": None, "resource_properties": "", "allocations": [], } diff --git a/coral_credits/api/tests/consumer_tests.py b/coral_credits/api/tests/consumer_tests.py index a49f031..dfbd9d8 100644 --- a/coral_credits/api/tests/consumer_tests.py +++ b/coral_credits/api/tests/consumer_tests.py @@ -157,6 +157,65 @@ def test_flavor_delete_upcoming_request( ) +@pytest.mark.parametrize( + "allocation_hours,request_data", + [ + ( + {"VCPU": 96.0, "MEMORY_MB": 24000.0, "DISK_GB": 840.0}, + lazy_fixture("start_early_request_data"), + ), + ], +) +@pytest.mark.django_db +def test_flavor_delete_current_request( + resource_classes, + early_credit_allocation, + create_credit_allocation_resources, + resource_provider_account, + api_client, + request_data, + allocation_hours, + request, # contains pytest global vars +): + # Allocate resource credit + create_credit_allocation_resources( + early_credit_allocation, resource_classes, allocation_hours + ) + + # Create + consumer_create_request(api_client, request_data, status.HTTP_204_NO_CONTENT) + + # Delete + consumer_delete_request(api_client, request_data, status.HTTP_204_NO_CONTENT) + + # Find consumer and check end date is changed. + new_consumer = models.Consumer.objects.filter( + consumer_ref=request.config.LEASE_NAME + ).first() + assert new_consumer is not None + + # END_EARLY_DATE is 3/4 the duration of the original allocation. + # on_end will set the end_time to datetime.now() + # so we should have consumed 75% of the reservation + # and refunded 25%. + + # Check our allocations are correct. + for resource_class in resource_classes: + c = models.CreditAllocationResource.objects.filter( + resource_class=resource_class.id + ).first() + assert c.resource_hours == allocation_hours[resource_class.name] * 0.25 + + # Check consumption records are correct + for resource_class in resource_classes: + rcr = models.ResourceConsumptionRecord.objects.get( + consumer=new_consumer, resource_class=resource_class.id + ) + assert rcr.resource_hours == pytest.approx( + allocation_hours[resource_class.name] * 0.75, 0.5 + ) + + @pytest.mark.parametrize( "allocation_hours,request_data,shorten_request_data", [ diff --git a/coral_credits/api/views.py b/coral_credits/api/views.py index e5fef0e..a076cf2 100644 --- a/coral_credits/api/views.py +++ b/coral_credits/api/views.py @@ -147,6 +147,7 @@ class ConsumerViewSet(viewsets.ModelViewSet): @action(detail=False, methods=["post"], url_path="create") def create_consumer(self, request): + LOG.info(f"About to process create commit:\n{request.data}") return self._create_or_update(request) @action(detail=False, methods=["post"], url_path="update") @@ -176,15 +177,18 @@ def on_end(self, request): # We can't just set everything to current time as we don't know # the latency between blazars request and our reception. # Unless we decide on how to round credit allocations. - if ( - datetime.fromisoformat(request.data["lease"]["start_date"]) - < time_now - ): - request.data["lease"]["end_date"] = request.data["lease"][ - "start_date" - ] - else: + req_start_date = datetime.fromisoformat( + request.data["lease"]["start_date"] + ) + if req_start_date.tzinfo is None: + req_start_date = make_aware(req_start_date) + if req_start_date < time_now: + # TODO(johngarbutt) we need to check what we have in the db! request.data["lease"]["end_date"] = time_now.isoformat() + else: + request.data["lease"]["end_date"] = req_start_date.isoformat() + request.data["current_lease"] = request.data["lease"] + LOG.info(f"About to process on-end request:\n{request.data}") return self._create_or_update( request, current_lease_required=True, dry_run=False )