Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Check create method + db unit test #6

Merged
merged 39 commits into from
Jul 23, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
39 commits
Select commit Hold shift + click to select a range
06ab246
ResourceProviderAccount
scrungus Jul 4, 2024
38e5725
add consumer_uuid + user_ref
scrungus Jul 4, 2024
4e4ae36
black
scrungus Jul 4, 2024
7bc0b86
create method
scrungus Jul 5, 2024
384202d
add to request
scrungus Jul 5, 2024
7fc3aa5
check_create method
scrungus Jul 5, 2024
242e23b
ConsumerRequest method adapted for blazar requests
scrungus Jul 5, 2024
23625a9
todo select_for_update
scrungus Jul 5, 2024
2449be5
black
scrungus Jul 5, 2024
758eea8
move to ConsumerViewSet
scrungus Jul 9, 2024
fe911f7
handle update case
scrungus Jul 9, 2024
2687b5b
arbitrary length resource request
scrungus Jul 9, 2024
c7eda83
ResourceProviderAccount
scrungus Jul 4, 2024
16b10ad
add consumer_uuid + user_ref
scrungus Jul 4, 2024
b2773b4
black
scrungus Jul 4, 2024
05b7591
Add helm chart with initial functional test (#2)
scrungus Jul 9, 2024
1417ac9
black
scrungus Jul 5, 2024
5dfadcf
move to ConsumerViewSet
scrungus Jul 9, 2024
16ed741
add dry run methods
scrungus Jul 9, 2024
ba767b3
look for 'total' in resource request
scrungus Jul 9, 2024
db201a1
todo
scrungus Jul 9, 2024
4f7ac49
allow empty
scrungus Jul 9, 2024
0bef819
black
scrungus Jul 9, 2024
db4e6fd
unittest-style tests
scrungus Jul 10, 2024
ce4d9b9
test settings
scrungus Jul 10, 2024
1eb6b69
use pytest
scrungus Jul 10, 2024
25fc0ed
consumer route
scrungus Jul 10, 2024
0ae13c7
formatting
scrungus Jul 10, 2024
cd4f5b8
fixes
scrungus Jul 10, 2024
2639563
init test file
scrungus Jul 10, 2024
ebf8d37
add unit test
scrungus Jul 10, 2024
47703e5
black
scrungus Jul 10, 2024
4be7fe2
Merge branch 'add-resourceprovideraccount-table' into check-create-me…
scrungus Jul 10, 2024
fc82077
black
scrungus Jul 10, 2024
56e3e0f
request object classes
scrungus Jul 16, 2024
2eddd7e
exceptions
scrungus Jul 16, 2024
b50b40d
refactor
scrungus Jul 16, 2024
f8944e3
black
scrungus Jul 16, 2024
528fdf3
separate unit tests
scrungus Jul 22, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 0 additions & 3 deletions .stestr.conf

This file was deleted.

62 changes: 62 additions & 0 deletions coral_credits/api/business_objects.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
from dataclasses import dataclass, field
from datetime import datetime
from typing import Any, Dict, List
from uuid import UUID


@dataclass
class Context:
user_id: UUID
project_id: UUID
auth_url: str
region_name: str


@dataclass
class Inventory:
data: Dict[str, Any]


@dataclass
class ResourceRequest:
inventories: Inventory
# TODO(tylerchristie)
# resource_provider_generation: int = None


@dataclass
class Allocation:
id: str
hypervisor_hostname: UUID
extra: Dict[str, Any]


@dataclass
class Reservation:
resource_type: str
min: int
max: int
resource_requests: ResourceRequest
hypervisor_properties: str = None
resource_properties: str = None
allocations: List[Allocation] = field(default_factory=list)


@dataclass
class Lease:
lease_id: UUID
lease_name: str
start_date: datetime
end_time: datetime
reservations: List[Reservation]

@property
def duration(self):
return (self.end_time - self.start_date).total_seconds() / 3600


@dataclass
class ConsumerRequest:
context: Context
lease: Lease
current_lease: Lease = None
16 changes: 16 additions & 0 deletions coral_credits/api/db_exceptions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
class ResourceRequestFormatError(Exception):
"""Raised when the resource request format is incorrect"""

pass


class InsufficientCredits(Exception):
"""Raised when an account has insufficient credits for a request"""

pass


class NoCreditAllocation(Exception):
"""Raised when an account has no credit allocated for a given resource"""

pass
179 changes: 179 additions & 0 deletions coral_credits/api/db_utils.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,179 @@
from django.shortcuts import get_object_or_404
from django.utils import timezone

from coral_credits.api import db_exceptions, models


def get_current_lease(current_lease):
current_consumer = get_object_or_404(
models.Consumer, consumer_uuid=current_lease.lease_id
)
current_resource_requests = models.CreditAllocationResource.objects.filter(
consumer=current_consumer,
)
return current_consumer, current_resource_requests


def get_resource_provider_account(project_id):
resource_provider_account = models.ResourceProviderAccount.objects.get(
project_id=project_id
)
return resource_provider_account


def get_credit_allocations(resource_provider_account):
# Find all associated active CreditAllocations
# Make sure we only look for CreditAllocations valid for the current time
now = timezone.now()
credit_allocations = models.CreditAllocation.objects.filter(
account=resource_provider_account.account, start__lte=now, end__gte=now
).order_by("-start")

return credit_allocations


def get_credit_allocation_resources(credit_allocations, resource_classes):
"""Returns a dictionary of the form:

{
"resource_class": "credit_resource_allocation"
}
"""
resource_allocations = {}
for credit_allocation in credit_allocations:
for resource_class in resource_classes:
credit_allocation_resource = models.CreditAllocationResource.objects.filter(
allocation=credit_allocation, resource_class=resource_class
).first()
if not credit_allocation_resource:
raise db_exceptions.NoCreditAllocation(
f"No credit allocated for resource_type {resource_class}"
)
resource_allocations[resource_class] = credit_allocation_resource
return resource_allocations


def get_resource_requests(lease, current_resource_requests=None):
"""Returns a dictionary of the form:

{
"resource_class": "resource_hours"
}
"""
resource_requests = {}

for reservation in lease.reservations:
for (
resource_type,
amount,
) in reservation.resource_requests.inventories.data.items():
resource_class = get_object_or_404(models.ResourceClass, name=resource_type)
try:
# Keep it simple, ust take min for now
# TODO(tylerchristie): check we can allocate max
# CreditAllocationResource is a record of the number of resource_hours
# available for one unit of a ResourceClass, so we multiply
# lease_duration by units required.
requested_resource_hours = round(
float(amount["total"]) * reservation.min * lease.duration,
1,
)
if current_resource_requests:
delta_resource_hours = calculate_delta_resource_hours(
requested_resource_hours,
current_resource_requests,
resource_class,
)
else:
delta_resource_hours = requested_resource_hours

resource_requests[resource_class] = delta_resource_hours

except KeyError:
raise db_exceptions.ResourceRequestFormatError(
f"Unable to recognize {resource_type} format {amount}"
)

return resource_requests


def calculate_delta_resource_hours(
requested_resource_hours, current_resource_requests, resource_class
):
# Case: user requests the same resource
current_resource_request = current_resource_requests.filter(
resource_class=resource_class
).first()
if current_resource_request:
current_resource_hours = current_resource_request.resource_hours
return requested_resource_hours - current_resource_hours
# Case: user requests a new resource
return requested_resource_hours


def check_credit_allocations(resource_requests, credit_allocations):
"""Subtracts resources requested from credit allocations.

Fails if any result is negative.
"""

result = {}
for resource_class in credit_allocations:
result[resource_class] = (
credit_allocations[resource_class].resource_hours
- resource_requests[resource_class]
)

if result[resource_class] < 0:
raise db_exceptions.InsufficientCredits(
f"Insufficient {resource_class.name} credits available. "
f"Requested:{resource_requests[resource_class]}, "
f"Available:{credit_allocations[resource_class]}"
)

return result


def check_credit_balance(credit_allocations, resource_requests):
# TODO(tylerchristie) Fresh DB query
credit_allocation_resources = get_credit_allocation_resources(
credit_allocations, resource_requests.keys()
)
for allocation in credit_allocation_resources.values():

if allocation.resource_hours < 0:
# We raise an exception so the rollback is handled
raise db_exceptions.InsufficientCredits(
(
f"Insufficient "
f"{allocation.resource_class.name} "
f"credits after allocation."
)
)


def spend_credits(
lease, resource_provider_account, context, resource_requests, credit_allocations
):

consumer = models.Consumer.objects.create(
consumer_ref=lease.lease_name,
consumer_uuid=lease.lease_id,
resource_provider_account=resource_provider_account,
user_ref=context.user_id,
start=lease.start_date,
end=lease.end_time,
)

for resource_class in resource_requests:
models.ResourceConsumptionRecord.objects.create(
consumer=consumer,
resource_class=resource_class,
resource_hours=resource_requests[resource_class],
)
# Subtract expenditure from CreditAllocationResource
credit_allocations[resource_class].resource_hours = (
credit_allocations[resource_class].resource_hours
- resource_requests[resource_class]
)
credit_allocations[resource_class].save()
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
# Generated by Django 5.0.6 on 2024-07-04 15:54

import django.db.models.deletion
from django.db import migrations, models


class Migration(migrations.Migration):
dependencies = [
("api", "0003_rename_consume_ref_consumer_consumer_ref_and_more"),
]

operations = [
migrations.CreateModel(
name="ResourceProviderAccount",
fields=[
(
"id",
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
("project_id", models.UUIDField()),
(
"account",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
to="api.creditaccount",
),
),
(
"provider",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
to="api.resourceprovider",
),
),
],
options={
"unique_together": {
("account", "provider"),
("provider", "project_id"),
},
},
),
migrations.AlterUniqueTogether(
name="consumer",
unique_together=set(),
),
migrations.AddField(
model_name="consumer",
name="resource_provider_account",
field=models.ForeignKey(
default=1,
on_delete=django.db.models.deletion.DO_NOTHING,
to="api.resourceprovideraccount",
),
preserve_default=False,
),
migrations.AlterUniqueTogether(
name="consumer",
unique_together={("consumer_ref", "resource_provider_account")},
),
migrations.RemoveField(
model_name="consumer",
name="account",
),
migrations.RemoveField(
model_name="consumer",
name="resource_provider",
),
]
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
# Generated by Django 5.0.6 on 2024-07-04 16:20

import uuid

from django.db import migrations, models


class Migration(migrations.Migration):
dependencies = [
("api", "0004_resourceprovideraccount_and_more"),
]

operations = [
migrations.AddField(
model_name="consumer",
name="consumer_uuid",
field=models.UUIDField(default=uuid.uuid4),
preserve_default=False,
),
migrations.AddField(
model_name="consumer",
name="user_ref",
field=models.UUIDField(default=uuid.uuid4),
preserve_default=False,
),
]
Loading
Loading