Implement feature 5949
[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 time import time
37
38 from ..utils import filter_dict_keys as filter_keys
39 from ..utils import merge_dicts, remove_none_items, safe_get, truncate
40 from .actions import CreateAction, DeleteAction, FindAction
41 from .errors import (
42 InconsistentState,
43 MultipleRecordsFound,
44 NoRecordFound,
45 )
46 from wimconn import WimConnectorError
47
48 INSTANCE_NET_STATUS_ERROR = ('DOWN', 'ERROR', 'VIM_ERROR',
49 'DELETED', 'SCHEDULED_DELETION')
50 INSTANCE_NET_STATUS_PENDING = ('BUILD', 'INACTIVE', 'SCHEDULED_CREATION')
51 INSTANCE_VM_STATUS_ERROR = ('ERROR', 'VIM_ERROR',
52 'DELETED', 'SCHEDULED_DELETION')
53
54
55 class RefreshMixin(object):
56 def refresh(self, connector, persistence):
57 """Ask the external WAN Infrastructure Manager system for updates on
58 the status of the task.
59
60 Arguments:
61 connector: object with API for accessing the WAN
62 Infrastructure Manager system
63 persistence: abstraction layer for the database
64 """
65 fields = ('wim_status', 'wim_info', 'error_msg')
66 result = dict.fromkeys(fields)
67
68 try:
69 result.update(
70 connector
71 .get_connectivity_service_status(self.wim_internal_id))
72 except WimConnectorError as ex:
73 self.logger.exception(ex)
74 result.update(wim_status='WIM_ERROR', error_msg=truncate(ex))
75
76 result = filter_keys(result, fields)
77
78 action_changes = remove_none_items({
79 'extra': merge_dicts(self.extra, result),
80 'status': 'BUILD' if result['wim_status'] == 'BUILD' else None,
81 'error_msg': result['error_msg'],
82 'modified_at': time()})
83 link_changes = merge_dicts(result, status=result.pop('wim_status'))
84 # ^ Rename field: wim_status => status
85
86 persistence.update_wan_link(self.item_id,
87 remove_none_items(link_changes))
88
89 self.save(persistence, **action_changes)
90
91 return result
92
93
94 class WanLinkCreate(RefreshMixin, CreateAction):
95 def fail(self, persistence, reason, status='FAILED'):
96 changes = {'status': 'ERROR', 'error_msg': truncate(reason)}
97 persistence.update_wan_link(self.item_id, changes)
98 return super(WanLinkCreate, self).fail(persistence, reason, status)
99
100 def process(self, connector, persistence, ovim):
101 """Process the current task.
102 First we check if all the dependencies are ready,
103 then we call ``execute`` to actually execute the action.
104
105 Arguments:
106 connector: object with API for accessing the WAN
107 Infrastructure Manager system
108 persistence: abstraction layer for the database
109 ovim: instance of openvim, abstraction layer that enable
110 SDN-related operations
111 """
112 wan_link = persistence.get_by_uuid('instance_wim_nets', self.item_id)
113
114 # First we check if all the dependencies are solved
115 instance_nets = persistence.get_instance_nets(
116 wan_link['instance_scenario_id'], wan_link['sce_net_id'])
117
118 try:
119 dependency_statuses = [n['status'] for n in instance_nets]
120 except KeyError:
121 self.logger.debug('`status` not found in\n\n%s\n\n',
122 json.dumps(instance_nets, indent=4))
123 errored = [instance_nets[i]
124 for i, status in enumerate(dependency_statuses)
125 if status in INSTANCE_NET_STATUS_ERROR]
126 if errored:
127 return self.fail(
128 persistence,
129 'Impossible to stablish WAN connectivity due to an issue '
130 'with the local networks:\n\t' +
131 '\n\t'.join('{uuid}: {status}'.format(**n) for n in errored))
132
133 pending = [instance_nets[i]
134 for i, status in enumerate(dependency_statuses)
135 if status in INSTANCE_NET_STATUS_PENDING]
136 if pending:
137 return self.defer(
138 persistence,
139 'Still waiting for the local networks to be active:\n\t' +
140 '\n\t'.join('{uuid}: {status}'.format(**n) for n in pending))
141
142 return self.execute(connector, persistence, ovim, instance_nets)
143
144 def get_endpoint(self, persistence, ovim, instance_net):
145 """Retrieve the endpoint (information about the connection PoP <> WAN
146 """
147 wim_account = persistence.get_wim_account_by(uuid=self.wim_account_id)
148
149 # TODO: make more generic to support networks that are not created with
150 # the SDN assist. This method should have a consistent way of getting
151 # the endpoint for all different types of networks used in the VIM
152 # (provider networks, SDN assist, overlay networks, ...)
153 if instance_net.get('sdn_net_id'):
154 return self.get_endpoint_sdn(
155 persistence, ovim, instance_net, wim_account['wim_id'])
156 else:
157 raise InconsistentState(
158 'The field `instance_nets.sdn_net_id` was expected to be '
159 'found in the database for the record %s after the network '
160 'become active, but it is still NULL', instance_net['uuid'])
161
162 def get_endpoint_sdn(self, persistence, ovim, instance_net, wim_id):
163 criteria = {'net_id': instance_net['sdn_net_id']}
164 local_port_mapping = ovim.get_ports(filter=criteria)
165
166 if len(local_port_mapping) > 1:
167 raise MultipleRecordsFound(criteria, 'ovim.ports')
168 local_port_mapping = local_port_mapping[0]
169
170 criteria = {
171 'wim_id': wim_id,
172 'pop_switch_dpid': local_port_mapping['switch_dpid'],
173 'pop_switch_port': local_port_mapping['switch_port'],
174 'datacenter_id': instance_net['datacenter_id']}
175
176 wan_port_mapping = persistence.query_one(
177 FROM='wim_port_mappings',
178 WHERE=criteria)
179
180 if local_port_mapping.get('vlan'):
181 wan_port_mapping['wan_service_mapping_info']['vlan'] = (
182 local_port_mapping['vlan'])
183
184 return wan_port_mapping
185
186 @staticmethod
187 def _derive_connection_point(endpoint):
188 point = {'service_endpoint_id': endpoint['wan_service_endpoint_id']}
189 # TODO: Cover other scenarios, e.g. VXLAN.
190 info = endpoint.get('wan_service_mapping_info', {})
191 if 'vlan' in info:
192 point['service_endpoint_encapsulation_type'] = 'dot1q'
193 point['service_endpoint_encapsulation_info'] = {
194 'vlan': info['vlan']
195 }
196 else:
197 point['service_endpoint_encapsulation_type'] = 'none'
198 return point
199
200 @staticmethod
201 def _derive_service_type(connection_points):
202 # TODO: add multipoint and L3 connectivity.
203 if len(connection_points) == 2:
204 return 'ELINE'
205 else:
206 raise NotImplementedError('Multipoint connectivity is not '
207 'supported yet.')
208
209 def _update_persistent_data(self, persistence, service_uuid,
210 endpoints, conn_info):
211 """Store plugin/connector specific information in the database"""
212 persistence.update_wan_link(self.item_id, {
213 'wim_internal_id': service_uuid,
214 'wim_info': {'conn_info': conn_info},
215 'status': 'BUILD'})
216
217 def execute(self, connector, persistence, ovim, instance_nets):
218 """Actually execute the action, since now we are sure all the
219 dependencies are solved
220 """
221 try:
222 endpoints = [self.get_endpoint(persistence, ovim, net)
223 for net in instance_nets]
224 connection_points = [self._derive_connection_point(e)
225 for e in endpoints]
226
227 uuid, info = connector.create_connectivity_service(
228 self._derive_service_type(connection_points),
229 connection_points
230 # TODO: other properties, e.g. bandwidth
231 )
232 except (WimConnectorError, InconsistentState) as ex:
233 self.logger.exception(ex)
234 return self.fail(
235 persistence,
236 'Impossible to stablish WAN connectivity.\n\t{}'.format(ex))
237
238 self.logger.debug('WAN connectivity established %s\n%s\n',
239 uuid, json.dumps(info, indent=4))
240 self.wim_internal_id = uuid
241 self._update_persistent_data(persistence, uuid, endpoints, info)
242 self.succeed(persistence)
243 return uuid
244
245
246 class WanLinkDelete(DeleteAction):
247 def succeed(self, persistence):
248 try:
249 persistence.update_wan_link(self.item_id, {'status': 'DELETED'})
250 except NoRecordFound:
251 self.logger.debug('%s(%s) record already deleted',
252 self.item, self.item_id)
253
254 return super(WanLinkDelete, self).succeed(persistence)
255
256 def get_wan_link(self, persistence):
257 """Retrieve information about the wan_link
258
259 It might be cached, or arrive from the database
260 """
261 if self.extra.get('wan_link'):
262 # First try a cached version of the data
263 return self.extra['wan_link']
264
265 return persistence.get_by_uuid(
266 'instance_wim_nets', self.item_id)
267
268 def process(self, connector, persistence, ovim):
269 """Delete a WAN link previously created"""
270 wan_link = self.get_wan_link(persistence)
271 if 'ERROR' in (wan_link.get('status') or ''):
272 return self.fail(
273 persistence,
274 'Impossible to delete WAN connectivity, '
275 'it was never successfully established:'
276 '\n\t{}'.format(wan_link['error_msg']))
277
278 internal_id = wan_link.get('wim_internal_id') or self.internal_id
279
280 if not internal_id:
281 self.logger.debug('No wim_internal_id found in\n%s\n%s\n'
282 'Assuming no network was created yet, '
283 'so no network have to be deleted.',
284 json.dumps(wan_link, indent=4),
285 json.dumps(self.as_dict(), indent=4))
286 return self.succeed(persistence)
287
288 try:
289 id = self.wim_internal_id
290 conn_info = safe_get(wan_link, 'wim_info.conn_info')
291 self.logger.debug('Connection Service %s (wan_link: %s):\n%s\n',
292 id, wan_link['uuid'],
293 json.dumps(conn_info, indent=4))
294 result = connector.delete_connectivity_service(id, conn_info)
295 except (WimConnectorError, InconsistentState) as ex:
296 self.logger.exception(ex)
297 return self.fail(
298 persistence,
299 'Impossible to delete WAN connectivity.\n\t{}'.format(ex))
300
301 self.logger.debug('WAN connectivity removed %s', result)
302 self.succeed(persistence)
303
304 return result
305
306
307 class WanLinkFind(RefreshMixin, FindAction):
308 pass
309
310
311 ACTIONS = {
312 'CREATE': WanLinkCreate,
313 'DELETE': WanLinkDelete,
314 'FIND': WanLinkFind,
315 }