Statistics
| Branch: | Tag: | Revision:

root / snf-cyclades-app / synnefo / api / test / servers.py @ f2080d16

History | View | Annotate | Download (21.7 kB)

1
# Copyright 2012 GRNET S.A. All rights reserved.
2
#
3
# Redistribution and use in source and binary forms, with or
4
# without modification, are permitted provided that the following
5
# conditions are met:
6
#
7
#   1. Redistributions of source code must retain the above
8
#      copyright notice, this list of conditions and the following
9
#      disclaimer.
10
#
11
#   2. Redistributions in binary form must reproduce the above
12
#      copyright notice, this list of conditions and the following
13
#      disclaimer in the documentation and/or other materials
14
#      provided with the distribution.
15
#
16
# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS
17
# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
18
# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
19
# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR
20
# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
23
# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED
24
# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
25
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
26
# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
27
# POSSIBILITY OF SUCH DAMAGE.
28
#
29
# The views and conclusions contained in the software and
30
# documentation are those of the authors and should not be
31
# interpreted as representing official policies, either expressed
32
# or implied, of GRNET S.A.
33

    
34
import json
35

    
36
from snf_django.utils.testing import BaseAPITest, mocked_quotaholder
37
from synnefo.db.models import VirtualMachine, VirtualMachineMetadata
38
from synnefo.db import models_factory as mfactory
39
from synnefo.logic.utils import get_rsapi_state
40
from synnefo.cyclades_settings import cyclades_services
41
from synnefo.lib.services import get_service_path
42
from synnefo.lib import join_urls
43
from synnefo.logic.rapi import GanetiApiError
44

    
45
from mock import patch
46

    
47

    
48
class ComputeAPITest(BaseAPITest):
49
    def setUp(self, *args, **kwargs):
50
        super(ComputeAPITest, self).setUp(*args, **kwargs)
51
        self.compute_path = get_service_path(cyclades_services, 'compute',
52
                                             version='v2.0')
53
    def myget(self, path, *args, **kwargs):
54
        path = join_urls(self.compute_path, path)
55
        return self.get(path, *args, **kwargs)
56

    
57
    def myput(self, path, *args, **kwargs):
58
        path = join_urls(self.compute_path, path)
59
        return self.put(path, *args, **kwargs)
60

    
61
    def mypost(self, path, *args, **kwargs):
62
        path = join_urls(self.compute_path, path)
63
        return self.post(path, *args, **kwargs)
64

    
65
    def mydelete(self, path, *args, **kwargs):
66
        path = join_urls(self.compute_path, path)
67
        return self.delete(path, *args, **kwargs)
68

    
69

    
70
class ServerAPITest(ComputeAPITest):
71
    def setUp(self):
72
        self.user1 = 'user1'
73
        self.user2 = 'user2'
74
        self.vm1 = mfactory.VirtualMachineFactory(userid=self.user1)
75
        self.vm2 = mfactory.VirtualMachineFactory(userid=self.user2)
76
        self.vm3 = mfactory.VirtualMachineFactory(deleted=True,
77
                                                  userid=self.user1)
78
        self.vm4 = mfactory.VirtualMachineFactory(userid=self.user2)
79
        super(ServerAPITest, self).setUp()
80

    
81
    def test_server_list_1(self):
82
        """Test if the expected list of servers is returned."""
83
        response = self.myget('servers')
84
        self.assertSuccess(response)
85
        servers = json.loads(response.content)['servers']
86
        self.assertEqual(servers, [])
87

    
88
    def test_server_list_2(self):
89
        """Test if the expected list of servers is returned."""
90
        response = self.myget('servers', self.user1)
91
        self.assertSuccess(response)
92
        servers = json.loads(response.content)['servers']
93
        db_server = self.vm1
94
        server = servers[0]
95
        self.assertEqual(server["name"], db_server.name)
96
        self.assertEqual(server["id"], db_server.id)
97

    
98
    def test_server_list_detail(self):
