Revision 00b4f1be

b/.gitignore
1
settings.py
2
*.pyc
b/api/authentication.py
1
# vim: ts=4 sts=4 et ai sw=4 fileencoding=utf-8
2
#
3
# Copyright © 2010 Greek Research and Technology Network
4
#
5

  
6
from django.contrib.auth.models import User, AnonymousUser
7
from synnefo.api.faults import fault
8

  
9
# XXX: we need to add a Vary X-Auth-Token, somehow
10
# XXX: or use a standard auth middleware instead?
11
#      but watch out for CSRF issues:
12
#      http://andrew.io/weblog/2010/01/django-piston-and-handling-csrf-tokens/
13

  
14
class TokenAuthentication(object):
15
    def is_authenticated(self, request):
16
        token = request.META.get('HTTP_X_AUTH_TOKEN', None)
17
        if not token:
18
            return False
19

  
20
        # XXX: lookup token in models and set request.user
21
        if token:
22
            request.user = AnonymousUser()
23
            return True
24

  
25
    def challenge(self):
26
        return fault.unauthorized
27

  
b/api/emitter.py
1
# vim: ts=4 sts=4 et ai sw=4 fileencoding=utf-8
2
#
3
# Copyright © 2010 Greek Research and Technology Network
4
#
5

  
6
from piston.resource import Resource as BaseResource
7
import re
8

  
9
_accept_re = re.compile(r'([^\s;,]+)(?:[^,]*?;\s*q=(\d*(?:\.\d+)?))?')
10

  
11
def parse_accept_header(value):
12
    """Parse an HTTP Accept header
13

  
14
    Returns an ordered by quality list of tuples (value, quality)
15
    """
16
    if not value:
17
        return []
18

  
19
    result = []
20
    for match in _accept_re.finditer(value):
21
        quality = match.group(2)
22
        if not quality:
23
            quality = 1
24
        else:
25
            quality = max(min(float(quality), 1), 0)
26
        result.append((match.group(1), quality))
27

  
28
    # sort by quality
29
    result.sort(key=lambda x: x[1])
30

  
31
    return result
32

  
33
# XXX: works as intended but not used since piston's XMLEmitter doesn't output
34
# XML according to our spec :(
35
class Resource(BaseResource):
36
    def determine_emitter(self, request, *args, **kwargs):
37
        """
38
        Override default emitter policy to account for Accept header
39

  
40
        emitter_format (.json or .xml suffix in URL) always takes precedence.
41

  
42
        After that, the Accept header is checked; if both JSON and XML are
43
        equally preferred, use JSON.
44

  
45
        If none of the two were provided, then use JSON as per the
46
        specification.
47
        """
48

  
49
        em = request.GET.get('format', 'xml')
50
        if 'emitter_format' in kwargs:
51
            em = kwargs.pop('emitter_format')
52
        elif 'HTTP_ACCEPT' in request.META:
53
            accepts = parse_accept_header(request.META['HTTP_ACCEPT'])
54
            for content_type, quality in accepts:
55
                if content_type == 'application/json':
56
                    break
57
                elif content_type == 'application/xml':
58
                    em = request.GET.get('format', 'xml')
59
                    break
60

  
61
        return em
b/api/faults.py
1
# vim: ts=4 sts=4 et ai sw=4 fileencoding=utf-8
2
#
3
# Copyright © 2010 Greek Research and Technology Network
4
#
5

  
6
from django.http import HttpResponse
7
from django.utils import simplejson
8

  
9
class _fault_factory(object):
10
    """
11
    Openstack API Faults factory
12
    """
