Revision 7e2f9d4b

b/api/errors.py
1
#
2
# Copyright (c) 2010 Greek Research and Technology Network
3
#
4

  
5
def camelCase(s):
6
    return s[0].lower() + s[1:]
7

  
8

  
9
class Fault(BaseException):
10
    def __init__(self, message='', details='', name=''):
11
        BaseException.__init__(self, message, details, name)
12
        self.message = message
13
        self.details = details
14
        self.name = name or camelCase(self.__class__.__name__)
15

  
16
class BadRequest(Fault):
17
    code = 400
18

  
19
class Unauthorized(Fault):
20
    code = 401
21

  
22
class ItemNotFound(Fault):
23
    code = 404
24

  
b/api/fixtures/api_redux_test_data.json
1
[
2
    {
3
        "model": "db.Image",
4
        "pk": 1,
5
        "fields": {
6
            "name": "Debian Unstable",
7
            "created": "2011-02-06 00:00:00",
8
            "updated": "2011-02-06 00:00:00",
9
            "state": "ACTIVE",
10
            "description": "Debian Sid, full installation",
11
            "size": 4096
12
        }
13
    },
14
    {
15
        "model": "db.Image",
16
        "pk": 2,
17
        "fields": {
18
            "name": "Red Hat Enterprise Linux",
19
            "created": "2011-02-06 00:00:00",
20
            "updated": "2011-02-06 00:00:00",
21
            "state": "ACTIVE",
22
            "description": "Red Hat Enterprise Linux, full installation",
23
            "size": 2048
24
        }
25
    },
26
    {
27
        "model": "db.Image",
28
        "pk": 3,
29
        "fields": {
30
            "name": "Ubuntu 10.10",
31
            "created": "2011-02-06 00:00:00",
32
            "updated": "2011-02-06 00:00:00",
33
            "state": "ACTIVE",
34
            "description": "Ubuntu 10.10, full installation",
35
            "size": 8192
36
        }
37
    },
38
    
39
    {
40
        "model": "db.Flavor",
41
        "pk": 1,
42
        "fields": {
43
            "cpu": 1,
44
            "ram": 1024,
45
            "disk": 20 
46
        }
47
    },
48
    {
49
        "model": "db.Flavor",
50
        "pk": 2,
51
        "fields": {
52
            "cpu": 1,
53
            "ram": 1024,
54
            "disk": 40 
55
        }
56
    },
57
    {
58
        "model": "db.Flavor",
59
        "pk": 3,
60
        "fields": {
61
            "cpu": 1,
62
            "ram": 1024,
63
            "disk": 80 
64
        }
65
    }
66
]
b/api/servers.py
1
#
2
# Copyright (c) 2010 Greek Research and Technology Network
3
#
4

  
5
from synnefo.api.errors import *
6
from synnefo.api.util import *
7
from synnefo.db.models import *
8
from synnefo.util.rapi import GanetiRapiClient
9

  
10
from django.conf.urls.defaults import *
11
from django.http import HttpResponse
12
from django.template.loader import render_to_string
13

  
14
from logging import getLogger
15

  
16
import json
17

  
18

  
19
log = getLogger('synnefo.api.servers')
20
rapi = GanetiRapiClient(*settings.GANETI_CLUSTER_INFO)
21

  
22
urlpatterns = patterns('synnefo.api.servers',
23
    (r'^(?:/|.json|.xml)?$', 'demux'),
24
    (r'^/detail(?:.json|.xml)?$', 'list_servers', {'detail': True}),
25
    (r'^/(\d+)(?:.json|.xml)?$', 'server_demux'),
26
)
27

  
28

  
29
def demux(request):
30
    if request.method == 'GET':
31
        return list_servers(request)
32
    elif request.method == 'POST':
33
        return create_server(request)
34
    else:
35
        return HttpResponse(status=404)
36

  
37
def server_demux(request, server_id):
38
    if request.method == 'GET':
39
        return get_server_details(request, server_id)