99
        """Test if the servers list details are returned."""
100
        user = self.user2
101
        user_vms = {self.vm2.id: self.vm2,
102
                    self.vm4.id: self.vm4}
103

    
104
        response = self.myget('servers/detail', user)
105
        servers = json.loads(response.content)['servers']
106
        self.assertEqual(len(servers), len(user_vms))
107
        for api_vm in servers:
108
            db_vm = user_vms[api_vm['id']]
109
            self.assertEqual(api_vm['flavor']["id"], db_vm.flavor.id)
110
            self.assertEqual(api_vm['hostId'], db_vm.hostid)
111
            self.assertEqual(api_vm['id'], db_vm.id)
112
            self.assertEqual(api_vm['image']["id"], db_vm.imageid)
113
            self.assertEqual(api_vm['name'], db_vm.name)
114
            self.assertEqual(api_vm['status'], get_rsapi_state(db_vm))
115
            self.assertSuccess(response)
116

    
117
    def test_server_detail(self):
118
        """Test if a server details are returned."""
119
        db_vm = self.vm2
120
        user = self.vm2.userid
121
        net = mfactory.NetworkFactory()
122
        nic = mfactory.NetworkInterfaceFactory(machine=self.vm2, network=net,
123
                                              ipv6="::babe")
124

    
125
        db_vm_meta = mfactory.VirtualMachineMetadataFactory(vm=db_vm)
126

    
127
        response = self.myget('servers/%d' % db_vm.id, user)
128
        server = json.loads(response.content)['server']
129

    
130
        self.assertEqual(server['flavor']["id"], db_vm.flavor.id)
131
        self.assertEqual(server['hostId'], db_vm.hostid)
132
        self.assertEqual(server['id'], db_vm.id)
133
        self.assertEqual(server['image']["id"], db_vm.imageid)
134
        self.assertEqual(server['name'], db_vm.name)
135
        self.assertEqual(server['status'], get_rsapi_state(db_vm))
136
        api_nic = server['attachments'][0]
137
        self.assertEqual(api_nic['network_id'], str(net.id))
138
        self.assertEqual(api_nic['mac_address'], nic.mac)
139
        self.assertEqual(api_nic['firewallProfile'], nic.firewall_profile)
140
        self.assertEqual(api_nic['ipv4'], nic.ipv4)
141
        self.assertEqual(api_nic['ipv6'], nic.ipv6)
142
        self.assertEqual(api_nic['id'], 'nic-%s-%s' % (db_vm.id, nic.index))
143
        api_address = server["addresses"]
144
        self.assertEqual(api_address[str(net.id)],
145
               [{"version": 4, "addr": nic.ipv4, "OS-EXT-IPS:type": "fixed"},
146
                {"version": 6, "addr": nic.ipv6, "OS-EXT-IPS:type": "fixed"}])
147

    
148
        metadata = server['metadata']
149
        self.assertEqual(len(metadata), 1)
150
        self.assertEqual(metadata[db_vm_meta.meta_key], db_vm_meta.meta_value)
151
        self.assertSuccess(response)
152

    
153
    def test_server_building_nics(self):
154
        db_vm = self.vm2
155
        user = self.vm2.userid
156
        net1 = mfactory.NetworkFactory()
157
        net2 = mfactory.NetworkFactory()
158
        net3 = mfactory.NetworkFactory()
159
        mfactory.NetworkInterfaceFactory(machine=self.vm2, network=net1,
160
                                         state="BUILDING")
161
        nic2 = mfactory.NetworkInterfaceFactory(machine=self.vm2, network=net2,
162
                                                state="ACTIVE")
163
        mfactory.NetworkInterfaceFactory(machine=self.vm2, network=net3,
164
                                         state="BUILDING")
165

    
166
        response = self.myget('servers/%d' % db_vm.id, user)
167
        server = json.loads(response.content)['server']
168
        nics = server["attachments"]
169
        self.assertEqual(len(nics), 1)