13

  
14
    faults = {
15
        'serviceUnavailable': {
16
                'code': 503,
17
                'message': 'Service Unavailable',
18
            },
19
        'unauthorized': {
20
                'code': 401,
21
                'message': 'Unauthorized',
22
            },
23
        'badRequest': {
24
                'code': 400,
25
                'message': 'Bad request',
26
            },
27
        'overLimit': {
28
                'code': 413,
29
                'message': 'Overlimit',
30
            },
31
        'badMediaType': {
32
                'code': 415,
33
                'message': 'Bad media type',
34
            },
35
        'badMethod': {
36
                'code': 405,
37
                'message': 'Bad method',
38
            },
39
        'itemNotFound': {
40
                'code': 404,
41
                'message': 'Not Found',
42
            },
43
        'buildInProgress': {
44
                'code': 409,
45
                'message': 'Build in progress',
46
            },
47
        'serverCapacityUnavailable': {
48
                'code': 503,
49
                'message': 'Server capacity unavailable',
50
            },
51
        'backupOrResizeInProgress': {
52
                'code': 409,
53
                'message': 'Backup or resize in progress',
54
            },
55
        'resizeNotAllowed': {
56
                'code': 403,
57
                'message': 'Resize not allowed',
58
            },
59
        'notImplemented': {
60
                'code': 501,
61
                'message': 'Not Implemented',
62
            },
63
        }
64

  
65
    def __getattr__(self, attr):
66
        try:
67
            m = self.faults.get(attr)
68
        except TypeError:
69
            raise AttributeError(attr)
70

  
71
        # XXX: piston > 0.2.2 does the serialization for us, but be compatible
72
        message = simplejson.dumps({ attr: m }, ensure_ascii=False, indent=4)
73
        code = m['code']
74

  
75
        return HttpResponse(message, status=code)
76

  
77

  
78
fault = _fault_factory()
79
noContent = HttpResponse(status=204)
80
accepted = HttpResponse(status=202)
b/api/handlers.py
1
# vim: ts=4 sts=4 et ai sw=4 fileencoding=utf-8
2
#
3
# Copyright © 2010 Greek Research and Technology Network
4
#
5

  
6
from piston.handler import BaseHandler, AnonymousBaseHandler
7
from synnefo.api.faults import fault, noContent, accepted
8

  
9
VERSIONS = [
10
    {
11
        "status": "CURRENT",
12
        "id": "v1.0",
13
        "docURL" : "http://docs.rackspacecloud.com/servers/api/v1.0/cs-devguide-20090714.pdf ",
14
        "wadl" : "http://docs.rackspacecloud.com/servers/api/v1.0/application.wadl"
15
    },
16
]
17

  
18
class VersionHandler(AnonymousBaseHandler):
19
    allowed_methods = ('GET',)
20

  
21
    def read(self, request, number=None):
22
        if number is None:
23
            versions = map(lambda v: {
24
                        "status": v["status"],
25
                        "id": v["id"],
26
                    }, VERSIONS)
27
            return { "versions": versions }
28
        else:
29
            for version in VERSIONS:
30
                if version["id"] == number:
31
                    return { "version": version }
32
            return fault.itemNotFound
33

  
34

  
35
# XXX: incomplete
36
class ServerHandler(BaseHandler):
37
    def read(self, request, id=None):
38
        if id is None:
39
            return self.read_all(request)
40
        elif id is "detail":
41
            return self.read_all(request, detail=True)
42
        else:
43
            return self.read_one(request, id)
44

  
45
    def read_one(self, request, id):
46
        print ("server info %s" % id)
47
        return {}
48

  
49
    def read_all(self, request, detail=False):
50
        if not detail:
51
            print "server info all"
52
        else:
53
            print "server info all detail"
54
        return {}
55

  
56
    def create(self, request):
57
        return accepted
58

  
59
    def update(self, request, id):
60
        return noContent
61

  
62
    def delete(self, request, id):
63
        return accepted
64

  
65

  
66
class ServerAddressHandler(BaseHandler):
67
    allowed_methods = ('GET', 'PUT', 'DELETE')
68

  
69
    def read(self, request, id, type=None):
70
        """List IP addresses for a server"""
71
        if type is None:
72
            pass
73
        elif type == "private":
74
            pass
75
        elif type == "public":
76
            pass
77
        return {}
78

  
79
    def update(self, request, id, address):
80
        """Share an IP address to another in the group"""
81
        return accepted
82

  
83
    def delete(self, request, id, address):
84
        """Unshare an IP address"""
85
        return accepted
86

  
87

  
88
class ServerActionHandler(BaseHandler):
89
    allowed_methods = ('POST',)
90

  
91
    def create(self, request, id):
92
        """Reboot, rebuild, resize, confirm resized, revert resized"""
93
        print ("server action %s" % id)
94
        return accepted
b/api/openstack-api.txt
1
auth api
2
========
3

  
4
cloud api
5
=========
6
* auth
7
* version (DONE)
8
* faults (DONE)
9
* limits
10
* servers
11
  + list
12
    - index:  GET /servers
13
    - detail: GET /servers/detail
14
  + create
15
    - create: POST /servers
16
  + get details
17
    - show:   GET /servers/<id>
18
  + update password
19
    - update: PUT /servers/<id>
20
  + delete servers
21
    - delete: DELETE /servers/<id>
22
  * addresses
23
    + list
24
    + list public
25
    + list private
26
    + share
27
    + unshare
28
  * actions
29
    + reboot
30
    + rebuild
31
    + resize
32
    + confirm resized
33
    + revert resized
34
* flavors
35
  + list
36
  + get details
37
* images
38
  + list images
39
  + create an image
40
  + get details
41
  + delete image
42
* backup schedule
43
  + list
44
  + create/update
45
  + disable
46
* shared ip groups
47
  + list
48
  + create
49
  + get details
50
  + delete
b/api/urls.py
1
# vim: ts=4 sts=4 et ai sw=4 fileencoding=utf-8
2
#
3
# Copyright © 2010 Greek Research and Technology Network
4
#
5

  
6
from django.conf.urls.defaults import *
7
from piston.resource import Resource
8
from synnefo.api.handlers import *
9
from synnefo.api.authentication import TokenAuthentication
10

  
11
auth = TokenAuthentication()
12

  
13
server_handler = Resource(ServerHandler, auth)
14
server_address_handler = Resource(ServerAddressHandler, auth)
15
server_actions_handler = Resource(ServerActionHandler, auth)
16

  
17
v10patterns = patterns('',
18
    url(r'^servers/(?P<id>[^/]+)?$', server_handler),
19
    url(r'^servers/(?P<id>[^/]+)/action$', server_actions_handler),
20
    url(r'^servers/(?P<id>[^/]+)/ips$', server_address_handler),
21
    url(r'^servers/(?P<id>[^/]+)/ips/private$', server_address_handler),
22
    url(r'^servers/(?P<id>[^/]+)/ips/public/(?P<address>[^/]+)$', server_address_handler),
23
)
24

  
25
version_handler = Resource(VersionHandler)
26

  
27
urlpatterns = patterns('',
28
    url(r'^(?P<number>[^/]+)/$', version_handler),
29
    url(r'^/?$', version_handler),
30
    (r'^v1.0/', include(v10patterns)),
31
)
b/manage.py
1
#!/usr/bin/python
2
from django.core.management import execute_manager
3
try:
4
    import settings # Assumed to be in the same directory.
5
except ImportError:
6
    import sys
7
    sys.stderr.write("Error: Can't find the file 'settings.py' in the directory containing %r. It appears you've customized things.\nYou'll have to run django-admin.py, passing it your settings module.\n(If the file settings.py does indeed exist, it's causing an ImportError somehow.)\n" % __file__)
8
    sys.exit(1)
9

  
10
if __name__ == "__main__":
11
    execute_manager(settings)
b/settings.py.dist
1
# Django settings for synnefo project.
2

  
3
DEBUG = True
4
TEMPLATE_DEBUG = DEBUG
5

  
6
ADMINS = (
7
    # ('Your Name', 'your_email@domain.com'),
8
)
9

  
10
MANAGERS = ADMINS
11

  
12
DATABASES = {
13
    'default': {
14
        'ENGINE': 'django.db.backends.', # Add 'postgresql_psycopg2', 'postgresql', 'mysql', 'sqlite3' or 'oracle'.
15
        'NAME': '',                      # Or path to database file if using sqlite3.
16
        'USER': '',                      # Not used with sqlite3.
17
        'PASSWORD': '',                  # Not used with sqlite3.
18
        'HOST': '',                      # Set to empty string for localhost. Not used with sqlite3.
19
        'PORT': '',                      # Set to empty string for default. Not used with sqlite3.
20
    }
21
}
22

  
23
# Local time zone for this installation. Choices can be found here:
24
# http://en.wikipedia.org/wiki/List_of_tz_zones_by_name
25
# although not all choices may be available on all operating systems.
26
# On Unix systems, a value of None will cause Django to use the same
27
# timezone as the operating system.
28
# If running in a Windows environment this must be set to the same as your
29
# system time zone.
30
TIME_ZONE = 'America/Chicago'
31

  
32
# Language code for this installation. All choices can be found here:
33
# http://www.i18nguy.com/unicode/language-identifiers.html
34
LANGUAGE_CODE = 'en-us'
35

  
36
SITE_ID = 1
37

  
38
# If you set this to False, Django will make some optimizations so as not
39
# to load the internationalization machinery.
40
USE_I18N = True
41

  
42
# If you set this to False, Django will not format dates, numbers and
43
# calendars according to the current locale
44
USE_L10N = True
45

  
46
# Absolute path to the directory that holds media.
47
# Example: "/home/media/media.lawrence.com/"
48
MEDIA_ROOT = ''
49

  
50
# URL that handles the media served from MEDIA_ROOT. Make sure to use a
51
# trailing slash if there is a path component (optional in other cases).
52
# Examples: "http://media.lawrence.com", "http://example.com/media/"
53
MEDIA_URL = ''
54

  
55
# URL prefix for admin media -- CSS, JavaScript and images. Make sure to use a
56
# trailing slash.
57
# Examples: "http://foo.com/media/", "/media/".
58
ADMIN_MEDIA_PREFIX = '/media/'
59

  
60
# Make this unique, and don't share it with anybody.
61
SECRET_KEY = 'ly6)mw6a7x%n)-e#zzk4jo6f2=uqu!1o%)2-(7lo+f9yd^k^bg'
62

  
63
# List of callables that know how to import templates from various sources.
64
TEMPLATE_LOADERS = (
65
    'django.template.loaders.filesystem.Loader',
66
    'django.template.loaders.app_directories.Loader',
67
#     'django.template.loaders.eggs.Loader',
68
)
69

  
70
MIDDLEWARE_CLASSES = (
71
    'django.middleware.common.CommonMiddleware',
72
    'django.contrib.sessions.middleware.SessionMiddleware',
73
#    'django.middleware.csrf.CsrfViewMiddleware',
74
#    'django.contrib.auth.middleware.AuthenticationMiddleware',
75
    'django.contrib.messages.middleware.MessageMiddleware',
76
)
77

  
78
ROOT_URLCONF = 'synnefo.urls'
79

  
80
TEMPLATE_DIRS = (
81
    # Put strings here, like "/home/html/django_templates" or "C:/www/django/templates".
82
    # Always use forward slashes, even on Windows.
83
    # Don't forget to use absolute paths, not relative paths.
84
)
85

  
86
INSTALLED_APPS = (
87
    'django.contrib.auth',
88
    'django.contrib.contenttypes',
89
    'django.contrib.sessions',
90
    'django.contrib.sites',
91
    'django.contrib.messages',
92
    # 'django.contrib.admin',
93
    # 'django.contrib.admindocs',
94
    'synnefo.api',
95
)
b/urls.py
1
# vim: ts=4 sts=4 et ai sw=4 fileencoding=utf-8
2
#
3
# Copyright © 2010 Greek Research and Technology Network
4
#
5

  
6
from django.conf.urls.defaults import *
7

  
8
urlpatterns = patterns('',
9
    (r'^api/', include('synnefo.api.urls')),
10
)

Also available in: Unified diff