Statistics
| Branch: | Tag: | Revision:

root / api / handlers.py @ ac7188e6

History | View | Annotate | Download (21.6 kB)

1
# vim: ts=4 sts=4 et ai sw=4 fileencoding=utf-8
2
#
3
# Copyright © 2010 Greek Research and Technology Network
4

    
5
import simplejson as json
6
from django.conf import settings
7
from django.http import HttpResponse
8
from piston.handler import BaseHandler, AnonymousBaseHandler
9
from synnefo.api.faults import fault, noContent, accepted, created
10
from synnefo.api.helpers import instance_to_server, paginator
11
from synnefo.util.rapi import GanetiRapiClient, GanetiApiError, CertificateError
12
from synnefo.db.models import *
13
from time import sleep
14
import logging
15

    
16
log = logging.getLogger('synnefo.api.handlers')
17

    
18
try:
19
    rapi = GanetiRapiClient(*settings.GANETI_CLUSTER_INFO)
20
    rapi.GetVersion()
21
except Exception, e:
22
    raise fault.serviceUnavailable
23
#If we can't connect to the rapi successfully, don't do anything
24
#TODO: add logging/admin alerting
25

    
26
backend_prefix_id = settings.BACKEND_PREFIX_ID
27

    
28
VERSIONS = [
29
    {
30
        "status": "CURRENT",
31
        "id": "v1.0",
32
        "docURL" : "http://docs.rackspacecloud.com/servers/api/v1.0/cs-devguide-20110112.pdf",
33
        "wadl" : "http://docs.rackspacecloud.com/servers/api/v1.0/application.wadl"
34
    },
35
    {
36
        "status": "CURRENT",
37
        "id": "v1.0grnet1",
38
        "docURL" : "None yet",
39
        "wad1" : "None yet"
40
    }
41
]
42

    
43
#read is called on GET requests
44
#create is called on POST, and creates new objects
45
#update is called on PUT, and should update an existing product
46
#delete is called on DELETE, and should delete an existing object
47

    
48

    
49
class VersionHandler(AnonymousBaseHandler):
50
    allowed_methods = ('GET',)
51

    
52
    def read(self, request, number=None):
53
        if number is None:
54
            versions = map(lambda v: {
55
                        "status": v["status"],
56
                        "id": v["id"],
57
                    }, VERSIONS)
58
            return { "versions": versions }
59
        else:
60
            for version in VERSIONS:
61
                if version["id"] == number:
62
                    return { "version": version }
63
            raise fault.itemNotFound
64

    
65

    
66
class ServerHandler(BaseHandler):
67
    """Handler responsible for the Servers
68

69
     handles the listing of Virtual Machines, Creates and Destroys VM's
70

71
     @HTTP methods: POST, DELETE, PUT, GET
72
     @Parameters: POST data with the create data (cpu, ram, etc)
73
     @Responses: HTTP 202 if successfully call rapi, itemNotFound, serviceUnavailable otherwise
74

75
    """
76
    allowed_methods = ('GET', 'POST', 'PUT', 'DELETE')
77

    
78
    def read(self, request, id=None):
79
        from time import sleep
80
        sleep(0.5)
81
        #TODO: delete the sleep once the mock objects are removed
82
        if id is None:
83
            return self.read_all(request)
84
        elif id == "detail":
85
            return self.read_all(request, detail=True)
86
        else:
87
            return self.read_one(request, id)
88

    
89
    def read_one(self, request, id):
90
        try:
91
            server = VirtualMachine.objects.get(id=id)
92
        except VirtualMachine.DoesNotExist:
93
            raise fault.itemNotFound
94
        except VirtualMachine.MultipleObjectsReturned:
95
            raise fault.serviceUnavailable
96
        except Exception, e:
97
            log.error('Unexpected error: %s' % e)
98
            raise fault.serviceUnavailable
