1 # -*- coding: utf-8 -*-
3 # Copyright 2018 University of Bristol - High Performance Networks Research
7 # Contributors: Anderson Bravalheri, Dimitrios Gkounis, Abubakar Siddique
8 # Muqaddas, Navdeep Uniyal, Reza Nejabati and Dimitra Simeonidou
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
14 # http://www.apache.org/licenses/LICENSE-2.0
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
22 # For those usages not covered by the Apache License, Version 2.0 please
23 # contact with: <highperformance-networks@bristol.ac.uk>
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.
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.
35 from __future__
import unicode_literals
40 from mock
import MagicMock
, patch
41 from webtest
import TestApp
43 from . import fixtures
as eg
# "examples"
44 from ...http_tools
.errors
import Conflict
, Not_Found
45 from ...tests
.db_helpers
import TestCaseWithDatabasePerTest
, uuid
46 from ...utils
import merge_dicts
47 from ..http_handler
import WimHandler
52 @patch('osm_ro.wim.wim_thread.CONNECTORS', MagicMock()) # Avoid external calls
53 @patch('osm_ro.wim.wim_thread.WimThread.start', MagicMock()) # Avoid running
54 class TestHttpHandler(TestCaseWithDatabasePerTest
):
56 super(TestHttpHandler
, self
).setUp()
58 handler
= WimHandler(db
=self
.db
)
59 self
.engine
= handler
.engine
60 self
.addCleanup(self
.engine
.stop_threads
)
61 self
.app
= TestApp(handler
.wsgi_app
)
63 def populate(self
, seeds
=None):
64 super(TestHttpHandler
, self
).populate(seeds
or eg
.consistent_set())
66 def test_list_wims(self
):
67 # Given some wims are registered in the database
69 # when a GET /<tenant_id>/wims request arrives
70 tenant_id
= uuid('tenant0')
71 response
= self
.app
.get('/{}/wims'.format(tenant_id
))
73 # then the request should be well succeeded
74 self
.assertEqual(response
.status_code
, OK
)
75 # and all the registered wims should be present
76 retrieved_wims
= {v
['name']: v
for v
in response
.json
['wims']}
77 for name
in retrieved_wims
:
78 identifier
= int(name
.replace('wim', ''))
79 self
.assertDictContainsSubset(
80 eg
.wim(identifier
), retrieved_wims
[name
])
82 def test_show_wim(self
):
83 # Given some wims are registered in the database
85 # when a GET /<tenant_id>/wims/<wim_id> request arrives
86 tenant_id
= uuid('tenant0')
88 response
= self
.app
.get('/{}/wims/{}'.format(tenant_id
, wim_id
))
90 # then the request should be well succeeded
91 self
.assertEqual(response
.status_code
, OK
)
92 # and the registered wim (wim1) should be present
93 self
.assertDictContainsSubset(eg
.wim(1), response
.json
['wim'])
94 # Moreover, it also works with tenant_id = all
95 response
= self
.app
.get('/any/wims/{}'.format(wim_id
))
96 self
.assertEqual(response
.status_code
, OK
)
97 self
.assertDictContainsSubset(eg
.wim(1), response
.json
['wim'])
99 def test_show_wim__wim_doesnt_exists(self
):
100 # Given wim_id does not refer to any already registered wim
102 # when a GET /<tenant_id>/wims/<wim_id> request arrives
103 tenant_id
= uuid('tenant0')
104 wim_id
= uuid('wim999')
105 response
= self
.app
.get(
106 '/{}/wims/{}'.format(tenant_id
, wim_id
),
109 # then the result should not be well succeeded
110 self
.assertEqual(response
.status_code
, Not_Found
)
112 def test_show_wim__tenant_doesnt_exists(self
):
113 # Given wim_id does not refer to any already registered wim
115 # when a GET /<tenant_id>/wims/<wim_id> request arrives
116 tenant_id
= uuid('tenant999')
117 wim_id
= uuid('wim0')
118 response
= self
.app
.get(
119 '/{}/wims/{}'.format(tenant_id
, wim_id
),
122 # then the result should not be well succeeded
123 self
.assertEqual(response
.status_code
, Not_Found
)
125 def test_edit_wim(self
):
126 # Given a WIM exists in the database
128 # when a PUT /wims/<wim_id> request arrives
129 wim_id
= uuid('wim1')
130 response
= self
.app
.put_json('/wims/{}'.format(wim_id
), {
131 'wim': {'name': 'My-New-Name'}})
133 # then the request should be well succeeded
134 self
.assertEqual(response
.status_code
, OK
)
135 # and the registered wim (wim1) should be present
136 self
.assertDictContainsSubset(
137 merge_dicts(eg
.wim(1), name
='My-New-Name'),
138 response
.json
['wim'])
140 def test_delete_wim(self
):
141 # Given a WIM exists in the database
143 num_accounts
= self
.count('wim_accounts')
144 num_associations
= self
.count('wim_nfvo_tenants')
145 num_mappings
= self
.count('wim_port_mappings')
147 with self
.engine
.threads_running():
148 num_threads
= len(self
.engine
.threads
)
149 # when a DELETE /wims/<wim_id> request arrives
150 wim_id
= uuid('wim1')
151 response
= self
.app
.delete('/wims/{}'.format(wim_id
))
152 num_threads_after
= len(self
.engine
.threads
)
154 # then the request should be well succeeded
155 self
.assertEqual(response
.status_code
, OK
)
156 self
.assertIn('deleted', response
.json
['result'])
157 # and the registered wim1 should be deleted
158 response
= self
.app
.get(
159 '/any/wims/{}'.format(wim_id
),
161 self
.assertEqual(response
.status_code
, Not_Found
)
162 # and all the dependent records in other tables should be deleted:
163 # wim_accounts, wim_nfvo_tenants, wim_port_mappings
164 self
.assertEqual(self
.count('wim_nfvo_tenants'),
165 num_associations
- eg
.NUM_TENANTS
)
166 self
.assertLess(self
.count('wim_port_mappings'), num_mappings
)
167 self
.assertEqual(self
.count('wim_accounts'),
168 num_accounts
- eg
.NUM_TENANTS
)
169 # And the threads associated with the wim accounts should be stopped
170 self
.assertEqual(num_threads_after
, num_threads
- eg
.NUM_TENANTS
)
172 def test_create_wim(self
):
173 # Given no WIM exists yet
174 # when a POST /wims request arrives with the right payload
175 response
= self
.app
.post_json('/wims', {'wim': eg
.wim(999)})
177 # then the request should be well succeeded
178 self
.assertEqual(response
.status_code
, OK
)
179 self
.assertEqual(response
.json
['wim']['name'], 'wim999')
181 def test_create_wim_account(self
):
182 # Given a WIM and a NFVO tenant exist but are not associated
183 self
.populate([{'wims': [eg
.wim(0)]},
184 {'nfvo_tenants': [eg
.tenant(0)]}])
186 with self
.engine
.threads_running():
187 num_threads
= len(self
.engine
.threads
)
188 # when a POST /<tenant_id>/wims/<wim_id> arrives
189 response
= self
.app
.post_json(
190 '/{}/wims/{}'.format(uuid('tenant0'), uuid('wim0')),
191 {'wim_account': eg
.wim_account(0, 0)})
193 num_threads_after
= len(self
.engine
.threads
)
195 # then a new thread should be created
196 self
.assertEqual(num_threads_after
, num_threads
+ 1)
198 # and the request should be well succeeded
199 self
.assertEqual(response
.status_code
, OK
)
200 self
.assertEqual(response
.json
['wim_account']['name'], 'wim-account00')
202 # and a new association record should be created
203 association
= self
.db
.get_rows(FROM
='wim_nfvo_tenants')
205 self
.assertEqual(len(association
), 1)
206 self
.assertEqual(association
[0]['wim_id'], uuid('wim0'))
207 self
.assertEqual(association
[0]['nfvo_tenant_id'], uuid('tenant0'))
208 self
.assertEqual(association
[0]['wim_account_id'],
209 response
.json
['wim_account']['uuid'])
211 def test_create_wim_account__existing_account(self
):
212 # Given a WIM, a WIM account and a NFVO tenants exist
213 # But the NFVO and the WIM are not associated
215 {'wims': [eg
.wim(0)]},
216 {'nfvo_tenants': [eg
.tenant(0)]},
217 {'wim_accounts': [eg
.wim_account(0, 0)]}])
219 # when a POST /<tenant_id>/wims/<wim_id> arrives
220 # and it refers to an existing wim account
221 response
= self
.app
.post_json(
222 '/{}/wims/{}'.format(uuid('tenant0'), uuid('wim0')),
223 {'wim_account': {'name': 'wim-account00'}})
225 # then the request should be well succeeded
226 self
.assertEqual(response
.status_code
, OK
)
227 # and the association should be created
228 association
= self
.db
.get_rows(
229 FROM
='wim_nfvo_tenants',
230 WHERE
={'wim_id': uuid('wim0'),
231 'nfvo_tenant_id': uuid('tenant0')})
233 self
.assertEqual(len(association
), 1)
234 # but no new wim_account should be created
235 wim_accounts
= self
.db
.get_rows(FROM
='wim_accounts')
236 self
.assertEqual(len(wim_accounts
), 1)
237 self
.assertEqual(wim_accounts
[0]['name'], 'wim-account00')
239 def test_create_wim_account__existing_account__differing(self
):
240 # Given a WIM, a WIM account and a NFVO tenants exist
241 # But the NFVO and the WIM are not associated
243 {'wims': [eg
.wim(0)]},
244 {'nfvo_tenants': [eg
.tenant(0)]},
245 {'wim_accounts': [eg
.wim_account(0, 0)]}])
247 # when a POST /<tenant_id>/wims/<wim_id> arrives
248 # and it refers to an existing wim account,
249 # but with different fields
250 response
= self
.app
.post_json(
251 '/{}/wims/{}'.format(uuid('tenant0'), uuid('wim0')), {
253 'name': 'wim-account00',
255 'password': 'abc123'}},
258 # then the request should not be well succeeded
259 self
.assertEqual(response
.status_code
, Conflict
)
260 # some useful message should be displayed
261 response
.mustcontain('attempt to overwrite', 'user', 'password')
262 # and the association should not be created
263 association
= self
.db
.get_rows(
264 FROM
='wim_nfvo_tenants',
265 WHERE
={'wim_id': uuid('wim0'),
266 'nfvo_tenant_id': uuid('tenant0')})
267 assert not association
269 def test_create_wim_account__association_already_exists(self
):
270 # Given a WIM, a WIM account and a NFVO tenants exist
271 # and are correctly associated
273 num_assoc_before
= self
.count('wim_nfvo_tenants')
275 # when a POST /<tenant_id>/wims/<wim_id> arrives trying to connect a
276 # WIM and a tenant for the second time
277 response
= self
.app
.post_json(
278 '/{}/wims/{}'.format(uuid('tenant0'), uuid('wim0')), {
281 'password': 'password999'}},
284 # then the request should not be well succeeded
285 self
.assertEqual(response
.status_code
, Conflict
)
286 # the message should be useful
287 response
.mustcontain('There is already', uuid('wim0'), uuid('tenant0'))
289 num_assoc_after
= self
.count('wim_nfvo_tenants')
291 # and the number of association record should not be increased
292 self
.assertEqual(num_assoc_before
, num_assoc_after
)
294 def test_create_wim__tenant_doesnt_exist(self
):
295 # Given a tenant not exists
298 # But the user tries to create a wim_account anyway
299 response
= self
.app
.post_json(
300 '/{}/wims/{}'.format(uuid('tenant999'), uuid('wim0')), {
303 'password': 'password999'}},
306 # then the request should not be well succeeded
307 self
.assertEqual(response
.status_code
, Not_Found
)
308 # the message should be useful
309 response
.mustcontain('No record was found', uuid('tenant999'))
311 def test_create_wim__wim_doesnt_exist(self
):
312 # Given a tenant not exists
315 # But the user tries to create a wim_account anyway
316 response
= self
.app
.post_json(
317 '/{}/wims/{}'.format(uuid('tenant0'), uuid('wim999')), {
320 'password': 'password999'}},
323 # then the request should not be well succeeded
324 self
.assertEqual(response
.status_code
, Not_Found
)
325 # the message should be useful
326 response
.mustcontain('No record was found', uuid('wim999'))
328 def test_update_wim_account(self
):
329 # Given a WIM account connecting a tenant and a WIM exists
332 with self
.engine
.threads_running():
333 num_threads
= len(self
.engine
.threads
)
335 thread
= self
.engine
.threads
[uuid('wim-account00')]
336 reload = MagicMock(wraps
=thread
.reload)
338 with patch
.object(thread
, 'reload', reload):
339 # when a PUT /<tenant_id>/wims/<wim_id> arrives
340 response
= self
.app
.put_json(
341 '/{}/wims/{}'.format(uuid('tenant0'), uuid('wim0')), {
343 'name': 'account888',
346 num_threads_after
= len(self
.engine
.threads
)
348 # then the wim thread should be restarted
349 reload.assert_called_once()
350 # and no thread should be added or removed
351 self
.assertEqual(num_threads_after
, num_threads
)
353 # and the request should be well succeeded
354 self
.assertEqual(response
.status_code
, OK
)
355 self
.assertEqual(response
.json
['wim_account']['name'], 'account888')
356 self
.assertEqual(response
.json
['wim_account']['user'], 'user888')
358 def test_update_wim_account__multiple(self
):
359 # Given a WIM account connected to several tenants
362 with self
.engine
.threads_running():
363 # when a PUT /any/wims/<wim_id> arrives
364 response
= self
.app
.put_json(
365 '/any/wims/{}'.format(uuid('wim0')), {
368 'config': {'x': 888}}})
370 # then the request should be well succeeded
371 self
.assertEqual(response
.status_code
, OK
)
372 self
.assertEqual(len(response
.json
['wim_accounts']), eg
.NUM_TENANTS
)
374 for account
in response
.json
['wim_accounts']:
375 self
.assertEqual(account
['user'], 'user888')
376 self
.assertEqual(account
['config']['x'], 888)
378 def test_delete_wim_account(self
):
379 # Given a WIM account exists and it is connected to a tenant
382 num_accounts_before
= self
.count('wim_accounts')
384 with self
.engine
.threads_running():
385 thread
= self
.engine
.threads
[uuid('wim-account00')]
386 exit
= MagicMock(wraps
=thread
.exit
)
387 num_threads
= len(self
.engine
.threads
)
389 with patch
.object(thread
, 'exit', exit
):
390 # when a PUT /<tenant_id>/wims/<wim_id> arrives
391 response
= self
.app
.delete_json(
392 '/{}/wims/{}'.format(uuid('tenant0'), uuid('wim0')))
394 num_threads_after
= len(self
.engine
.threads
)
396 # then the wim thread should exit
397 self
.assertEqual(num_threads_after
, num_threads
- 1)
398 exit
.assert_called_once()
400 # and the request should be well succeeded
401 self
.assertEqual(response
.status_code
, OK
)
402 response
.mustcontain('account `wim-account00` deleted')
404 # and the number of wim_accounts should decrease
405 num_accounts_after
= self
.count('wim_accounts')
406 self
.assertEqual(num_accounts_after
, num_accounts_before
- 1)
408 def test_delete_wim_account__multiple(self
):
409 # Given a WIM account exists and it is connected to several tenants
412 num_accounts_before
= self
.count('wim_accounts')
414 with self
.engine
.threads_running():
415 # when a PUT /<tenant_id>/wims/<wim_id> arrives
416 response
= self
.app
.delete_json(
417 '/any/wims/{}'.format(uuid('wim0')))
419 # then the request should be well succeeded
420 self
.assertEqual(response
.status_code
, OK
)
421 response
.mustcontain('account `wim-account00` deleted')
422 response
.mustcontain('account `wim-account10` deleted')
424 # and the number of wim_accounts should decrease
425 num_accounts_after
= self
.count('wim_accounts')
426 self
.assertEqual(num_accounts_after
,
427 num_accounts_before
- eg
.NUM_TENANTS
)
429 def test_delete_wim_account__doesnt_exist(self
):
430 # Given we have a tenant that is not connected to a WIM
432 tenant
= {'uuid': uuid('tenant888'), 'name': 'tenant888'}
433 self
.populate([{'nfvo_tenants': [tenant
]}])
435 num_accounts_before
= self
.count('wim_accounts')
437 # when a PUT /<tenant_id>/wims/<wim_id> arrives
438 response
= self
.app
.delete(
439 '/{}/wims/{}'.format(uuid('tenant888'), uuid('wim0')),
442 # then the request should not succeed
443 self
.assertEqual(response
.status_code
, Not_Found
)
445 # and the number of wim_accounts should not decrease
446 num_accounts_after
= self
.count('wim_accounts')
447 self
.assertEqual(num_accounts_after
, num_accounts_before
)
449 def test_create_port_mappings(self
):
450 # Given we have a wim and datacenter without any port mappings
451 self
.populate([{'nfvo_tenants': eg
.tenant(0)}] +
452 eg
.datacenter_set(888, 0) +
455 # when a POST /<tenant_id>/wims/<wim_id>/port_mapping arrives
456 response
= self
.app
.post_json(
457 '/{}/wims/{}/port_mapping'.format(uuid('tenant0'), uuid('wim999')),
458 {'wim_port_mapping': [{
459 'datacenter_name': 'dc888',
460 'pop_wan_mappings': [
461 {'pop_switch_dpid': 'AA:AA:AA:AA:AA:AA:AA:AA',
462 'pop_switch_port': 1,
463 'wan_service_mapping_info': {
464 'mapping_type': 'dpid-port',
465 'wan_switch_dpid': 'BB:BB:BB:BB:BB:BB:BB:BB',
471 # the request should be well succeeded
472 self
.assertEqual(response
.status_code
, OK
)
473 # and port mappings should be stored in the database
474 port_mapping
= self
.db
.get_rows(FROM
='wim_port_mappings')
475 self
.assertEqual(len(port_mapping
), 1)
477 def test_get_port_mappings(self
):
478 # Given WIMS and datacenters exist with port mappings between them
480 # when a GET /<tenant_id>/wims/<wim_id>/port_mapping arrives
481 response
= self
.app
.get(
482 '/{}/wims/{}/port_mapping'.format(uuid('tenant0'), uuid('wim0')))
483 # the request should be well succeeded
484 self
.assertEqual(response
.status_code
, OK
)
485 # and we should see port mappings for each WIM, datacenter pair
486 mappings
= response
.json
['wim_port_mapping']
487 self
.assertEqual(len(mappings
), eg
.NUM_DATACENTERS
)
488 # ^ In the fixture set all the datacenters are connected to all wims
490 def test_delete_port_mappings(self
):
491 # Given WIMS and datacenters exist with port mappings between them
493 num_mappings_before
= self
.count('wim_port_mappings')
495 # when a DELETE /<tenant_id>/wims/<wim_id>/port_mapping arrives
496 response
= self
.app
.delete(
497 '/{}/wims/{}/port_mapping'.format(uuid('tenant0'), uuid('wim0')))
498 # the request should be well succeeded
499 self
.assertEqual(response
.status_code
, OK
)
500 # and the number of port mappings should decrease
501 num_mappings_after
= self
.count('wim_port_mappings')
502 self
.assertEqual(num_mappings_after
,
503 num_mappings_before
- eg
.NUM_DATACENTERS
)
504 # ^ In the fixture set all the datacenters are connected to all wims
507 if __name__
== '__main__':