034e41599c2ef4f1719b65eeb15f2fae2dfa51ff
[osm/RO.git] / 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 six import reraise
41
42 from ..utils import filter_dict_keys as filter_keys
43 from ..utils import merge_dicts, remove_none_items, safe_get, truncate
44 from .actions import CreateAction, DeleteAction, FindAction
45 from .errors import (
46 InconsistentState,
47 NoRecordFound,
48 NoExternalPortFound
49 )
50 from wimconn import WimConnectorError
51
52 INSTANCE_NET_STATUS_ERROR = ('DOWN', 'ERROR', 'VIM_ERROR',
53 'DELETED', 'SCHEDULED_DELETION')
54 INSTANCE_NET_STATUS_PENDING = ('BUILD', 'INACTIVE', 'SCHEDULED_CREATION')
55 INSTANCE_VM_STATUS_ERROR = ('ERROR', 'VIM_ERROR',
56 'DELETED', 'SCHEDULED_DELETION')
57
58
59 class RefreshMixin(object):
60 def refresh(self, connector, persistence):
61 """Ask the external WAN Infrastructure Manager system for updates on
62 the status of the task.
63
64 Arguments:
65 connector: object with API for accessing the WAN
66 Infrastructure Manager system
67 persistence: abstraction layer for the database
68 """
69 fields = ('wim_status', 'wim_info', 'error_msg')
70 result = dict.fromkeys(fields)
71
72 try:
73 result.update(
74 connector
75 .get_connectivity_service_status(self.wim_internal_id))
76 except WimConnectorError as ex:
77 self.logger.exception(ex)
78 result.update(wim_status='WIM_ERROR', error_msg=truncate(ex))
79
80 result = filter_keys(result, fields)
81
82 action_changes = remove_none_items({
83 'extra': merge_dicts(self.extra, result),
84 'status': 'BUILD' if result['wim_status'] == 'BUILD' else None,
85 'error_msg': result['error_msg'],
86 'modified_at': time()})
87 link_changes = merge_dicts(result, status=result.pop('wim_status'))
88 # ^ Rename field: wim_status => status
89
90 persistence.update_wan_link(self.item_id,
91 remove_none_items(link_changes))
92
93 self.save(persistence, **action_changes)
94
95 return result
96
97
98 class WanLinkCreate(RefreshMixin, CreateAction):
99 def fail(self, persistence, reason, status='FAILED'):
100 changes = {'status': 'ERROR', 'error_msg': truncate(reason)}
101 persistence.update_wan_link(self.item_id, changes)
102 return super(WanLinkCreate, self).fail(persistence, reason, status)
103
104 def process(self, connector, persistence, ovim):
105 """Process the current task.
106 First we check if all the dependencies are ready,
107 then we call ``execute`` to actually execute the action.
108
109 Arguments:
110 connector: object with API for accessing the WAN
111 Infrastructure Manager system
112 persistence: abstraction layer for the database
113 ovim: instance of openvim, abstraction layer that enable
114 SDN-related operations
115 """
116 wan_link = persistence.get_by_uuid('instance_wim_nets', self.item_id)
117
118 # First we check if all the dependencies are solved
119 instance_nets = persistence.get_instance_nets(
120 wan_link['instance_scenario_id'], wan_link['sce_net_id'])
121
122 try:
123 dependency_statuses = [n['status'] for n in instance_nets]
124 except KeyError:
125 self.logger.debug('`status` not found in\n\n%s\n\n',
126 json.dumps(instance_nets, indent=4))
127 errored = [instance_nets[i]
128 for i, status in enumerate(dependency_statuses)
129 if status in INSTANCE_NET_STATUS_ERROR]
130 if errored:
131 return self.fail(
132 persistence,
133 'Impossible to stablish WAN connectivity due to an issue '
134 'with the local networks:\n\t' +
135 '\n\t'.join('{uuid}: {status}'.format(**n) for n in errored))
136
137 pending = [instance_nets[i]
138 for i, status in enumerate(dependency_statuses)
139 if status in INSTANCE_NET_STATUS_PENDING]
140 if pending:
141 return self.defer(
142 persistence,
143 'Still waiting for the local networks to be active:\n\t' +
144 '\n\t'.join('{uuid}: {status}'.format(**n) for n in pending))
145
146 return self.execute(connector, persistence, ovim, instance_nets)
147
148 def _get_connection_point_info(self, persistence, ovim, instance_net):
149 """Retrieve information about the connection PoP <> WAN
150
151 Arguments:
152 persistence: object that encapsulates persistence logic
153 (e.g. db connection)
154 ovim: object that encapsulates network management logic (openvim)
155 instance_net: record with the information about a local network
156 (inside a VIM). This network will be connected via a WAN link
157 to a different network in a distinct VIM.
158 This method is used to trace what would be the way this network
159 can be accessed from the outside world.
160
161 Returns:
162 dict: Record representing the wan_port_mapping associated to the
163 given instance_net. The expected fields are:
164 **wim_id**, **datacenter_id**, **pop_switch_dpid** (the local
165 network is expected to be connected at this switch),
166 **pop_switch_port**, **wan_service_endpoint_id**,
167 **wan_service_mapping_info**.
168 """
169 # First, we need to find a route from the datacenter to the outside
170 # world. For that, we can use the rules given in the datacenter
171 # configuration:
172 datacenter_id = instance_net['datacenter_id']
173 datacenter = persistence.get_datacenter_by(datacenter_id)
174 rules = safe_get(datacenter, 'config.external_connections', {}) or {}
175 vim_info = instance_net.get('vim_info', {}) or {}
176 # Alternatively, we can look for it, using the SDN assist
177 external_port = (self._evaluate_rules(rules, vim_info) or
178 self._get_port_sdn(ovim, instance_net))
179
180 if not external_port:
181 raise NoExternalPortFound(instance_net)
182
183 # Then, we find the WAN switch that is connected to this external port
184 try:
185 wim_account = persistence.get_wim_account_by(
186 uuid=self.wim_account_id)
187
188 criteria = {
189 'wim_id': wim_account['wim_id'],
190 'pop_switch_dpid': external_port[0],
191 'pop_switch_port': external_port[1],
192 'datacenter_id': datacenter_id}
193
194 wan_port_mapping = persistence.query_one(
195 FROM='wim_port_mappings',
196 WHERE=criteria)
197 except NoRecordFound:
198 ex = InconsistentState('No WIM port mapping found:'
199 'wim_account: {}\ncriteria:\n{}'.format(
200 self.wim_account_id, pformat(criteria)))
201 reraise(ex.__class__, ex, exc_info()[2])
202
203 # It is important to return encapsulation information if present
204 mapping = merge_dicts(
205 wan_port_mapping.get('wan_service_mapping_info'),
206 filter_keys(vim_info, ('encapsulation_type', 'encapsulation_id'))
207 )
208
209 return merge_dicts(wan_port_mapping, wan_service_mapping_info=mapping)
210
211 def _get_port_sdn(self, ovim, instance_net):
212 criteria = {'net_id': instance_net['sdn_net_id']}
213 try:
214 local_port_mapping = ovim.get_ports(filter=criteria)
215
216 if local_port_mapping:
217 return (local_port_mapping[0]['switch_dpid'],
218 local_port_mapping[0]['switch_port'])
219 except: # noqa
220 self.logger.exception('Problems when calling OpenVIM')
221
222 self.logger.debug('No ports found using criteria:\n%r\n.', criteria)
223 return None
224
225 def _evaluate_rules(self, rules, vim_info):
226 """Given a ``vim_info`` dict from a ``instance_net`` record, evaluate
227 the set of rules provided during the VIM/datacenter registration to
228 determine an external port used to connect that VIM/datacenter to
229 other ones where different parts of the NS will be instantiated.
230
231 For example, considering a VIM/datacenter is registered like the
232 following::
233
234 vim_record = {
235 "uuid": ...
236 ... # Other properties associated with the VIM/datacenter
237 "config": {
238 ... # Other configuration
239 "external_connections": [
240 {
241 "condition": {
242 "provider:physical_network": "provider_net1",
243 ... # This method will look up all the keys listed here
244 # in the instance_nets.vim_info dict and compare the
245 # values. When all the values match, the associated
246 # vim_external_port will be selected.
247 },
248 "vim_external_port": {"switch": "switchA", "port": "portB"}
249 },
250 ... # The user can provide as many rules as needed, however
251 # only the first one to match will be applied.
252 ]
253 }
254 }
255
256 When an ``instance_net`` record is instantiated in that datacenter with
257 the following information::
258
259 instance_net = {
260 "uuid": ...
261 ...
262 "vim_info": {
263 ...
264 "provider_physical_network": "provider_net1",
265 }
266 }
267
268 Then, ``switchA`` and ``portB`` will be used to stablish the WAN
269 connection.
270
271 Arguments:
272 rules (list): Set of dicts containing the keys ``condition`` and
273 ``vim_external_port``. This list should be extracted from
274 ``vim['config']['external_connections']`` (as stored in the
275 database).
276 vim_info (dict): Information given by the VIM Connector, against
277 which the rules will be evaluated.
278
279 Returns:
280 tuple: switch id (local datacenter switch) and port or None if
281 the rule does not match.
282 """
283 rule = next((r for r in rules if self._evaluate_rule(r, vim_info)), {})
284 if 'vim_external_port' not in rule:
285 self.logger.debug('No external port found.\n'
286 'rules:\n%r\nvim_info:\n%r\n\n', rules, vim_info)
287 return None
288
289 return (rule['vim_external_port']['switch'],
290 rule['vim_external_port']['port'])
291
292 @staticmethod
293 def _evaluate_rule(rule, vim_info):
294 """Evaluate the conditions from a single rule to ``vim_info`` and
295 determine if the rule should be applicable or not.
296
297 Please check :obj:`~._evaluate_rules` for more information.
298
299 Arguments:
300 rule (dict): Data structure containing the keys ``condition`` and
301 ``vim_external_port``. This should be one of the elements in
302 ``vim['config']['external_connections']`` (as stored in the
303 database).
304 vim_info (dict): Information given by the VIM Connector, against
305 which the rules will be evaluated.
306
307 Returns:
308 True or False: If all the conditions are met.
309 """
310 condition = rule.get('condition', {}) or {}
311 return all(safe_get(vim_info, k) == v for k, v in condition.items())
312
313 @staticmethod
314 def _derive_connection_point(wan_info):
315 point = {'service_endpoint_id': wan_info['wan_service_endpoint_id']}
316 # TODO: Cover other scenarios, e.g. VXLAN.
317 details = wan_info.get('wan_service_mapping_info', {})
318 if details.get('encapsulation_type') == 'vlan':
319 point['service_endpoint_encapsulation_type'] = 'dot1q'
320 point['service_endpoint_encapsulation_info'] = {
321 'vlan': details['encapsulation_id']
322 }
323 else:
324 point['service_endpoint_encapsulation_type'] = 'none'
325 return point
326
327 @staticmethod
328 def _derive_service_type(connection_points):
329 # TODO: add multipoint and L3 connectivity.
330 if len(connection_points) == 2:
331 return 'ELINE'
332 else:
333 raise NotImplementedError('Multipoint connectivity is not '
334 'supported yet.')
335
336 def _update_persistent_data(self, persistence, service_uuid, conn_info):
337 """Store plugin/connector specific information in the database"""
338 persistence.update_wan_link(self.item_id, {
339 'wim_internal_id': service_uuid,
340 'wim_info': {'conn_info': conn_info},
341 'status': 'BUILD'})
342
343 def execute(self, connector, persistence, ovim, instance_nets):
344 """Actually execute the action, since now we are sure all the
345 dependencies are solved
346 """
347 try:
348 wan_info = (self._get_connection_point_info(persistence, ovim, net)
349 for net in instance_nets)
350 connection_points = [self._derive_connection_point(w)
351 for w in wan_info]
352
353 uuid, info = connector.create_connectivity_service(
354 self._derive_service_type(connection_points),
355 connection_points
356 # TODO: other properties, e.g. bandwidth
357 )
358 except (WimConnectorError, InconsistentState,
359 NoExternalPortFound) as ex:
360 self.logger.exception(ex)
361 return self.fail(
362 persistence,
363 'Impossible to stablish WAN connectivity.\n\t{}'.format(ex))
364
365 self.logger.debug('WAN connectivity established %s\n%s\n',
366 uuid, json.dumps(info, indent=4))
367 self.wim_internal_id = uuid
368 self._update_persistent_data(persistence, uuid, info)
369 self.succeed(persistence)
370 return uuid
371
372
373 class WanLinkDelete(DeleteAction):
374 def succeed(self, persistence):
375 try:
376 persistence.update_wan_link(self.item_id, {'status': 'DELETED'})
377 except NoRecordFound:
378 self.logger.debug('%s(%s) record already deleted',
379 self.item, self.item_id)
380
381 return super(WanLinkDelete, self).succeed(persistence)
382
383 def get_wan_link(self, persistence):
384 """Retrieve information about the wan_link
385
386 It might be cached, or arrive from the database
387 """
388 if self.extra.get('wan_link'):
389 # First try a cached version of the data
390 return self.extra['wan_link']
391
392 return persistence.get_by_uuid(
393 'instance_wim_nets', self.item_id)
394
395 def process(self, connector, persistence, ovim):
396 """Delete a WAN link previously created"""
397 wan_link = self.get_wan_link(persistence)
398 if 'ERROR' in (wan_link.get('status') or ''):
399 return self.fail(
400 persistence,
401 'Impossible to delete WAN connectivity, '
402 'it was never successfully established:'
403 '\n\t{}'.format(wan_link['error_msg']))
404
405 internal_id = wan_link.get('wim_internal_id') or self.internal_id
406
407 if not internal_id:
408 self.logger.debug('No wim_internal_id found in\n%s\n%s\n'
409 'Assuming no network was created yet, '
410 'so no network have to be deleted.',
411 json.dumps(wan_link, indent=4),
412 json.dumps(self.as_dict(), indent=4))
413 return self.succeed(persistence)
414
415 try:
416 id = self.wim_internal_id
417 conn_info = safe_get(wan_link, 'wim_info.conn_info')
418 self.logger.debug('Connection Service %s (wan_link: %s):\n%s\n',
419 id, wan_link['uuid'],
420 json.dumps(conn_info, indent=4))
421 result = connector.delete_connectivity_service(id, conn_info)
422 except (WimConnectorError, InconsistentState) as ex:
423 self.logger.exception(ex)
424 return self.fail(
425 persistence,
426 'Impossible to delete WAN connectivity.\n\t{}'.format(ex))
427
428 self.logger.debug('WAN connectivity removed %s', result)
429 self.succeed(persistence)
430
431 return result
432
433
434 class WanLinkFind(RefreshMixin, FindAction):
435 pass
436
437
438 ACTIONS = {
439 'CREATE': WanLinkCreate,
440 'DELETE': WanLinkDelete,
441 'FIND': WanLinkFind,
442 }