99

    
100
        server = {'status': server.rsapi_state, 
101
                                     'flavorId': server.flavor.id, 
102
                                     'name': server.name, 
103
                                     'id': server.id, 
104
                                     'imageId': server.sourceimage.id, 
105
                                     'hostId': server.hostid, 
106
                                     #'metadata': {'Server_Label': server.description },
107
                                     'metadata':[{'meta': { 'key': {metadata.meta_key: metadata.meta_value}}} for metadata in server.virtualmachinemetadata_set.all()],                                    
108
                                     'addresses': {'public': { 'ip': {'addr': server.ipfour}, 'ip6': {'addr': server.ipsix}},'private': ''},      
109
                }
110
        return { "server": server } 
111

    
112

    
113
    @paginator
114
    def read_all(self, request, detail=False):
115
        virtual_servers = VirtualMachine.objects.filter(deleted=False) 
116
        #get all VM's for now, FIX it to take the user's VMs only yet. also don't get deleted VM's
117

    
118
        if not detail:
119
            return { "servers": [ { "id": s.id, "name": s.name } for s in virtual_servers ] }
120
        else:
121
            virtual_servers_list = [{'status': server.rsapi_state, 
122
                                     'flavorId': server.flavor.id, 
123
                                     'name': server.name, 
124
                                     'id': server.id, 
125
                                     'imageId': server.sourceimage.id, 
126
                                     'hostId': server.hostid, 
127
                                     #'metadata': {'Server_Label': server.description },
128
                                     'metadata':[{'meta': { 'key': {metadata.meta_key: metadata.meta_value}}} for metadata in server.virtualmachinemetadata_set.all()],                                    
129
                                     'addresses': {'public': { 'ip': {'addr': server.ipfour}, 'ip6': {'addr': server.ipsix}},'private': ''},      
130

    
131
                                    } for server in virtual_servers]
132
            #pass some fake data regarding ip, since we don't have any such data            
133
            return { "servers":  virtual_servers_list }                
134

    
135

    
136
    def create(self, request):
137
        """ Parse RackSpace API create request to generate rapi create request
138
        
139
            TODO: auto generate and set password
140
        """
141
        #Check if we have all the necessary data in the JSON request       
142
        try:
143
            server = json.loads(request.raw_post_data)['server']
144
            descr = server['name']
145
            flavorId = server['flavorId']
146
            flavor = Flavor.objects.get(id=flavorId)
147
            imageId = server['imageId']
148
            metadata = server['metadata']
149
            personality = server.get('personality', None)
150
        except Exception as e:
151
            log.error('Malformed create request: %s - %s' % (e, request.raw_post_data))    
152
            raise fault.badRequest
153

    
154
        # add the new VM to the local db
155
        try:
156
            vm = VirtualMachine.objects.create(sourceimage=Image.objects.get(id=imageId),ipfour='0.0.0.0',flavor_id=flavorId)
157
        except Exception as e:
158
            log.error("Can't save vm: %s" % e)
159
            raise fault.serviceUnavailable
160

    
161
        try:
162
            vm.name = 'snf-%s' % vm.id
163
            vm.description = descr
164
            vm.save()            
165
            jobId = rapi.CreateInstance(
166
                'create',
167
                request.META['SERVER_NAME'] == 'testserver' and 'test-server' or 'snf-%s' % vm.id,
168
                'plain',
169
                # disk field of Flavor object is in GB, value specified here is in MB
170
                # FIXME: Always ask for a 2GB disk, current LVM physical groups are too small:
171
                # [{"size": flavor.disk * 1000}],
172
                [{"size": 2000}],
173
                [{}],
174
                #TODO: select OS from imageId
175
                os='debootstrap+default',
176
                ip_check=False,
177
                name_check=False,
178
                #TODO: verify if this is necessary
179
                pnode = rapi.GetNodes()[0],
180
                # Dry run when called by unit tests
181
                dry_run = request.META['SERVER_NAME'] == 'testserver',
182
                beparams={
183
                            'auto_balance': True,
184
                            'vcpus': flavor.cpu,
185
                            'memory': flavor.ram,
186
                        },
187
                )
