Statistics
| Branch: | Tag: | Revision:

root / api / handlers.py @ 038383b1

History | View | Annotate | Download (39.7 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
from django.utils import simplejson as json
6
from django.http import HttpResponse
7
from piston.handler import BaseHandler, AnonymousBaseHandler
8
from synnefo.api.faults import fault, noContent, accepted, created, notModified
9
from synnefo.api.helpers import instance_to_server, paginator, authenticate
10
from synnefo.util.rapi import GanetiRapiClient, GanetiApiError
11
from synnefo.util.rapi import CertificateError
12
from synnefo.db.models import *
13
import random
14
import string
15
import logging
16
from datetime import datetime, timedelta
17
import iso8601
18
from logic import backend, utils
19

    
20
log = logging.getLogger('synnefo.api.handlers')
21

    
22
try:
23
    rapi = GanetiRapiClient(*settings.GANETI_CLUSTER_INFO)
24
    rapi.GetVersion()
25
except Exception, e:
26
    log.exception('Unexpected error: %s' % e)
27
    raise fault.serviceUnavailable
28
#If we can't connect to the rapi successfully, don't do anything
29

    
30
VERSIONS = [
31
    {
32
        "status": "CURRENT",
33
        "id": "v1.0",
34
        "docURL":
35
    "http://docs.rackspacecloud.com/servers/api/v1.0/cs-devguide-20110112.pdf",
36
        "wadl":
37
            "http://docs.rackspacecloud.com/servers/api/v1.0/application.wadl"
38
    },
39
    {
40
        "status": "CURRENT",
41
        "id": "v1.1",
42
        "docURL":
43
            "http://docs.openstack.org/openstack-compute/developer/content/",
44
        "wadl": "None yet"
45
    },
46
    {
47
        "status": "CURRENT",
48
        "id": "v1.0grnet1",
49
        "docURL": "None yet",
50
        "wad1": "None yet"
51
    }
52
]
53

    
54
#read is called on GET requests
55
#create is called on POST, and creates new objects
56
#update is called on PUT, and should update an existing product
57
#delete is called on DELETE, and should delete an existing object
58

    
59

    
60
class VersionHandler(AnonymousBaseHandler):
61
    allowed_methods = ('GET',)
62

    
63
    def read(self, request, number=None):
64
        try:
65
            if number is None:
66
                versions = map(lambda v: {
67
                            "status": v["status"],
68
                            "id": v["id"],
69
                        }, VERSIONS)
70
                return {"versions": versions}
71
            else:
72
                for version in VERSIONS:
73
                    if version["id"] == number:
74
                        return {"version": version}
75
                raise fault.itemNotFound
76
        except Exception, e:
77
            log.exception('Unexpected error: %s' % e)
78
            raise fault.serviceUnavailable
79

    
80

    
81
class ServerHandler(BaseHandler):
82
    """Handler responsible for the Servers
83

84
     handles the listing of Virtual Machines, Creates and Destroys VM's
85

86
     @HTTP methods: POST, DELETE, PUT, GET
87
     @Parameters: POST data with the create data (cpu, ram, etc)
88
     @Responses: HTTP 200 if successfully call rapi, 304 if not modified,
89
                 itemNotFound or serviceUnavailable otherwise
90

91
    """
92
    allowed_methods = ('GET', 'POST', 'PUT', 'DELETE')
93

    
94
    #@authenticate
95
    def read(self, request, id=None):
96
        from time import sleep
97
        sleep(0.5)
98
        #TODO: delete the sleep once the mock objects are removed
99
        if id is None:
100
            return self.read_all(request)
101
        elif id == "detail":
102
            return self.read_all(request, detail=True)
103
        else:
104
            return self.read_one(request, id)
105

    
106
    def read_one(self, request, id):
107
        try:
108
            server = VirtualMachine.objects.get(id=id)
109
            server = {'status': utils.get_rsapi_state(server),
110
                     'flavorRef': server.flavor.id,
111
                     'name': server.name,
112
                     'id': server.id,
113
                     'imageRef': server.sourceimage.id,
114
                     'created': server.created,
115
                     'updated': server.updated,
116
                     'hostId': server.hostid,
117
                     'progress': utils.get_rsapi_state(server) == 'ACTIVE' and 100 or 0,
118
                     #'metadata': {'Server_Label': server.description },
119
                     'metadata': [{'meta': {
120
                                    'key': {
121
                                     metadata.meta_key: metadata.meta_value
122
                                    }
123
                                   }
124
                        }
125
                            for metadata in
126
                            server.virtualmachinemetadata_set.all()],
127
                     'addresses': {'public': {
128
                                    'ip': {'addr': server.ipfour},
129
                                    'ip6': {'addr': server.ipsix}},
130
                                            'private': ''},
131
                    }
132
            return {"server": server}
133
        except VirtualMachine.DoesNotExist:
134
            raise fault.itemNotFound
135
        except VirtualMachine.MultipleObjectsReturned:
136
            raise fault.serviceUnavailable
137
        except Exception, e:
138
            log.exception('Unexpected error: %s' % e)
139
            raise fault.serviceUnavailable
140

    
141
    @paginator
142
    def read_all(self, request, detail=False):
143
        #changes_since should be on ISO 8601 format
144
        try:
145
            changes_since = request.GET.get("changes-since", 0)
146
            if changes_since:
147
                last_update = iso8601.parse_date(changes_since)
148
                #return a badRequest if the changes_since is older than a limit
149
                if (datetime.now(last_update.tzinfo) - last_update >
150
                    timedelta(seconds=settings.POLL_LIMIT)):
151
                    raise fault.badRequest
152
                virtual_servers = VirtualMachine.objects.filter(
153
                    updated__gt=last_update)
154
                if not len(virtual_servers):
155
                    return notModified
156
            else:
157
                virtual_servers = VirtualMachine.objects.filter(deleted=False)
158
            # get all VM's for now, FIX it to take the user's VMs only yet.
159
            # also don't get deleted VM's
160
        except Exception, e:
161
            raise fault.badRequest
162
        try:
163
            if not detail:
164
                return {"servers": [{
165
                    "id": s.id, "name": s.name} for s in virtual_servers]}
166
            else:
167
                virtual_servers_list = [
168
                    {'status': utils.get_rsapi_state(server),
169
                     'flavorRef': server.flavor.id,
170
                    'name': server.name,
171
                    'id': server.id,
172
                    'created': server.created,
173
                    'updated': server.updated,
174
                    'imageRef': server.sourceimage.id,
175
                    'hostId': server.hostid,
176
                    'progress': (utils.get_rsapi_state(server) == 'ACTIVE' and 100 or 0),
177
                     #'metadata': {'Server_Label': server.description },
178
                    'metadata':[
179
                        {'meta':
180
                            {'key': {metadata.meta_key: metadata.meta_value}}}
181
                                for metadata in
182
                                server.virtualmachinemetadata_set.all()
183
                        ],
184
                    'addresses': {
185
                        'public': {
186
                            'ip': {'addr': server.ipfour},
187
                            'ip6': {'addr': server.ipsix}
188
                        },
189
                        'private': ''},
190
                    } for server in virtual_servers]
191
                #pass fake data regarding ip, since we don't have it yet
192
                return {"servers":  virtual_servers_list}
193
        except Exception, e:
194
            log.exception('Unexpected error: %s' % e)
195
            raise fault.serviceUnavailable
196

    
197
    def create(self, request):
198
        """ Parse RackSpace API create request to generate rapi create request
199
            TODO: auto generate and set password
200
        """
201
        # Check if we have all the necessary data in the JSON request
202
        try:
203
            server = json.loads(request.raw_post_data)['server']
204
            name = server['name']
205
            flavorRef = server['flavorRef']
206
            flavor = Flavor.objects.get(id=flavorRef)
207
            imageRef = server['imageRef']
208
            image = Image.objects.get(id=imageRef)
209
            metadata = server['metadata']
210
            personality = server.get('personality', None)
211
        except (Flavor.DoesNotExist, Image.DoesNotExist):
212
            raise fault.itemNotFound
213
        except (Flavor.MultipleObjectsReturned, Image.MultipleObjectsReturned):
214
            raise fault.serviceUnavailable
215
        except Exception as e:
216
            log.exception('Malformed create request: %s - %s' %
217
                          (e, request.raw_post_data))
218
            raise fault.badRequest
219

    
220
        # TODO: Proper Authn, Authz
221
        # Everything belongs to a single SynnefoUser for now.
222
        try:
223
            owner = SynnefoUser.objects.all()[0]
224
        except Exception as e:
225
            log.exception('Cannot find a single SynnefoUser in the DB: %s' %
226
                          (e))
227
            raise fault.unauthorized
228

    
229
        # add the new VM to the local db
230
        try:
231
            vm = VirtualMachine.objects.create(sourceimage=image,
232
                                               ipfour='0.0.0.0',
233
                                               ipsix='::1',
234
                                               flavor=flavor,
235
                                               owner=owner)
236
        except Exception as e:
237
            log.exception("Can't save vm: %s" % e)
238
            raise fault.serviceUnavailable
239

    
240
        try:
241
            vm.name = name
242
            #vm.description = descr
243
            vm.save()
244
            jobId = rapi.CreateInstance(
245
                'create',
246
                (request.META['SERVER_NAME'] == 'testserver' and
247
                'test-server' or vm.backend_id),
248
                'plain',
249
                # disk field of Flavor object is in GB,
250
                # value specified here is in MB
251
                # FIXME: Always ask for a 2GB disk,
252
                # current LVM physical groups are too small:
253
                # [{"size": flavor.disk * 1000}],
254
                [{"size": 2000}],
255
                [{}],
256
                #TODO: select OS from imageRef
257
                os='debootstrap+default',
258
                ip_check=False,
259
                name_check=False,
260
                #TODO: verify if this is necessary
261
                pnode=rapi.GetNodes()[0],
262
                # Dry run when called by unit tests
263
                dry_run=request.META['SERVER_NAME'] == 'testserver',
264
                beparams={
265
                            'auto_balance': True,
266
                            'vcpus': flavor.cpu,
267
                            'memory': flavor.ram,
268
                        },
269
                )
270
            log.info('created vm with %s cpus, %s ram and %s storage' %
271
                     (flavor.cpu, flavor.ram, flavor.disk))
272
        except (GanetiApiError, CertificateError) as e:
273
            log.exception('CreateInstance failed: %s' % e)
274
            # FIX: if the instance creation have failed, why it is saved in the db?
275
            vm.deleted = True
276
            vm.save()
277
            raise fault.serviceUnavailable
278
        except Exception as e:
279
            log.exception('Unexpected error: %s' % e)
280
            vm.deleted = True
281
            vm.save()
282
            raise fault.serviceUnavailable
283

    
284
        ret = {'server': {
285
                'id': vm.id,
286
                'name': vm.name,
287
                "imageRef": imageRef,
288
                "flavorRef": flavorRef,
289
                "hostId": vm.hostid,
290
                "progress": 0,
291
                "status": 'BUILD',
292
                "adminPass": self.random_password(),
293
                "metadata": {"My Server Name": vm.name},
294
                "addresses": {
295
                    "public": [],
296
                    "private": [],
297
                    },
298
                },
299
        }
300
        return HttpResponse(json.dumps(ret),
301
                            mimetype="application/json", status=202)
302

    
303
    def random_password(self):
304
        "return random password"
305
        number_of_chars = 8
306
        possible_chars = string.ascii_uppercase + string.ascii_lowercase + \
307
                         string.digits
308
        return ''.join(random.choice(possible_chars) \
309
                       for x in range(number_of_chars))
310

    
311
    def update(self, request, id):
312
        """Sets and updates Virtual Machine Metadata.
313

314
        """
315
        try:
316
            metadata_request = json.loads(request.raw_post_data)['metadata']
317
            metadata_key = metadata_request.get('metadata_key')
318
            metadata_value = metadata_request.get('metadata_value')
319

    
320
            vm = VirtualMachine.objects.get(id=id)
321
            #we only update virtual machine's name atm
322
            if metadata_key == 'name':
323
                vm.name = metadata_value
324
                vm.save()
325
                return accepted
326
        except VirtualMachine.DoesNotExist:
327
            raise fault.itemNotFound
328
        except VirtualMachine.MultipleObjectsReturned:
329
            raise fault.serviceUnavailable
330
        except Exception, e:
331
            log.exception('Unexpected error: %s' % e)
332
            raise fault.serviceUnavailable
333

    
334
        raise fault.itemNotFound
335

    
336
    def delete(self, request, id):
337
        try:
338
            vm = VirtualMachine.objects.get(id=id)
339
            #TODO: set the status to DESTROYED
340
            backend.start_action(vm, 'DESTROY')
341
            rapi.DeleteInstance(vm.backend_id)
342
            return accepted
343
        except VirtualMachine.DoesNotExist:
344
            raise fault.itemNotFound
345
        except VirtualMachine.MultipleObjectsReturned:
346
            raise fault.serviceUnavailable
347
        except GanetiApiError, CertificateError:
348
            raise fault.serviceUnavailable
349
        except Exception, e:
350
            log.exception('Unexpected error: %s' % e)
351
            raise fault.serviceUnavailable
352

    
353

    
354
class ServerAddressHandler(BaseHandler):
355
    """Handler responsible for Server Addresses
356

357
     handles Reboot, Shutdown and Start actions.
358

359
     @HTTP methods: GET
360
     @Parameters: Id of server and networkID (eg public, private)
361
     @Responses: HTTP 200 if successfully call rapi, itemNotFound,
362
     serviceUnavailable otherwise
363

364
    """
365
    allowed_methods = ('GET',)
366

    
367
    def read(self, request, id, networkID=None):
368
        """List IP addresses for a server"""
369

    
370
        try:
371
            server = VirtualMachine.objects.get(id=id)
372
            address = {'public': {'ip': {'addr': server.ipfour}, \
373
                'ip6': {'addr': server.ipsix}}, 'private': ''}
374
        except VirtualMachine.DoesNotExist:
375
            raise fault.itemNotFound
376
        except VirtualMachine.MultipleObjectsReturned:
377
            raise fault.serviceUnavailable
378
        except Exception, e:
379
            log.exception('Unexpected error: %s' % e)
380
            raise fault.serviceUnavailable
381

    
382
        if networkID == "public":
383
            address = {'public': {'ip': {'addr': server.ipfour}, \
384
                'ip6': {'addr': server.ipsix}}}
385
        elif networkID == "private":
386
            address = {'private': ''}
387
        elif networkID != None:
388
            raise fault.badRequest
389
        return {"addresses": address}
390

    
391

    
392
class ServerActionHandler(BaseHandler):
393
    """Handler responsible for Server Actions
394

395
     handles Reboot, Shutdown and Start actions.
396

397
     @HTTP methods: POST, DELETE, PUT
398
     @Parameters: POST data with the action (reboot, shutdown, start)
399
     @Responses: HTTP 202 if successfully call rapi, itemNotFound,
400
     serviceUnavailable otherwise
401

402
    """
403

    
404
    allowed_methods = ('POST', 'DELETE',  'PUT')
405

    
406
    def create(self, request, id):
407
        """Reboot, Shutdown, Start virtual machine"""
408

    
409
        try:
410
            requested_action = json.loads(request.raw_post_data)
411
            reboot_request = requested_action.get('reboot', None)
412
            shutdown_request = requested_action.get('shutdown', None)
413
            start_request = requested_action.get('start', None)
414
            #action not implemented
415
            action = reboot_request and 'REBOOT' or shutdown_request \
416
                     and 'STOP' or start_request and 'START'
417

    
418
            if not action:
419
                raise fault.notImplemented
420
            #test if we can get the vm
421
            vm = VirtualMachine.objects.get(id=id)
422
            backend.start_action(vm, action)
423

    
424
            if reboot_request:
425
                rapi.RebootInstance(vm.backend_id)
426
            elif shutdown_request:
427
                rapi.ShutdownInstance(vm.backend_id)
428
            elif start_request:
429
                rapi.StartupInstance(vm.backend_id)
430
            return accepted
431
        except VirtualMachine.DoesNotExist:
432
            raise fault.itemNotFound
433
        except VirtualMachine.MultipleObjectsReturned:
434
            raise fault.serviceUnavailable
435
        except GanetiApiError, CertificateError:
436
            raise fault.serviceUnavailable
437
        except Exception, e:
438
            log.exception('Unexpected error: %s' % e)
439
            raise fault.serviceUnavailable
440

    
441
    def delete(self, request, id):
442
        """Delete an Instance"""
443
        return accepted
444

    
445
    def update(self, request, id):
446
        raise fault.itemNotFound
447

    
448

    
449
class ServerBackupHandler(BaseHandler):
450
    """ Backup Schedules are not implemented yet, return notImplemented """
451
    allowed_methods = ('GET', 'POST', 'DELETE')
452

    
453
    def read(self, request, id):
454
        raise fault.notImplemented
455

    
456
    def create(self, request, id):
457
        raise fault.notImplemented
458

    
459
    def delete(self, request, id):
460
        raise fault.notImplemented
461

    
462

    
463
class ServerMetadataHandler(BaseHandler):
464
    """Handles Metadata of a specific Server
465

466
    the handler Lists, Creates, Updates and Deletes Metadata values
467

468
    @HTTP methods: POST, DELETE, PUT, GET
469
    @Parameters: POST data with key value pairs
470

471
    """
472
    allowed_methods = ('GET', 'POST', 'PUT', 'DELETE')
473

    
474
    def read(self, request, id, key=None):
475
        """List metadata of the specific server"""
476
        if key is None:
477
            return self.read_allkeys(request, id)
478
        else:
479
            return self.read_onekey(request, id, key)
480

    
481
    def read_allkeys(self, request, id):
482
        """Returns all the key value pairs of the specified server"""
483
        try:
484
            server = VirtualMachine.objects.get(pk=id)
485
            return {
486
                "metadata": {
487
                    "values": [
488
                        {m.meta_key: m.meta_value} \
489
                        for m in server.virtualmachinemetadata_set.all()
490
                    ]
491
                }
492
            }
493
        except VirtualMachine.DoesNotExist:
494
            raise fault.itemNotFound
495
        except VirtualMachine.MultipleObjectsReturned:
496
            raise fault.serviceUnavailable
497
        except Exception, e:
498
            log.exception('Unexpected error: %s' % e)
499
            raise fault.serviceUnavailable
500

    
501
    def read_onekey(self, request, id, key):
502
        """Returns the specified metadata key of the specified server"""
503
        try:
504
            server = VirtualMachine.objects.get(pk=id)
505
            return {
506
                "metadata": {
507
                    "values": [
508
                        {m.meta_key: m.meta_value} for m in
509
                        server.virtualmachinemetadata_set.filter(meta_key=key)
510
                    ]
511
                }
512
            }
513
        except VirtualMachineMetadata.DoesNotExist:
514
            raise fault.itemNotFound
515
        except VirtualMachine.DoesNotExist:
516
            raise fault.itemNotFound
517
        except VirtualMachine.MultipleObjectsReturned:
518
            raise fault.serviceUnavailable
519
        except Exception, e:
520
            log.exception('Unexpected error: %s' % e)
521
            raise fault.serviceUnavailable
522

    
523
    def create(self, request, id, key=None):
524
        """Create or Update all metadata for the specified VM"""
525
        if key is not None:
526
            log.exception('The POST request should not pass a key in the URL')
527
            raise fault.badRequest
528
        try:
529
            metadata = json.loads(request.raw_post_data)['metadata']
530
        except Exception as e:
531
            log.exception('Malformed create request: %s - %s' \
532
                          % (e, request.raw_post_data))
533
            raise fault.badRequest
534

    
535
        try:
536
            vm = VirtualMachine.objects.get(pk=id)
537
            for x in metadata.keys():
538
                vm_meta, created = (vm.virtualmachinemetadata_set.
539
                                    get_or_create(meta_key=x))
540
                vm_meta.meta_value = metadata[x]
541
                vm_meta.save()
542
            return {
543
                "metadata": [{
544
                    "meta": {
545
                        "key": {m.meta_key: m.meta_value}}} \
546
                    for m in vm.virtualmachinemetadata_set.all()]
547
            }
548
        except VirtualMachine.DoesNotExist:
549
            raise fault.itemNotFound
550
        except VirtualMachine.MultipleObjectsReturned:
551
            raise fault.serviceUnavailable
552
        except VirtualMachineMetadata.DoesNotExist:
553
            raise fault.itemNotFound
554
        except VirtualMachineMetadata.MultipleObjectsReturned:
555
            raise fault.serviceUnavailable
556
        except Exception, e:
557
            log.exception('Unexpected error: %s' % e)
558
            raise fault.serviceUnavailable
559

    
560
    def update(self, request, id, key=None):
561
        """Update or Create the specified metadata key for the specified VM"""
562
        if key is None:
563
            log.exception('No metadata key specified in URL')
564
            raise fault.badRequest
565
        try:
566
            metadata = json.loads(request.raw_post_data)['meta']
567
            metadata_value = metadata[key]
568
        except Exception as e:
569
            log.exception('Malformed create request: %s - %s' \
570
                          % (e, request.raw_post_data))
571
            raise fault.badRequest
572

    
573
        try:
574
            server = VirtualMachine.objects.get(pk=id)
575
            vm_meta, created = (server.virtualmachinemetadata_set.
576
                                get_or_create(meta_key=key))
577
            vm_meta.meta_value = metadata_value
578
            vm_meta.save()
579
            return {"meta": {vm_meta.meta_key: vm_meta.meta_value}}
580

    
581
        except VirtualMachine.DoesNotExist:
582
            raise fault.itemNotFound
583
        except VirtualMachine.MultipleObjectsReturned:
584
            raise fault.serviceUnavailable
585
        except VirtualMachineMetadata.DoesNotExist:
586
            raise fault.itemNotFound
587
        except VirtualMachineMetadata.MultipleObjectsReturned:
588
            raise fault.serviceUnavailable
589
        except Exception, e:
590
            log.exception('Unexpected error: %s' % e)
591
            raise fault.serviceUnavailable
592

    
593
    def delete(self, request, id, key=None):
594
        """Delete the specified metadata key"""
595
        if key is None:
596
            log.exception('No metadata key specified in URL')
597
            raise fault.badRequest
598
        try:
599
            server = VirtualMachine.objects.get(pk=id)
600
            server.virtualmachinemetadata_set.get(meta_key=key).delete()
601
        except VirtualMachineMetadata.DoesNotExist:
602
            raise fault.itemNotFound
603
        except VirtualMachine.DoesNotExist:
604
            raise fault.itemNotFound
605
        except VirtualMachineMetadata.MultipleObjectsReturned:
606
            raise fault.serviceUnavailable
607
        except VirtualMachine.MultipleObjectsReturned:
608
            raise fault.serviceUnavailable
609
        except Exception, e:
610
            log.exception('Unexpected error: %s' % e)
611
            raise fault.serviceUnavailable
612

    
613

    
614
class FlavorHandler(BaseHandler):
615
    """Handler responsible for Flavors
616

617
    """
618
    allowed_methods = ('GET',)
619

    
620
    def read(self, request, id=None):
621
        """
622
        List flavors or retrieve one
623

624
        Returns: OK
625
        Faults: cloudServersFault, serviceUnavailable, unauthorized,
626
                badRequest, itemNotFound
627
        """
628
        try:
629
            flavors = Flavor.objects.all()
630
            flavors = [{'id': flavor.id,
631
                        'name': flavor.name,
632
                        'ram': flavor.ram,
633
                        'disk': flavor.disk,
634
                        'cpu': flavor.cpu}
635
                        for flavor in flavors]
636

    
637
            if id is None:
638
                simple = map(lambda v: {
639
                            "id": v['id'],
640
                            "name": v['name'],
641
                        }, flavors)
642
                return {"flavors": simple}
643
            elif id == "detail":
644
                return {"flavors": flavors}
645
            else:
646
                flavor = Flavor.objects.get(id=id)
647
                return {"flavor":  {
648
                    'id': flavor.id,
649
                    'name': flavor.name,
650
                    'ram': flavor.ram,
651
                    'disk': flavor.disk,
652
                    'cpu': flavor.cpu,
653
                   }}
654

    
655
        except Flavor.DoesNotExist:
656
            raise fault.itemNotFound
657
        except Flavor.MultipleObjectsReturned:
658
            raise fault.serviceUnavailable
659
        except Exception, e:
660
            log.exception('Unexpected error: %s' % e)
661
            raise fault.serviceUnavailable
662

    
663

    
664
class ImageHandler(BaseHandler):
665
    """Handler responsible for Images
666

667
     handles the listing, creation and delete of Images.
668

669
     @HTTP methods: GET, POST
670
     @Parameters: POST data
671
     @Responses: HTTP 202 if successfully create Image or get the Images list,
672
     itemNotFound, serviceUnavailable otherwise
673

674
    """
675

    
676
    allowed_methods = ('GET', 'POST')
677

    
678
    def read(self, request, id=None):
679
        """
680
        List images or retrieve one
681

682
        Returns: OK
683
        Faults: cloudServersFault, serviceUnavailable, unauthorized,
684
                badRequest, itemNotFound
685
        """
686

    
687
        #changes_since should be on ISO 8601 format
688
        try:
689
            changes_since = request.GET.get("changes-since", 0)
690
            if changes_since:
691
                last_update = datetime.strptime(changes_since,
692
                                                "%Y-%m-%dT%H:%M:%S")
693
                #return a badRequest if the changes_since is older than a limit
694
                if datetime.now() - last_update > timedelta(
695
                    seconds=settings.POLL_LIMIT):
696
                    raise fault.badRequest
697
                images = Image.objects.filter(updated__gt=last_update)
698
                if not len(images):
699
                    return notModified
700
            else:
701
                images = Image.objects.all()
702
        except Exception, e:
703
            raise fault.badRequest
704
        try:
705
            images_list = [{'created': image.created.isoformat(),
706
                        'id': image.id,
707
                        'name': image.name,
708
                        'updated': image.updated.isoformat(),
709
                        'status': image.state,
710
                        'progress': image.state == 'ACTIVE' and 100 or 0,
711
                        'size': image.size,
712
                        'serverId': image.sourcevm and image.sourcevm.id or "",
713
                        #'metadata':[{'meta':
714
                        #{ 'key': {metadata.meta_key: metadata.meta_value}}}
715
                        #for metadata in image.imagemetadata_set.all()]
716
                        'metadata':{'meta':
717
                            {'key': {'description': image.description}}},
718
                       } for image in images]
719
            # Images info is stored in the DB. Ganeti is not aware of this
720
            if id == "detail":
721
                return {"images": images_list}
722
            elif id is None:
723
                return {"images": [{"id": s['id'], "name": s['name']} \
724
                    for s in images_list]}
725
            else:
726
                image = images.get(id=id)
727
                return {"image":  {'created': image.created.isoformat(),
728
                    'id': image.id,
729
                    'name': image.name,
730
                    'updated': image.updated.isoformat(),
731
                    'description': image.description,
732
                    'status': image.state,
733
                    'progress': image.state == 'ACTIVE' and 100 or 0,
734
                    'size': image.size,
735
                    'serverId': image.sourcevm and image.sourcevm.id or "",
736
                    #'metadata':[{'meta': { 'key':
737
                    #{metadata.meta_key: metadata.meta_value}}}
738
                    #for metadata in image.imagemetadata_set.all()]
739
                    'metadata': {
740
                        'meta': {'key': {'description': image.description}}},
741
                   }}
742
        except Image.DoesNotExist:
743
                    raise fault.itemNotFound
744
        except Image.MultipleObjectsReturned:
745
                    raise fault.serviceUnavailable
746
        except Exception, e:
747
                    log.exception('Unexpected error: %s' % e)
748
                    raise fault.serviceUnavailable
749

    
750
    def create(self, request):
751
        """Create a new image"""
752
        return accepted
753

    
754

    
755
class ImageMetadataHandler(BaseHandler):
756
    """Handles Metadata of a specific Image
757

758
    the handler Lists, Creates, Updates and Deletes Metadata values
759

760
    @HTTP methods: POST, DELETE, PUT, GET
761
    @Parameters: POST data with key value pairs
762

763
    """
764
    allowed_methods = ('GET', 'POST', 'PUT', 'DELETE')
765

    
766
    def read(self, request, id, key=None):
767
        """List metadata of the specific server"""
768
        if key is None:
769
            return self.read_allkeys(request, id)
770
        else:
771
            return self.read_onekey(request, id, key)
772

    
773
    def read_allkeys(self, request, id):
774
        """Returns all the key value pairs of the specified server"""
775
        try:
776
            image = Image.objects.get(pk=id)
777
            return {
778
                "metadata": [{
779
                    "meta": {
780
                        "key": {m.meta_key: m.meta_value}}} \
781
                    for m in image.imagemetadata_set.all()]
782
            }
783
        except Image.DoesNotExist:
784
            raise fault.itemNotFound
785
        except Image.MultipleObjectsReturned:
786
            raise fault.serviceUnavailable
787
        except Exception, e:
788
            log.exception('Unexpected error: %s' % e)
789
            raise fault.serviceUnavailable
790

    
791
    def read_onekey(self, request, id, key):
792
        """Returns the specified metadata key of the specified server"""
793
        try:
794
            image = Image.objects.get(pk=id)
795
            return {
796
                "metadata": {
797
                    "values": [
798
                        {m.meta_key: m.meta_value} \
799
                        for m in image.imagemetadata_set.filter(meta_key=key)
800
                    ]
801
                }
802
            }
803
        except ImageMetadata.DoesNotExist:
804
            raise fault.itemNotFound
805
        except Image.DoesNotExist:
806
            raise fault.itemNotFound
807
        except Image.MultipleObjectsReturned:
808
            raise fault.serviceUnavailable
809
        except Exception, e:
810
            log.exception('Unexpected error: %s' % e)
811
            raise fault.serviceUnavailable
812

    
813
    def create(self, request, id, key=None):
814
        """Create or Update all metadata for the specified Image"""
815
        if key is not None:
816
            log.exception('The POST request should not pass a key in the URL')
817
            raise fault.badRequest
818
        try:
819
            metadata = json.loads(request.raw_post_data)['metadata']
820
        except Exception as e:
821
            log.exception('Malformed create request: %s - %s' \
822
                          % (e, request.raw_post_data))
823
            raise fault.badRequest
824

    
825
        try:
826
            image = Image.objects.get(pk=id)
827
            for x in metadata.keys():
828
                img_meta, created = (image.imagemetadata_set.
829
                                     get_or_create(meta_key=x))
830
                img_meta.meta_value = metadata[x]
831
                img_meta.save()
832
            return {
833
                "metadata": [{
834
                    "meta": {
835
                        "key": {m.meta_key: m.meta_value}}} \
836
                    for m in image.imagemetadata_set.all()]
837
            }
838
        except Image.DoesNotExist:
839
            raise fault.itemNotFound
840
        except Image.MultipleObjectsReturned:
841
            raise fault.serviceUnavailable
842
        except ImageMetadata.DoesNotExist:
843
            raise fault.itemNotFound
844
        except ImageMetadata.MultipleObjectsReturned:
845
            raise fault.serviceUnavailable
846
        except Exception, e:
847
            log.exception('Unexpected error: %s' % e)
848
            raise fault.serviceUnavailable
849

    
850
    def update(self, request, id, key=None):
851
        """Update or Create the specified metadata key for the
852
        specified Image"""
853
        if key is None:
854
            log.exception('No metadata key specified in URL')
855
            raise fault.badRequest
856
        try:
857
            metadata = json.loads(request.raw_post_data)['meta']
858
            metadata_value = metadata[key]
859
        except Exception as e:
860
            log.exception('Malformed create request: %s - %s' \
861
                          % (e, request.raw_post_data))
862
            raise fault.badRequest
863

    
864
        try:
865
            image = Image.objects.get(pk=id)
866
            img_meta, created = (image.imagemetadata_set.
867
                                 get_or_create(meta_key=key))
868
            img_meta.meta_value = metadata_value
869
            img_meta.save()
870
            return {"meta": {img_meta.meta_key: img_meta.meta_value}}
871

    
872
        except Image.DoesNotExist:
873
            raise fault.itemNotFound
874
        except Image.MultipleObjectsReturned:
875
            raise fault.serviceUnavailable
876
        except ImageMetadata.DoesNotExist:
877
            raise fault.itemNotFound
878
        except ImageMetadata.MultipleObjectsReturned:
879
            raise fault.serviceUnavailable
880
        except Exception, e:
881
            log.exception('Unexpected error: %s' % e)
882
            raise fault.serviceUnavailable
883

    
884
    def delete(self, request, id, key=None):
885
        """Delete the specified metadata key"""
886
        if key is None:
887
            log.exception('No metadata key specified in URL')
888
            raise fault.badRequest
889
        try:
890
            image = Image.objects.get(pk=id)
891
            image.imagemetadata_set.get(meta_key=key).delete()
892
        except ImageMetadata.DoesNotExist:
893
            raise fault.itemNotFound
894
        except Image.DoesNotExist:
895
            raise fault.itemNotFound
896
        except ImageMetadata.MultipleObjectsReturned:
897
            raise fault.serviceUnavailable
898
        except Image.MultipleObjectsReturned:
899
            raise fault.serviceUnavailable
900
        except Exception, e:
901
            log.exception('Unexpected error: %s' % e)
902
            raise fault.serviceUnavailable
903

    
904

    
905
class SharedIPGroupHandler(BaseHandler):
906
    allowed_methods = ('GET', 'POST', 'DELETE')
907

    
908
    def read(self, request, id=None):
909
        """List Shared IP Groups"""
910
        if id is None:
911
            return {}
912
        elif id == "detail":
913
            return {}
914
        else:
915
            raise fault.itemNotFound
916

    
917
    def create(self, request, id):
918
        """Creates a new Shared IP Group"""
919
        return created
920

    
921
    def delete(self, request, id):
922
        """Deletes a Shared IP Group"""
923
        raise fault.itemNotFound
924

    
925

    
926
class VirtualMachineGroupHandler(BaseHandler):
927
    """Handler responsible for Virtual Machine Groups
928

929
     creates, lists, deletes virtual machine groups
930

931
     @HTTP methods: GET, POST, DELETE
932
     @Parameters: POST data
933
     @Responses: HTTP 202 if successfully get the Groups list,
934
     itemNotFound, serviceUnavailable otherwise
935

936
    """
937

    
938
    allowed_methods = ('GET', 'POST', 'DELETE')
939

    
940
    def read(self, request, id=None):
941
        """List Groups"""
942
        try:
943
            vmgroups = VirtualMachineGroup.objects.all()
944
            vmgroups_list = [{'id': vmgroup.id, \
945
                  'name': vmgroup.name,  \
946
                   'server_id':
947
                    [machine.id for machine in vmgroup.machines.all()]
948
                   } for vmgroup in vmgroups]
949
            # Group info is stored in the DB. Ganeti is not aware of this
950
            if id == "detail":
951
                return {"groups": vmgroups_list}
952
            elif id is None:
953
                return {"groups": [{"id": s['id'],
954
                                    "name": s['name']} for s in vmgroups_list]}
955
            else:
956
                vmgroup = vmgroups.get(id=id)
957

    
958
                return {"group":  {'id': vmgroup.id,
959
                  'name': vmgroup.name,
960
                   'server_id':
961
                    [machine.id for machine in vmgroup.machines.all()]
962
                   }}
963

    
964
        except VirtualMachineGroup.DoesNotExist:
965
                    raise fault.itemNotFound
966
        except VirtualMachineGroup.MultipleObjectsReturned:
967
                    raise fault.serviceUnavailable
968
        except Exception, e:
969
                    log.exception('Unexpected error: %s' % e)
970
                    raise fault.serviceUnavailable
971

    
972
    def create(self, request, id):
973
        """Creates a Group"""
974
        return created
975

    
976
    def delete(self, request, id):
977
        """Deletes a  Group"""
978
        raise fault.itemNotFound
979

    
980

    
981
class LimitHandler(BaseHandler):
982
    allowed_methods = ('GET',)
983

    
984
    # XXX: hookup with @throttle
985

    
986
    rate = [
987
        {
988
           "verb": "POST",
989
           "URI": "*",
990
           "regex": ".*",
991
           "value": 10,
992
           "remaining": 2,
993
           "unit": "MINUTE",
994
           "resetTime": 1244425439
995
        },
996
        {
997
           "verb": "POST",
998
           "URI": "*/servers",
999
           "regex": "^/servers",
1000
           "value": 25,
1001
           "remaining": 24,
1002
           "unit": "DAY",
1003
           "resetTime": 1244511839
1004
        },
1005
        {
1006
           "verb": "PUT",
1007
           "URI": "*",
1008
           "regex": ".*",
1009
           "value": 10,
1010
           "remaining": 2,
1011
           "unit": "MINUTE",
1012
           "resetTime": 1244425439
1013
        },
1014
        {
1015
           "verb": "GET",
1016
           "URI": "*",
1017
           "regex": ".*",
1018
           "value": 3,
1019
           "remaining": 3,
1020
           "unit": "MINUTE",
1021
           "resetTime": 1244425439
1022
        },
1023
        {
1024
           "verb": "DELETE",
1025
           "URI": "*",
1026
           "regex": ".*",
1027
           "value": 100,
1028
           "remaining": 100,
1029
           "unit": "MINUTE",
1030
           "resetTime": 1244425439
1031
        }
1032
    ]
1033

    
1034
    absolute = {
1035
        "maxTotalRAMSize": 51200,
1036
        "maxIPGroups": 50,
1037
        "maxIPGroupMembers": 25
1038
    }
1039

    
1040
    def read(self, request):
1041
        return {"limits": {
1042
                "rate": self.rate,
1043
                "absolute": self.absolute,
1044
               }
1045
            }
1046

    
1047

    
1048
class DiskHandler(BaseHandler):
1049
    allowed_methods = ('GET', 'POST', 'PUT', 'DELETE')
1050

    
1051
    def read(self, request, id=None):
1052
        """List Disks"""
1053
        if id is None:
1054
            return self.read_all(request)
1055
        elif id == "detail":
1056
            return self.read_all(request, detail=True)
1057
        else:
1058
            return self.read_one(request, id)
1059

    
1060
    def read_one(self, request, id):
1061
        """List one Disk with the specified id with all details"""
1062
        # FIXME Get detailed info from the DB
1063
        # for the Disk with the specified id
1064
        try:
1065
            disk = Disk.objects.get(pk=id)
1066
            disk_details = {
1067
                "id": disk.id,
1068
                "name": disk.name,
1069
                "size": disk.size,
1070
                "created": disk.created,
1071
                "serverId": disk.vm.id
1072
            }
1073
            return {"disks": disk_details}
1074
        except:
1075
            raise fault.itemNotFound
1076

    
1077
    @paginator
1078
    def read_all(self, request, detail=False):
1079
        """List all Disks. If -detail- is set list them with all details"""
1080
        if not detail:
1081
            disks = Disk.objects.filter(owner=SynnefoUser.objects.all()[0])
1082
            return {"disks": [{"id": disk.id, "name": disk.name} \
1083
                for disk in disks]}
1084
        else:
1085
            disks = Disk.objects.filter(owner=SynnefoUser.objects.all()[0])
1086
            disks_details = [{
1087
                "id": disk.id,
1088
                "name": disk.name,
1089
                "size": disk.size,
1090
                "created": disk.created,
1091
                "serverId": disk.vm.id,
1092
            } for disk in disks]
1093
            return {"disks":  disks_details}
1094

    
1095
    def create(self, request):
1096
        """Create a new Disk"""
1097
        # FIXME Create a partial DB entry,
1098
        # then call the backend for actual creation
1099
        pass
1100

    
1101
    def update(self, request, id):
1102
        """Rename the Disk with the specified id"""
1103
        # FIXME Change the Disk's name in the DB
1104
        pass
1105

    
1106
    def delete(self, request, id):
1107
        """Destroy the Disk with the specified id"""
1108
        # Call the backend for actual destruction
1109
        pass