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.
34 # pylint: disable=E1101,E0203,W0201
36 from pprint
import pformat
37 # from sys import exc_info
40 from ..utils
import filter_dict_keys
as filter_keys
41 from ..utils
import merge_dicts
, remove_none_items
, safe_get
, truncate
42 from .actions
import CreateAction
, DeleteAction
, FindAction
48 from osm_ro_plugin
.sdnconn
import SdnConnectorError
50 INSTANCE_NET_STATUS_ERROR
= ('DOWN', 'ERROR', 'VIM_ERROR',
51 'DELETED', 'SCHEDULED_DELETION')
52 INSTANCE_NET_STATUS_PENDING
= ('BUILD', 'INACTIVE', 'SCHEDULED_CREATION')
53 INSTANCE_VM_STATUS_ERROR
= ('ERROR', 'VIM_ERROR',
54 'DELETED', 'SCHEDULED_DELETION')
57 class RefreshMixin(object):
58 def refresh(self
, connector
, persistence
):
59 """Ask the external WAN Infrastructure Manager system for updates on
60 the status of the task.
63 connector: object with API for accessing the WAN
64 Infrastructure Manager system
65 persistence: abstraction layer for the database
67 fields
= ('sdn_status', 'sdn_info', 'error_msg')
68 result
= dict.fromkeys(fields
)
73 .get_connectivity_service_status(self
.wim_internal_id
))
74 except SdnConnectorError
as ex
:
75 self
.logger
.exception(ex
)
76 result
.update(sdn_status
='WIM_ERROR', error_msg
=truncate(ex
))
78 result
= filter_keys(result
, fields
)
80 action_changes
= remove_none_items({
81 'extra': merge_dicts(self
.extra
, result
),
82 'status': 'BUILD' if result
['sdn_status'] == 'BUILD' else None,
83 'error_msg': result
['error_msg'],
84 'modified_at': time()})
85 link_changes
= merge_dicts(result
, status
=result
.pop('sdn_status'))
86 # ^ Rename field: sdn_status => status
88 persistence
.update_wan_link(self
.item_id
,
89 remove_none_items(link_changes
))
91 self
.save(persistence
, **action_changes
)
96 class WanLinkCreate(RefreshMixin
, CreateAction
):
97 def fail(self
, persistence
, reason
, status
='FAILED'):
98 changes
= {'status': 'ERROR', 'error_msg': truncate(reason
)}
99 persistence
.update_wan_link(self
.item_id
, changes
)
100 return super(WanLinkCreate
, self
).fail(persistence
, reason
, status
)
102 def process(self
, connector
, persistence
, ovim
):
103 """Process the current task.
104 First we check if all the dependencies are ready,
105 then we call ``execute`` to actually execute the action.
108 connector: object with API for accessing the WAN
109 Infrastructure Manager system
110 persistence: abstraction layer for the database
111 ovim: instance of openvim, abstraction layer that enable
112 SDN-related operations
114 wan_link
= persistence
.get_by_uuid('instance_wim_nets', self
.item_id
)
116 # First we check if all the dependencies are solved
117 instance_nets
= persistence
.get_instance_nets(
118 wan_link
['instance_scenario_id'], wan_link
['sce_net_id'])
121 dependency_statuses
= [n
['status'] for n
in instance_nets
]
123 self
.logger
.debug('`status` not found in\n\n%s\n\n',
124 json
.dumps(instance_nets
, indent
=4))
125 errored
= [instance_nets
[i
]
126 for i
, status
in enumerate(dependency_statuses
)
127 if status
in INSTANCE_NET_STATUS_ERROR
]
131 'Impossible to stablish WAN connectivity due to an issue '
132 'with the local networks:\n\t' +
133 '\n\t'.join('{uuid}: {status}'.format(**n
) for n
in errored
))
135 pending
= [instance_nets
[i
]
136 for i
, status
in enumerate(dependency_statuses
)
137 if status
in INSTANCE_NET_STATUS_PENDING
]
141 'Still waiting for the local networks to be active:\n\t' +
142 '\n\t'.join('{uuid}: {status}'.format(**n
) for n
in pending
))
144 return self
.execute(connector
, persistence
, ovim
, instance_nets
)
146 def _get_connection_point_info(self
, persistence
, ovim
, instance_net
):
147 """Retrieve information about the connection PoP <> WAN
150 persistence: object that encapsulates persistence logic
152 ovim: object that encapsulates network management logic (openvim)
153 instance_net: record with the information about a local network
154 (inside a VIM). This network will be connected via a WAN link
155 to a different network in a distinct VIM.
156 This method is used to trace what would be the way this network
157 can be accessed from the outside world.
160 dict: Record representing the wan_port_mapping associated to the
161 given instance_net. The expected fields are:
162 **wim_id**, **datacenter_id**, **device_id** (the local
163 network is expected to be connected at this switch dpid),
164 **device_interface_id**, **service_endpoint_id**,
165 **service_mapping_info**.
167 # First, we need to find a route from the datacenter to the outside
168 # world. For that, we can use the rules given in the datacenter
170 datacenter_id
= instance_net
['datacenter_id']
171 datacenter
= persistence
.get_datacenter_by(datacenter_id
)
172 rules
= safe_get(datacenter
, 'config.external_connections', {}) or {}
173 vim_info
= instance_net
.get('vim_info', {}) or {}
174 # Alternatively, we can look for it, using the SDN assist
175 external_port
= (self
._evaluate
_rules
(rules
, vim_info
) or
176 self
._get
_port
_sdn
(ovim
, instance_net
))
178 if not external_port
:
179 raise NoExternalPortFound(instance_net
)
181 # Then, we find the WAN switch that is connected to this external port
183 wim_account
= persistence
.get_wim_account_by(
184 uuid
=self
.wim_account_id
)
187 'wim_id': wim_account
['wim_id'],
188 'device_id': external_port
[0],
189 'device_interface_id': external_port
[1],
190 'datacenter_id': datacenter_id
}
192 wan_port_mapping
= persistence
.query_one(
193 FROM
='wim_port_mappings',
195 except NoRecordFound
as e
:
196 ex
= InconsistentState('No WIM port mapping found:'
197 'wim_account: {}\ncriteria:\n{}'.format(
198 self
.wim_account_id
, pformat(criteria
)))
201 # It is important to return encapsulation information if present
202 mapping
= merge_dicts(
203 wan_port_mapping
.get('service_mapping_info'),
204 filter_keys(vim_info
, ('encapsulation_type', 'encapsulation_id'))
207 return merge_dicts(wan_port_mapping
, service_mapping_info
=mapping
)
209 def _get_port_sdn(self
, ovim
, instance_net
):
211 local_port_mapping
= ovim
.get_ports(instance_net
['sdn_net_id'])
213 if local_port_mapping
:
214 return (local_port_mapping
[0]['switch_dpid'],
215 local_port_mapping
[0]['switch_port'])
217 self
.logger
.exception('Problems when calling OpenVIM')
219 self
.logger
.debug("No ports found for sdn_net_id='{}'", instance_net
['sdn_net_id'])
222 def _evaluate_rules(self
, rules
, vim_info
):
223 """Given a ``vim_info`` dict from a ``instance_net`` record, evaluate
224 the set of rules provided during the VIM/datacenter registration to
225 determine an external port used to connect that VIM/datacenter to
226 other ones where different parts of the NS will be instantiated.
228 For example, considering a VIM/datacenter is registered like the
233 ... # Other properties associated with the VIM/datacenter
235 ... # Other configuration
236 "external_connections": [
239 "provider:physical_network": "provider_net1",
240 ... # This method will look up all the keys listed here
241 # in the instance_nets.vim_info dict and compare the
242 # values. When all the values match, the associated
243 # vim_external_port will be selected.
245 "vim_external_port": {"switch": "switchA", "port": "portB"}
247 ... # The user can provide as many rules as needed, however
248 # only the first one to match will be applied.
253 When an ``instance_net`` record is instantiated in that datacenter with
254 the following information::
261 "provider_physical_network": "provider_net1",
265 Then, ``switchA`` and ``portB`` will be used to stablish the WAN
269 rules (list): Set of dicts containing the keys ``condition`` and
270 ``vim_external_port``. This list should be extracted from
271 ``vim['config']['external_connections']`` (as stored in the
273 vim_info (dict): Information given by the VIM Connector, against
274 which the rules will be evaluated.
277 tuple: switch id (local datacenter switch) and port or None if
278 the rule does not match.
280 rule
= next((r
for r
in rules
if self
._evaluate
_rule
(r
, vim_info
)), {})
281 if 'vim_external_port' not in rule
:
282 self
.logger
.debug('No external port found.\n'
283 'rules:\n%r\nvim_info:\n%r\n\n', rules
, vim_info
)
286 return (rule
['vim_external_port']['switch'],
287 rule
['vim_external_port']['port'])
290 def _evaluate_rule(rule
, vim_info
):
291 """Evaluate the conditions from a single rule to ``vim_info`` and
292 determine if the rule should be applicable or not.
294 Please check :obj:`~._evaluate_rules` for more information.
297 rule (dict): Data structure containing the keys ``condition`` and
298 ``vim_external_port``. This should be one of the elements in
299 ``vim['config']['external_connections']`` (as stored in the
301 vim_info (dict): Information given by the VIM Connector, against
302 which the rules will be evaluated.
305 True or False: If all the conditions are met.
307 condition
= rule
.get('condition', {}) or {}
308 return all(safe_get(vim_info
, k
) == v
for k
, v
in condition
.items())
311 def _derive_connection_point(wan_info
):
312 point
= {'service_endpoint_id': wan_info
['service_endpoint_id']}
313 # TODO: Cover other scenarios, e.g. VXLAN.
314 details
= wan_info
.get('service_mapping_info', {})
315 if details
.get('encapsulation_type') == 'vlan':
316 point
['service_endpoint_encapsulation_type'] = 'dot1q'
317 point
['service_endpoint_encapsulation_info'] = {
318 'vlan': details
['encapsulation_id'],
319 'switch_dpid': wan_info
['switch_dpid'],
320 'switch_port': wan_info
['switch_port']
323 point
['service_endpoint_encapsulation_type'] = 'none'
327 def _derive_service_type(connection_points
):
328 # TODO: add multipoint and L3 connectivity.
329 if len(connection_points
) == 2:
332 # added to support DPB WIM connector
335 def _update_persistent_data(self
, persistence
, service_uuid
, conn_info
):
336 """Store plugin/connector specific information in the database"""
337 persistence
.update_wan_link(self
.item_id
, {
338 'wim_internal_id': service_uuid
,
339 'sdn_info': {'conn_info': conn_info
},
342 def execute(self
, connector
, persistence
, ovim
, instance_nets
):
343 """Actually execute the action, since now we are sure all the
344 dependencies are solved
347 wan_info
= (self
._get
_connection
_point
_info
(persistence
, ovim
, net
)
348 for net
in instance_nets
)
349 connection_points
= [self
._derive
_connection
_point
(w
)
352 uuid
, info
= connector
.create_connectivity_service(
353 self
._derive
_service
_type
(connection_points
),
355 # TODO: other properties, e.g. bandwidth
357 except (SdnConnectorError
, InconsistentState
,
358 NoExternalPortFound
) as ex
:
359 self
.logger
.exception(ex
)
362 'Impossible to stablish WAN connectivity.\n\t{}'.format(ex
))
364 self
.logger
.debug('WAN connectivity established %s\n%s\n',
365 uuid
, json
.dumps(info
, indent
=4))
366 self
.wim_internal_id
= uuid
367 self
._update
_persistent
_data
(persistence
, uuid
, info
)
368 self
.succeed(persistence
)
372 class WanLinkDelete(DeleteAction
):
373 def succeed(self
, persistence
):
375 persistence
.update_wan_link(self
.item_id
, {'status': 'DELETED'})
376 except NoRecordFound
:
377 self
.logger
.debug('%s(%s) record already deleted',
378 self
.item
, self
.item_id
)
380 return super(WanLinkDelete
, self
).succeed(persistence
)
382 def get_wan_link(self
, persistence
):
383 """Retrieve information about the wan_link
385 It might be cached, or arrive from the database
387 if self
.extra
.get('wan_link'):
388 # First try a cached version of the data
389 return self
.extra
['wan_link']
391 return persistence
.get_by_uuid(
392 'instance_wim_nets', self
.item_id
)
394 def process(self
, connector
, persistence
, ovim
):
395 """Delete a WAN link previously created"""
396 wan_link
= self
.get_wan_link(persistence
)
397 if 'ERROR' in (wan_link
.get('status') or ''):
400 'Impossible to delete WAN connectivity, '
401 'it was never successfully established:'
402 '\n\t{}'.format(wan_link
['error_msg']))
404 internal_id
= wan_link
.get('wim_internal_id') or self
.internal_id
407 self
.logger
.debug('No wim_internal_id found in\n%s\n%s\n'
408 'Assuming no network was created yet, '
409 'so no network have to be deleted.',
410 json
.dumps(wan_link
, indent
=4),
411 json
.dumps(self
.as_dict(), indent
=4))
412 return self
.succeed(persistence
)
415 id = self
.wim_internal_id
416 conn_info
= safe_get(wan_link
, 'sdn_info.conn_info')
417 self
.logger
.debug('Connection Service %s (wan_link: %s):\n%s\n',
418 id, wan_link
['uuid'],
419 json
.dumps(conn_info
, indent
=4))
420 result
= connector
.delete_connectivity_service(id, conn_info
)
421 except (SdnConnectorError
, InconsistentState
) as ex
:
422 self
.logger
.exception(ex
)
425 'Impossible to delete WAN connectivity.\n\t{}'.format(ex
))
427 self
.logger
.debug('WAN connectivity removed %s', result
)
428 self
.succeed(persistence
)
433 class WanLinkFind(RefreshMixin
, FindAction
):
438 'CREATE': WanLinkCreate
,
439 'DELETE': WanLinkDelete
,