188
            log.info('created vm with %s cpus, %s ram and %s storage' % (flavor.cpu, flavor.ram, flavor.disk))
189
        except (GanetiApiError, CertificateError) as e:
190
            log.error('CreateInstance failed: %s' % e)
191
            vm.deleted = True
192
            vm.save()
193
            raise fault.serviceUnavailable
194
        except Exception as e:
195
            log.error('Unexpected error: %s' % e)
196
            vm.deleted = True
197
            vm.save()
198
            raise fault.notImplemented            
199
        
200

    
201
        # take a power nap but don't forget to poll the ganeti job right after
202
        sleep(1)
203
        job = rapi.GetJobStatus(jobId)
204
        
205
        if job['status'] == 'error':
206
            log.error('Create Job failed: %s' % job['opresult'])
207
            raise fault.badRequest
208
        elif job['status'] in ['running', 'success', 'queued', 'waiting']:
209
            log.info('creating instance %s' % job['ops'][0]['instance_name'])     
210
            # Build the response
211
            status = job['status'] == 'running' and 'BUILD' or 'ACTIVE';
212
            ret = {'server': {
213
                    'id' : vm.id,
214
                    'name' : vm.name,
215
                    "imageId" : imageId,
216
                    "flavorId" : flavorId,
217
                    "hostId" : vm.hostid,
218
                    "progress" : 0,
219
                    "status" : status,
220
                    "adminPass" : "GFf1j9aP",
221
                    "metadata" : {"My Server Name" : vm.description},
222
                    "addresses" : {
223
                        "public" : [  ],
224
                        "private" : [  ],
225
                        },
226
                    },
227
            }
228
            return HttpResponse(json.dumps(ret), mimetype="application/json", status=202)
229
        else:
230
            # TODO: handle all possible job statuses
231
            log.error('Unhandled job status: %s' % job['status'])
232
            return fault.notImplemented
233

    
234
    def update(self, request, id):
235
        return noContent
236

    
237
    def delete(self, request, id):
238
        try:
239
            vm = VirtualMachine.objects.get(id=id)
240
        except VirtualMachine.DoesNotExist:
241
            raise fault.itemNotFound
242
        except VirtualMachine.MultipleObjectsReturned:
243
            raise fault.serviceUnavailable
244
        except Exception, e:
245
            log.error('Unexpected error: %s' % e)
246
            raise fault.serviceUnavailable
247

    
248
        #TODO: set the status to DESTROYED
249
        try:
250
            vm.start_action('DESTROY')
251
        except Exception, e:
252
            log.error('Unexpected error: %s' % e)            
253
            raise fault.serviceUnavailable
254

    
255
        try:
256
            rapi.DeleteInstance(vm.backend_id)
257
        except GanetiApiError, CertificateError:
258
            raise fault.serviceUnavailable
259
        except Exception, e:
260
            log.error('Unexpected error: %s' % e)
261
            raise fault.serviceUnavailable
262

    
263
        return accepted        
264

    
265
class ServerAddressHandler(BaseHandler):
266
    allowed_methods = ('GET', 'PUT', 'DELETE')
267

    
268
    def read(self, request, id, type=None):
269
        """List IP addresses for a server"""
270

    
271
        if type is None:
272
            pass
273
        elif type == "private":
274
            pass
275
        elif type == "public":
276
            pass
277
        return {}
278

    
279
    def update(self, request, id, address):
280
        """Share an IP address to another in the group"""
281
        return accepted
282

    
283
    def delete(self, request, id, address):
284
        """Unshare an IP address"""
285
        return accepted