170
        self.assertEqual(nics[0]["network_id"], str(nic2.network_id))
171

    
172
    def test_noauthorized(self):
173
        """Test 404 for detail of other user vm"""
174
        db_vm = self.vm2
175

    
176
        response = self.myget('servers/%d' % db_vm.id, 'wrong_user')
177
        self.assertItemNotFound(response)
178

    
179
    def test_wrong_server(self):
180
        """Test 404 response if server does not exist."""
181
        response = self.myget('servers/%d' % 5000)
182
        self.assertItemNotFound(response)
183

    
184
    def test_create_server_empty(self):
185
        """Test if the create server call returns a 400 badRequest if
186
           no attributes are specified."""
187

    
188
        response = self.mypost('servers', params={})
189
        self.assertBadRequest(response)
190

    
191
    def test_rename_server(self):
192
        vm = self.vm2
193
        request = {'server': {'name': 'new_name'}}
194
        response = self.myput('servers/%d' % vm.id, vm.userid,
195
                              json.dumps(request), 'json')
196
        self.assertSuccess(response)
197
        self.assertEqual(VirtualMachine.objects.get(id=vm.id).name, "new_name")
198

    
199
    def test_catch_wrong_api_paths(self):
200
        response = self.myget('nonexistent')
201
        self.assertEqual(response.status_code, 400)
202
        try:
203
            error = json.loads(response.content)
204
        except ValueError:
205
            self.assertTrue(False)
206

    
207
    def test_method_not_allowed(self, *args):
208
        # /servers/ allows only POST, GET
209
        response = self.myput('servers', '', '')
210
        self.assertMethodNotAllowed(response)
211
        response = self.mydelete('servers')
212
        self.assertMethodNotAllowed(response)
213

    
214
        # /servers/<srvid>/ allows only GET, PUT, DELETE
215
        response = self.mypost("servers/42")
216
        self.assertMethodNotAllowed(response)
217

    
218
        # /imags/<srvid>/metadata/ allows only POST, GET
219
        response = self.myput('servers/42/metadata', '', '')
220
        self.assertMethodNotAllowed(response)
221
        response = self.mydelete('servers/42/metadata')
222
        self.assertMethodNotAllowed(response)
223

    
224
        # /imags/<srvid>/metadata/ allows only POST, GET
225
        response = self.myput('servers/42/metadata', '', '')
226
        self.assertMethodNotAllowed(response)
227
        response = self.mydelete('servers/42/metadata')
228
        self.assertMethodNotAllowed(response)
229

    
230
        # /imags/<srvid>/metadata/<key> allows only PUT, GET, DELETE
231
        response = self.mypost('servers/42/metadata/foo')
232
        self.assertMethodNotAllowed(response)
233

    
234

    
235
@patch('synnefo.api.util.get_image')
236
@patch('synnefo.logic.rapi_pool.GanetiRapiClient')
237
class ServerCreateAPITest(ComputeAPITest):
238
    def test_create_server(self, mrapi, mimage):
239
        """Test if the create server call returns the expected response
240
           if a valid request has been speficied."""
241
        mimage.return_value = {'location': 'pithos://foo',
242
                               'checksum': '1234',
243
                               "id": 1,
244
                               "name": "test_image",
245
                               'disk_format': 'diskdump'}
246
        mrapi().CreateInstance.return_value = 12
247
        flavor = mfactory.FlavorFactory()
248
        # Create public network and backend
249
        network = mfactory.NetworkFactory(public=True)
250
        backend = mfactory.BackendFactory()
251
        mfactory.BackendNetworkFactory(network=network, backend=backend)
252

    
253
        request = {
254
                    "server": {
255
                        "name": "new-server-test",
256
                        "userid": "test_user",
257
                        "imageRef": 1,
258
                        "flavorRef": flavor.id,
259
                        "metadata": {
260
                            "My Server Name": "Apache1"
261
                        },
262
                        "personality": []
263
                    }
264
        }
265
        with mocked_quotaholder():