40
    elif request.method == 'PUT':
41
        return update_server_name(request, server_id)
42
    elif request.method == 'DELETE':
43
        return delete_server(request, server_id)
44
    else:
45
        return HttpResponse(status=404)
46

  
47
def server_dict(vm, detail=False):
48
    d = dict(id=vm.id, name=vm.name)
49
    if detail:
50
        d['status'] = vm.rsapi_state
51
        d['progress'] = 100 if vm.rsapi_state == 'ACTIVE' else 0
52
        d['hostId'] = vm.hostid
53
        d['updated'] = vm.updated.isoformat()
54
        d['created'] = vm.created.isoformat()
55
        d['flavorId'] = vm.flavor.id            # XXX Should use flavorRef instead?
56
        d['imageId'] = vm.sourceimage.id        # XXX Should use imageRef instead?
57
        d['description'] = vm.description       # XXX Not in OpenStack docs
58
        
59
        vm_meta = vm.virtualmachinemetadata_set.all()
60
        metadata = dict((meta.meta_key, meta.meta_value) for meta in vm_meta)
61
        if metadata:
62
            d['metadata'] = dict(values=metadata)
63
        
64
        public_addrs = [dict(version=4, addr=vm.ipfour), dict(version=6, addr=vm.ipsix)]
65
        d['addresses'] = {'values': []}
66
        d['addresses']['values'].append({'id': 'public', 'values': public_addrs})
67
    return d
68

  
69
def render_server(server, request, status=200):
70
    if request.type == 'xml':
71
        mimetype = 'application/xml'
72
        data = render_to_string('server.xml', dict(server=server, is_root=True))
73
    else:
74
        mimetype = 'application/json'
75
        data = json.dumps({'server': server})
76
    return HttpResponse(data, mimetype=mimetype, status=status)    
77

  
78

  
79
@api_method
80
def list_servers(request, detail=False):
81
    # Normal Response Codes: 200, 203
82
    # Error Response Codes: computeFault (400, 500),
83
    #                       serviceUnavailable (503),
84
    #                       unauthorized (401),
85
    #                       badRequest (400),
86
    #                       overLimit (413)
87
    owner = get_user()
88
    vms = VirtualMachine.objects.filter(owner=owner, deleted=False)
89
    servers = [server_dict(vm, detail) for vm in vms]
90
    if request.type == 'xml':
91
        mimetype = 'application/xml'
92
        data = render_to_string('list_servers.xml', dict(servers=servers, detail=detail))
93
    else:
94
        mimetype = 'application/json'
95
        data = json.dumps({'servers': servers})
96
    return HttpResponse(data, mimetype=mimetype, status=200)
97

  
98
@api_method
99
def create_server(request):
100
    # Normal Response Code: 202
101
    # Error Response Codes: computeFault (400, 500),
102
    #                       serviceUnavailable (503),
103
    #                       unauthorized (401),
104
    #                       badMediaType(415),
105
    #                       itemNotFound (404),
106
    #                       badRequest (400),
107
    #                       serverCapacityUnavailable (503),
108
    #                       overLimit (413)
109
    
110
    req = get_request_dict(request)
111
    
112
    try:
113
        server = req['server']
114
        name = server['name']
115
        sourceimage = Image.objects.get(id=server['imageId'])
116
        flavor = Flavor.objects.get(id=server['flavorId'])
117
    except KeyError:
118
        raise BadRequest
119
    except Image.DoesNotExist:
120
        raise ItemNotFound
121
    except Flavor.DoesNotExist:
122
        raise ItemNotFound
123
    
124
    vm = VirtualMachine.objects.create(
125
        name=name,
126
        owner=get_user(),
127
        sourceimage=sourceimage,
128
        ipfour='0.0.0.0',
129
        ipsix='::1',
130
        flavor=flavor)
131
                
132
    if request.META.get('SERVER_NAME', None) == 'testserver':
133
        name = 'test-server'
134
        dry_run = True
135
    else:
136
        name = vm.backend_id
