Skip to content

Commit

Permalink
WIP for deis#231. Moved services app into api app. Client receives an…
Browse files Browse the repository at this point in the history
…d adds service URI to config
  • Loading branch information
tombh committed Jan 18, 2014
1 parent fd8ac0c commit 0603166
Show file tree
Hide file tree
Showing 37 changed files with 680 additions and 974 deletions.
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -65,4 +65,4 @@ contrib/vagrant/nodes/
logs
venv
static
!static/
!static/
6 changes: 6 additions & 0 deletions api/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@
from .models import Node
from .models import Provider
from .models import Release
from .models import Service
from .models import ServiceProvider


class AppAdmin(GuardedModelAdmin):
Expand Down Expand Up @@ -130,3 +132,7 @@ class ReleaseAdmin(admin.ModelAdmin):
list_display = ('owner', 'app', 'version')
list_filter = ('owner', 'app')
admin.site.register(Release, ReleaseAdmin)


admin.site.register(Service)
admin.site.register(ServiceProvider)
4 changes: 4 additions & 0 deletions api/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,3 +20,7 @@ class BuildNodeError(APIException):

def __init__(self, detail=None):
self.detail = detail


class ServiceProviderError(Exception):
pass
246 changes: 246 additions & 0 deletions api/migrations/0007_auto__add_service__add_serviceprovider.py

Large diffs are not rendered by default.

60 changes: 59 additions & 1 deletion api/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@
from api import fields, tasks
from provider import import_provider_module
from utils import dict_diff

from .exceptions import ServiceProviderError


logger = logging.getLogger(__name__)
Expand Down Expand Up @@ -872,6 +872,64 @@ def save(self, *args, **kwargs):
super(Release, self).save(*args, **kwargs)


class ServiceProvider(models.Model):

PROVIDERS = (
('mysql', 'local MySQL database'),
('mock', 'Mock service provider'),
)

PLANS = (
('free', 'free tier'),
('paid', 'paid tier'),
)

owner = models.ForeignKey(settings.AUTH_USER_MODEL)
type = models.SlugField(max_length=16, choices=PROVIDERS, primary_key=True)
enabled = models.BooleanField()
dashboard = models.CharField(max_length=100)
docs = models.CharField(max_length=100)

def __str__(self):
return self.type


class Service(models.Model):

app = models.ForeignKey('App')
owner = models.ForeignKey(settings.AUTH_USER_MODEL)
name = models.CharField(max_length=50, unique=True)
provider = models.ForeignKey('ServiceProvider')
plan = models.SlugField(max_length=16, choices=ServiceProvider.PLANS)
uri = models.CharField(max_length=300)

def __str__(self):
return self.name

def build(self):
if not self.provider.enabled:
raise ServiceProviderError('cannot provision; provider is disabled')
self.uri = tasks.build_service.delay(self).wait()

def destroy(self):
if not self.provider.enabled:
raise ServiceProviderError('cannot deprovision; provider is disabled')
return tasks.destroy_service.delay(self).wait()

def update(self, new_service):
if not self.provider.enabled:
raise ServiceProviderError('cannot update; provider is disabled')
self.uri = tasks.update_service.delay(self, new_service)

def flat(self):
return {'id': self.id,
'name': self.name,
'provider': self.provider,
'plan': self.plan,
'dashboard': self.provider.dashboard,
'docs': self.provider.docs}


@receiver(release_signal)
def new_release(sender, **kwargs):
"""
Expand Down
36 changes: 36 additions & 0 deletions api/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
from __future__ import unicode_literals

import re
import importlib

from django.contrib.auth.models import User
from rest_framework import serializers
Expand Down Expand Up @@ -210,3 +211,38 @@ class Meta:
"""Metadata options for a :class:`ContainerSerializer`."""
model = models.Container
read_only_fields = ('created', 'updated')


class ServiceProviderSerializer(serializers.ModelSerializer):

owner = serializers.Field(source='owner.username')
dashboard = serializers.SerializerMethodField('get_dashboard_uri')
docs = serializers.SerializerMethodField('get_docs_uri')

class Meta:
model = models.ServiceProvider

def get_dashboard_uri(self, obj):
module = importlib.import_module('services.' + obj.type)
return module.DASHBOARD

def get_docs_uri(self, obj):
module = importlib.import_module('services.' + obj.type)
return module.DOCS


class ServiceSerializer(serializers.ModelSerializer):

app = OwnerSlugRelatedField(slug_field='id')
owner = serializers.Field(source='owner.username')
name = serializers.SlugField(default=utils.generate_service_name)
plan = serializers.SlugField(default='free')
uri = serializers.CharField(required=False)

class Meta:
model = models.Service
fields = ('uri', 'provider', 'app', 'owner', 'name', 'plan')

def get_validation_exclusions(self):
exclusions = super(ServiceSerializer, self).get_validation_exclusions()
return exclusions + ['uri']
33 changes: 33 additions & 0 deletions api/tasks.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,17 @@
import importlib

from celery import task
from celery.utils.log import get_task_logger

from deis import settings
from provider import import_provider_module
from services import import_services_module
from .exceptions import BuildNodeError


logger = get_task_logger(__name__)


# import user-defined config management module
CM = importlib.import_module(settings.CM_MODULE)

Expand Down Expand Up @@ -108,3 +113,31 @@ def converge_controller():
"""
CM.converge_controller()
return None


@task
def build_service(service):
"""
Create an instance of a service.
Eg; a MySQL database that a single app can use.
"""
provider = import_services_module(service.provider.type)
return provider.build_service(service.flat())


@task
def destroy_service(service):
"""
Destroy an instance of a service such as a MySQL database.
"""
provider = import_services_module(service.provider.type)
provider.destroy_service(service.flat())


@task
def update_service(service, new_service):
"""
Upadate an instance of a service.
"""
provider = import_services_module(service.provider.type)
return provider.update_service(service.flat(), new_service.flat())
1 change: 1 addition & 0 deletions api/tests/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,3 +54,4 @@ def run_tests(self, test_labels, extra_tests=None, **kwargs):
from .test_perm import * # noqa
from .test_provider import * # noqa
from .test_release import * # noqa
from .test_services import * # noqa
93 changes: 93 additions & 0 deletions api/tests/test_services.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
"""
Unit tests for the Deis api app.
Run the tests with "./manage.py test api"
"""

from __future__ import unicode_literals

from django.test import TestCase
from django.test.utils import override_settings

import json


class ServiceProvidersTest(TestCase):

"""Tests creation of services"""

fixtures = ['tests.json']

def setUp(self):
self.assertTrue(self.client.login(username='autotest', password='password'))

def test_service_providers(self):
"""
Test that a user can list and enable services
"""
# Listing providers
url = '/api/service_providers'
response = self.client.get(url, content_type='application/json')
self.assertEqual(response.status_code, 200)
mock_data = {u'enabled': False, 'description': u'Mock service provider'}
self.assertEqual(response.data['mock'], mock_data)

# Enabling a provider
body = {'type': 'mock', 'enabled': True}
response = self.client.post(url, json.dumps(body), content_type='application/json')
self.assertEqual(response.status_code, 201)
response = self.client.get(url, content_type='application/json')
mock_data = {u'enabled': True, 'description': u'Mock service provider'}
self.assertEqual(response.data['mock'], mock_data)


@override_settings(CELERY_ALWAYS_EAGER=True)
class ServicesTest(TestCase):

"""Tests creation of services"""

fixtures = ['tests.json']

def setUp(self):
self.assertTrue(self.client.login(username='autotest', password='password'))