266
            response = self.mypost('servers', 'test_user',
267
                                   json.dumps(request), 'json')
268
        self.assertEqual(response.status_code, 202)
269
        mrapi().CreateInstance.assert_called_once()
270

    
271
        api_server = json.loads(response.content)['server']
272
        self.assertEqual(api_server['status'], "BUILD")
273
        self.assertEqual(api_server['progress'], 0)
274
        self.assertEqual(api_server['metadata'],
275
                        {"My Server Name":  "Apache1"})
276
        self.assertTrue('adminPass' in api_server)
277

    
278
        db_vm = VirtualMachine.objects.get(userid='test_user')
279
        self.assertEqual(api_server['name'], db_vm.name)
280
        self.assertEqual(api_server['status'], db_vm.operstate)
281

    
282
        # Test drained flag in Network:
283
        network.drained = True
284
        network.save()
285
        with mocked_quotaholder():
286
            response = self.mypost('servers', 'test_user',
287
                                    json.dumps(request), 'json')
288
        self.assertEqual(response.status_code, 503, "serviceUnavailable")
289

    
290
    def test_create_server_no_flavor(self, mrapi, mimage):
291
        request = {
292
                    "server": {
293
                        "name": "new-server-test",
294
                        "userid": "test_user",
295
                        "imageRef": 1,
296
                        "flavorRef": 42,
297
                        "metadata": {
298
                            "My Server Name": "Apache1"
299
                        },
300
                        "personality": []
301
                    }
302
        }
303
        response = self.mypost('servers', 'test_user',
304
                               json.dumps(request), 'json')
305
        self.assertItemNotFound(response)
306

    
307
    def test_create_server_error(self, mrapi, mimage):
308
        """Test if the create server call returns the expected response
309
           if a valid request has been speficied."""
310
        mimage.return_value = {'location': 'pithos://foo',
311
                               'checksum': '1234',
312
                               "id": 1,
313
                               "name": "test_image",
314
                               'disk_format': 'diskdump'}
315
        mrapi().CreateInstance.side_effect = GanetiApiError("..ganeti is down")
316
        flavor = mfactory.FlavorFactory()
317
        # Create public network and backend
318
        network = mfactory.NetworkFactory(public=True)
319
        backend = mfactory.BackendFactory()
320
        mfactory.BackendNetworkFactory(network=network, backend=backend)
321

    
322
        request = {
323
                    "server": {
324
                        "name": "new-server-test",
325
                        "userid": "test_user",
326
                        "imageRef": 1,
327
                        "flavorRef": flavor.id,
328
                        "metadata": {
329
                            "My Server Name": "Apache1"
330
                        },
331
                        "personality": []
332
                    }
333
        }
334
        with mocked_quotaholder():
335
            response = self.mypost('servers', 'test_user',
336
                                   json.dumps(request), 'json')
337
        self.assertEqual(response.status_code, 500)
338
        mrapi().CreateInstance.assert_called_once()
339
        vm = VirtualMachine.objects.get()
340
        # The VM has been deleted
341
        self.assertTrue(vm.deleted)
342
        # and it has no nics
343
        self.assertEqual(len(vm.nics.all()), 0)
344
        self.assertEqual(vm.backendjobid, 0)
345

    
346

    
347
@patch('synnefo.logic.rapi_pool.GanetiRapiClient')
348
class ServerDestroyAPITest(ComputeAPITest):
349
    def test_delete_server(self, mrapi):
350
        vm = mfactory.VirtualMachineFactory()
351
        response = self.mydelete('servers/%d' % vm.id, vm.userid)
352
        self.assertEqual(response.status_code, 204)
353
        mrapi().DeleteInstance.assert_called_once()
354

    
355
    def test_non_existing_delete_server(self, mrapi):
356
        vm = mfactory.VirtualMachineFactory()
357
        response = self.mydelete('servers/%d' % 42, vm.userid)
358
        self.assertItemNotFound(response)
359
        self.assertFalse(mrapi.mock_calls)