137
        dry_run = False
138
    
139
    jobId = rapi.CreateInstance(
140
        mode='create',
141
        name=name,
142
        disk_template='plain',
143
        disks=[{"size": 2000}],         #FIXME: Always ask for a 2GB disk for now
144
        nics=[{}],
145
        os='debootstrap+default',       #TODO: select OS from imageId
146
        ip_check=False,
147
        nam_check=False,
148
        pnode=rapi.GetNodes()[0],       #TODO: verify if this is necessary
149
        dry_run=dry_run,
150
        beparams=dict(auto_balance=True, vcpus=flavor.cpu, memory=flavor.ram))
151
    
152
    vm.save()
153
        
154
    log.info('created vm with %s cpus, %s ram and %s storage' % (flavor.cpu, flavor.ram, flavor.disk))
155
    
156
    server = server_dict(vm, detail=True)
157
    server['status'] = 'BUILD'
158
    server['adminPass'] = random_password()
159
    return render_server(server, request, status=202)
160

  
161
@api_method
162
def get_server_details(request, server_id):
163
    try:
164
        vm = VirtualMachine.objects.get(id=int(server_id))
165
    except VirtualMachine.DoesNotExist:
166
        raise NotFound
167
    
168
    server = server_dict(vm, detail=True)
169
    return render_server(server, request)
170

  
171
@api_method
172
def update_server_name(request, server_id):    
173
    req = get_request_dict(request)
174
    
175
    try:
176
        name = req['server']['name']
177
        vm = VirtualMachine.objects.get(id=int(server_id))
178
    except KeyError:
179
        raise BadRequest
180
    except VirtualMachine.DoesNotExist:
181
        raise NotFound
182
    
183
    vm.name = name
184
    vm.save()
185
    
186
    return HttpResponse(status=204)
187

  
188
@api_method
189
def delete_server(request, server_id):
190
    try:
191
        vm = VirtualMachine.objects.get(id=int(server_id))
192
    except VirtualMachine.DoesNotExist:
193
        raise NotFound
194
    
195
    vm.start_action('DESTROY')
196
    rapi.DeleteInstance(vm.backend_id)
197
    vm.state = 'DESTROYED'
198
    vm.save()
199
    return HttpResponse(status=204)
b/api/templates/fault.xml
1
<?xml version="1.0" encoding="UTF-8"?>
2
<{{ fault.name }} xmlns="http://docs.openstack.org/compute/api/v1.1" code="{{ fault.code }}">
3
  <message>{{ fault.message }}</message>
4
  <details>{{ fault.details }}</details>
5
</{{ fault.name }}>
b/api/templates/list_servers.xml
1
{% spaceless %}
2
<?xml version="1.0" encoding="UTF-8"?>
3
<servers xmlns="http://docs.openstack.org/compute/api/v1.1" xmlns:atom="http://www.w3.org/2005/Atom">
4
{% for server in servers %}
5
{% if detail %}
6
{% include "server.xml" %}
7
{% else %}
8
<server id="{{ server.id }}" name="{{ server.name }}"></server>
9
{% endif %}
10
{% endfor %}
11
</servers>
12
{% endspaceless %}
b/api/templates/server.xml
1
{% spaceless %}
2

  
3
{% if is_root %}
4
<?xml version="1.0" encoding="UTF-8"?>
5
<server xmlns="http://docs.openstack.org/compute/api/v1.1" xmlns:atom="http://www.w3.org/2005/Atom"{% else %}<server{% endif %}
6
    {% if server.adminPass %}adminPass="{{ server.adminPass }}"{% endif %}
7
    created="{{ server.created }}"
8
    description="{{ server.description }}"
9
    flavorId="{{ server.flavorId }}"
10
    hostId="{{ server.hostId }}"
11
    id="{{ server.id }}"
12
    imageId="{{ server.imageId }}"
13
    name="{{ server.name }}"
14
    progress="{{ server.progress }}"
15
    status="{{ server.status }}"
