servers.py 14.6 KB
Newer Older
1
2
3
4
#
# Copyright (c) 2010 Greek Research and Technology Network
#

Giorgos Verigakis's avatar
Giorgos Verigakis committed
5
from django.conf import settings
Giorgos Verigakis's avatar
Giorgos Verigakis committed
6
from django.conf.urls.defaults import patterns
7
8
from django.http import HttpResponse
from django.template.loader import render_to_string
9
from django.utils import simplejson as json
10

11
from synnefo.api.actions import server_actions
Giorgos Verigakis's avatar
Giorgos Verigakis committed
12
13
from synnefo.api.common import method_not_allowed
from synnefo.api.faults import BadRequest, ItemNotFound
14
from synnefo.api.util import *
Giorgos Verigakis's avatar
Giorgos Verigakis committed
15
16
from synnefo.db.models import Image, Flavor, VirtualMachine, VirtualMachineMetadata
from synnefo.logic.utils import get_rsapi_state
17
from synnefo.util.rapi import GanetiRapiClient, GanetiApiError
Giorgos Verigakis's avatar
Giorgos Verigakis committed
18
19
20
21
from synnefo.logic import backend

import logging

22
23
24
25
26
27
28

rapi = GanetiRapiClient(*settings.GANETI_CLUSTER_INFO)

urlpatterns = patterns('synnefo.api.servers',
    (r'^(?:/|.json|.xml)?$', 'demux'),
    (r'^/detail(?:.json|.xml)?$', 'list_servers', {'detail': True}),
    (r'^/(\d+)(?:.json|.xml)?$', 'server_demux'),
Giorgos Verigakis's avatar
Giorgos Verigakis committed
29
30
31
    (r'^/(\d+)/action(?:.json|.xml)?$', 'server_action'),
    (r'^/(\d+)/ips(?:.json|.xml)?$', 'list_addresses'),
    (r'^/(\d+)/ips/(.+?)(?:.json|.xml)?$', 'list_addresses_by_network'),
Giorgos Verigakis's avatar
Giorgos Verigakis committed
32
33
    (r'^/(\d+)/meta(?:.json|.xml)?$', 'metadata_demux'),
    (r'^/(\d+)/meta/(.+?)(?:.json|.xml)?$', 'metadata_item_demux'),
34
35
36
37
38
39
40
41
42
)


def demux(request):
    if request.method == 'GET':
        return list_servers(request)
    elif request.method == 'POST':
        return create_server(request)
    else:
Giorgos Verigakis's avatar
Giorgos Verigakis committed
43
        return method_not_allowed(request)
44
45
46
47
48
49
50
51
52

def server_demux(request, server_id):
    if request.method == 'GET':
        return get_server_details(request, server_id)
    elif request.method == 'PUT':
        return update_server_name(request, server_id)
    elif request.method == 'DELETE':
        return delete_server(request, server_id)
    else:
Giorgos Verigakis's avatar
Giorgos Verigakis committed
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
        return method_not_allowed(request)

def metadata_demux(request, server_id):
    if request.method == 'GET':
        return list_metadata(request, server_id)
    elif request.method == 'POST':
        return update_metadata(request, server_id)
    else:
        return method_not_allowed(request)

def metadata_item_demux(request, server_id, key):
    if request.method == 'GET':
        return get_metadata_item(request, server_id, key)
    elif request.method == 'PUT':
        return create_metadata_item(request, server_id, key)
    elif request.method == 'DELETE':
        return delete_metadata_item(request, server_id, key)
    else:
        return method_not_allowed(request)
72

73

Giorgos Verigakis's avatar
Giorgos Verigakis committed
74
75
76
77
def address_to_dict(ipfour, ipsix):
    return {'id': 'public',
            'values': [{'version': 4, 'addr': ipfour}, {'version': 6, 'addr': ipsix}]}