286

    
287

    
288
class ServerActionHandler(BaseHandler):
289
    """Handler responsible for Server Actions
290

291
     handles Reboot, Shutdown and Start actions. 
292

293
     @HTTP methods: POST, DELETE, PUT
294
     @Parameters: POST data with the action (reboot, shutdown, start)
295
     @Responses: HTTP 202 if successfully call rapi, itemNotFound, serviceUnavailable otherwise
296

297
    """
298

    
299
    allowed_methods = ('POST', 'DELETE',  'PUT')
300

    
301
    def create(self, request, id):
302
        """Reboot, Shutdown, Start virtual machine"""
303
        
304
        requested_action = json.loads(request.raw_post_data)
305
        reboot_request = requested_action.get('reboot', None)
306
        shutdown_request = requested_action.get('shutdown', None)
307
        start_request = requested_action.get('start', None)
308
        #action not implemented
309
        action = reboot_request and 'REBOOT' or shutdown_request and 'SUSPEND' or start_request and 'START'
310
        if not action:
311
            raise fault.notImplemented 
312
        #test if we can get the vm
313
        try:
314
            vm = VirtualMachine.objects.get(id=id)
315
        except VirtualMachine.DoesNotExist:
316
            raise fault.itemNotFound
317
        except VirtualMachine.MultipleObjectsReturned:
318
            raise fault.serviceUnavailable
319
        except Exception, e:
320
            log.error('Unexpected error: %s' % e)
321
            raise fault.serviceUnavailable
322

    
323
        try:
324
            vm.start_action(action)
325
        except Exception, e:
326
            log.error('Unexpected error: %s' % e)
327
            raise fault.serviceUnavailable
328

    
329
        try:
330
            if reboot_request:
331
                rapi.RebootInstance(vm.backend_id)
332
            elif shutdown_request:
333
                rapi.ShutdownInstance(vm.backend_id)
334
            elif start_request:
335
                rapi.StartupInstance(vm.backend_id)
336
            return accepted
337
        except GanetiApiError, CertificateError:
338
            raise fault.serviceUnavailable
339
        except Exception, e:
340
            log.error('Unexpected error: %s' % e)
341
            raise fault.serviceUnavailable
342

    
343
    def delete(self, request, id):
344
        """Delete an Instance"""
345
        return accepted
346

    
347
    def update(self, request, id):
348
        return noContent
349

    
350

    
351

    
352

    
353
class ServerBackupHandler(BaseHandler):
354
    """ Backup Schedules are not implemented yet, return notImplemented """
355
    allowed_methods = ('GET', 'POST', 'DELETE')
356

    
357
    def read(self, request, id):
358
        raise fault.notImplemented
359

    
360
    def create(self, request, id):
361
        raise fault.notImplemented
362

    
363
    def delete(self, request, id):
364
        raise fault.notImplemented
365

    
366

    
367
class FlavorHandler(BaseHandler):
368
    allowed_methods = ('GET',)
369
    flavors = Flavor.objects.all()
370
    flavors = [ {'id': flavor.id, 'name': flavor.name, 'ram': flavor.ram, \
371
             'disk': flavor.disk, 'cpu': flavor.cpu} for flavor in flavors]
372

    
373
    def read(self, request, id=None):
374
        """
375
        List flavors or retrieve one
376

377
        Returns: OK
378
        Faults: cloudServersFault, serviceUnavailable, unauthorized,
379
                badRequest, itemNotFound
380
        """
381
        if id is None:
382
            simple = map(lambda v: {
383
                        "id": v['id'],
384
                        "name": v['name'],
385
                    }, self.flavors)
386
            return { "flavors": simple }
387
        elif id == "detail":
388
            return { "flavors": self.flavors }
389
        else:
390
            for flavor in self.flavors:
391
                if str(flavor['id']) == id:
392
                    return { "flavor": flavor }
393
            raise fault.itemNotFound
