Implement Exoscale onboarding API endpoint #199

Merged
rixx merged 14 commits from 37-exoscale-onboarding into main 2025-10-03 07:04:44 +00:00
3 changed files with 70 additions and 70 deletions
Showing only changes of commit e459047622 - Show all commits

View file

@ -14,7 +14,7 @@ from django.views.decorators.csrf import csrf_exempt
from servala.api.permissions import OSBBasicAuthPermission from servala.api.permissions import OSBBasicAuthPermission
from servala.core.exoscale import get_exoscale_origin from servala.core.exoscale import get_exoscale_origin
from servala.core.models import BillingEntity, Organization, User from servala.core.models import BillingEntity, Organization, User
from servala.core.models.service import Plan, Service from servala.core.models.service import Service, ServiceOffering
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -90,11 +90,13 @@ class OSBServiceInstanceView(OSBBasicAuthPermission, View):
return self._error(f"Unable to create user: {e}") return self._error(f"Unable to create user: {e}")
try: try:
service = Service.objects.get(id=service_id) service = Service.objects.get(osb_service_id=service_id)
plan = Plan.objects.get(id=plan_id, service_offering__service=service) service_offering = ServiceOffering.objects.get(
osb_plan_id=plan_id, service=service
)
except Service.DoesNotExist: except Service.DoesNotExist:
return self._error(f"Unknown service_id: {service_id}") return self._error(f"Unknown service_id: {service_id}")
except Plan.DoesNotExist: except ServiceOffering.DoesNotExist:
return self._error( return self._error(
f"Unknown plan_id: {plan_id} for service_id: {service_id}" f"Unknown plan_id: {plan_id} for service_id: {service_id}"
) )
@ -104,7 +106,9 @@ class OSBServiceInstanceView(OSBBasicAuthPermission, View):
organization = Organization.objects.get( organization = Organization.objects.get(
osb_guid=organization_guid, origin=exoscale_origin osb_guid=organization_guid, origin=exoscale_origin
) )
self._send_service_welcome_email(request, organization, user, service, plan) self._send_service_welcome_email(
request, organization, user, service, service_offering
)
return JsonResponse({"message": "Service already enabled"}, status=200) return JsonResponse({"message": "Service already enabled"}, status=200)
odoo_data = { odoo_data = {
@ -126,7 +130,7 @@ class OSBServiceInstanceView(OSBBasicAuthPermission, View):
self._send_invitation_email(request, organization, user) self._send_invitation_email(request, organization, user)
self._send_service_welcome_email( self._send_service_welcome_email(
request, organization, user, service, plan request, organization, user, service, service_offering
) )
return JsonResponse( return JsonResponse(
@ -159,8 +163,10 @@ The Servala Team"""
fail_silently=False, fail_silently=False,
) )
def _send_service_welcome_email(self, request, organization, user, service, plan): def _send_service_welcome_email(
service_path = f"{organization.urls.services}{service.slug}/offering/{plan.service_offering.id}/" self, request, organization, user, service, service_offering
):
service_path = f"{organization.urls.services}{service.slug}/offering/{service_offering.id}/"
service_url = request.build_absolute_uri(service_path) service_url = request.build_absolute_uri(service_path)
subject = f"Get started with {service.name} - {organization.name}" subject = f"Get started with {service.name} - {organization.name}"

View file

@ -10,7 +10,6 @@ from servala.core.models import (
) )
from servala.core.models.service import ( from servala.core.models.service import (
CloudProvider, CloudProvider,
Plan,
Service, Service,
ServiceCategory, ServiceCategory,
ServiceOffering, ServiceOffering,
@ -56,6 +55,7 @@ def test_service(test_service_category):
slug="redis", slug="redis",
category=test_service_category, category=test_service_category,
description="Redis database service", description="Redis database service",
osb_service_id="test-service-123",
) )
@ -73,18 +73,7 @@ def test_service_offering(test_service, test_cloud_provider):
service=test_service, service=test_service,
provider=test_cloud_provider, provider=test_cloud_provider,
description="Redis on Exoscale", description="Redis on Exoscale",
) osb_plan_id="test-plan-123",
@pytest.fixture
def test_plan(test_service_offering):
return Plan.objects.create(
name="Small",
description="Small Redis plan",
term=1,
service_offering=test_service_offering,
features={"memory": "1GB", "connections": 100},
pricing={"monthly": 10.0},
) )

View file

@ -50,16 +50,16 @@ def test_successful_onboarding_new_organization(
mock_odoo_success, mock_odoo_success,
osb_client, osb_client,
test_service, test_service,
test_plan, test_service_offering,
valid_osb_payload, valid_osb_payload,
exoscale_origin, exoscale_origin,
instance_id, instance_id,
): ):
valid_osb_payload["service_id"] = test_service.id valid_osb_payload["service_id"] = test_service.osb_service_id
valid_osb_payload["plan_id"] = test_plan.id valid_osb_payload["plan_id"] = test_service_offering.osb_plan_id
response = osb_client.put( response = osb_client.put(
f"/v2/service_instances/{instance_id}", f"/api/osb/v2/service_instances/{instance_id}",
data=json.dumps(valid_osb_payload), data=json.dumps(valid_osb_payload),
content_type="application/json", content_type="application/json",
) )
@ -102,7 +102,7 @@ def test_successful_onboarding_new_organization(
def test_duplicate_organization_returns_existing( def test_duplicate_organization_returns_existing(
osb_client, osb_client,
test_service, test_service,
test_plan, test_service_offering,
valid_osb_payload, valid_osb_payload,
exoscale_origin, exoscale_origin,
instance_id, instance_id,
@ -113,11 +113,11 @@ def test_duplicate_organization_returns_existing(
origin=exoscale_origin, origin=exoscale_origin,
) )
valid_osb_payload["service_id"] = test_service.id valid_osb_payload["service_id"] = test_service.osb_service_id
valid_osb_payload["plan_id"] = test_plan.id valid_osb_payload["plan_id"] = test_service_offering.osb_plan_id
response = osb_client.put( response = osb_client.put(
f"/v2/service_instances/{instance_id}", f"/api/osb/v2/service_instances/{instance_id}",
data=json.dumps(valid_osb_payload), data=json.dumps(valid_osb_payload),
content_type="application/json", content_type="application/json",
) )
@ -133,15 +133,15 @@ def test_duplicate_organization_returns_existing(
def test_unauthenticated_osb_api_request_fails( def test_unauthenticated_osb_api_request_fails(
client, client,
test_service, test_service,
test_plan, test_service_offering,
valid_osb_payload, valid_osb_payload,
instance_id, instance_id,
): ):
valid_osb_payload["service_id"] = test_service.id valid_osb_payload["service_id"] = test_service.osb_service_id
valid_osb_payload["plan_id"] = test_plan.id valid_osb_payload["plan_id"] = test_service_offering.osb_plan_id
response = client.put( response = client.put(
f"/v2/service_instances/{instance_id}", f"/api/osb/v2/service_instances/{instance_id}",
data=json.dumps(valid_osb_payload), data=json.dumps(valid_osb_payload),
content_type="application/json", content_type="application/json",
) )
@ -169,14 +169,14 @@ def test_unauthenticated_osb_api_request_fails(
def test_missing_required_fields_error( def test_missing_required_fields_error(
osb_client, osb_client,
test_service, test_service,
test_plan, test_service_offering,
valid_osb_payload, valid_osb_payload,
field_to_remove, field_to_remove,
expected_error, expected_error,
instance_id, instance_id,
): ):
valid_osb_payload["service_id"] = test_service.id valid_osb_payload["service_id"] = test_service.osb_service_id
valid_osb_payload["plan_id"] = test_plan.id valid_osb_payload["plan_id"] = test_service_offering.osb_plan_id
if isinstance(field_to_remove, tuple): if isinstance(field_to_remove, tuple):
if field_to_remove[0] == "context": if field_to_remove[0] == "context":
@ -188,7 +188,7 @@ def test_missing_required_fields_error(
del valid_osb_payload[field_to_remove] del valid_osb_payload[field_to_remove]
response = osb_client.put( response = osb_client.put(
f"/v2/service_instances/{instance_id}", f"/api/osb/v2/service_instances/{instance_id}",
data=json.dumps(valid_osb_payload), data=json.dumps(valid_osb_payload),
content_type="application/json", content_type="application/json",
) )
@ -204,7 +204,7 @@ def test_invalid_service_id_error(osb_client, valid_osb_payload, instance_id):
valid_osb_payload["plan_id"] = 1 valid_osb_payload["plan_id"] = 1
response = osb_client.put( response = osb_client.put(
f"/v2/service_instances/{instance_id}", f"/api/osb/v2/service_instances/{instance_id}",
data=json.dumps(valid_osb_payload), data=json.dumps(valid_osb_payload),
content_type="application/json", content_type="application/json",
) )
@ -218,11 +218,11 @@ def test_invalid_service_id_error(osb_client, valid_osb_payload, instance_id):
def test_invalid_plan_id_error( def test_invalid_plan_id_error(
osb_client, test_service, valid_osb_payload, instance_id osb_client, test_service, valid_osb_payload, instance_id
): ):
valid_osb_payload["service_id"] = test_service.id valid_osb_payload["service_id"] = test_service.osb_service_id
valid_osb_payload["plan_id"] = 99999 valid_osb_payload["plan_id"] = 99999
response = osb_client.put( response = osb_client.put(
f"/v2/service_instances/{instance_id}", f"/api/osb/v2/service_instances/{instance_id}",
data=json.dumps(valid_osb_payload), data=json.dumps(valid_osb_payload),
content_type="application/json", content_type="application/json",
) )
@ -230,21 +230,21 @@ def test_invalid_plan_id_error(
assert response.status_code == 400 assert response.status_code == 400
response_data = json.loads(response.content) response_data = json.loads(response.content)
assert ( assert (
f"Unknown plan_id: 99999 for service_id: {test_service.id}" f"Unknown plan_id: 99999 for service_id: {test_service.osb_service_id}"
in response_data["error"] in response_data["error"]
) )
@pytest.mark.django_db @pytest.mark.django_db
def test_empty_users_array_error( def test_empty_users_array_error(
osb_client, test_service, test_plan, valid_osb_payload, instance_id osb_client, test_service, test_service_offering, valid_osb_payload, instance_id
): ):
valid_osb_payload["service_id"] = test_service.id valid_osb_payload["service_id"] = test_service.osb_service_id
valid_osb_payload["plan_id"] = test_plan.id valid_osb_payload["plan_id"] = test_service_offering.osb_plan_id
valid_osb_payload["parameters"]["users"] = [] valid_osb_payload["parameters"]["users"] = []
response = osb_client.put( response = osb_client.put(
f"/v2/service_instances/{instance_id}", f"/api/osb/v2/service_instances/{instance_id}",
data=json.dumps(valid_osb_payload), data=json.dumps(valid_osb_payload),
content_type="application/json", content_type="application/json",
) )
@ -256,17 +256,17 @@ def test_empty_users_array_error(
@pytest.mark.django_db @pytest.mark.django_db
def test_multiple_users_error( def test_multiple_users_error(
osb_client, test_service, test_plan, valid_osb_payload, instance_id osb_client, test_service, test_service_offering, valid_osb_payload, instance_id
): ):
valid_osb_payload["service_id"] = test_service.id valid_osb_payload["service_id"] = test_service.osb_service_id
valid_osb_payload["plan_id"] = test_plan.id valid_osb_payload["plan_id"] = test_service_offering.osb_plan_id
valid_osb_payload["parameters"]["users"] = [ valid_osb_payload["parameters"]["users"] = [
{"email": "user1@example.com", "full_name": "User One"}, {"email": "user1@example.com", "full_name": "User One"},
{"email": "user2@example.com", "full_name": "User Two"}, {"email": "user2@example.com", "full_name": "User Two"},
] ]
response = osb_client.put( response = osb_client.put(
f"/v2/service_instances/{instance_id}", f"/api/osb/v2/service_instances/{instance_id}",
data=json.dumps(valid_osb_payload), data=json.dumps(valid_osb_payload),
content_type="application/json", content_type="application/json",
) )
@ -278,16 +278,16 @@ def test_multiple_users_error(
@pytest.mark.django_db @pytest.mark.django_db
def test_empty_email_address_error( def test_empty_email_address_error(
osb_client, test_service, test_plan, valid_osb_payload, instance_id osb_client, test_service, test_service_offering, valid_osb_payload, instance_id
): ):
valid_osb_payload["service_id"] = test_service.id valid_osb_payload["service_id"] = test_service.osb_service_id
valid_osb_payload["plan_id"] = test_plan.id valid_osb_payload["plan_id"] = test_service_offering.osb_plan_id
valid_osb_payload["parameters"]["users"] = [ valid_osb_payload["parameters"]["users"] = [
{"email": "", "full_name": "User With No Email"}, {"email": "", "full_name": "User With No Email"},
] ]
response = osb_client.put( response = osb_client.put(
f"/v2/service_instances/{instance_id}", f"/api/osb/v2/service_instances/{instance_id}",
data=json.dumps(valid_osb_payload), data=json.dumps(valid_osb_payload),
content_type="application/json", content_type="application/json",
) )
@ -300,7 +300,7 @@ def test_empty_email_address_error(
@pytest.mark.django_db @pytest.mark.django_db
def test_invalid_json_error(osb_client, instance_id): def test_invalid_json_error(osb_client, instance_id):
response = osb_client.put( response = osb_client.put(
f"/v2/service_instances/{instance_id}", f"/api/osb/v2/service_instances/{instance_id}",
data="invalid json{", data="invalid json{",
content_type="application/json", content_type="application/json",
) )
@ -315,17 +315,17 @@ def test_user_creation_with_name_parsing(
mock_odoo_success, mock_odoo_success,
osb_client, osb_client,
test_service, test_service,
test_plan, test_service_offering,
valid_osb_payload, valid_osb_payload,
exoscale_origin, exoscale_origin,
instance_id, instance_id,
): ):
valid_osb_payload["service_id"] = test_service.id valid_osb_payload["service_id"] = test_service.osb_service_id
valid_osb_payload["plan_id"] = test_plan.id valid_osb_payload["plan_id"] = test_service_offering.osb_plan_id
valid_osb_payload["parameters"]["users"][0]["full_name"] = "John Doe Smith" valid_osb_payload["parameters"]["users"][0]["full_name"] = "John Doe Smith"
response = osb_client.put( response = osb_client.put(
f"/v2/service_instances/{instance_id}", f"/api/osb/v2/service_instances/{instance_id}",
data=json.dumps(valid_osb_payload), data=json.dumps(valid_osb_payload),
content_type="application/json", content_type="application/json",
) )
@ -341,17 +341,17 @@ def test_email_normalization(
mock_odoo_success, mock_odoo_success,
osb_client, osb_client,
test_service, test_service,
test_plan, test_service_offering,
valid_osb_payload, valid_osb_payload,
exoscale_origin, exoscale_origin,
instance_id, instance_id,
): ):
valid_osb_payload["service_id"] = test_service.id valid_osb_payload["service_id"] = test_service.osb_service_id
valid_osb_payload["plan_id"] = test_plan.id valid_osb_payload["plan_id"] = test_service_offering.osb_plan_id
valid_osb_payload["parameters"]["users"][0]["email"] = " TEST@EXAMPLE.COM " valid_osb_payload["parameters"]["users"][0]["email"] = " TEST@EXAMPLE.COM "
response = osb_client.put( response = osb_client.put(
f"/v2/service_instances/{instance_id}", f"/api/osb/v2/service_instances/{instance_id}",
data=json.dumps(valid_osb_payload), data=json.dumps(valid_osb_payload),
content_type="application/json", content_type="application/json",
) )
@ -366,16 +366,16 @@ def test_odoo_integration_failure_handling(
mock_odoo_failure, mock_odoo_failure,
osb_client, osb_client,
test_service, test_service,
test_plan, test_service_offering,
valid_osb_payload, valid_osb_payload,
exoscale_origin, exoscale_origin,
instance_id, instance_id,
): ):
valid_osb_payload["service_id"] = test_service.id valid_osb_payload["service_id"] = test_service.osb_service_id
valid_osb_payload["plan_id"] = test_plan.id valid_osb_payload["plan_id"] = test_service_offering.osb_plan_id
response = osb_client.put( response = osb_client.put(
f"/v2/service_instances/{instance_id}", f"/api/osb/v2/service_instances/{instance_id}",
data=json.dumps(valid_osb_payload), data=json.dumps(valid_osb_payload),
content_type="application/json", content_type="application/json",
) )
@ -387,11 +387,16 @@ def test_odoo_integration_failure_handling(
@pytest.mark.django_db @pytest.mark.django_db
def test_organization_creation_with_context_only( def test_organization_creation_with_context_only(
mock_odoo_success, osb_client, test_service, test_plan, exoscale_origin, instance_id mock_odoo_success,
osb_client,
test_service,
test_service_offering,
exoscale_origin,
instance_id,
): ):
payload = { payload = {
"service_id": test_service.id, "service_id": test_service.osb_service_id,
"plan_id": test_plan.id, "plan_id": test_service_offering.osb_plan_id,
"context": { "context": {
"organization_guid": "fallback-org-guid", "organization_guid": "fallback-org-guid",
"organization_name": "Fallback Organization", "organization_name": "Fallback Organization",
@ -407,7 +412,7 @@ def test_organization_creation_with_context_only(
} }
response = osb_client.put( response = osb_client.put(
f"/v2/service_instances/{instance_id}", f"/api/osb/v2/service_instances/{instance_id}",
data=json.dumps(payload), data=json.dumps(payload),
content_type="application/json", content_type="application/json",
) )