360

    
361

    
362
class ServerMetadataAPITest(ComputeAPITest):
363
    def setUp(self):
364
        self.vm = mfactory.VirtualMachineFactory()
365
        self.metadata = mfactory.VirtualMachineMetadataFactory(vm=self.vm)
366
        super(ServerMetadataAPITest, self).setUp()
367

    
368
    def test_get_metadata(self):
369
        vm = self.vm
370
        create_meta = lambda: mfactory.VirtualMachineMetadataFactory(vm=vm)
371
        metadata = [create_meta(), create_meta(), create_meta()]
372
        response = self.myget('servers/%d/metadata' % vm.id, vm.userid)
373
        self.assertTrue(response.status_code in [200, 203])
374
        api_metadata = json.loads(response.content)['metadata']
375
        self.assertEqual(len(api_metadata), len(metadata) + 1)
376
        for db_m in metadata:
377
            self.assertEqual(api_metadata[db_m.meta_key], db_m.meta_value)
378

    
379
        request = {'metadata':
380
                        {'foo': 'bar'},
381
                        metadata[0].meta_key: 'bar2'
382
                  }
383
        response = self.mypost('servers/%d/metadata' % vm.id,
384
                             vm.userid, json.dumps(request), 'json')
385
        metadata2 = VirtualMachineMetadata.objects.filter(vm=vm)
386
        response = self.myget('servers/%d/metadata' % vm.id, vm.userid)
387
        self.assertTrue(response.status_code in [200, 203])
388
        api_metadata2 = json.loads(response.content)['metadata']
389
        self.assertTrue('foo' in api_metadata2.keys())
390
        self.assertTrue(api_metadata2[metadata[0].meta_key], 'bar2')
391
        self.assertEqual(len(api_metadata2), len(metadata2))
392
        for db_m in metadata2:
393
            self.assertEqual(api_metadata2[db_m.meta_key], db_m.meta_value)
394

    
395
        # Create new meta
396
        request = {'meta': {'foo2': 'bar2'}}
397
        response = self.myput('servers/%d/metadata/foo2' % vm.id,
398
                              vm.userid, json.dumps(request), 'json')
399

    
400
        # Get the new meta
401
        response = self.myget('servers/%d/metadata/foo2' % vm.id, vm.userid)
402
        meta = json.loads(response.content)['meta']
403
        self.assertEqual(meta['foo2'], 'bar2')
404

    
405
        # Delete the new meta
406
        response = self.mydelete('servers/%d/metadata/foo2' % vm.id, vm.userid)
407
        self.assertEqual(response.status_code, 204)
408

    
409
        # Try to get the deleted meta: should raise 404
410
        response = self.myget('servers/%d/metadata/foo2' % vm.id, vm.userid)
411
        self.assertEqual(response.status_code, 404)
412

    
413
    def test_invalid_metadata(self):
414
        vm = self.vm
415
        response = self.mypost('servers/%d/metadata' % vm.id, vm.userid)
416
        self.assertBadRequest(response)
417
        self.assertEqual(len(vm.metadata.all()), 1)
418

    
419
    def test_invalid_metadata_server(self):
420
        response = self.mypost('servers/42/metadata', 'user')
421
        self.assertItemNotFound(response)
422

    
423
    def test_get_meta_invalid_key(self):
424
        vm = self.vm
425
        response = self.myget('servers/%d/metadata/foo2' % vm.id, vm.userid)
426
        self.assertItemNotFound(response)
427

    
428

    
429
@patch('synnefo.api.util.get_image')
430
@patch('synnefo.logic.rapi_pool.GanetiRapiClient')
431
class ServerActionAPITest(ComputeAPITest):
432
    def test_actions(self, mrapi, mimage):
433
        actions = ['start', 'shutdown', 'reboot']
434
        vm = mfactory.VirtualMachineFactory()
435
        vm.operstate = "STOPPED"
436
        vm.save()
437
        for action in actions:
438
            val = {'type': 'HARD'} if action == 'reboot' else {}