Giorgos Verigakis's avatar
Giorgos Verigakis committed
78
79
80
81
82
83
def metadata_to_dict(vm):
    vm_meta = vm.virtualmachinemetadata_set.all()
    return dict((meta.meta_key, meta.meta_value) for meta in vm_meta)

def vm_to_dict(vm, detail=False):
    d = dict(id=vm.id, name=vm.name)
84
    if detail:
Giorgos Verigakis's avatar
Giorgos Verigakis committed
85
86
87
88
89
90
91
        d['status'] = get_rsapi_state(vm)
        d['progress'] = 100 if get_rsapi_state(vm) == 'ACTIVE' else 0
        d['hostId'] = vm.hostid
        d['updated'] = isoformat(vm.updated)
        d['created'] = isoformat(vm.created)
        d['flavorRef'] = vm.flavor.id
        d['imageRef'] = vm.sourceimage.id
92
        
Giorgos Verigakis's avatar
Giorgos Verigakis committed
93
        metadata = metadata_to_dict(vm)
94
        if metadata:
95
            d['metadata'] = {'values': metadata}
96
        
Giorgos Verigakis's avatar
Giorgos Verigakis committed
97
        addresses = [address_to_dict(vm.ipfour, vm.ipsix)]
98
        d['addresses'] = {'values': addresses}
99
100
    return d

Giorgos Verigakis's avatar
Giorgos Verigakis committed
101
102
103
104

def render_server(request, server, status=200):
    if request.serialization == 'xml':
        data = render_to_string('server.xml', {'server': server, 'is_root': True})
105
    else:
Giorgos Verigakis's avatar
Giorgos Verigakis committed
106
        data = json.dumps({'server': server})
107
    return HttpResponse(data, status=status)
Giorgos Verigakis's avatar
Giorgos Verigakis committed
108
    
109

110
@api_method('GET')
111
112
113
114
115
116
117
def list_servers(request, detail=False):
    # Normal Response Codes: 200, 203
    # Error Response Codes: computeFault (400, 500),
    #                       serviceUnavailable (503),
    #                       unauthorized (401),
    #                       badRequest (400),
    #                       overLimit (413)
118
    
119
    owner = get_user()
Giorgos Verigakis's avatar
Giorgos Verigakis committed
120
121
122
    since = isoparse(request.GET.get('changes-since'))
    
    if since:
123
        user_vms = VirtualMachine.objects.filter(updated__gte=since)
Giorgos Verigakis's avatar
Giorgos Verigakis committed
124
125
126
127
128
        if not user_vms:
            return HttpResponse(status=304)
    else:
        user_vms = VirtualMachine.objects.filter(owner=owner, deleted=False)
    servers = [vm_to_dict(server, detail) for server in user_vms]
129
    
Giorgos Verigakis's avatar
Giorgos Verigakis committed
130
131
    if request.serialization == 'xml':
        data = render_to_string('list_servers.xml', {'servers': servers, 'detail': detail})
132
    else:
133
134
135
        data = json.dumps({'servers': {'values': servers}})
    
    return HttpResponse(data, status=200)
136

137
@api_method('POST')
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
def create_server(request):
    # Normal Response Code: 202
    # Error Response Codes: computeFault (400, 500),
    #                       serviceUnavailable (503),
    #                       unauthorized (401),
    #                       badMediaType(415),
    #                       itemNotFound (404),
    #                       badRequest (400),
    #                       serverCapacityUnavailable (503),
    #                       overLimit (413)
    
    req = get_request_dict(request)
    
    try:
        server = req['server']
        name = server['name']
154
155
        metadata = server.get('metadata', {})
        assert isinstance(metadata, dict)
Giorgos Verigakis's avatar
Giorgos Verigakis committed
156
157
        sourceimage = Image.objects.get(id=server['imageRef'])
        flavor = Flavor.objects.get(id=server['flavorRef'])
158
    except (KeyError, AssertionError):
Giorgos Verigakis's avatar
Giorgos Verigakis committed
159
        raise BadRequest('Malformed request.')
