Revert "Removing deprecated/unused/outdated code"
[osm/RO.git] / RO / osm_ro / wim / tests / test_http_handler.py
1 # -*- coding: utf-8 -*-
2 ##
3 # Copyright 2018 University of Bristol - High Performance Networks Research
4 # Group
5 # All Rights Reserved.
6 #
7 # Contributors: Anderson Bravalheri, Dimitrios Gkounis, Abubakar Siddique
8 # Muqaddas, Navdeep Uniyal, Reza Nejabati and Dimitra Simeonidou
9 #
10 # Licensed under the Apache License, Version 2.0 (the "License"); you may
11 # not use this file except in compliance with the License. You may obtain
12 # a copy of the License at
13 #
14 # http://www.apache.org/licenses/LICENSE-2.0
15 #
16 # Unless required by applicable law or agreed to in writing, software
17 # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
18 # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
19 # License for the specific language governing permissions and limitations
20 # under the License.
21 #
22 # For those usages not covered by the Apache License, Version 2.0 please
23 # contact with: <highperformance-networks@bristol.ac.uk>
24 #
25 # Neither the name of the University of Bristol nor the names of its
26 # contributors may be used to endorse or promote products derived from
27 # this software without specific prior written permission.
28 #
29 # This work has been performed in the context of DCMS UK 5G Testbeds
30 # & Trials Programme and in the framework of the Metro-Haul project -
31 # funded by the European Commission under Grant number 761727 through the
32 # Horizon 2020 and 5G-PPP programmes.
33 ##
34
35 import unittest
36
37 import bottle
38 from unittest.mock import MagicMock, patch
39 from webtest import TestApp
40
41 from . import fixtures as eg # "examples"
42 from ...http_tools.errors import Conflict, Not_Found
43 from ...tests.db_helpers import TestCaseWithDatabasePerTest, uuid
44 from ...utils import merge_dicts
45 from ..http_handler import WimHandler
46
47 OK = 200
48
49
50 @patch('osm_ro.wim.wim_thread.CONNECTORS', MagicMock()) # Avoid external calls
51 @patch('osm_ro.wim.wim_thread.WimThread.start', MagicMock()) # Avoid running
52 class TestHttpHandler(TestCaseWithDatabasePerTest):
53 def setUp(self):
54 super(TestHttpHandler, self).setUp()
55 bottle.debug(True)
56 handler = WimHandler(db=self.db)
57 self.engine = handler.engine
58 self.addCleanup(self.engine.stop_threads)
59 self.app = TestApp(handler.wsgi_app)
60
61 def populate(self, seeds=None):
62 super(TestHttpHandler, self).populate(seeds or eg.consistent_set())
63
64 def test_list_wims(self):
65 # Given some wims are registered in the database
66 self.populate()
67 # when a GET /<tenant_id>/wims request arrives
68 tenant_id = uuid('tenant0')
69 response = self.app.get('/{}/wims'.format(tenant_id))
70
71 # then the request should be well succeeded
72 self.assertEqual(response.status_code, OK)
73 # and all the registered wims should be present
74 retrieved_wims = {v['name']: v for v in response.json['wims']}
75 for name in retrieved_wims:
76 identifier = int(name.replace('wim', ''))
77 self.assertDictContainsSubset(
78 eg.wim(identifier), retrieved_wims[name])
79
80 def test_show_wim(self):
81 # Given some wims are registered in the database
82 self.populate()
83 # when a GET /<tenant_id>/wims/<wim_id> request arrives
84 tenant_id = uuid('tenant0')
85 wim_id = uuid('wim1')
86 response = self.app.get('/{}/wims/{}'.format(tenant_id, wim_id))
87
88 # then the request should be well succeeded
89 self.assertEqual(response.status_code, OK)
90 # and the registered wim (wim1) should be present
91 self.assertDictContainsSubset(eg.wim(1), response.json['wim'])
92 # Moreover, it also works with tenant_id = all
93 response = self.app.get('/any/wims/{}'.format(wim_id))
94 self.assertEqual(response.status_code, OK)
95 self.assertDictContainsSubset(eg.wim(1), response.json['wim'])
96
97 def test_show_wim__wim_doesnt_exists(self):
98 # Given wim_id does not refer to any already registered wim
99 self.populate()
100 # when a GET /<tenant_id>/wims/<wim_id> request arrives
101 tenant_id = uuid('tenant0')
102 wim_id = uuid('wim999')
103 response = self.app.get(
104 '/{}/wims/{}'.format(tenant_id, wim_id),
105 expect_errors=True)
106
107 # then the result should not be well succeeded
108 self.assertEqual(response.status_code, Not_Found)
109
110 def test_show_wim__tenant_doesnt_exists(self):
111 # Given wim_id does not refer to any already registered wim
112 self.populate()
113 # when a GET /<tenant_id>/wims/<wim_id> request arrives
114 tenant_id = uuid('tenant999')
115 wim_id = uuid('wim0')
116 response = self.app.get(
117 '/{}/wims/{}'.format(tenant_id, wim_id),
118 expect_errors=True)
119
120 # then the result should not be well succeeded
121 self.assertEqual(response.status_code, Not_Found)
122
123 def test_edit_wim(self):
124 # Given a WIM exists in the database
125 self.populate()
126 # when a PUT /wims/<wim_id> request arrives
127 wim_id = uuid('wim1')
128 response = self.app.put_json('/wims/{}'.format(wim_id), {
129 'wim': {'name': 'My-New-Name'}})
130
131 # then the request should be well succeeded
132 self.assertEqual(response.status_code, OK)
133 # and the registered wim (wim1) should be present
134 self.assertDictContainsSubset(
135 merge_dicts(eg.wim(1), name='My-New-Name'),
136 response.json['wim'])
137
138 def test_edit_wim__port_mappings(self):
139 # Given a WIM exists in the database
140 self.populate()
141 # when a PUT /wims/<wim_id> request arrives
142 wim_id = uuid('wim1')
143 response = self.app.put_json(
144 '/wims/{}'.format(wim_id), {
145 'wim': dict(
146 name='My-New-Name',
147 config={'wim_port_mapping': [{
148 'datacenter_name': 'dc0',
149 'pop_wan_mappings': [{
150 'device_id': '00:AA:11:BB:22:CC:33:DD',
151 'device_interface_id': 1,
152 'service_mapping_info': {
153 'mapping_type': 'dpid-port',
154 'wan_switch_dpid': 'BB:BB:BB:BB:BB:BB:BB:0A',
155 'wan_switch_port': 1
156 }
157 }]}]
158 }
159 )
160 }
161 )
162
163 # then the request should be well succeeded
164 self.assertEqual(response.status_code, OK)
165 # and the registered wim (wim1) should be present
166 self.assertDictContainsSubset(
167 merge_dicts(eg.wim(1), name='My-New-Name'),
168 response.json['wim'])
169 # and the port mappings hould be updated
170 mappings = response.json['wim']['config']['wim_port_mapping']
171 self.assertEqual(len(mappings), 1)
172 self.assertEqual(
173 mappings[0]['pop_wan_mappings'][0]['device_id'],
174 '00:AA:11:BB:22:CC:33:DD')
175
176 def test_delete_wim(self):
177 # Given a WIM exists in the database
178 self.populate()
179 num_accounts = self.count('wim_accounts')
180 num_associations = self.count('wim_nfvo_tenants')
181 num_mappings = self.count('wim_port_mappings')
182
183 with self.engine.threads_running():
184 num_threads = len(self.engine.threads)
185 # when a DELETE /wims/<wim_id> request arrives
186 wim_id = uuid('wim1')
187 response = self.app.delete('/wims/{}'.format(wim_id))
188 num_threads_after = len(self.engine.threads)
189
190 # then the request should be well succeeded
191 self.assertEqual(response.status_code, OK)
192 self.assertIn('deleted', response.json['result'])
193 # and the registered wim1 should be deleted
194 response = self.app.get(
195 '/any/wims/{}'.format(wim_id),
196 expect_errors=True)
197 self.assertEqual(response.status_code, Not_Found)
198 # and all the dependent records in other tables should be deleted:
199 # wim_accounts, wim_nfvo_tenants, wim_port_mappings
200 self.assertEqual(self.count('wim_nfvo_tenants'),
201 num_associations - eg.NUM_TENANTS)
202 self.assertLess(self.count('wim_port_mappings'), num_mappings)
203 self.assertEqual(self.count('wim_accounts'),
204 num_accounts - eg.NUM_TENANTS)
205 # And the threads associated with the wim accounts should be stopped
206 self.assertEqual(num_threads_after, num_threads - eg.NUM_TENANTS)
207
208 def test_create_wim(self):
209 # Given no WIM exists yet
210 # when a POST /wims request arrives with the right payload
211 response = self.app.post_json('/wims', {'wim': eg.wim(999)})
212
213 # then the request should be well succeeded
214 self.assertEqual(response.status_code, OK)
215 self.assertEqual(response.json['wim']['name'], 'wim999')
216
217 def test_create_wim__port_mappings(self):
218 self.populate()
219 # when a POST /wims request arrives with the right payload
220 response = self.app.post_json(
221 '/wims', {
222 'wim': merge_dicts(
223 eg.wim(999),
224 config={'wim_port_mapping': [{
225 'datacenter_name': 'dc0',
226 'pop_wan_mappings': [{
227 'device_id': 'AA:AA:AA:AA:AA:AA:AA:01',
228 'device_interface_id': 1,
229 'service_mapping_info': {
230 'mapping_type': 'dpid-port',
231 'wan_switch_dpid': 'BB:BB:BB:BB:BB:BB:BB:01',
232 'wan_switch_port': 1
233 }
234 }]}]
235 }
236 )
237 }
238 )
239
240 # then the request should be well succeeded
241 self.assertEqual(response.status_code, OK)
242 self.assertEqual(response.json['wim']['name'], 'wim999')
243 self.assertEqual(
244 len(response.json['wim']['config']['wim_port_mapping']), 1)
245
246 def test_create_wim_account(self):
247 # Given a WIM and a NFVO tenant exist but are not associated
248 self.populate([{'wims': [eg.wim(0)]},
249 {'nfvo_tenants': [eg.tenant(0)]}])
250
251 with self.engine.threads_running():
252 num_threads = len(self.engine.threads)
253 # when a POST /<tenant_id>/wims/<wim_id> arrives
254 response = self.app.post_json(
255 '/{}/wims/{}'.format(uuid('tenant0'), uuid('wim0')),
256 {'wim_account': eg.wim_account(0, 0)})
257
258 num_threads_after = len(self.engine.threads)
259
260 # then a new thread should be created
261 self.assertEqual(num_threads_after, num_threads + 1)
262
263 # and the request should be well succeeded
264 self.assertEqual(response.status_code, OK)
265 self.assertEqual(response.json['wim_account']['name'], 'wim-account00')
266
267 # and a new association record should be created
268 association = self.db.get_rows(FROM='wim_nfvo_tenants')
269 assert association
270 self.assertEqual(len(association), 1)
271 self.assertEqual(association[0]['wim_id'], uuid('wim0'))
272 self.assertEqual(association[0]['nfvo_tenant_id'], uuid('tenant0'))
273 self.assertEqual(association[0]['wim_account_id'],
274 response.json['wim_account']['uuid'])
275
276 def test_create_wim_account__existing_account(self):
277 # Given a WIM, a WIM account and a NFVO tenants exist
278 # But the NFVO and the WIM are not associated
279 self.populate([
280 {'wims': [eg.wim(0)]},
281 {'nfvo_tenants': [eg.tenant(0)]},
282 {'wim_accounts': [eg.wim_account(0, 0)]}])
283
284 # when a POST /<tenant_id>/wims/<wim_id> arrives
285 # and it refers to an existing wim account
286 response = self.app.post_json(
287 '/{}/wims/{}'.format(uuid('tenant0'), uuid('wim0')),
288 {'wim_account': {'name': 'wim-account00'}})
289
290 # then the request should be well succeeded
291 self.assertEqual(response.status_code, OK)
292 # and the association should be created
293 association = self.db.get_rows(
294 FROM='wim_nfvo_tenants',
295 WHERE={'wim_id': uuid('wim0'),
296 'nfvo_tenant_id': uuid('tenant0')})
297 assert association
298 self.assertEqual(len(association), 1)
299 # but no new wim_account should be created
300 wim_accounts = self.db.get_rows(FROM='wim_accounts')
301 self.assertEqual(len(wim_accounts), 1)
302 self.assertEqual(wim_accounts[0]['name'], 'wim-account00')
303
304 def test_create_wim_account__existing_account__differing(self):
305 # Given a WIM, a WIM account and a NFVO tenants exist
306 # But the NFVO and the WIM are not associated
307 self.populate([
308 {'wims': [eg.wim(0)]},
309 {'nfvo_tenants': [eg.tenant(0)]},
310 {'wim_accounts': [eg.wim_account(0, 0)]}])
311
312 # when a POST /<tenant_id>/wims/<wim_id> arrives
313 # and it refers to an existing wim account,
314 # but with different fields
315 response = self.app.post_json(
316 '/{}/wims/{}'.format(uuid('tenant0'), uuid('wim0')), {
317 'wim_account': {
318 'name': 'wim-account00',
319 'user': 'john',
320 'password': 'abc123'}},
321 expect_errors=True)
322
323 # then the request should not be well succeeded
324 self.assertEqual(response.status_code, Conflict)
325 # some useful message should be displayed
326 response.mustcontain('attempt to overwrite', 'user', 'password')
327 # and the association should not be created
328 association = self.db.get_rows(
329 FROM='wim_nfvo_tenants',
330 WHERE={'wim_id': uuid('wim0'),
331 'nfvo_tenant_id': uuid('tenant0')})
332 assert not association
333
334 def test_create_wim_account__association_already_exists(self):
335 # Given a WIM, a WIM account and a NFVO tenants exist
336 # and are correctly associated
337 self.populate()
338 num_assoc_before = self.count('wim_nfvo_tenants')
339
340 # when a POST /<tenant_id>/wims/<wim_id> arrives trying to connect a
341 # WIM and a tenant for the second time
342 response = self.app.post_json(
343 '/{}/wims/{}'.format(uuid('tenant0'), uuid('wim0')), {
344 'wim_account': {
345 'user': 'user999',
346 'password': 'password999'}},
347 expect_errors=True)
348
349 # then the request should not be well succeeded
350 self.assertEqual(response.status_code, Conflict)
351 # the message should be useful
352 response.mustcontain('There is already', uuid('wim0'), uuid('tenant0'))
353
354 num_assoc_after = self.count('wim_nfvo_tenants')
355
356 # and the number of association record should not be increased
357 self.assertEqual(num_assoc_before, num_assoc_after)
358
359 def test_create_wim__tenant_doesnt_exist(self):
360 # Given a tenant not exists
361 self.populate()
362
363 # But the user tries to create a wim_account anyway
364 response = self.app.post_json(
365 '/{}/wims/{}'.format(uuid('tenant999'), uuid('wim0')), {
366 'wim_account': {
367 'user': 'user999',
368 'password': 'password999'}},
369 expect_errors=True)
370
371 # then the request should not be well succeeded
372 self.assertEqual(response.status_code, Not_Found)
373 # the message should be useful
374 response.mustcontain('No record was found', uuid('tenant999'))
375
376 def test_create_wim__wim_doesnt_exist(self):
377 # Given a tenant not exists
378 self.populate()
379
380 # But the user tries to create a wim_account anyway
381 response = self.app.post_json(
382 '/{}/wims/{}'.format(uuid('tenant0'), uuid('wim999')), {
383 'wim_account': {
384 'user': 'user999',
385 'password': 'password999'}},
386 expect_errors=True)
387
388 # then the request should not be well succeeded
389 self.assertEqual(response.status_code, Not_Found)
390 # the message should be useful
391 response.mustcontain('No record was found', uuid('wim999'))
392
393 def test_update_wim_account(self):
394 # Given a WIM account connecting a tenant and a WIM exists
395 self.populate()
396
397 with self.engine.threads_running():
398 num_threads = len(self.engine.threads)
399
400 thread = self.engine.threads[uuid('wim-account00')]
401 reload = MagicMock(wraps=thread.reload)
402
403 with patch.object(thread, 'reload', reload):
404 # when a PUT /<tenant_id>/wims/<wim_id> arrives
405 response = self.app.put_json(
406 '/{}/wims/{}'.format(uuid('tenant0'), uuid('wim0')), {
407 'wim_account': {
408 'name': 'account888',
409 'user': 'user888'}})
410
411 num_threads_after = len(self.engine.threads)
412
413 # then the wim thread should be restarted
414 reload.assert_called_once()
415 # and no thread should be added or removed
416 self.assertEqual(num_threads_after, num_threads)
417
418 # and the request should be well succeeded
419 self.assertEqual(response.status_code, OK)
420 self.assertEqual(response.json['wim_account']['name'], 'account888')
421 self.assertEqual(response.json['wim_account']['user'], 'user888')
422
423 def test_update_wim_account__multiple(self):
424 # Given a WIM account connected to several tenants
425 self.populate()
426
427 with self.engine.threads_running():
428 # when a PUT /any/wims/<wim_id> arrives
429 response = self.app.put_json(
430 '/any/wims/{}'.format(uuid('wim0')), {
431 'wim_account': {
432 'user': 'user888',
433 'config': {'x': 888}}})
434
435 # then the request should be well succeeded
436 self.assertEqual(response.status_code, OK)
437 self.assertEqual(len(response.json['wim_accounts']), eg.NUM_TENANTS)
438
439 for account in response.json['wim_accounts']:
440 self.assertEqual(account['user'], 'user888')
441 self.assertEqual(account['config']['x'], 888)
442
443 def test_delete_wim_account(self):
444 # Given a WIM account exists and it is connected to a tenant
445 self.populate()
446
447 num_accounts_before = self.count('wim_accounts')
448
449 with self.engine.threads_running():
450 thread = self.engine.threads[uuid('wim-account00')]
451 exit = MagicMock(wraps=thread.exit)
452 num_threads = len(self.engine.threads)
453
454 with patch.object(thread, 'exit', exit):
455 # when a PUT /<tenant_id>/wims/<wim_id> arrives
456 response = self.app.delete_json(
457 '/{}/wims/{}'.format(uuid('tenant0'), uuid('wim0')))
458
459 num_threads_after = len(self.engine.threads)
460
461 # then the wim thread should exit
462 self.assertEqual(num_threads_after, num_threads - 1)
463 exit.assert_called_once()
464
465 # and the request should be well succeeded
466 self.assertEqual(response.status_code, OK)
467 response.mustcontain('account `wim-account00` deleted')
468
469 # and the number of wim_accounts should decrease
470 num_accounts_after = self.count('wim_accounts')
471 self.assertEqual(num_accounts_after, num_accounts_before - 1)
472
473 def test_delete_wim_account__multiple(self):
474 # Given a WIM account exists and it is connected to several tenants
475 self.populate()
476
477 num_accounts_before = self.count('wim_accounts')
478
479 with self.engine.threads_running():
480 # when a PUT /<tenant_id>/wims/<wim_id> arrives
481 response = self.app.delete_json(
482 '/any/wims/{}'.format(uuid('wim0')))
483
484 # then the request should be well succeeded
485 self.assertEqual(response.status_code, OK)
486 response.mustcontain('account `wim-account00` deleted')
487 response.mustcontain('account `wim-account10` deleted')
488
489 # and the number of wim_accounts should decrease
490 num_accounts_after = self.count('wim_accounts')
491 self.assertEqual(num_accounts_after,
492 num_accounts_before - eg.NUM_TENANTS)
493
494 def test_delete_wim_account__doesnt_exist(self):
495 # Given we have a tenant that is not connected to a WIM
496 self.populate()
497 tenant = {'uuid': uuid('tenant888'), 'name': 'tenant888'}
498 self.populate([{'nfvo_tenants': [tenant]}])
499
500 num_accounts_before = self.count('wim_accounts')
501
502 # when a PUT /<tenant_id>/wims/<wim_id> arrives
503 response = self.app.delete(
504 '/{}/wims/{}'.format(uuid('tenant888'), uuid('wim0')),
505 expect_errors=True)
506
507 # then the request should not succeed
508 self.assertEqual(response.status_code, Not_Found)
509
510 # and the number of wim_accounts should not decrease
511 num_accounts_after = self.count('wim_accounts')
512 self.assertEqual(num_accounts_after, num_accounts_before)
513
514 def test_create_port_mappings(self):
515 # Given we have a wim and datacenter without any port mappings
516 self.populate([{'nfvo_tenants': eg.tenant(0)}] +
517 eg.datacenter_set(888, 0) +
518 eg.wim_set(999, 0))
519
520 # when a POST /<tenant_id>/wims/<wim_id>/port_mapping arrives
521 response = self.app.post_json(
522 '/{}/wims/{}/port_mapping'.format(uuid('tenant0'), uuid('wim999')),
523 {'wim_port_mapping': [{
524 'datacenter_name': 'dc888',
525 'pop_wan_mappings': [
526 {'device_id': 'AA:AA:AA:AA:AA:AA:AA:AA',
527 'device_interface_id': 1,
528 'service_mapping_info': {
529 'mapping_type': 'dpid-port',
530 'wan_switch_dpid': 'BB:BB:BB:BB:BB:BB:BB:BB',
531 'wan_switch_port': 1
532 }}
533 ]}
534 ]})
535
536 # the request should be well succeeded
537 self.assertEqual(response.status_code, OK)
538 # and port mappings should be stored in the database
539 port_mapping = self.db.get_rows(FROM='wim_port_mappings')
540 self.assertEqual(len(port_mapping), 1)
541
542 def test_get_port_mappings(self):
543 # Given WIMS and datacenters exist with port mappings between them
544 self.populate()
545 # when a GET /<tenant_id>/wims/<wim_id>/port_mapping arrives
546 response = self.app.get(
547 '/{}/wims/{}/port_mapping'.format(uuid('tenant0'), uuid('wim0')))
548 # the request should be well succeeded
549 self.assertEqual(response.status_code, OK)
550 # and we should see port mappings for each WIM, datacenter pair
551 mappings = response.json['wim_port_mapping']
552 self.assertEqual(len(mappings), eg.NUM_DATACENTERS)
553 # ^ In the fixture set all the datacenters are connected to all wims
554
555 def test_delete_port_mappings(self):
556 # Given WIMS and datacenters exist with port mappings between them
557 self.populate()
558 num_mappings_before = self.count('wim_port_mappings')
559
560 # when a DELETE /<tenant_id>/wims/<wim_id>/port_mapping arrives
561 response = self.app.delete(
562 '/{}/wims/{}/port_mapping'.format(uuid('tenant0'), uuid('wim0')))
563 # the request should be well succeeded
564 self.assertEqual(response.status_code, OK)
565 # and the number of port mappings should decrease
566 num_mappings_after = self.count('wim_port_mappings')
567 self.assertEqual(num_mappings_after,
568 num_mappings_before - eg.NUM_DATACENTERS)
569 # ^ In the fixture set all the datacenters are connected to all wims
570
571
572 if __name__ == '__main__':
573 unittest.main()