394

    
395

    
396
class ImageHandler(BaseHandler):
397
    """Handler responsible for Images
398

399
     handles the listing, creation and delete of Images. 
400

401
     @HTTP methods: GET, POST
402
     @Parameters: POST data 
403
     @Responses: HTTP 202 if successfully create Image or get the Images list, itemNotFound, serviceUnavailable otherwise
404

405
    """
406

    
407

    
408
    allowed_methods = ('GET', 'POST')
409

    
410
    def read(self, request, id=None):
411
        """
412
        List images or retrieve one
413

414
        Returns: OK
415
        Faults: cloudServersFault, serviceUnavailable, unauthorized,
416
                badRequest, itemNotFound
417
        """
418
        images = Image.objects.all()
419
        images_list = [ {'created': image.created.isoformat(), 
420
                    'id': image.id,
421
                    'name': image.name,
422
                    'updated': image.updated.isoformat(),    
423
                    'description': image.description, 
424
                    'status': image.state, 
425
                    'size': image.size, 
426
                    'serverId': image.sourcevm and image.sourcevm.id or ""
427
                   } for image in images]
428
        # Images info is stored in the DB. Ganeti is not aware of this
429
        if id == "detail":
430
            return { "images": images_list }
431
        elif id is None:
432
            return { "images": [ { "id": s['id'], "name": s['name'] } for s in images_list ] }
433
        else:
434
            try:
435
                image = images.get(id=id)
436
            except Image.DoesNotExist:
437
                raise fault.itemNotFound
438
            except Image.MultipleObjectsReturned:
439
                raise fault.serviceUnavailable
440
            except Exception, e:
441
                log.error('Unexpected error: %s' % e)
442
                raise fault.serviceUnavailable
443
            return { "image":  {'created': image.created.isoformat(), 
444
                'id': image.id,
445
                'name': image.name,
446
                'updated': image.updated.isoformat(),    
447
                'description': image.description, 
448
                'status': image.state, 
449
                'size': image.size, 
450
                'serverId': image.sourcevm and image.sourcevm.id or ""
451
               } }
452

    
453

    
454
    def create(self, request):
455
        """Create a new image"""
456
        return accepted
457

    
458

    
459
class SharedIPGroupHandler(BaseHandler):
460
    allowed_methods = ('GET', 'POST', 'DELETE')
461

    
462
    def read(self, request, id=None):
463
        """List Shared IP Groups"""
464
        if id is None:
465
            return {}
466
        elif id == "detail":
467
            return {}
468
        else:
469
            raise fault.itemNotFound
470

    
471
    def create(self, request, id):
472
        """Creates a new Shared IP Group"""
473
        return created
474

    
475
    def delete(self, request, id):
476
        """Deletes a Shared IP Group"""
477
        return noContent
478

    
479

    
480
class VirtualMachineGroupHandler(BaseHandler):
481
    allowed_methods = ('GET', 'POST', 'DELETE')
482

    
483
    def read(self, request, id=None):
484
        """List Groups"""
485
        vmgroups = VirtualMachineGroup.objects.all() 
486
        vmgroups = [ {'id': vmgroup.id, \
487
              'name': vmgroup.name,  \
488
               'server_id': [machine.id for machine in vmgroup.machines.all()] \
489
               } for vmgroup in vmgroups]
490
        # Group info is stored in the DB. Ganeti is not aware of this
491
        if id == "detail":
492
            return { "groups": vmgroups }
493
        elif id is None:
494
            return { "groups": [ { "id": s['id'], "name": s['name'] } for s in vmgroups ] }
495
        else:
496
            return { "groups": vmgroups[0] }
497

    
498

    
499
    def create(self, request, id):
500
        """Creates a Group"""
501
        return created
502

    
503
    def delete(self, request, id):
504
        """Deletes a  Group"""
505
        return noContent
506

    
507

    
508
class LimitHandler(BaseHandler):
509
    allowed_methods = ('GET',)
510

    
511
    # XXX: hookup with @throttle