16
    updated="{{ server.updated}}">
17

  
18
    {% if server.metadata.values %}
19
    <metadata>
20
        {% for key, val in server.metadata.values.items %}<meta key="{{ key }}">{{ val }}</meta>{% endfor %}
21
    </metadata>
22
    {% endif %}
23
    
24
    <addresses>
25
        {% for network in server.addresses.values %}
26
        <network id="{{ network.id }}">
27
            {% for ip in network.values %}
28
            <ip version="{{ ip.version }}" addr="{{ ip.addr }}"/>
29
            {% endfor %}
30
        </network>
31
        {% endfor %}
32
    </addresses>
33

  
34
</server>
35

  
36
{% endspaceless %}
b/api/tests.py
10 10
from django.test.client import Client
11 11
import simplejson as json
12 12
from synnefo.db.models import VirtualMachine, Flavor, Image, VirtualMachineGroup
13
from synnefo.api.tests_redux import APIReduxTestCase
14

  
13 15

  
14 16
class APITestCase(TestCase):
15 17
    fixtures = [ 'api_test_data' ]
b/api/tests_redux.py
1
#
2
# Copyright (c) 2010 Greek Research and Technology Network
3
#
4

  
5
from django.test import TestCase
6
from django.test.client import Client
7

  
8
import json
9

  
10
API = 'v1.1redux'
11

  
12

  
13
class APIReduxTestCase(TestCase):
14
    fixtures = [ 'api_redux_test_data' ]
15
    
16
    def setUp(self):
17
        self.client = Client()
18
        self.server_id = 0
19
    
20
    def create_server_name(self):
21
        self.server_id += 1
22
        return 'server%d' % self.server_id
23
    
24
    def test_create_server_json(self):
25
        TEMPLATE = '''
26
        {
27
            "server" : {
28
                "name" : "%(name)s",
29
                "flavorId" : "%(flavorId)s",
30
                "imageId" : "%(imageId)s"
31
            }
32
        }
33
        '''
34
        
35
        def new_server(imageId=1, flavorId=1):
36
            name = self.create_server_name()
37
            return name, TEMPLATE % dict(name=name, imageId=imageId, flavorId=flavorId)
38
        
39
        def verify_response(response, name):
40
            assert response.status_code == 202
41
            reply =  json.loads(response.content)
42
            server = reply['server']
43
            assert server['name'] == name
44
            assert server['imageId'] == 1
45
            assert server['flavorId'] == 1
46
            assert server['status'] == 'BUILD'
47
            assert server['adminPass']
48
            assert server['addresses']
49
        
50
        def verify_error(response, code, name):
51
            assert response.status_code == code
52
            reply =  json.loads(response.content)
53
            assert name in reply
54
            assert reply[name]['code'] == code
55
        
56
        name, data = new_server()
57
        url = '/api/%s/servers' % API
58
        response = self.client.post(url, content_type='application/json', data=data)
59
        verify_response(response, name)
60
        
61
        name, data = new_server()
62
        url = '/api/%s/servers.json' % API
63
        response = self.client.post(url, content_type='application/json', data=data)
64
        verify_response(response, name)
65
        
66
        name, data = new_server()
67
        url = '/api/%s/servers.json' % API
68
        response = self.client.post(url, content_type='application/json', data=data,
69
                                    HTTP_ACCEPT='application/xml')
70
        verify_response(response, name)
71
        
72
        name, data = new_server(imageId=0)
73
        url = '/api/%s/servers' % API
74
        response = self.client.post(url, content_type='application/json', data=data)
75
        verify_error(response, 404, 'itemNotFound')
76
        
77
        name, data = new_server(flavorId=0)
78
        url = '/api/%s/servers' % API
79
        response = self.client.post(url, content_type='application/json', data=data)
80
        verify_error(response, 404, 'itemNotFound')
81
        
82
        url = '/api/%s/servers' % API
83
        response = self.client.post(url, content_type='application/json', data='INVALID')
84
        verify_error(response, 400, 'badRequest')
