Fix for bug 1054
[osm/RO.git] / RO / osm_ro / wim / wan_link_actions.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 # pylint: disable=E1101,E0203,W0201
35 import json
36 from pprint import pformat
37 # from sys import exc_info
38 from time import time
39
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
43 from .errors import (
44 InconsistentState,
45 NoRecordFound,
46 NoExternalPortFound
47 )
48 from .sdnconn import SdnConnectorError
49
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')
55
56
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.
61
62 Arguments:
63 connector: object with API for accessing the WAN
64 Infrastructure Manager system
65 persistence: abstraction layer for the database
66 """
67 fields = ('sdn_status', 'sdn_info', 'error_msg')
68 result = dict.fromkeys(fields)
69
70 try:
71 result.update(
72 connector
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))
77
78 result = filter_keys(result, fields)
79
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
87
88 persistence.update_wan_link(self.item_id,
89 remove_none_items(link_changes))
90
91 self.save(persistence, **action_changes)
92
93 return result
94
95
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)
101
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.
106
107 Arguments:
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
113 """
114 wan_link = persistence.get_by_uuid('instance_wim_nets', self.item_id)
115
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'])
119
120 try:
121 dependency_statuses = [n['status'] for n in instance_nets]
122 except KeyError:
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]
128 if errored:
129 return self.fail(
130 persistence,
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))
134
135 pending = [instance_nets[i]
136 for i, status in enumerate(dependency_statuses)
137 if status in INSTANCE_NET_STATUS_PENDING]
138 if pending:
139 return self.defer(
140 persistence,
141 'Still waiting for the local networks to be active:\n\t' +
142 '\n\t'.join('{uuid}: {status}'.format(**n) for n in pending))
143
144 return self.execute(connector, persistence, ovim, instance_nets)
145
146 def _get_connection_point_info(self, persistence, ovim, instance_net):
147 """Retrieve information about the connection PoP <> WAN
148
149 Arguments:
150 persistence: object that encapsulates persistence logic
151 (e.g. db connection)
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.
158
159 Returns:
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**.
166 """
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
169 # configuration:
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))
177
178 if not external_port:
179 raise NoExternalPortFound(instance_net)
180
181 # Then, we find the WAN switch that is connected to this external port
182 try:
183 wim_account = persistence.get_wim_account_by(
184 uuid=self.wim_account_id)
185
186 criteria = {
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}
191
192 wan_port_mapping = persistence.query_one(
193 FROM='wim_port_mappings',
194 WHERE=criteria)
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)))
199 raise ex from e
200
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'))
205 )
206
207 return merge_dicts(wan_port_mapping, service_mapping_info=mapping)
208
209 def _get_port_sdn(self, ovim, instance_net):
210 try:
211 local_port_mapping = ovim.get_ports(instance_net['sdn_net_id'])
212
213 if local_port_mapping:
214 return (local_port_mapping[0]['switch_dpid'],
215 local_port_mapping[0]['switch_port'])
216 except: # noqa
217 self.logger.exception('Problems when calling OpenVIM')
218
219 self.logger.debug("No ports found for sdn_net_id='{}'", instance_net['sdn_net_id'])
220 return None
221
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.
227
228 For example, considering a VIM/datacenter is registered like the
229 following::
230
231 vim_record = {
232 "uuid": ...
233 ... # Other properties associated with the VIM/datacenter
234 "config": {
235 ... # Other configuration
236 "external_connections": [
237 {
238 "condition": {
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.
244 },
245 "vim_external_port": {"switch": "switchA", "port": "portB"}
246 },
247 ... # The user can provide as many rules as needed, however
248 # only the first one to match will be applied.
249 ]
250 }
251 }
252
253 When an ``instance_net`` record is instantiated in that datacenter with
254 the following information::
255
256 instance_net = {
257 "uuid": ...
258 ...
259 "vim_info": {
260 ...
261 "provider_physical_network": "provider_net1",
262 }
263 }
264
265 Then, ``switchA`` and ``portB`` will be used to stablish the WAN
266 connection.
267
268 Arguments:
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
272 database).
273 vim_info (dict): Information given by the VIM Connector, against
274 which the rules will be evaluated.
275
276 Returns:
277 tuple: switch id (local datacenter switch) and port or None if
278 the rule does not match.
279 """
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)
284 return None
285
286 return (rule['vim_external_port']['switch'],
287 rule['vim_external_port']['port'])
288
289 @staticmethod
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.
293
294 Please check :obj:`~._evaluate_rules` for more information.
295
296 Arguments:
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
300 database).
301 vim_info (dict): Information given by the VIM Connector, against
302 which the rules will be evaluated.
303
304 Returns:
305 True or False: If all the conditions are met.
306 """
307 condition = rule.get('condition', {}) or {}
308 return all(safe_get(vim_info, k) == v for k, v in condition.items())
309
310 @staticmethod
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']
321 }
322 else:
323 point['service_endpoint_encapsulation_type'] = 'none'
324 return point
325
326 @staticmethod
327 def _derive_service_type(connection_points):
328 # TODO: add multipoint and L3 connectivity.
329 if len(connection_points) == 2:
330 return 'ELINE'
331 else:
332 raise NotImplementedError('Multipoint connectivity is not '
333 'supported yet.')
334
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},
340 'status': 'BUILD'})
341
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
345 """
346 try:
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)
350 for w in wan_info]
351
352 uuid, info = connector.create_connectivity_service(
353 self._derive_service_type(connection_points),
354 connection_points
355 # TODO: other properties, e.g. bandwidth
356 )
357 except (SdnConnectorError, InconsistentState,
358 NoExternalPortFound) as ex:
359 self.logger.exception(ex)
360 return self.fail(
361 persistence,
362 'Impossible to stablish WAN connectivity.\n\t{}'.format(ex))
363
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)
369 return uuid
370
371
372 class WanLinkDelete(DeleteAction):
373 def succeed(self, persistence):
374 try:
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)
379
380 return super(WanLinkDelete, self).succeed(persistence)
381
382 def get_wan_link(self, persistence):
383 """Retrieve information about the wan_link
384
385 It might be cached, or arrive from the database
386 """
387 if self.extra.get('wan_link'):
388 # First try a cached version of the data
389 return self.extra['wan_link']
390
391 return persistence.get_by_uuid(
392 'instance_wim_nets', self.item_id)
393
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 ''):
398 return self.fail(
399 persistence,
400 'Impossible to delete WAN connectivity, '
401 'it was never successfully established:'
402 '\n\t{}'.format(wan_link['error_msg']))
403
404 internal_id = wan_link.get('wim_internal_id') or self.internal_id
405
406 if not 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)
413
414 try:
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)
423 return self.fail(
424 persistence,
425 'Impossible to delete WAN connectivity.\n\t{}'.format(ex))
426
427 self.logger.debug('WAN connectivity removed %s', result)
428 self.succeed(persistence)
429
430 return result
431
432
433 class WanLinkFind(RefreshMixin, FindAction):
434 pass
435
436
437 ACTIONS = {
438 'CREATE': WanLinkCreate,
439 'DELETE': WanLinkDelete,
440 'FIND': WanLinkFind,
441 }