initial work
This commit is contained in:
parent
8c885b2b24
commit
81e55a2134
13 changed files with 335 additions and 0 deletions
0
hub/broker/__init__.py
Normal file
0
hub/broker/__init__.py
Normal file
13
hub/broker/admin.py
Normal file
13
hub/broker/admin.py
Normal file
|
@ -0,0 +1,13 @@
|
|||
from django.contrib import admin
|
||||
from .models import ServiceBrokerUser, ServiceInstance
|
||||
|
||||
|
||||
@admin.register(ServiceBrokerUser)
|
||||
class ServiceBrokerUserAdmin(admin.ModelAdmin):
|
||||
list_display = ["user"]
|
||||
search_fields = ["user"]
|
||||
|
||||
|
||||
@admin.register(ServiceInstance)
|
||||
class ServiceInstanceAdmin(admin.ModelAdmin):
|
||||
list_display = ("instance_id", "offering")
|
6
hub/broker/apps.py
Normal file
6
hub/broker/apps.py
Normal file
|
@ -0,0 +1,6 @@
|
|||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class BrokerConfig(AppConfig):
|
||||
default_auto_field = "django.db.models.BigAutoField"
|
||||
name = "hub.broker"
|
22
hub/broker/authentication.py
Normal file
22
hub/broker/authentication.py
Normal file
|
@ -0,0 +1,22 @@
|
|||
from rest_framework.authentication import BasicAuthentication
|
||||
from rest_framework.exceptions import AuthenticationFailed
|
||||
from django.contrib.auth.models import User
|
||||
from .models import ServiceBrokerUser
|
||||
|
||||
|
||||
class ServiceBrokerAuthentication(BasicAuthentication):
|
||||
def authenticate_credentials(self, userid, password, request=None):
|
||||
try:
|
||||
user = User.objects.get(username=userid)
|
||||
if not user.check_password(password):
|
||||
raise AuthenticationFailed("Invalid password")
|
||||
|
||||
# Ensure user has broker permissions
|
||||
try:
|
||||
broker_user = ServiceBrokerUser.objects.get(user=user)
|
||||
except ServiceBrokerUser.DoesNotExist:
|
||||
raise AuthenticationFailed("User is not authorized for broker access")
|
||||
|
||||
return (user, None)
|
||||
except User.DoesNotExist:
|
||||
raise AuthenticationFailed("Invalid username")
|
86
hub/broker/migrations/0001_initial.py
Normal file
86
hub/broker/migrations/0001_initial.py
Normal file
|
@ -0,0 +1,86 @@
|
|||
# Generated by Django 5.1.5 on 2025-01-31 14:25
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
("services", "0002_lead_offering"),
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name="ServiceBrokerUser",
|
||||
fields=[
|
||||
(
|
||||
"id",
|
||||
models.BigAutoField(
|
||||
auto_created=True,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
verbose_name="ID",
|
||||
),
|
||||
),
|
||||
(
|
||||
"allowed_offerings",
|
||||
models.ManyToManyField(blank=True, to="services.serviceoffering"),
|
||||
),
|
||||
(
|
||||
"user",
|
||||
models.OneToOneField(
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
to=settings.AUTH_USER_MODEL,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name="ServiceInstance",
|
||||
fields=[
|
||||
(
|
||||
"id",
|
||||
models.BigAutoField(
|
||||
auto_created=True,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
verbose_name="ID",
|
||||
),
|
||||
),
|
||||
("instance_id", models.CharField(max_length=100, unique=True)),
|
||||
("organization_guid", models.CharField(max_length=100)),
|
||||
("space_guid", models.CharField(max_length=100)),
|
||||
("organization_name", models.CharField(max_length=200)),
|
||||
("parameters", models.JSONField(default=dict)),
|
||||
("context", models.JSONField(default=dict)),
|
||||
("created_at", models.DateTimeField(auto_now_add=True)),
|
||||
("updated_at", models.DateTimeField(auto_now=True)),
|
||||
(
|
||||
"lead",
|
||||
models.ForeignKey(
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.SET_NULL,
|
||||
to="services.lead",
|
||||
),
|
||||
),
|
||||
(
|
||||
"offering",
|
||||
models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.PROTECT,
|
||||
to="services.serviceoffering",
|
||||
),
|
||||
),
|
||||
(
|
||||
"plan",
|
||||
models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.PROTECT, to="services.plan"
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
]
|
0
hub/broker/migrations/__init__.py
Normal file
0
hub/broker/migrations/__init__.py
Normal file
28
hub/broker/models.py
Normal file
28
hub/broker/models.py
Normal file
|
@ -0,0 +1,28 @@
|
|||
from django.db import models
|
||||
from django.contrib.auth.models import User
|
||||
from hub.services.models import ServiceOffering
|
||||
|
||||
|
||||
class ServiceBrokerUser(models.Model):
|
||||
user = models.OneToOneField(User, on_delete=models.CASCADE)
|
||||
allowed_offerings = models.ManyToManyField(ServiceOffering, blank=True)
|
||||
|
||||
def __str__(self):
|
||||
return f"Broker User: {self.user.username}"
|
||||
|
||||
|
||||
class ServiceInstance(models.Model):
|
||||
instance_id = models.CharField(max_length=100, unique=True)
|
||||
offering = models.ForeignKey("services.ServiceOffering", on_delete=models.PROTECT)
|
||||
plan = models.ForeignKey("services.Plan", on_delete=models.PROTECT)
|
||||
organization_guid = models.CharField(max_length=100)
|
||||
space_guid = models.CharField(max_length=100)
|
||||
organization_name = models.CharField(max_length=200)
|
||||
parameters = models.JSONField(default=dict)
|
||||
context = models.JSONField(default=dict)
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
updated_at = models.DateTimeField(auto_now=True)
|
||||
lead = models.ForeignKey("services.Lead", on_delete=models.SET_NULL, null=True)
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.offering.service.name} on {self.offering.cloud_provider.name} - {self.instance_id}"
|
49
hub/broker/serializers.py
Normal file
49
hub/broker/serializers.py
Normal file
|
@ -0,0 +1,49 @@
|
|||
from rest_framework import serializers
|
||||
from hub.services.models import ServiceOffering, Plan
|
||||
|
||||
|
||||
class ServiceUserSerializer(serializers.Serializer):
|
||||
email = serializers.EmailField()
|
||||
full_name = serializers.CharField()
|
||||
role = serializers.CharField()
|
||||
|
||||
|
||||
class ProvisioningContextSerializer(serializers.Serializer):
|
||||
platform = serializers.CharField()
|
||||
organization_guid = serializers.CharField()
|
||||
space_guid = serializers.CharField()
|
||||
organization_name = serializers.CharField()
|
||||
organization_display_name = serializers.CharField()
|
||||
|
||||
|
||||
class ProvisioningParametersSerializer(serializers.Serializer):
|
||||
users = ServiceUserSerializer(many=True)
|
||||
|
||||
|
||||
class ServiceInstanceProvisioningSerializer(serializers.Serializer):
|
||||
service_id = serializers.CharField() # This will now represent the offering_id
|
||||
plan_id = serializers.CharField()
|
||||
organization_guid = serializers.CharField()
|
||||
space_guid = serializers.CharField()
|
||||
parameters = ProvisioningParametersSerializer()
|
||||
context = ProvisioningContextSerializer()
|
||||
|
||||
def validate_service_id(self, value):
|
||||
"""Validate the service_id which now represents the offering_id"""
|
||||
try:
|
||||
return ServiceOffering.objects.get(pk=value)
|
||||
except ServiceOffering.DoesNotExist:
|
||||
raise serializers.ValidationError("Invalid offering_id")
|
||||
|
||||
def validate_plan_id(self, value):
|
||||
try:
|
||||
# Get the plan and ensure it belongs to the selected offering
|
||||
plan = Plan.objects.get(pk=value)
|
||||
offering = self.initial_data.get("service_id")
|
||||
if str(plan.offering.id) != offering:
|
||||
raise serializers.ValidationError(
|
||||
"Plan does not belong to the selected offering"
|
||||
)
|
||||
return plan
|
||||
except Plan.DoesNotExist:
|
||||
raise serializers.ValidationError("Invalid plan_id")
|
3
hub/broker/tests.py
Normal file
3
hub/broker/tests.py
Normal file
|
@ -0,0 +1,3 @@
|
|||
from django.test import TestCase
|
||||
|
||||
# Create your tests here.
|
12
hub/broker/urls.py
Normal file
12
hub/broker/urls.py
Normal file
|
@ -0,0 +1,12 @@
|
|||
from django.urls import path, include
|
||||
from rest_framework.routers import DefaultRouter
|
||||
from .views import ServiceBrokerViewSet
|
||||
|
||||
app_name = "broker"
|
||||
|
||||
router = DefaultRouter(trailing_slash=False)
|
||||
router.register("v2", ServiceBrokerViewSet, basename="broker")
|
||||
|
||||
urlpatterns = [
|
||||
path("", include(router.urls)),
|
||||
]
|
114
hub/broker/views.py
Normal file
114
hub/broker/views.py
Normal file
|
@ -0,0 +1,114 @@
|
|||
import logging
|
||||
from rest_framework import viewsets, status
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.decorators import action
|
||||
from django.shortcuts import get_object_or_404
|
||||
from hub.services.models import ServiceOffering, Lead
|
||||
from hub.services.odoo import OdooAPI
|
||||
from .models import ServiceInstance, ServiceBrokerUser
|
||||
from .serializers import ServiceInstanceProvisioningSerializer
|
||||
from .authentication import ServiceBrokerAuthentication
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class ServiceBrokerViewSet(viewsets.ViewSet):
|
||||
authentication_classes = [ServiceBrokerAuthentication]
|
||||
|
||||
def _create_lead_from_provision_data(self, offering, plan, parameters, context):
|
||||
"""Create a lead in Odoo from provisioning data"""
|
||||
# Get the first user from parameters as the main contact
|
||||
user_data = parameters["users"][0]
|
||||
|
||||
lead = Lead(
|
||||
service=offering.service,
|
||||
offering=offering,
|
||||
plan=plan,
|
||||
name=user_data["full_name"],
|
||||
company=context["organization_display_name"],
|
||||
email=user_data["email"],
|
||||
phone="", # Not provided in broker API
|
||||
)
|
||||
|
||||
try:
|
||||
odoo = OdooAPI()
|
||||
lead.odoo_lead_id = odoo.create_lead(lead)
|
||||
lead.save()
|
||||
return lead
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to create lead in Odoo: {str(e)}")
|
||||
raise
|
||||
|
||||
@action(
|
||||
detail=False,
|
||||
methods=["put"],
|
||||
url_path="service_instances/(?P<instance_id>[^/.]+)",
|
||||
)
|
||||
def provision_instance(self, request, instance_id):
|
||||
# Validate request data
|
||||
serializer = ServiceInstanceProvisioningSerializer(data=request.data)
|
||||
if not serializer.is_valid():
|
||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
# Get broker user and check offering access
|
||||
broker_user = get_object_or_404(ServiceBrokerUser, user=request.user)
|
||||
offering = serializer.validated_data["service_id"] # This is now the offering
|
||||
|
||||
if not broker_user.allowed_offerings.filter(id=offering.id).exists():
|
||||
return Response(
|
||||
{"error": "Service offering not available for this user"},
|
||||
status=status.HTTP_403_FORBIDDEN,
|
||||
)
|
||||
|
||||
# Check if instance already exists
|
||||
if ServiceInstance.objects.filter(instance_id=instance_id).exists():
|
||||
return Response(status=status.HTTP_200_OK)
|
||||
|
||||
try:
|
||||
# Create lead in Odoo
|
||||
lead = self._create_lead_from_provision_data(
|
||||
offering=offering,
|
||||
plan=serializer.validated_data["plan_id"],
|
||||
parameters=serializer.validated_data["parameters"],
|
||||
context=serializer.validated_data["context"],
|
||||
)
|
||||
|
||||
# Create service instance
|
||||
instance = ServiceInstance.objects.create(
|
||||
instance_id=instance_id,
|
||||
offering=offering,
|
||||
plan=serializer.validated_data["plan_id"],
|
||||
organization_guid=serializer.validated_data["organization_guid"],
|
||||
space_guid=serializer.validated_data["space_guid"],
|
||||
organization_name=serializer.validated_data["context"][
|
||||
"organization_name"
|
||||
],
|
||||
parameters=serializer.validated_data["parameters"],
|
||||
context=serializer.validated_data["context"],
|
||||
lead=lead,
|
||||
)
|
||||
|
||||
return Response(
|
||||
{
|
||||
"dashboard_url": f"/services/{offering.service.slug}",
|
||||
"operation": "provision",
|
||||
},
|
||||
status=status.HTTP_201_CREATED,
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error provisioning instance: {str(e)}")
|
||||
return Response(
|
||||
{"error": "Failed to provision service instance"},
|
||||
status=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
)
|
||||
|
||||
@action(
|
||||
detail=False,
|
||||
methods=["delete"],
|
||||
url_path="service_instances/(?P<instance_id>[^/.]+)",
|
||||
)
|
||||
def deprovision_instance(self, request, instance_id):
|
||||
instance = get_object_or_404(ServiceInstance, instance_id=instance_id)
|
||||
instance.delete()
|
||||
return Response(status=status.HTTP_200_OK)
|
|
@ -60,6 +60,7 @@ INSTALLED_APPS = [
|
|||
"schema_viewer",
|
||||
# local
|
||||
"hub.services",
|
||||
"hub.broker",
|
||||
]
|
||||
if DEBUG:
|
||||
INSTALLED_APPS += ["django_browser_reload"]
|
||||
|
|
|
@ -6,6 +6,7 @@ from django.urls import path, include
|
|||
urlpatterns = [
|
||||
path("admin/", admin.site.urls),
|
||||
path("", include("hub.services.urls")),
|
||||
path("broker/", include("hub.broker.urls", namespace="broker")),
|
||||
]
|
||||
if settings.DEBUG:
|
||||
urlpatterns += [
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue