initial version of osb
This commit is contained in:
parent
7143234e22
commit
022f0ad60f
14 changed files with 281 additions and 22 deletions
|
@ -8,6 +8,29 @@ env.read_env()
|
|||
# Build paths inside the project like this: BASE_DIR / 'subdir'.
|
||||
BASE_DIR = Path(__file__).resolve().parent.parent
|
||||
|
||||
LOGGING = {
|
||||
"version": 1,
|
||||
"disable_existing_loggers": False,
|
||||
"formatters": {
|
||||
"verbose": {
|
||||
"format": "{levelname} {asctime} {module} {process:d} {thread:d} {message}",
|
||||
"style": "{",
|
||||
},
|
||||
},
|
||||
"handlers": {
|
||||
"console": {
|
||||
"class": "logging.StreamHandler",
|
||||
"formatter": "verbose",
|
||||
}
|
||||
},
|
||||
"loggers": {
|
||||
"odoo_api": {
|
||||
"handlers": ["console"],
|
||||
"level": "DEBUG",
|
||||
"propagate": True,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
# Quick-start development settings - unsuitable for production
|
||||
# See https://docs.djangoproject.com/en/5.1/howto/deployment/checklist/
|
||||
|
@ -33,8 +56,10 @@ INSTALLED_APPS = [
|
|||
"django.contrib.staticfiles",
|
||||
# 3rd party
|
||||
"django_prose_editor",
|
||||
"rest_framework",
|
||||
# local
|
||||
"services",
|
||||
"servicebroker",
|
||||
]
|
||||
if DEBUG:
|
||||
INSTALLED_APPS += ["django_browser_reload"]
|
||||
|
@ -139,26 +164,13 @@ ODOO_CONFIG = {
|
|||
"password": env.str("ODOO_PASSWORD"),
|
||||
}
|
||||
|
||||
LOGGING = {
|
||||
"version": 1,
|
||||
"disable_existing_loggers": False,
|
||||
"formatters": {
|
||||
"verbose": {
|
||||
"format": "{levelname} {asctime} {module} {process:d} {thread:d} {message}",
|
||||
"style": "{",
|
||||
},
|
||||
},
|
||||
"handlers": {
|
||||
"console": {
|
||||
"class": "logging.StreamHandler",
|
||||
"formatter": "verbose",
|
||||
}
|
||||
},
|
||||
"loggers": {
|
||||
"odoo_api": {
|
||||
"handlers": ["console"],
|
||||
"level": "DEBUG",
|
||||
"propagate": True,
|
||||
},
|
||||
},
|
||||
BROKER_USERNAME = env.str("BROKER_USERNAME")
|
||||
BROKER_PASSWORD = env.str("BROKER_PASSWORD")
|
||||
BASE_URL = "https://your-domain.com"
|
||||
|
||||
REST_FRAMEWORK = {
|
||||
"DEFAULT_AUTHENTICATION_CLASSES": [
|
||||
"servicebroker.authentication.ServiceBrokerAuthentication",
|
||||
],
|
||||
"UNAUTHENTICATED_USER": None,
|
||||
}
|
||||
|
|
|
@ -6,6 +6,7 @@ from django.urls import path, include
|
|||
urlpatterns = [
|
||||
path("admin/", admin.site.urls),
|
||||
path("", include("services.urls")),
|
||||
path("broker/", include("servicebroker.urls")),
|
||||
]
|
||||
if settings.DEBUG:
|
||||
urlpatterns += [
|
||||
|
|
0
hub/servicebroker/__init__.py
Normal file
0
hub/servicebroker/__init__.py
Normal file
3
hub/servicebroker/admin.py
Normal file
3
hub/servicebroker/admin.py
Normal file
|
@ -0,0 +1,3 @@
|
|||
from django.contrib import admin
|
||||
|
||||
# Register your models here.
|
6
hub/servicebroker/apps.py
Normal file
6
hub/servicebroker/apps.py
Normal file
|
@ -0,0 +1,6 @@
|
|||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class ServicebrokerConfig(AppConfig):
|
||||
default_auto_field = "django.db.models.BigAutoField"
|
||||
name = "servicebroker"
|
34
hub/servicebroker/authentication.py
Normal file
34
hub/servicebroker/authentication.py
Normal file
|
@ -0,0 +1,34 @@
|
|||
from django.conf import settings
|
||||
from django.contrib.auth.models import User
|
||||
from rest_framework import authentication
|
||||
from rest_framework import exceptions
|
||||
|
||||
|
||||
class ServiceBrokerAuthentication(authentication.BaseAuthentication):
|
||||
def authenticate(self, request):
|
||||
auth = request.META.get("HTTP_AUTHORIZATION")
|
||||
if not auth:
|
||||
return None
|
||||
|
||||
try:
|
||||
import base64
|
||||
|
||||
auth_type, auth_string = auth.split(" ")
|
||||
if auth_type.lower() != "basic":
|
||||
return None
|
||||
|
||||
decoded = base64.b64decode(auth_string).decode("utf-8")
|
||||
username, password = decoded.split(":")
|
||||
|
||||
if (
|
||||
username == settings.BROKER_USERNAME
|
||||
and password == settings.BROKER_PASSWORD
|
||||
):
|
||||
# Use a dummy user for authentication
|
||||
user = User(username=username, is_staff=True)
|
||||
return (user, None)
|
||||
|
||||
except Exception as e:
|
||||
raise exceptions.AuthenticationFailed("Invalid credentials")
|
||||
|
||||
raise exceptions.AuthenticationFailed("Invalid credentials")
|
0
hub/servicebroker/migrations/__init__.py
Normal file
0
hub/servicebroker/migrations/__init__.py
Normal file
3
hub/servicebroker/models.py
Normal file
3
hub/servicebroker/models.py
Normal file
|
@ -0,0 +1,3 @@
|
|||
from django.db import models
|
||||
|
||||
# Create your models here.
|
51
hub/servicebroker/serializers.py
Normal file
51
hub/servicebroker/serializers.py
Normal file
|
@ -0,0 +1,51 @@
|
|||
from rest_framework import serializers
|
||||
from services.models import Service, ServiceLevel, CloudProvider
|
||||
|
||||
|
||||
class ServicePlanSerializer(serializers.Serializer):
|
||||
id = serializers.CharField()
|
||||
name = serializers.CharField()
|
||||
description = serializers.CharField()
|
||||
metadata = serializers.DictField()
|
||||
free = serializers.BooleanField(default=False)
|
||||
|
||||
|
||||
class ServiceSerializer(serializers.Serializer):
|
||||
id = serializers.CharField()
|
||||
name = serializers.CharField()
|
||||
description = serializers.CharField()
|
||||
bindable = serializers.BooleanField(default=True)
|
||||
plans = ServicePlanSerializer(many=True)
|
||||
metadata = serializers.DictField()
|
||||
tags = serializers.ListField(child=serializers.CharField())
|
||||
|
||||
|
||||
class CatalogSerializer(serializers.Serializer):
|
||||
services = ServiceSerializer(many=True)
|
||||
|
||||
|
||||
class ProvisionRequestSerializer(serializers.Serializer):
|
||||
service_id = serializers.CharField()
|
||||
plan_id = serializers.CharField()
|
||||
organization_guid = serializers.CharField(required=False)
|
||||
space_guid = serializers.CharField(required=False)
|
||||
parameters = serializers.DictField(required=False)
|
||||
|
||||
|
||||
class ProvisionResponseSerializer(serializers.Serializer):
|
||||
dashboard_url = serializers.URLField(required=False)
|
||||
operation = serializers.CharField(required=False, allow_null=True)
|
||||
|
||||
|
||||
class BindingRequestSerializer(serializers.Serializer):
|
||||
service_id = serializers.CharField()
|
||||
plan_id = serializers.CharField()
|
||||
bind_resource = serializers.DictField(required=False)
|
||||
parameters = serializers.DictField(required=False)
|
||||
|
||||
|
||||
class BindingResponseSerializer(serializers.Serializer):
|
||||
credentials = serializers.DictField()
|
||||
syslog_drain_url = serializers.URLField(required=False, allow_null=True)
|
||||
route_service_url = serializers.URLField(required=False, allow_null=True)
|
||||
volume_mounts = serializers.ListField(required=False)
|
3
hub/servicebroker/tests.py
Normal file
3
hub/servicebroker/tests.py
Normal file
|
@ -0,0 +1,3 @@
|
|||
from django.test import TestCase
|
||||
|
||||
# Create your tests here.
|
18
hub/servicebroker/urls.py
Normal file
18
hub/servicebroker/urls.py
Normal file
|
@ -0,0 +1,18 @@
|
|||
from django.urls import path
|
||||
from . import views
|
||||
|
||||
app_name = "servicebroker"
|
||||
|
||||
urlpatterns = [
|
||||
path("v2/catalog", views.CatalogView.as_view(), name="catalog"),
|
||||
path(
|
||||
"v2/service_instances/<str:instance_id>",
|
||||
views.ProvisioningView.as_view(),
|
||||
name="provisioning",
|
||||
),
|
||||
path(
|
||||
"v2/service_instances/<str:instance_id>/service_bindings/<str:binding_id>",
|
||||
views.BindingView.as_view(),
|
||||
name="binding",
|
||||
),
|
||||
]
|
113
hub/servicebroker/views.py
Normal file
113
hub/servicebroker/views.py
Normal file
|
@ -0,0 +1,113 @@
|
|||
from rest_framework.views import APIView
|
||||
from rest_framework.response import Response
|
||||
from rest_framework import status
|
||||
from django.conf import settings
|
||||
from .authentication import ServiceBrokerAuthentication
|
||||
from .serializers import (
|
||||
CatalogSerializer,
|
||||
ProvisionRequestSerializer,
|
||||
ProvisionResponseSerializer,
|
||||
BindingRequestSerializer,
|
||||
BindingResponseSerializer,
|
||||
)
|
||||
from services.models import Service, ServiceLevel
|
||||
|
||||
|
||||
class ServiceBrokerView(APIView):
|
||||
authentication_classes = [ServiceBrokerAuthentication]
|
||||
|
||||
def get_broker_version(self, request):
|
||||
return request.META.get("HTTP_X_BROKER_API_VERSION", "2.14")
|
||||
|
||||
|
||||
class CatalogView(ServiceBrokerView):
|
||||
def get(self, request):
|
||||
services = []
|
||||
marketplace_services = Service.objects.all().prefetch_related(
|
||||
"cloud_provider", "service_level", "categories"
|
||||
)
|
||||
|
||||
for service in marketplace_services:
|
||||
plans = []
|
||||
service_levels = ServiceLevel.objects.all()
|
||||
|
||||
for level in service_levels:
|
||||
plan = {
|
||||
"id": f"plan-{service.id}-{level.id}",
|
||||
"name": level.name,
|
||||
"description": level.description,
|
||||
"metadata": {
|
||||
"costs": [{"amount": float(service.price), "unit": "MONTHLY"}],
|
||||
"bullets": [level.description],
|
||||
},
|
||||
"free": False,
|
||||
}
|
||||
plans.append(plan)
|
||||
|
||||
service_data = {
|
||||
"id": f"service-{service.id}",
|
||||
"name": service.name,
|
||||
"description": service.description,
|
||||
"bindable": True,
|
||||
"plans": plans,
|
||||
"metadata": {
|
||||
"displayName": service.name,
|
||||
"provider": service.cloud_provider.name,
|
||||
"imageUrl": service.logo.url if service.logo else None,
|
||||
"categories": [cat.name for cat in service.categories.all()],
|
||||
},
|
||||
"tags": [cat.name for cat in service.categories.all()],
|
||||
}
|
||||
services.append(service_data)
|
||||
|
||||
catalog = {"services": services}
|
||||
serializer = CatalogSerializer(data=catalog)
|
||||
serializer.is_valid(raise_exception=True)
|
||||
return Response(serializer.data)
|
||||
|
||||
|
||||
class ProvisioningView(ServiceBrokerView):
|
||||
def put(self, request, instance_id):
|
||||
serializer = ProvisionRequestSerializer(data=request.data)
|
||||
serializer.is_valid(raise_exception=True)
|
||||
|
||||
service_id = serializer.validated_data["service_id"]
|
||||
marketplace_service_id = service_id.replace("service-", "")
|
||||
service = Service.objects.get(id=marketplace_service_id)
|
||||
|
||||
response_data = {
|
||||
"dashboard_url": f"{settings.BASE_URL}/service/{service.slug}/",
|
||||
"operation": None,
|
||||
}
|
||||
|
||||
response_serializer = ProvisionResponseSerializer(data=response_data)
|
||||
response_serializer.is_valid(raise_exception=True)
|
||||
return Response(response_serializer.data, status=status.HTTP_201_CREATED)
|
||||
|
||||
def delete(self, request, instance_id):
|
||||
return Response(status=status.HTTP_200_OK)
|
||||
|
||||
|
||||
class BindingView(ServiceBrokerView):
|
||||
def put(self, request, instance_id, binding_id):
|
||||
serializer = BindingRequestSerializer(data=request.data)
|
||||
serializer.is_valid(raise_exception=True)
|
||||
|
||||
service_id = serializer.validated_data["service_id"]
|
||||
marketplace_service_id = service_id.replace("service-", "")
|
||||
service = Service.objects.get(id=marketplace_service_id)
|
||||
|
||||
credentials = {
|
||||
"service_name": service.name,
|
||||
"provider": service.cloud_provider.name,
|
||||
"service_url": f"{settings.BASE_URL}/service/{service.slug}/",
|
||||
}
|
||||
|
||||
response_data = {"credentials": credentials}
|
||||
|
||||
response_serializer = BindingResponseSerializer(data=response_data)
|
||||
response_serializer.is_valid(raise_exception=True)
|
||||
return Response(response_serializer.data, status=status.HTTP_201_CREATED)
|
||||
|
||||
def delete(self, request, instance_id, binding_id):
|
||||
return Response(status=status.HTTP_200_OK)
|
|
@ -7,6 +7,7 @@ requires-python = ">=3.13"
|
|||
dependencies = [
|
||||
"django>=5.1.5",
|
||||
"django-prose-editor[sanitize]>=0.10.3",
|
||||
"djangorestframework>=3.15.2",
|
||||
"environs[django]~=14.0",
|
||||
"odoorpc>=0.10.1",
|
||||
"pillow>=11.1.0",
|
||||
|
|
14
uv.lock
generated
14
uv.lock
generated
|
@ -98,6 +98,18 @@ sanitize = [
|
|||
{ name = "nh3" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "djangorestframework"
|
||||
version = "3.15.2"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "django" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/2c/ce/31482eb688bdb4e271027076199e1aa8d02507e530b6d272ab8b4481557c/djangorestframework-3.15.2.tar.gz", hash = "sha256:36fe88cd2d6c6bec23dca9804bab2ba5517a8bb9d8f47ebc68981b56840107ad", size = 1067420 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/7c/b6/fa99d8f05eff3a9310286ae84c4059b08c301ae4ab33ae32e46e8ef76491/djangorestframework-3.15.2-py3-none-any.whl", hash = "sha256:2b8871b062ba1aefc2de01f773875441a961fefbf79f5eed1e32b2f096944b20", size = 1071235 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "environs"
|
||||
version = "14.1.0"
|
||||
|
@ -222,6 +234,7 @@ source = { virtual = "." }
|
|||
dependencies = [
|
||||
{ name = "django" },
|
||||
{ name = "django-prose-editor", extra = ["sanitize"] },
|
||||
{ name = "djangorestframework" },
|
||||
{ name = "environs", extra = ["django"] },
|
||||
{ name = "odoorpc" },
|
||||
{ name = "pillow" },
|
||||
|
@ -237,6 +250,7 @@ requires-dist = [
|
|||
{ name = "django", specifier = ">=5.1.5" },
|
||||
{ name = "django-browser-reload", marker = "extra == 'dev'", specifier = "~=1.13" },
|
||||
{ name = "django-prose-editor", extras = ["sanitize"], specifier = ">=0.10.3" },
|
||||
{ name = "djangorestframework", specifier = ">=3.15.2" },
|
||||
{ name = "environs", extras = ["django"], specifier = "~=14.0" },
|
||||
{ name = "odoorpc", specifier = ">=0.10.1" },
|
||||
{ name = "pillow", specifier = ">=11.1.0" },
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue