Disable the check of the release notes
[osm/RO.git] / RO-SDN-dpb / osm_rosdn_dpb / wimconn_dpb.py
1 #
2 # Copyright 2020 University of Lancaster - High Performance Networks Research
3 # Group
4 # All Rights Reserved.
5 #
6 # Contributors: Will Fantom, Paul McCherry
7 #
8 # Licensed under the Apache License, Version 2.0 (the "License"); you may
9 # not use this file except in compliance with the License. You may obtain
10 # a copy of the License at
11 #
12 # http://www.apache.org/licenses/LICENSE-2.0
13 #
14 # Unless required by applicable law or agreed to in writing, software
15 # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
16 # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
17 # License for the specific language governing permissions and limitations
18 # under the License.
19 #
20 # products derived from this software without specific prior written permission.
21 #
22 # This work has been performed in the context of DCMS UK 5G Testbeds
23 # & Trials Programme and in the framework of the Metro-Haul project -
24 # funded by the European Commission under Grant number 761727 through the
25 # Horizon 2020 and 5G-PPP programmes.
26 ##
27
28 import json
29 import logging
30 import struct
31
32 from osm_ro_plugin.sdnconn import SdnConnectorBase, SdnConnectorError
33 import paramiko
34 import requests
35
36
37 class DpbSshInterface:
38 """Communicate with the DPB via SSH"""
39
40 __LOGGER_NAME_EXT = ".ssh"
41 __FUNCTION_MAP_POS = 1
42
43 def __init__(
44 self, username, password, wim_url, wim_port, network, auth_data, logger_name
45 ):
46 self.logger = logging.getLogger(logger_name + self.__LOGGER_NAME_EXT)
47 self.__username = username
48 self.__password = password
49 self.__url = wim_url
50 self.__port = wim_port
51 self.__network = network
52 self.__auth_data = auth_data
53 self.__session_id = 1
54 self.__ssh_client = self.__create_client()
55 self.__stdin = None
56 self.__stdout = None
57 self.logger.info("SSH connection to DPB defined")
58
59 def _check_connection(self):
60 if not (self.__stdin and self.__stdout):
61 self.__stdin, self.__stdout = self.__connect()
62
63 def post(self, function, url_params="", data=None, get_response=True):
64 """post request to dpb via ssh
65
66 notes:
67 - session_id need only be unique per ssh session, thus is currently safe if
68 ro is restarted
69 """
70 self._check_connection()
71
72 if data is None:
73 data = {}
74
75 url_ext_info = url_params.split("/")
76
77 for i in range(0, len(url_ext_info)):
78 if url_ext_info[i] == "service":
79 data["service-id"] = int(url_ext_info[i + 1])
80
81 data["type"] = function[self.__FUNCTION_MAP_POS]
82 data = {
83 "session": self.__session_id,
84 "content": data,
85 }
86 self.__session_id += 1
87
88 try:
89 data = json.dumps(data).encode("utf-8")
90 data_packed = struct.pack(">I" + str(len(data)) + "s", len(data), data)
91 self.__stdin.write(data_packed)
92 self.logger.debug("Data sent to DPB via SSH")
93 except Exception as e:
94 raise SdnConnectorError("Failed to write via SSH | text: {}".format(e), 500)
95
96 try:
97 data_len = struct.unpack(">I", self.__stdout.read(4))[0]
98 data = struct.unpack(str(data_len) + "s", self.__stdout.read(data_len))[0]
99
100 return json.loads(data).get("content", {})
101 except Exception as e:
102 raise SdnConnectorError(
103 "Could not get response from WIM | text: {}".format(e), 500
104 )
105
106 def get(self, function, url_params=""):
107 raise SdnConnectorError("SSH Get not implemented", 500)
108
109 def __create_client(self):
110 ssh_client = paramiko.SSHClient()
111 ssh_client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
112
113 return ssh_client
114
115 def __connect(self):
116 private_key = None
117 password = None
118
119 if self.__auth_data.get("auth_type", "PASS") == "KEY":
120 private_key = self.__build_private_key_obj()
121
122 if self.__auth_data.get("auth_type", "PASS") == "PASS":
123 password = self.__password
124
125 try:
126 self.__ssh_client.connect(
127 hostname=self.__url,
128 port=self.__port,
129 username=self.__username,
130 password=password,
131 pkey=private_key,
132 look_for_keys=False,
133 compress=False,
134 )
135 stdin, stdout, stderr = self.__ssh_client.exec_command(
136 command=self.__network
137 )
138 except paramiko.BadHostKeyException as e:
139 raise SdnConnectorError(
140 "Could not add SSH host key | text: {}".format(e), 500
141 )
142 except paramiko.AuthenticationException as e:
143 raise SdnConnectorError(
144 "Could not authorize SSH connection | text: {}".format(e), 400
145 )
146 except paramiko.SSHException as e:
147 raise SdnConnectorError(
148 "Could not establish the SSH connection | text: {}".format(e), 500
149 )
150 except Exception as e:
151 raise SdnConnectorError(
152 "Unknown error occurred when connecting via SSH | text: {}".format(e),
153 500,
154 )
155
156 try:
157 data_len = struct.unpack(">I", stdout.read(4))[0]
158 data = json.loads(
159 struct.unpack(str(data_len) + "s", stdout.read(data_len))[0]
160 )
161 except Exception as e:
162 raise SdnConnectorError(
163 "Failed to get response from DPB | text: {}".format(e), 500
164 )
165
166 if "error" in data:
167 raise SdnConnectorError(data.get("msg", data.get("error", "ERROR")), 500)
168
169 self.logger.info("SSH connection to DPB established OK")
170
171 return stdin, stdout
172
173 def __build_private_key_obj(self):
174 try:
175 with open(self.__auth_data.get("key_file"), "r") as key_file:
176 if self.__auth_data.get("key_type") == "RSA":
177 return paramiko.RSAKey.from_private_key(
178 key_file, password=self.__auth_data.get("key_pass", None)
179 )
180 elif self.__auth_data.get("key_type") == "ECDSA":
181 return paramiko.ECDSAKey.from_private_key(
182 key_file, password=self.__auth_data.get("key_pass", None)
183 )
184 else:
185 raise SdnConnectorError("Key type not supported", 400)
186 except Exception as e:
187 raise SdnConnectorError(
188 "Could not load private SSH key | text: {}".format(e), 500
189 )
190
191
192 class DpbRestInterface:
193 """Communicate with the DPB via the REST API"""
194
195 __LOGGER_NAME_EXT = ".rest"
196 __FUNCTION_MAP_POS = 0
197
198 def __init__(self, wim_url, wim_port, network, logger_name):
199 self.logger = logging.getLogger(logger_name + self.__LOGGER_NAME_EXT)
200 self.__base_url = "http://{}:{}/network/{}".format(
201 wim_url, str(wim_port), network
202 )
203 self.logger.info("REST defined OK")
204
205 def post(self, function, url_params="", data=None, get_response=True):
206 url = self.__base_url + url_params + "/" + function[self.__FUNCTION_MAP_POS]
207
208 try:
209 self.logger.info(data)
210 response = requests.post(url, json=data)
211
212 if response.status_code != 200:
213 raise SdnConnectorError(
214 "REST request failed (status code: {})".format(response.status_code)
215 )
216
217 if get_response:
218 return response.json()
219 except Exception as e:
220 raise SdnConnectorError("REST request failed | text: {}".format(e), 500)
221
222 def get(self, function, url_params=""):
223 url = self.__base_url + url_params + function[self.__FUNCTION_MAP_POS]
224
225 try:
226 return requests.get(url)
227 except Exception as e:
228 raise SdnConnectorError("REST request failed | text: {}".format(e), 500)
229
230
231 class DpbConnector(SdnConnectorBase):
232 """Use the DPB to establish multipoint connections"""
233
234 __LOGGER_NAME = "ro.sdn.dpb"
235 __SUPPORTED_SERV_TYPES = ["ELAN (L2)", "ELINE (L2)"]
236 __SUPPORTED_CONNECTION_TYPES = ["REST", "SSH"]
237 __SUPPORTED_SSH_AUTH_TYPES = ["KEY", "PASS"]
238 __SUPPORTED_SSH_KEY_TYPES = ["ECDSA", "RSA"]
239 __STATUS_MAP = {"ACTIVE": "ACTIVE", "ACTIVATING": "BUILD", "FAILED": "ERROR"}
240 __ACTIONS_MAP = {
241 "CREATE": ("create-service", "new-service"),
242 "DEFINE": ("define", "define-service"),
243 "ACTIVATE": ("activate", "activate-service"),
244 "RELEASE": ("release", "release-service"),
245 "DEACTIVATE": ("deactivate", "deactivate-service"),
246 "CHECK": ("await-status", "await-service-status"),
247 "GET": ("services", "NOT IMPLEMENTED"),
248 "RESET": ("reset", "NOT IMPLEMENTED"),
249 }
250
251 def __init__(self, wim, wim_account, config):
252 self.logger = logging.getLogger(self.__LOGGER_NAME)
253
254 self.__wim = wim
255 self.__account = wim_account
256 self.__config = config
257 self.__cli_config = self.__account.pop("config", None)
258
259 self.__url = self.__wim.get("wim_url", "")
260 self.__password = self.__account.get("passwd", "")
261 self.__username = self.__account.get("user", "")
262 self.__network = self.__cli_config.get("network", "")
263 self.__connection_type = self.__cli_config.get("connection_type", "REST")
264 self.__port = self.__cli_config.get(
265 "port", (80 if self.__connection_type == "REST" else 22)
266 )
267 self.__ssh_auth = self.__cli_config.get("ssh_auth", None)
268
269 if self.__connection_type == "SSH":
270 interface = DpbSshInterface(
271 self.__username,
272 self.__password,
273 self.__url,
274 self.__port,
275 self.__network,
276 self.__ssh_auth,
277 self.__LOGGER_NAME,
278 )
279 elif self.__connection_type == "REST":
280 interface = DpbRestInterface(
281 self.__url, self.__port, self.__network, self.__LOGGER_NAME
282 )
283 else:
284 raise SdnConnectorError(
285 "Connection type not supported (must be SSH or REST)", 400
286 )
287
288 self.__post = interface.post
289 self.__get = interface.get
290 self.logger.info("DPB WimConn Init OK")
291
292 def create_connectivity_service(self, service_type, connection_points, **kwargs):
293 self.logger.info("Creating a connectivity service")
294
295 try:
296 response = self.__post(self.__ACTIONS_MAP.get("CREATE"))
297
298 if "service-id" in response:
299 service_id = int(response.get("service-id"))
300 self.logger.debug("created service id {}".format(service_id))
301 else:
302 raise SdnConnectorError(
303 "Invalid create service response (could be an issue with the DPB)",
304 500,
305 )
306
307 data = {"segment": []}
308
309 for point in connection_points:
310 data["segment"].append(
311 {
312 "terminal-name": point.get("service_endpoint_id"),
313 "label": int(
314 (point.get("service_endpoint_encapsulation_info")).get(
315 "vlan"
316 )
317 ),
318 "ingress-bw": 10.0,
319 "egress-bw": 10.0,
320 }
321 )
322 # "ingress-bw": (bandwidth.get(point.get("service_endpoint_id"))).get("ingress"),
323 # "egress-bw": (bandwidth.get(point.get("service_endpoint_id"))).get("egress")}
324
325 self.__post(
326 self.__ACTIONS_MAP.get("DEFINE"),
327 "/service/" + str(service_id),
328 data,
329 get_response=False,
330 )
331 self.__post(
332 self.__ACTIONS_MAP.get("ACTIVATE"),
333 "/service/" + str(service_id),
334 get_response=False,
335 )
336 self.logger.debug("Created connectivity service id:{}".format(service_id))
337
338 return (str(service_id), None)
339 except Exception as e:
340 raise SdnConnectorError(
341 "Connectivity service could not be made | text: {}".format(e), 500
342 )
343
344 def get_connectivity_service_status(self, service_uuid, conn_info=None):
345 self.logger.info(
346 "Checking connectivity service status id:{}".format(service_uuid)
347 )
348 data = {"timeout-millis": 10000, "acceptable": ["ACTIVE", "FAILED"]}
349
350 try:
351 response = self.__post(
352 self.__ACTIONS_MAP.get("CHECK"),
353 "/service/" + service_uuid,
354 data,
355 )
356
357 if "status" in response:
358 status = response.get("status", None)
359 self.logger.info("CHECKED CONNECTIVITY SERVICE STATUS")
360
361 return {"wim_status": self.__STATUS_MAP.get(status)}
362 else:
363 raise SdnConnectorError(
364 "Invalid status check response (could be an issue with the DPB)",
365 500,
366 )
367 except Exception as e:
368 raise SdnConnectorError(
369 "Failed to check service status | text: {}".format(e), 500
370 )
371
372 def delete_connectivity_service(self, service_uuid, conn_info=None):
373 self.logger.info("Deleting connectivity service id: {}".format(service_uuid))
374
375 try:
376 self.__post(
377 self.__ACTIONS_MAP.get("RELEASE"),
378 "/service/" + service_uuid,
379 get_response=False,
380 )
381 except Exception as e:
382 raise SdnConnectorError(
383 "Could not delete service id:{} (could be an issue with the DPB): {}".format(
384 service_uuid, e
385 ),
386 500,
387 )
388
389 self.logger.debug("Deleted connectivity service id:{}".format(service_uuid))
390
391 return None
392
393 def edit_connectivity_service(
394 self, service_uuid, conn_info=None, connection_points=None, **kwargs
395 ):
396 self.logger.info("Editing connectivity service id: {}".format(service_uuid))
397 data = {"timeout-millis": 10000, "acceptable": ["DORMANT"]}
398
399 try:
400 self.__post(
401 self.__ACTIONS_MAP.get("RESET"),
402 "/service/" + service_uuid,
403 get_response=False,
404 )
405 response = self.__post(
406 self.__ACTIONS_MAP.get("CHECK"),
407 "/service/" + service_uuid,
408 data,
409 )
410
411 if "status" in response:
412 self.logger.debug("Connectivity service {} reset".format(service_uuid))
413 else:
414 raise SdnConnectorError(
415 "Invalid status check response (could be an issue with the DPB)",
416 500,
417 )
418 except Exception as e:
419 raise SdnConnectorError("Failed to reset service | text: {}".format(e), 500)
420
421 try:
422 data = {"segment": []}
423
424 for point in connection_points:
425 data["segment"].append(
426 {
427 "terminal-name": point.get("service_endpoint_id"),
428 "label": int(
429 (point.get("service_endpoint_encapsulation_info")).get(
430 "vlan"
431 )
432 ),
433 "ingress-bw": 10.0,
434 "egress-bw": 10.0,
435 }
436 )
437 # "ingress-bw": (bandwidth.get(point.get("service_endpoint_id"))).get("ingress"),
438 # "egress-bw": (bandwidth.get(point.get("service_endpoint_id"))).get("egress")}
439
440 self.__post(
441 self.__ACTIONS_MAP.get("DEFINE"),
442 "/service/" + str(service_uuid),
443 data,
444 get_response=False,
445 )
446 self.__post(
447 self.__ACTIONS_MAP.get("ACTIVATE"),
448 "/service/" + str(service_uuid),
449 get_response=False,
450 )
451 except Exception as e:
452 raise SdnConnectorError(
453 "Failed to edit connectivity service | text: {}".format(e), 500
454 )
455
456 self.logger.debug("Edited connectivity service {}".format(service_uuid))
457
458 return conn_info
459
460 def __check_service(self, serv_type, points, kwargs):
461 if serv_type not in self.__SUPPORTED_SERV_TYPES:
462 raise SdnConnectorError("Service type no supported", 400)
463 # Future: BW Checks here