0d878b2c6df9aa8dc294f01d53b76519bbe750bd
[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 .wimconn import WimConnectorError
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 = ('wim_status', 'wim_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 WimConnectorError as ex:
75 self.logger.exception(ex)
76 result.update(wim_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['wim_status'] == 'BUILD' else None,
83 'error_msg': result['error_msg'],
84 'modified_at': time()})
85 link_changes = merge_dicts(result, status=result.pop('wim_status'))
86 # ^ Rename field: wim_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**, **pop_switch_dpid** (the local
163 network is expected to be connected at this switch),
164 **pop_switch_port**, **wan_service_endpoint_id**,
165 **wan_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 'pop_switch_dpid': external_port[0],
189 'pop_switch_port': 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('wan_service_mapping_info'),
204 filter_keys(vim_info, ('encapsulation_type', 'encapsulation_id'))
205 )
206
207 return merge_dicts(wan_port_mapping, wan_service_mapping_info=mapping)
208
209 def _get_port_sdn(self, ovim, instance_net):
210 criteria = {'net_id': instance_net['sdn_net_id']}
211 try:
212 local_port_mapping = ovim.get_ports(filter=criteria)
213
214 if local_port_mapping:
215 return (local_port_mapping[0]['switch_dpid'],
216 local_port_mapping[0]['switch_port'])
217 except: # noqa
218 self.logger.exception('Problems when calling OpenVIM')
219
220 self.logger.debug('No ports found using criteria:\n%r\n.', criteria)
221 return None
222
223 def _evaluate_rules(self, rules, vim_info):
224 """Given a ``vim_info`` dict from a ``instance_net`` record, evaluate
225 the set of rules provided during the VIM/datacenter registration to
226 determine an external port used to connect that VIM/datacenter to
227 other ones where different parts of the NS will be instantiated.
228
229 For example, considering a VIM/datacenter is registered like the
230 following::
231
232 vim_record = {
233 "uuid": ...
234 ... # Other properties associated with the VIM/datacenter
235 "config": {
236 ... # Other configuration
237 "external_connections": [
238 {
239 "condition": {
240 "provider:physical_network": "provider_net1",
241 ... # This method will look up all the keys listed here
242 # in the instance_nets.vim_info dict and compare the
243 # values. When all the values match, the associated
244 # vim_external_port will be selected.
245 },
246 "vim_external_port": {"switch": "switchA", "port": "portB"}
247 },
248 ... # The user can provide as many rules as needed, however
249 # only the first one to match will be applied.
250 ]
251 }
252 }
253
254 When an ``instance_net`` record is instantiated in that datacenter with
255 the following information::
256
257 instance_net = {
258 "uuid": ...
259 ...
260 "vim_info": {
261 ...
262 "provider_physical_network": "provider_net1",
263 }
264 }
265
266 Then, ``switchA`` and ``portB`` will be used to stablish the WAN
267 connection.
268
269 Arguments:
270 rules (list): Set of dicts containing the keys ``condition`` and
271 ``vim_external_port``. This list should be extracted from
272 ``vim['config']['external_connections']`` (as stored in the
273 database).
274 vim_info (dict): Information given by the VIM Connector, against
275 which the rules will be evaluated.
276
277 Returns:
278 tuple: switch id (local datacenter switch) and port or None if
279 the rule does not match.
280 """
281 rule = next((r for r in rules if self._evaluate_rule(r, vim_info)), {})
282 if 'vim_external_port' not in rule:
283 self.logger.debug('No external port found.\n'
284 'rules:\n%r\nvim_info:\n%r\n\n', rules, vim_info)
285 return None
286
287 return (rule['vim_external_port']['switch'],
288 rule['vim_external_port']['port'])
289
290 @staticmethod
291 def _evaluate_rule(rule, vim_info):
292 """Evaluate the conditions from a single rule to ``vim_info`` and
293 determine if the rule should be applicable or not.
294
295 Please check :obj:`~._evaluate_rules` for more information.
296
297 Arguments:
298 rule (dict): Data structure containing the keys ``condition`` and
299 ``vim_external_port``. This should be one of the elements in
300 ``vim['config']['external_connections']`` (as stored in the
301 database).
302 vim_info (dict): Information given by the VIM Connector, against
303 which the rules will be evaluated.
304
305 Returns:
306 True or False: If all the conditions are met.
307 """
308 condition = rule.get('condition', {}) or {}
309 return all(safe_get(vim_info, k) == v for k, v in condition.items())
310
311 @staticmethod
312 def _derive_connection_point(wan_info):
313 point = {'service_endpoint_id': wan_info['wan_service_endpoint_id']}
314 # TODO: Cover other scenarios, e.g. VXLAN.
315 details = wan_info.get('wan_service_mapping_info', {})
316 if details.get('encapsulation_type') == 'vlan':
317 point['service_endpoint_encapsulation_type'] = 'dot1q'
318 point['service_endpoint_encapsulation_info'] = {
319 'vlan': details['encapsulation_id']
320 }
321 else:
322 point['service_endpoint_encapsulation_type'] = 'none'
323 return point
324
325 @staticmethod
326 def _derive_service_type(connection_points):
327 # TODO: add multipoint and L3 connectivity.
328 if len(connection_points) == 2:
329 return 'ELINE'
330 else:
331 raise NotImplementedError('Multipoint connectivity is not '
332 'supported yet.')
333
334 def _update_persistent_data(self, persistence, service_uuid, conn_info):
335 """Store plugin/connector specific information in the database"""
336 persistence.update_wan_link(self.item_id, {
337 'wim_internal_id': service_uuid,
338 'wim_info': {'conn_info': conn_info},
339 'status': 'BUILD'})
340
341 def execute(self, connector, persistence, ovim, instance_nets):
342 """Actually execute the action, since now we are sure all the
343 dependencies are solved
344 """
345 try:
346 wan_info = (self._get_connection_point_info(persistence, ovim, net)
347 for net in instance_nets)
348 connection_points = [self._derive_connection_point(w)
349 for w in wan_info]
350
351 uuid, info = connector.create_connectivity_service(
352 self._derive_service_type(connection_points),
353 connection_points
354 # TODO: other properties, e.g. bandwidth
355 )
356 except (WimConnectorError, InconsistentState,
357 NoExternalPortFound) as ex:
358 self.logger.exception(ex)
359 return self.fail(
360 persistence,
361 'Impossible to stablish WAN connectivity.\n\t{}'.format(ex))
362
363 self.logger.debug('WAN connectivity established %s\n%s\n',
364 uuid, json.dumps(info, indent=4))
365 self.wim_internal_id = uuid
366 self._update_persistent_data(persistence, uuid, info)
367 self.succeed(persistence)
368 return uuid
369
370
371 class WanLinkDelete(DeleteAction):
372 def succeed(self, persistence):
373 try:
374 persistence.update_wan_link(self.item_id, {'status': 'DELETED'})
375 except NoRecordFound:
376 self.logger.debug('%s(%s) record already deleted',
377 self.item, self.item_id)
378
379 return super(WanLinkDelete, self).succeed(persistence)
380
381 def get_wan_link(self, persistence):
382 """Retrieve information about the wan_link
383
384 It might be cached, or arrive from the database
385 """
386 if self.extra.get('wan_link'):
387 # First try a cached version of the data
388 return self.extra['wan_link']
389
390 return persistence.get_by_uuid(
391 'instance_wim_nets', self.item_id)
392
393 def process(self, connector, persistence, ovim):
394 """Delete a WAN link previously created"""
395 wan_link = self.get_wan_link(persistence)
396 if 'ERROR' in (wan_link.get('status') or ''):
397 return self.fail(
398 persistence,
399 'Impossible to delete WAN connectivity, '
400 'it was never successfully established:'
401 '\n\t{}'.format(wan_link['error_msg']))
402
403 internal_id = wan_link.get('wim_internal_id') or self.internal_id
404
405 if not internal_id:
406 self.logger.debug('No wim_internal_id found in\n%s\n%s\n'
407 'Assuming no network was created yet, '
408 'so no network have to be deleted.',
409 json.dumps(wan_link, indent=4),
410 json.dumps(self.as_dict(), indent=4))
411 return self.succeed(persistence)
412
413 try:
414 id = self.wim_internal_id
415 conn_info = safe_get(wan_link, 'wim_info.conn_info')
416 self.logger.debug('Connection Service %s (wan_link: %s):\n%s\n',
417 id, wan_link['uuid'],
418 json.dumps(conn_info, indent=4))
419 result = connector.delete_connectivity_service(id, conn_info)
420 except (WimConnectorError, InconsistentState) as ex:
421 self.logger.exception(ex)
422 return self.fail(
423 persistence,
424 'Impossible to delete WAN connectivity.\n\t{}'.format(ex))
425
426 self.logger.debug('WAN connectivity removed %s', result)
427 self.succeed(persistence)
428
429 return result
430
431
432 class WanLinkFind(RefreshMixin, FindAction):
433 pass
434
435
436 ACTIONS = {
437 'CREATE': WanLinkCreate,
438 'DELETE': WanLinkDelete,
439 'FIND': WanLinkFind,
440 }