Statistics
| Branch: | Tag: | Revision:

root / api / handlers.py @ 6130effc

History | View | Annotate | Download (24.1 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 random
15
import string
16
import logging
17

    
18
log = logging.getLogger('synnefo.api.handlers')
19

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

    
28
backend_prefix_id = settings.BACKEND_PREFIX_ID
29

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

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

    
50

    
51
class VersionHandler(AnonymousBaseHandler):
52
    allowed_methods = ('GET',)
53

    
54
    def read(self, request, number=None):
55
        try:
56
            if number is None:
57
                versions = map(lambda v: {
58
                            "status": v["status"],
59
                            "id": v["id"],
60
                        }, VERSIONS)
61
                return { "versions": versions }
62
            else:
63
                for version in VERSIONS:
64
                    if version["id"] == number:
65
                        return { "version": version }
66
                raise fault.itemNotFound
67
        except Exception, e:
68
            log.error('Unexpected error: %s' % e)
69
            raise fault.serviceUnavailable
70

    
71

    
72
class ServerHandler(BaseHandler):
73
    """Handler responsible for the Servers
74

75
     handles the listing of Virtual Machines, Creates and Destroys VM's
76

77
     @HTTP methods: POST, DELETE, PUT, GET
78
     @Parameters: POST data with the create data (cpu, ram, etc)
79
     @Responses: HTTP 202 if successfully call rapi, itemNotFound, serviceUnavailable otherwise
80

81
    """
82
    allowed_methods = ('GET', 'POST', 'PUT', 'DELETE')
83

    
84
    def read(self, request, id=None):
85
        from time import sleep
86
        sleep(0.5)
87
        #TODO: delete the sleep once the mock objects are removed
88
        if id is None:
89
            return self.read_all(request)
90
        elif id == "detail":
91
            return self.read_all(request, detail=True)
92
        else:
93
            return self.read_one(request, id)
94

    
95
    def read_one(self, request, id):
96
        try:
97
            server = VirtualMachine.objects.get(id=id)
98

    
99
            server = {'status': server.rsapi_state, 
100
                     'flavorId': server.flavor.id, 
101
                     'name': server.name, 
102
                     'description': server.description, 
103
                     'id': server.id, 
104
                     'imageId': server.sourceimage.id, 
105
                     'hostId': server.hostid, 
106
                     'progress': server.rsapi_state == 'ACTIVE' and 100 or 0, 
107
                     #'metadata': {'Server_Label': server.description },
108
                     'metadata':[{'meta': { 'key': {metadata.meta_key: metadata.meta_value}}} for metadata in server.virtualmachinemetadata_set.all()],                                    
109
                     'addresses': {'public': { 'ip': {'addr': server.ipfour}, 'ip6': {'addr': server.ipsix}},'private': ''},      
110
                    }
111
            return { "server": server } 
112
        except VirtualMachine.DoesNotExist:
113
            raise fault.itemNotFound
114
        except VirtualMachine.MultipleObjectsReturned:
115
            raise fault.serviceUnavailable
116
        except Exception, e:
117
            log.error('Unexpected error: %s' % e)
118
            raise fault.serviceUnavailable
119

    
120

    
121
    @paginator
122
    def read_all(self, request, detail=False):
123
        try:
124
            virtual_servers = VirtualMachine.objects.filter(deleted=False) 
125
            #get all VM's for now, FIX it to take the user's VMs only yet. also don't get deleted VM's
126

    
127
            if not detail:
128
                return { "servers": [ { "id": s.id, "name": s.name } for s in virtual_servers ] }
129
            else:
130
                virtual_servers_list = [{'status': server.rsapi_state, 
131
                                         'flavorId': server.flavor.id, 
132
                                         'name': server.name, 
133
                                         'id': server.id, 
134
                                         'description': server.description, 
135
                                         'imageId': server.sourceimage.id, 
136
                                         'hostId': server.hostid, 
137
                                         'progress': server.rsapi_state == 'ACTIVE' and 100 or 0, 
138
                                         #'metadata': {'Server_Label': server.description },
139
                                         'metadata':[{'meta': { 'key': {metadata.meta_key: metadata.meta_value}}} for metadata in server.virtualmachinemetadata_set.all()],                                    
140
                                         'addresses': {'public': { 'ip': {'addr': server.ipfour}, 'ip6': {'addr': server.ipsix}},'private': ''},      
141

    
142
                                        } for server in virtual_servers]
143
                #pass some fake data regarding ip, since we don't have any such data            
144
                return { "servers":  virtual_servers_list }                
145
        except Exception, e:
146
            log.error('Unexpected error: %s' % e)
147
            raise fault.serviceUnavailable
148

    
149

    
150
    def create(self, request):
151
        """ Parse RackSpace API create request to generate rapi create request
152
        
153
            TODO: auto generate and set password
154
        """
155
        # Check if we have all the necessary data in the JSON request       
156
        try:
157
            server = json.loads(request.raw_post_data)['server']
158
            name = server['name']
159
            flavorId = server['flavorId']
160
            flavor = Flavor.objects.get(id=flavorId)
161
            imageId = server['imageId']
162
            image = Image.objects.get(id=imageId)
163
            metadata = server['metadata']
164
            personality = server.get('personality', None)
165
        except Exception as e:
166
            log.error('Malformed create request: %s - %s' % (e, request.raw_post_data))    
167
            raise fault.badRequest
168

    
169
        # TODO: Proper Authn, Authz
170
        # Everything belongs to a single SynnefoUser for now.
171
        try:          
172
            owner = SynnefoUser.objects.all()[0]
173
        except Exception as e:
174
            log.error('Cannot find a single SynnefoUser in the DB: %s' % (e));
175
            raise fault.unauthorized
176

    
177
        # add the new VM to the local db
178
        try:
179
            vm = VirtualMachine.objects.create(sourceimage=image, ipfour='0.0.0.0', ipsix='::1', flavor=flavor, owner=owner)
180
        except Exception as e:
181
            log.error("Can't save vm: %s" % e)
182
            raise fault.serviceUnavailable
183

    
184
        try:
185
            vm.name = name
186
            #vm.description = descr
187
            vm.save()            
188
            jobId = rapi.CreateInstance(
189
                'create',
190
                request.META['SERVER_NAME'] == 'testserver' and 'test-server' or vm.backend_id,
191
                'plain',
192
                # disk field of Flavor object is in GB, value specified here is in MB
193
                # FIXME: Always ask for a 2GB disk, current LVM physical groups are too small:
194
                # [{"size": flavor.disk * 1000}],
195
                [{"size": 2000}],
196
                [{}],
197
                #TODO: select OS from imageId
198
                os='debootstrap+default',
199
                ip_check=False,
200
                name_check=False,
201
                #TODO: verify if this is necessary
202
                pnode = rapi.GetNodes()[0],
203
                # Dry run when called by unit tests
204
                dry_run = request.META['SERVER_NAME'] == 'testserver',
205
                beparams={
206
                            'auto_balance': True,
207
                            'vcpus': flavor.cpu,
208
                            'memory': flavor.ram,
209
                        },
210
                )
211
            log.info('created vm with %s cpus, %s ram and %s storage' % (flavor.cpu, flavor.ram, flavor.disk))
212
        except (GanetiApiError, CertificateError) as e:
213
            log.error('CreateInstance failed: %s' % e)
214
            vm.deleted = True
215
            vm.save()
216
            raise fault.serviceUnavailable
217
        except Exception as e:
218
            log.error('Unexpected error: %s' % e)
219
            vm.deleted = True
220
            vm.save()
221
            raise fault.serviceUnavailable            
222
        
223

    
224
        ret = {'server': {
225
                'id' : vm.id,
226
                'name' : vm.name,
227
                "imageId" : imageId,
228
                "flavorId" : flavorId,
229
                "hostId" : vm.hostid,
230
                "progress" : 0,
231
                "status" : 'BUILD',
232
                "adminPass" : self.random_password(),
233
                "metadata" : {"My Server Name" : vm.description},
234
                "addresses" : {
235
                    "public" : [  ],
236
                    "private" : [  ],
237
                    },
238
                },
239
        }
240
        return HttpResponse(json.dumps(ret), mimetype="application/json", status=202)
241

    
242

    
243
    def random_password(self):
244
        "return random password"
245
        number_of_chars = 8
246
        possible_chars = string.ascii_uppercase + string.ascii_lowercase + string.digits
247
        return ''.join(random.choice(possible_chars) for x in range(number_of_chars))
248

    
249

    
250
    def update(self, request, id):
251
        """Sets and updates Virtual Machine Metadata. 
252
 
253
        """
254
        try:
255
            metadata_request = json.loads(request.raw_post_data)['metadata']
256
            metadata_key = metadata_request.get('metadata_key')
257
            metadata_value = metadata_request.get('metadata_value')
258
 
259
            vm = VirtualMachine.objects.get(id=id)
260
            #we only update virtual machine's name atm
261
            if metadata_key == 'name':
262
                vm.name = metadata_value
263
                vm.save()
264
                return accepted
265
        except VirtualMachine.DoesNotExist:
266
            raise fault.itemNotFound
267
        except VirtualMachine.MultipleObjectsReturned:
268
            raise fault.serviceUnavailable
269
        except Exception, e:
270
            log.error('Unexpected error: %s' % e)
271
            raise fault.serviceUnavailable
272

    
273
        raise fault.itemNotFound
274

    
275

    
276
    def delete(self, request, id):
277
        try:
278
            vm = VirtualMachine.objects.get(id=id)
279
            #TODO: set the status to DESTROYED
280
            vm.start_action('DESTROY')
281
            rapi.DeleteInstance(vm.backend_id)
282
            return accepted        
283
        except VirtualMachine.DoesNotExist:
284
            raise fault.itemNotFound
285
        except VirtualMachine.MultipleObjectsReturned:
286
            raise fault.serviceUnavailable
287
        except GanetiApiError, CertificateError:
288
            raise fault.serviceUnavailable
289
        except Exception, e:
290
            log.error('Unexpected error: %s' % e)
291
            raise fault.serviceUnavailable
292

    
293

    
294

    
295
class ServerAddressHandler(BaseHandler):
296
    allowed_methods = ('GET', 'PUT', 'DELETE')
297

    
298
    def read(self, request, id, type=None):
299
        """List IP addresses for a server"""
300

    
301
        if type is None:
302
            pass
303
        elif type == "private":
304
            pass
305
        elif type == "public":
306
            pass
307
        return {}
308

    
309
    def update(self, request, id, address):
310
        """Share an IP address to another in the group"""
311
        return accepted
312

    
313
    def delete(self, request, id, address):
314
        """Unshare an IP address"""
315
        return accepted
316

    
317

    
318
class ServerActionHandler(BaseHandler):
319
    """Handler responsible for Server Actions
320

321
     handles Reboot, Shutdown and Start actions. 
322

323
     @HTTP methods: POST, DELETE, PUT
324
     @Parameters: POST data with the action (reboot, shutdown, start)
325
     @Responses: HTTP 202 if successfully call rapi, itemNotFound, serviceUnavailable otherwise
326

327
    """
328

    
329
    allowed_methods = ('POST', 'DELETE',  'PUT')
330

    
331
    def create(self, request, id):
332
        """Reboot, Shutdown, Start virtual machine"""
333
        
334
        try:
335
            requested_action = json.loads(request.raw_post_data)
336
            reboot_request = requested_action.get('reboot', None)
337
            shutdown_request = requested_action.get('shutdown', None)
338
            start_request = requested_action.get('start', None)
339
            #action not implemented
340
            action = reboot_request and 'REBOOT' or shutdown_request and 'STOP' or start_request and 'START'
341

    
342
            if not action:
343
                raise fault.notImplemented 
344
            #test if we can get the vm
345
            vm = VirtualMachine.objects.get(id=id)
346
            vm.start_action(action)
347

    
348
            if reboot_request:
349
                rapi.RebootInstance(vm.backend_id)
350
            elif shutdown_request:
351
                rapi.ShutdownInstance(vm.backend_id)
352
            elif start_request:
353
                rapi.StartupInstance(vm.backend_id)
354
            return accepted
355
        except VirtualMachine.DoesNotExist:
356
            raise fault.itemNotFound
357
        except VirtualMachine.MultipleObjectsReturned:
358
            raise fault.serviceUnavailable
359
        except GanetiApiError, CertificateError:
360
            raise fault.serviceUnavailable
361
        except Exception, e:
362
            log.error('Unexpected error: %s' % e)
363
            raise fault.serviceUnavailable
364

    
365
    def delete(self, request, id):
366
        """Delete an Instance"""
367
        return accepted
368

    
369
    def update(self, request, id):
370
        raise fault.itemNotFound
371

    
372

    
373

    
374

    
375
class ServerBackupHandler(BaseHandler):
376
    """ Backup Schedules are not implemented yet, return notImplemented """
377
    allowed_methods = ('GET', 'POST', 'DELETE')
378

    
379
    def read(self, request, id):
380
        raise fault.notImplemented
381

    
382
    def create(self, request, id):
383
        raise fault.notImplemented
384

    
385
    def delete(self, request, id):
386
        raise fault.notImplemented
387

    
388

    
389
class FlavorHandler(BaseHandler):
390
    """Handler responsible for Flavors
391

392
    """
393
    allowed_methods = ('GET',)
394

    
395
    def read(self, request, id=None):
396
        """
397
        List flavors or retrieve one
398

399
        Returns: OK
400
        Faults: cloudServersFault, serviceUnavailable, unauthorized,
401
                badRequest, itemNotFound
402
        """
403
        try:
404
            flavors = Flavor.objects.all()
405
            flavors = [ {'id': flavor.id, 'name': flavor.name, 'ram': flavor.ram, \
406
                     'disk': flavor.disk, 'cpu': flavor.cpu} for flavor in flavors]
407

    
408
            if id is None:
409
                simple = map(lambda v: {
410
                            "id": v['id'],
411
                            "name": v['name'],
412
                        }, flavors)
413
                return { "flavors": simple }
414
            elif id == "detail":
415
                return { "flavors": flavors }
416
            else:
417
                flavor = Flavor.objects.get(id=id)
418
                return { "flavor":  {
419
                    'id': flavor.id,
420
                    'name': flavor.name,
421
                    'ram': flavor.ram,
422
                    'disk': flavor.disk,  
423
                    'cpu': flavor.cpu,  
424
                   } }
425

    
426
        except Flavor.DoesNotExist:
427
            raise fault.itemNotFound
428
        except Flavor.MultipleObjectsReturned:
429
            raise fault.serviceUnavailable
430
        except Exception, e:
431
            log.error('Unexpected error: %s' % e)
432
            raise fault.serviceUnavailable
433

    
434

    
435
class ImageHandler(BaseHandler):
436
    """Handler responsible for Images
437

438
     handles the listing, creation and delete of Images. 
439

440
     @HTTP methods: GET, POST
441
     @Parameters: POST data 
442
     @Responses: HTTP 202 if successfully create Image or get the Images list, itemNotFound, serviceUnavailable otherwise
443

444
    """
445

    
446

    
447
    allowed_methods = ('GET', 'POST')
448

    
449
    def read(self, request, id=None):
450
        """
451
        List images or retrieve one
452

453
        Returns: OK
454
        Faults: cloudServersFault, serviceUnavailable, unauthorized,
455
                badRequest, itemNotFound
456
        """
457
        try:
458
            images = Image.objects.all()
459
            images_list = [ {'created': image.created.isoformat(), 
460
                        'id': image.id,
461
                        'name': image.name,
462
                        'updated': image.updated.isoformat(),    
463
                        'description': image.description, 
464
                        'status': image.state, 
465
                        'progress': image.state == 'ACTIVE' and 100 or 0, 
466
                        'size': image.size, 
467
                        'serverId': image.sourcevm and image.sourcevm.id or ""
468
                       } for image in images]
469
            # Images info is stored in the DB. Ganeti is not aware of this
470
            if id == "detail":
471
                return { "images": images_list }
472
            elif id is None:
473
                return { "images": [ { "id": s['id'], "name": s['name'] } for s in images_list ] }
474
            else:        
475
                image = images.get(id=id)
476
                return { "image":  {'created': image.created.isoformat(), 
477
                    'id': image.id,
478
                    'name': image.name,
479
                    'updated': image.updated.isoformat(),    
480
                    'description': image.description, 
481
                    'status': image.state, 
482
                    'progress': image.state == 'ACTIVE' and 100 or 0, 
483
                    'size': image.size, 
484
                    'serverId': image.sourcevm and image.sourcevm.id or ""
485
                   } }
486
        except Image.DoesNotExist:
487
                    raise fault.itemNotFound
488
        except Image.MultipleObjectsReturned:
489
                    raise fault.serviceUnavailable
490
        except Exception, e:
491
                    log.error('Unexpected error: %s' % e)
492
                    raise fault.serviceUnavailable
493

    
494
    def create(self, request):
495
        """Create a new image"""
496
        return accepted
497

    
498

    
499
class SharedIPGroupHandler(BaseHandler):
500
    allowed_methods = ('GET', 'POST', 'DELETE')
501

    
502
    def read(self, request, id=None):
503
        """List Shared IP Groups"""
504
        if id is None:
505
            return {}
506
        elif id == "detail":
507
            return {}
508
        else:
509
            raise fault.itemNotFound
510

    
511
    def create(self, request, id):
512
        """Creates a new Shared IP Group"""
513
        return created
514

    
515
    def delete(self, request, id):
516
        """Deletes a Shared IP Group"""
517
        raise fault.itemNotFound
518

    
519

    
520
class VirtualMachineGroupHandler(BaseHandler):
521
    """Handler responsible for Virtual Machine Groups
522

523
     creates, lists, deletes virtual machine groups
524

525
     @HTTP methods: GET, POST, DELETE
526
     @Parameters: POST data 
527
     @Responses: HTTP 202 if successfully get the Groups list, itemNotFound, serviceUnavailable otherwise
528

529
    """
530

    
531
    allowed_methods = ('GET', 'POST', 'DELETE')
532

    
533
    def read(self, request, id=None):
534
        """List Groups"""
535
        try:
536
            vmgroups = VirtualMachineGroup.objects.all() 
537
            vmgroups_list = [ {'id': vmgroup.id, \
538
                  'name': vmgroup.name,  \
539
                   'server_id': [machine.id for machine in vmgroup.machines.all()] \
540
                   } for vmgroup in vmgroups]
541
            # Group info is stored in the DB. Ganeti is not aware of this
542
            if id == "detail":
543
                return { "groups": vmgroups_list }
544
            elif id is None:
545
                return { "groups": [ { "id": s['id'], "name": s['name'] } for s in vmgroups_list ] }
546
            else:
547
                vmgroup = vmgroups.get(id=id)
548

    
549
                return { "group":  {'id': vmgroup.id, \
550
                  'name': vmgroup.name,  \
551
                   'server_id': [machine.id for machine in vmgroup.machines.all()] \
552
                   } }
553

    
554

    
555
        except VirtualMachineGroup.DoesNotExist:
556
                    raise fault.itemNotFound
557
        except VirtualMachineGroup.MultipleObjectsReturned:
558
                    raise fault.serviceUnavailable
559
        except Exception, e:
560
                    log.error('Unexpected error: %s' % e)
561
                    raise fault.serviceUnavailable
562

    
563

    
564

    
565
    def create(self, request, id):
566
        """Creates a Group"""
567
        return created
568

    
569
    def delete(self, request, id):
570
        """Deletes a  Group"""
571
        raise fault.itemNotFound
572

    
573

    
574
class LimitHandler(BaseHandler):
575
    allowed_methods = ('GET',)
576

    
577
    # XXX: hookup with @throttle
578

    
579
    rate = [
580
        {
581
           "verb" : "POST",
582
           "URI" : "*",
583
           "regex" : ".*",
584
           "value" : 10,
585
           "remaining" : 2,
586
           "unit" : "MINUTE",
587
           "resetTime" : 1244425439
588
        },
589
        {
590
           "verb" : "POST",
591
           "URI" : "*/servers",
592
           "regex" : "^/servers",
593
           "value" : 25,
594
           "remaining" : 24,
595
           "unit" : "DAY",
596
           "resetTime" : 1244511839
597
        },
598
        {
599
           "verb" : "PUT",
600
           "URI" : "*",
601
           "regex" : ".*",
602
           "value" : 10,
603
           "remaining" : 2,
604
           "unit" : "MINUTE",
605
           "resetTime" : 1244425439
606
        },
607
        {
608
           "verb" : "GET",
609
           "URI" : "*",
610
           "regex" : ".*",
611
           "value" : 3,
612
           "remaining" : 3,
613
           "unit" : "MINUTE",
614
           "resetTime" : 1244425439
615
        },
616
        {
617
           "verb" : "DELETE",
618
           "URI" : "*",
619
           "regex" : ".*",
620
           "value" : 100,
621
           "remaining" : 100,
622
           "unit" : "MINUTE",
623
           "resetTime" : 1244425439
624
        }
625
    ]
626

    
627
    absolute = {
628
        "maxTotalRAMSize" : 51200,
629
        "maxIPGroups" : 50,
630
        "maxIPGroupMembers" : 25
631
    }
632

    
633
    def read(self, request):
634
        return { "limits": {
635
                "rate": self.rate,
636
                "absolute": self.absolute,
637
               }
638
            }
639

    
640

    
641
class DiskHandler(BaseHandler):
642
    allowed_methods = ('GET', 'POST', 'PUT', 'DELETE')
643

    
644
    def read(self, request, id=None):
645
        """List Disks"""
646
        if id is None:
647
            return self.read_all(request)
648
        elif id == "detail":
649
            return self.read_all(request, detail=True)
650
        else:
651
            return self.read_one(request, id)
652

    
653
    def read_one(self, request, id):
654
        """List one Disk with the specified id with all details"""
655
        # FIXME Get detailed info from the DB 
656
        # for the Disk with the specified id
657
        try:
658
            disk = Disk.objects.get(pk=id)
659
            disk_details = {
660
                "id" : disk.id, 
661
                "name" : disk.name, 
662
                "size" : disk.size,
663
                "created" : disk.created, 
664
                "serverId" : disk.vm.id
665
            }
666
            return { "disks" : disk_details }
667
        except:
668
            raise fault.itemNotFound
669

    
670
    @paginator
671
    def read_all(self, request, detail=False):
672
        """List all Disks. If -detail- is set list them with all details"""
673
        if not detail:
674
            disks = Disk.objects.filter(owner=SynnefoUser.objects.all()[0])
675
            return { "disks": [ { "id": disk.id, "name": disk.name } for disk in disks ] }
676
        else:
677
            disks = Disk.objects.filter(owner=SynnefoUser.objects.all()[0])
678
            disks_details = [ {
679
                "id" : disk.id, 
680
                "name" : disk.name,
681
                "size" : disk.size,
682
                "created" : disk.created, 
683
                "serverId" : disk.vm.id,
684
            } for disk in disks ]
685
            return { "disks":  disks_details }                
686

    
687
    def create(self, request):
688
        """Create a new Disk"""
689
        # FIXME Create a partial DB entry, 
690
        # then call the backend for actual creation
691
        pass
692

    
693
    def update(self, request, id):
694
        """Rename the Disk with the specified id"""
695
        # FIXME Change the Disk's name in the DB
696
        pass
697

    
698
    def delete(self, request, id):
699
        """Destroy the Disk with the specified id"""
700
        # Call the backend for actual destruction
701
        pass