160
161
162
163
164
    except Image.DoesNotExist:
        raise ItemNotFound
    except Flavor.DoesNotExist:
        raise ItemNotFound
    
165
    vm = VirtualMachine(
166
167
168
169
170
171
        name=name,
        owner=get_user(),
        sourceimage=sourceimage,
        ipfour='0.0.0.0',
        ipsix='::1',
        flavor=flavor)
172
173
174
175
176
177
178
179

    # Pick a random password for the VM.
    # FIXME: This must be passed to the Ganeti OS provider via CreateInstance()
    passwd = random_password()

    # We *must* save the VM instance now,
    # so that it gets a vm.id and vm.backend_id is valid.
    vm.save() 
180
181
                
    if request.META.get('SERVER_NAME', None) == 'testserver':
182
        backend_name = 'test-server'
183
184
        dry_run = True
    else:
185
        backend_name = vm.backend_id
186
        dry_run = False
187
    
188
189
190
191
192
193
194
195
196
197
198
199
200
    try:
        jobId = rapi.CreateInstance(
            mode='create',
            name=backend_name,
            disk_template='plain',
            disks=[{"size": 2000}],         #FIXME: Always ask for a 2GB disk for now
            nics=[{}],
            os='debootstrap+default',       #TODO: select OS from imageRef
            ip_check=False,
            name_check=False,
            pnode=rapi.GetNodes()[0],       #TODO: verify if this is necessary
            dry_run=dry_run,
            beparams=dict(auto_balance=True, vcpus=flavor.cpu, memory=flavor.ram))
201
    except GanetiApiError:
202
        vm.delete()
203
        raise ServiceUnavailable('Could not create server.')
204
        
205
206
207
    for key, val in metadata.items():
        VirtualMachineMetadata.objects.create(meta_key=key, meta_value=val, vm=vm)
    
Giorgos Verigakis's avatar
Giorgos Verigakis committed
208
    logging.info('created vm with %s cpus, %s ram and %s storage' % (flavor.cpu, flavor.ram, flavor.disk))
209
    
Giorgos Verigakis's avatar
Giorgos Verigakis committed
210
211
    server = vm_to_dict(vm, detail=True)
    server['status'] = 'BUILD'
212
    server['adminPass'] = passwd
Giorgos Verigakis's avatar
Giorgos Verigakis committed
213
    return render_server(request, server, status=202)
214

215
@api_method('GET')
216
def get_server_details(request, server_id):
217
218
219
220
221
222
223
224
    # Normal Response Codes: 200, 203
    # Error Response Codes: computeFault (400, 500),
    #                       serviceUnavailable (503),
    #                       unauthorized (401),
    #                       badRequest (400),
    #                       itemNotFound (404),
    #                       overLimit (413)
    
Giorgos Verigakis's avatar
Giorgos Verigakis committed
225
226
227
    vm = get_vm(server_id)
    server = vm_to_dict(vm, detail=True)
    return render_server(request, server)
228

229
230
231
232
233
234
235
236
237
238
239
240
@api_method('PUT')
def update_server_name(request, server_id):
    # Normal Response Code: 204
    # Error Response Codes: computeFault (400, 500),
    #                       serviceUnavailable (503),
    #                       unauthorized (401),
    #                       badRequest (400),
    #                       badMediaType(415),
    #                       itemNotFound (404),
    #                       buildInProgress (409),
    #                       overLimit (413)
    
241
242
243
244
245
    req = get_request_dict(request)
    
    try:
        name = req['server']['name']
    except KeyError:
Giorgos Verigakis's avatar
Giorgos Verigakis committed
246
        raise BadRequest('Malformed request.')
247
    
Giorgos Verigakis's avatar
Giorgos Verigakis committed
248
249
250
    vm = get_vm(server_id)
    vm.name = name
    vm.save()
251
252
253
    
    return HttpResponse(status=204)

