servers.py 14.4 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
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
123
124
125
126
127
128
    since = isoparse(request.GET.get('changes-since'))
    
    if since:
        user_vms = VirtualMachine.objects.filter(updated__gt=since)
        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
    jobId = rapi.CreateInstance(
        mode='create',
190
        name=backend_name,
191
192
193
        disk_template='plain',
        disks=[{"size": 2000}],         #FIXME: Always ask for a 2GB disk for now
        nics=[{}],
Giorgos Verigakis's avatar
Giorgos Verigakis committed
194
        os='debootstrap+default',       #TODO: select OS from imageRef
195
        ip_check=False,
Giorgos Verigakis's avatar
Giorgos Verigakis committed
196
        name_check=False,
197
198
199
200
        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))
    
Giorgos Verigakis's avatar
Giorgos Verigakis committed
201
    vm.save()
202
203
204
205
    
    for key, val in metadata.items():
        VirtualMachineMetadata.objects.create(meta_key=key, meta_value=val, vm=vm)
    
Giorgos Verigakis's avatar
Giorgos Verigakis committed
206
    logging.info('created vm with %s cpus, %s ram and %s storage' % (flavor.cpu, flavor.ram, flavor.disk))
207
    
Giorgos Verigakis's avatar
Giorgos Verigakis committed
208
209
    server = vm_to_dict(vm, detail=True)
    server['status'] = 'BUILD'
210
    server['adminPass'] = passwd
Giorgos Verigakis's avatar
Giorgos Verigakis committed
211
    return render_server(request, server, status=202)
212

213
@api_method('GET')
214
def get_server_details(request, server_id):
215
216
217
218
219
220
221
222
    # 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
223
224
225
    vm = get_vm(server_id)
    server = vm_to_dict(vm, detail=True)
    return render_server(request, server)
226

227
228
229
230
231
232
233
234
235
236
237
238
@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)
    
239
240
241
242
243
    req = get_request_dict(request)
    
    try:
        name = req['server']['name']
    except KeyError:
Giorgos Verigakis's avatar
Giorgos Verigakis committed
244
        raise BadRequest('Malformed request.')
245
    
Giorgos Verigakis's avatar
Giorgos Verigakis committed
246
247
248
    vm = get_vm(server_id)
    vm.name = name
    vm.save()
249
250
251
    
    return HttpResponse(status=204)

252
@api_method('DELETE')
253
def delete_server(request, server_id):
254
255
256
257
258
259
260
261
262
    # 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
263
264
265
    vm = get_vm(server_id)
    backend.start_action(vm, 'DESTROY')
    rapi.DeleteInstance(vm.backend_id)
266
    return HttpResponse(status=204)
Giorgos Verigakis's avatar
Giorgos Verigakis committed
267
268
269

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

@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
295
296
    vm = get_vm(server_id)
    addresses = [address_to_dict(vm.ipfour, vm.ipsix)]
Giorgos Verigakis's avatar
Giorgos Verigakis committed
297
    
Giorgos Verigakis's avatar
Giorgos Verigakis committed
298
    if request.serialization == 'xml':
Giorgos Verigakis's avatar
Giorgos Verigakis committed
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
        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
315
    vm = get_vm(server_id)
Giorgos Verigakis's avatar
Giorgos Verigakis committed
316
    if network_id != 'public':
Giorgos Verigakis's avatar
Giorgos Verigakis committed
317
        raise ItemNotFound('Unknown network.')
Giorgos Verigakis's avatar
Giorgos Verigakis committed
318
    
Giorgos Verigakis's avatar
Giorgos Verigakis committed
319
    address = address_to_dict(vm.ipfour, vm.ipsix)
Giorgos Verigakis's avatar
Giorgos Verigakis committed
320
    
Giorgos Verigakis's avatar
Giorgos Verigakis committed
321
    if request.serialization == 'xml':
Giorgos Verigakis's avatar
Giorgos Verigakis committed
322
323
324
325
326
        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
327
328
329
330
331
332
333
334
335
336
337
338

@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)
339
    return render_metadata(request, metadata, use_values=True, status=200)
Giorgos Verigakis's avatar
Giorgos Verigakis committed
340
341
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

@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
    
371
    return render_metadata(request, metadata, status=201)
Giorgos Verigakis's avatar
Giorgos Verigakis committed
372
373
374
375
376
377
378
379
380
381
382
383

@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)
384
    return render_meta(request, meta, status=200)
Giorgos Verigakis's avatar
Giorgos Verigakis committed
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410

@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()
411
    return render_meta(request, meta, status=201)
Giorgos Verigakis's avatar
Giorgos Verigakis committed
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427

@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)