# Setup to allow creation of apps
url = '/api/providers'
creds = {'secret_key': 'x' * 64, 'access_key': 1 * 20}
body = {'id': 'autotest', 'type': 'mock', 'creds': json.dumps(creds)}
response = self.client.post(url, json.dumps(body), content_type='application/json')
self.assertEqual(response.status_code, 201)
url = '/api/flavors'
body = {'id': 'autotest', 'provider': 'autotest',
'params': json.dumps({'region': 'us-west-2'})}
response = self.client.post(url, json.dumps(body), content_type='application/json')
self.assertEqual(response.status_code, 201)
response = self.client.post('/api/formations', json.dumps(
{'id': 'autotest', 'domain': 'localhost.localdomain'}),
content_type='application/json')
self.assertEqual(response.status_code, 201)

# Enable the 'mock' service provider
url = '/api/service_providers'
body = {'type': 'mock', 'enabled': True}
response = self.client.post(url, json.dumps(body), content_type='application/json')
self.assertEqual(response.status_code, 201)

def test_services(self):
"""
Test that a user can list and enable services
"""
# Listing providers
url = '/api/service_providers'
response = self.client.get(url, content_type='application/json')
self.assertEqual(response.status_code, 200)
mock_data = {u'enabled': False, 'description': u'Mock service provider'}
self.assertEqual(response.data['mock'], mock_data)

# Enabling a provider
body = {'type': 'mock', 'enabled': True}
response = self.client.post(url, json.dumps(body), content_type='application/json')
self.assertEqual(response.status_code, 201)
response = self.client.get(url, content_type='application/json')
mock_data = {u'enabled': True, 'description': u'Mock service provider'}
self.assertEqual(response.data['mock'], mock_data)
4 changes: 4 additions & 0 deletions api/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -359,6 +359,10 @@

router = routers.ApiRouter()

# TODO: Should these roots be placed below, for consistency?
router.register(r'service_providers', views.ServiceProviderViewSet)
router.register(r'services', views.ServiceViewSet)

# Add the generated REST URLs and login/logout endpoint
urlpatterns = patterns(
'',
Expand Down
9 changes: 9 additions & 0 deletions api/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@
Helper functions used by the Deis server.
"""

import models
import random
import string


def generate_app_name():
Expand Down Expand Up @@ -65,6 +67,13 @@ def generate_app_name():
random.choice(adjectives), random.choice(nouns))


def generate_service_name():
while True:
name = ''.join(random.sample(string.letters*2, 8))
if not models.Service.objects.filter(name=name).exists():
return name


def dict_diff(dict1, dict2):
"""
Returns the added, changed, and deleted items in dict1 compared with dict2.
Expand Down
35 changes: 35 additions & 0 deletions api/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -682,3 +682,38 @@ def get_object(self, *args, **kwargs):
qs = self.get_queryset(**kwargs)
obj = qs.get(num=self.kwargs['num'])
return obj


class ServiceProviderViewSet(OwnerViewSet):
"""
API endpoint that allows providers to be viewed or edited.
"""

model = models.ServiceProvider
permission_classes = (permissions.IsAuthenticated, IsAdminOrSafeMethod)
queryset = models.ServiceProvider.objects.all()
serializer_class = serializers.ServiceProviderSerializer

def list(self, request):
response = {}
enabled_providers = self.model.objects.filter(enabled=True).values_list("type", flat=True)
for provider in self.model.PROVIDERS:
enabled = provider[0] in enabled_providers
response[provider[0]] = {'enabled': enabled, 'description': provider[1]}
return Response(response, status=status.HTTP_200_OK)


class ServiceViewSet(OwnerViewSet):
"""
API endpoint that allows services to be viewed or edited.
"""

queryset = models.Service.objects.all()
serializer_class = serializers.ServiceSerializer

def pre_save(self, obj):
super(ServiceViewSet, self).pre_save(obj)
obj.build()

def post_delete(self, obj):
obj.destroy()
Loading

0 comments on commit 0603166

Please sign in to comment.