512

    
513
    rate = [
514
        {
515
           "verb" : "POST",
516
           "URI" : "*",
517
           "regex" : ".*",
518
           "value" : 10,
519
           "remaining" : 2,
520
           "unit" : "MINUTE",
521
           "resetTime" : 1244425439
522
        },
523
        {
524
           "verb" : "POST",
525
           "URI" : "*/servers",
526
           "regex" : "^/servers",
527
           "value" : 25,
528
           "remaining" : 24,
529
           "unit" : "DAY",
530
           "resetTime" : 1244511839
531
        },
532
        {
533
           "verb" : "PUT",
534
           "URI" : "*",
535
           "regex" : ".*",
536
           "value" : 10,
537
           "remaining" : 2,
538
           "unit" : "MINUTE",
539
           "resetTime" : 1244425439
540
        },
541
        {
542
           "verb" : "GET",
543
           "URI" : "*",
544
           "regex" : ".*",
545
           "value" : 3,
546
           "remaining" : 3,
547
           "unit" : "MINUTE",
548
           "resetTime" : 1244425439
549
        },
550
        {
551
           "verb" : "DELETE",
552
           "URI" : "*",
553
           "regex" : ".*",
554
           "value" : 100,
555
           "remaining" : 100,
556
           "unit" : "MINUTE",
557
           "resetTime" : 1244425439
558
        }
559
    ]
560

    
561
    absolute = {
562
        "maxTotalRAMSize" : 51200,
563
        "maxIPGroups" : 50,
564
        "maxIPGroupMembers" : 25
565
    }
566

    
567
    def read(self, request):
568
        return { "limits": {
569
                "rate": self.rate,
570
                "absolute": self.absolute,
571
               }
572
            }
573

    
574

    
575
class DiskHandler(BaseHandler):
576
    allowed_methods = ('GET', 'POST', 'PUT', 'DELETE')
577

    
578
    def read(self, request, id=None):
579
        """List Disks"""
580
        if id is None:
581
            return self.read_all(request)
582
        elif id == "detail":
583
            return self.read_all(request, detail=True)
584
        else:
585
            return self.read_one(request, id)
586

    
587
    def read_one(self, request, id):
588
        """List one Disk with the specified id with all details"""
589
        # FIXME Get detailed info from the DB 
590
        # for the Disk with the specified id
591
        try:
592
            disk = Disk.objects.get(pk=id)
593
            disk_details = {
594
                "id" : disk.id, 
595
                "name" : disk.name, 
596
                "size" : disk.size,
597
                "created" : disk.created, 
598
                "serverId" : disk.vm.id
599
            }
600
            return { "disks" : disk_details }
601
        except:
602
            raise fault.itemNotFound
603

    
604
    @paginator
605
    def read_all(self, request, detail=False):
606
        """List all Disks. If -detail- is set list them with all details"""
607
        if not detail:
608
            disks = Disk.objects.filter(owner=SynnefoUser.objects.all()[0])
609
            return { "disks": [ { "id": disk.id, "name": disk.name } for disk in disks ] }
610
        else:
611
            disks = Disk.objects.filter(owner=SynnefoUser.objects.all()[0])
612
            disks_details = [ {
613
                "id" : disk.id, 
614
                "name" : disk.name,
615
                "size" : disk.size,
616
                "created" : disk.created, 
617
                "serverId" : disk.vm.id,
618
            } for disk in disks ]
619
            return { "disks":  disks_details }                
620

    
621
    def create(self, request):
622
        """Create a new Disk"""
623
        # FIXME Create a partial DB entry, 
624
        # then call the backend for actual creation
625
        pass
626

    
627
    def update(self, request, id):
628
        """Rename the Disk with the specified id"""
629
        # FIXME Change the Disk's name in the DB
630
        pass
631

    
632
    def delete(self, request, id):
633
        """Destroy the Disk with the specified id"""
634
        # Call the backend for actual destruction
635
        pass