254
@api_method('DELETE')
255
def delete_server(request, server_id):
256
257
258
259
260
261
262
263
264
    # Normal Response Codes: 204
    # Error Response Codes: computeFault (400, 500),
    #                       serviceUnavailable (503),
    #                       unauthorized (401),
    #                       itemNotFound (404),
    #                       unauthorized (401),
    #                       buildInProgress (409),
    #                       overLimit (413)
    
Giorgos Verigakis's avatar
Giorgos Verigakis committed
265
266
267
    vm = get_vm(server_id)
    backend.start_action(vm, 'DESTROY')
    rapi.DeleteInstance(vm.backend_id)
268
    return HttpResponse(status=204)
Giorgos Verigakis's avatar
Giorgos Verigakis committed
269
270
271

@api_method('POST')
def server_action(request, server_id):
Giorgos Verigakis's avatar
Giorgos Verigakis committed
272
    vm = get_vm(server_id)
Giorgos Verigakis's avatar
Giorgos Verigakis committed
273
274
    req = get_request_dict(request)
    if len(req) != 1:
Giorgos Verigakis's avatar
Giorgos Verigakis committed
275
        raise BadRequest('Malformed request.')
Giorgos Verigakis's avatar
Giorgos Verigakis committed
276
277
    
    key = req.keys()[0]
Giorgos Verigakis's avatar
Giorgos Verigakis committed
278
    val = req[key]
Giorgos Verigakis's avatar
Giorgos Verigakis committed
279
    
Giorgos Verigakis's avatar
Giorgos Verigakis committed
280
281
    try:
        assert isinstance(val, dict)
282
        return server_actions[key](request, vm, req[key])
Giorgos Verigakis's avatar
Giorgos Verigakis committed
283
284
285
286
    except KeyError:
        raise BadRequest('Unknown action.')
    except AssertionError:
        raise BadRequest('Invalid argument.')
Giorgos Verigakis's avatar
Giorgos Verigakis committed
287
288
289
290
291
292
293
294
295
296

@api_method('GET')
def list_addresses(request, server_id):
    # Normal Response Codes: 200, 203
    # Error Response Codes: computeFault (400, 500),
    #                       serviceUnavailable (503),
    #                       unauthorized (401),
    #                       badRequest (400),
    #                       overLimit (413)
    
Giorgos Verigakis's avatar
Giorgos Verigakis committed
297
298
    vm = get_vm(server_id)
    addresses = [address_to_dict(vm.ipfour, vm.ipsix)]
Giorgos Verigakis's avatar
Giorgos Verigakis committed
299
    
Giorgos Verigakis's avatar
Giorgos Verigakis committed
300
    if request.serialization == 'xml':
Giorgos Verigakis's avatar
Giorgos Verigakis committed
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
        data = render_to_string('list_addresses.xml', {'addresses': addresses})
    else:
        data = json.dumps({'addresses': {'values': addresses}})
    
    return HttpResponse(data, status=200)

@api_method('GET')
def list_addresses_by_network(request, server_id, network_id):
    # Normal Response Codes: 200, 203
    # Error Response Codes: computeFault (400, 500),
    #                       serviceUnavailable (503),
    #                       unauthorized (401),
    #                       badRequest (400),
    #                       itemNotFound (404),
    #                       overLimit (413)
    
Giorgos Verigakis's avatar
Giorgos Verigakis committed
317
    vm = get_vm(server_id)
Giorgos Verigakis's avatar
Giorgos Verigakis committed
318
    if network_id != 'public':
Giorgos Verigakis's avatar
Giorgos Verigakis committed
319
        raise ItemNotFound('Unknown network.')
Giorgos Verigakis's avatar
Giorgos Verigakis committed
320
    
Giorgos Verigakis's avatar
Giorgos Verigakis committed
321
    address = address_to_dict(vm.ipfour, vm.ipsix)
Giorgos Verigakis's avatar
Giorgos Verigakis committed
322
    
Giorgos Verigakis's avatar
Giorgos Verigakis committed
323
    if request.serialization == 'xml':