439
            request = {action: val}
440
            response = self.mypost('servers/%d/action' % vm.id,
441
                                   vm.userid, json.dumps(request), 'json')
442
            self.assertEqual(response.status_code, 202)
443
            if action == 'shutdown':
444
                self.assertEqual(VirtualMachine.objects.get(id=vm.id).action,
445
                                 "STOP")
446
            else:
447
                self.assertEqual(VirtualMachine.objects.get(id=vm.id).action,
448
                                 action.upper())
449

    
450
    def test_action_in_building_vm(self, mrapi, mimage):
451
        """Test building in progress"""
452
        vm = mfactory.VirtualMachineFactory()
453
        request = {'start': '{}'}
454
        response = self.mypost('servers/%d/action' % vm.id,
455
                               vm.userid, json.dumps(request), 'json')
456
        self.assertEqual(response.status_code, 409)
457
        self.assertFalse(mrapi.mock_calls)
458

    
459
    def test_destroy_build_vm(self, mrapi, mimage):
460
        """Test building in progress"""
461
        vm = mfactory.VirtualMachineFactory()
462
        response = self.mydelete('servers/%d' % vm.id, vm.userid)
463
        self.assertSuccess(response)
464
        mrapi().RemoveInstance.assert_called_once()
465

    
466
    def test_firewall(self, mrapi, mimage):
467
        vm = mfactory.VirtualMachineFactory()
468
        vm.operstate = "STOPPED"
469
        vm.save()
470
        request = {'firewallProfile': {'profile': 'PROTECTED'}}
471
        response = self.mypost('servers/%d/action' % vm.id,
472
                               vm.userid, json.dumps(request), 'json')
473
        self.assertEqual(response.status_code, 202)
474
        mrapi().ModifyInstance.assert_called_once()
475

    
476
    def test_unsupported_firewall(self, mrapi, mimage):
477
        vm = mfactory.VirtualMachineFactory()
478
        vm.operstate = "STOPPED"
479
        vm.save()
480
        request = {'firewallProfile': {'profile': 'FOO'}}
481
        response = self.mypost('servers/%d/action' % vm.id,
482
                               vm.userid, json.dumps(request), 'json')
483
        self.assertBadRequest(response)
484
        self.assertFalse(mrapi.mock_calls)
485

    
486

    
487
class ServerVNCConsole(ComputeAPITest):
488
    def test_not_active_server(self):
489
        """Test console req for not ACTIVE server returns badRequest"""
490
        vm = mfactory.VirtualMachineFactory()
491
        data = json.dumps({'console': {'type': 'vnc'}})
492
        response = self.mypost('servers/%d/action' % vm.id,
493
                               vm.userid, data, 'json')
494
        self.assertBadRequest(response)
495

    
496
    def test_active_server(self):
497
        """Test console req for ACTIVE server"""
498
        vm = mfactory.VirtualMachineFactory()
499
        vm.operstate = 'STARTED'
500
        vm.save()
501

    
502
        data = json.dumps({'console': {'type': 'vnc'}})
503
        response = self.mypost('servers/%d/action' % vm.id,
504
                               vm.userid, data, 'json')
505
        self.assertEqual(response.status_code, 200)
506
        reply = json.loads(response.content)
507
        self.assertEqual(reply.keys(), ['console'])
508
        console = reply['console']
509
        self.assertEqual(console['type'], 'vnc')
510
        self.assertEqual(set(console.keys()),
511
                         set(['type', 'host', 'port', 'password']))
512

    
513
    def test_wrong_console_type(self):
514
        """Test console req for ACTIVE server"""
515
        vm = mfactory.VirtualMachineFactory()
516
        vm.operstate = 'STARTED'
517
        vm.save()
518

    
519
        data = json.dumps({'console': {'type': 'foo'}})
520
        response = self.mypost('servers/%d/action' % vm.id,
521
                               vm.userid, data, 'json')
522
        self.assertBadRequest(response)