b/api/urls.py
101 101
    url(r'^.+', notFound), # catch-all
102 102
)
103 103

  
104
# The OpenStack Compute API v1.1 (REDUX)
105
v11redux_patterns = patterns('',
106
    (r'^servers', include('synnefo.api.servers')),
107
    (r'^.+', notFound), # catch-all
108
)
109

  
104 110
version_handler = Resource(VersionHandler)
105 111

  
106 112
urlpatterns = patterns('',
......
109 115
    url(r'^v1.0/', include(v10patterns)),
110 116
    url(r'^v1.1/', include(v11patterns)),
111 117
    url(r'^v1.0grnet1/', include(v10grnet10patterns)),
118
    url(r'^v1.1redux/', include(v11redux_patterns)),
112 119
    url(r'^.+', notFound), # catch-all
113 120
)
b/api/util.py
1
#
2
# Copyright (c) 2010 Greek Research and Technology Network
3
#
4

  
5
from synnefo.api.errors import *
6
from synnefo.db.models import *
7

  
8
from django.http import HttpResponse
9
from django.template.loader import render_to_string
10

  
11
from functools import wraps
12
from logging import getLogger
13
from random import choice
14
from string import ascii_letters, digits
15
from traceback import format_exc
16
from xml.etree import ElementTree
17
from xml.parsers.expat import ExpatError
18

  
19
import json
20

  
21

  
22
log = getLogger('synnefo.api')
23

  
24

  
25
def tag_name(e):
26
    ns, sep, name = e.tag.partition('}')
27
    return name if sep else e.tag
28

  
29
def xml_to_dict(s):
30
    def _xml_to_dict(e):
31
        root = {}
32
        d = root[tag_name(e)] = dict(e.items())
33
        for child in e.getchildren():
34
            d.update(_xml_to_dict(child))
35
        return root
36
    return _xml_to_dict(ElementTree.fromstring(s.strip()))
37

  
38
def get_user():
39
    # XXX Placeholder function, everything belongs to a single SynnefoUser for now
40
    try:
41
        return SynnefoUser.objects.all()[0]
42
    except IndexError:
43
        raise Unauthorized
44

  
45
def get_request_dict(request):
46
    data = request.raw_post_data
47
    if request.type == 'xml':
48
        try:
49
            return xml_to_dict(data)
50
        except ExpatError:
51
            raise BadRequest
52
    else:
53
        try:
54
            return json.loads(data)
55
        except ValueError:
56
            raise BadRequest
57

  
58
def random_password(length=8):
59
    pool = ascii_letters + digits
60
    return ''.join(choice(pool) for i in range(length))
61

  
62

  
63
def render_fault(fault, request):
64
    if settings.DEBUG or request.META.get('SERVER_NAME', None) == 'testserver':
65
        fault.details = format_exc(fault)
66
    if request.type == 'xml':
67
        mimetype = 'application/xml'
68
        data = render_to_string('fault.xml', dict(fault=fault))
69
    else:
70
        mimetype = 'application/json'
71
        d = {fault.name: {'code': fault.code, 'message': fault.message, 'details': fault.details}}
72
        data = json.dumps(d)
73
    return HttpResponse(data, mimetype=mimetype, status=fault.code)    
74

  
75
def api_method(func):
76
    @wraps(func)
77
    def wrapper(request, *args, **kwargs):
78
        try:
79
            if request.path.endswith('.json'):
80
                type = 'json'
81
            elif request.path.endswith('.xml'):
82
                type = 'xml'
83
            elif request.META.get('HTTP_ACCEPT', None) == 'application/xml':
84
                type = 'xml'
85
            else:
86
                type = 'json'
87
            request.type = type
88
            return func(request, *args, **kwargs)
89
        except Fault, fault:
90
            return render_fault(fault, request)
91
        except Exception, e:
92
            log.exception('Unexpected error: %s' % e)
93
            return HttpResponse(status=500)
94
    return wrapper

Also available in: Unified diff