Giorgos Verigakis's avatar
Giorgos Verigakis committed
324
325
326
327
328
        data = render_to_string('address.xml', {'address': address})
    else:
        data = json.dumps({'network': address})
    
    return HttpResponse(data, status=200)
Giorgos Verigakis's avatar
Giorgos Verigakis committed
329
330
331
332
333
334
335
336
337
338
339
340

@api_method('GET')
def list_metadata(request, server_id):
    # Normal Response Codes: 200, 203
    # Error Response Codes: computeFault (400, 500),
    #                       serviceUnavailable (503),
    #                       unauthorized (401),
    #                       badRequest (400),
    #                       overLimit (413)

    vm = get_vm(server_id)
    metadata = metadata_to_dict(vm)
341
    return render_metadata(request, metadata, use_values=True, status=200)
Giorgos Verigakis's avatar
Giorgos Verigakis committed
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372

@api_method('POST')
def update_metadata(request, server_id):
    # Normal Response Code: 201
    # Error Response Codes: computeFault (400, 500),
    #                       serviceUnavailable (503),
    #                       unauthorized (401),
    #                       badRequest (400),
    #                       buildInProgress (409),
    #                       badMediaType(415),
    #                       overLimit (413)

    vm = get_vm(server_id)
    req = get_request_dict(request)
    try:
        metadata = req['metadata']
        assert isinstance(metadata, dict)
    except (KeyError, AssertionError):
        raise BadRequest('Malformed request.')
    
    updated = {}
    
    for key, val in metadata.items():
        try:
            meta = VirtualMachineMetadata.objects.get(meta_key=key, vm=vm)
            meta.meta_value = val
            meta.save()
            updated[key] = val
        except VirtualMachineMetadata.DoesNotExist:
            pass    # Ignore non-existent metadata
    
373
    return render_metadata(request, metadata, status=201)
Giorgos Verigakis's avatar
Giorgos Verigakis committed
374
375
376
377
378
379
380
381
382
383
384
385

@api_method('GET')
def get_metadata_item(request, server_id, key):
    # Normal Response Codes: 200, 203
    # Error Response Codes: computeFault (400, 500),
    #                       serviceUnavailable (503),
    #                       unauthorized (401),
    #                       itemNotFound (404),
    #                       badRequest (400),
    #                       overLimit (413)

    meta = get_vm_meta(server_id, key)
386
    return render_meta(request, meta, status=200)
Giorgos Verigakis's avatar
Giorgos Verigakis committed
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412

@api_method('PUT')
def create_metadata_item(request, server_id, key):
    # Normal Response Code: 201
    # Error Response Codes: computeFault (400, 500),
    #                       serviceUnavailable (503),
    #                       unauthorized (401),
    #                       itemNotFound (404),
    #                       badRequest (400),
    #                       buildInProgress (409),
    #                       badMediaType(415),
    #                       overLimit (413)

    vm = get_vm(server_id)
    req = get_request_dict(request)
    try:
        metadict = req['meta']
        assert isinstance(metadict, dict)
        assert len(metadict) == 1
        assert key in metadict
    except (KeyError, AssertionError):
        raise BadRequest('Malformed request.')
    
    meta, created = VirtualMachineMetadata.objects.get_or_create(meta_key=key, vm=vm)
    meta.meta_value = metadict[key]
    meta.save()
413
    return render_meta(request, meta, status=201)
Giorgos Verigakis's avatar
Giorgos Verigakis committed
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429

@api_method('DELETE')
def delete_metadata_item(request, server_id, key):
    # Normal Response Code: 204
    # Error Response Codes: computeFault (400, 500),
    #                       serviceUnavailable (503),
    #                       unauthorized (401),
    #                       itemNotFound (404),
    #                       badRequest (400),
    #                       buildInProgress (409),
    #                       badMediaType(415),
    #                       overLimit (413),

    meta = get_vm_meta(server_id, key)
    meta.delete()
    return HttpResponse(status=204)