blob: 075b1a84494cdd664a7ff50405f65560c9c8bb53 [file] [log] [blame]
fantom36068fd2019-11-29 14:18:50 +00001#
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
28import json
29import logging
fantom36068fd2019-11-29 14:18:50 +000030import struct
sousaedu80135b92021-02-17 15:05:18 +010031
tierno72774862020-05-04 11:44:15 +000032from osm_ro_plugin.sdnconn import SdnConnectorBase, SdnConnectorError
sousaedu049cbb12022-01-05 11:39:35 +000033import paramiko
34import requests
fantom36068fd2019-11-29 14:18:50 +000035
36
sousaedu80135b92021-02-17 15:05:18 +010037class DpbSshInterface:
garciadeblasdfad9cd2021-05-14 17:22:01 +020038 """Communicate with the DPB via SSH"""
fantom36068fd2019-11-29 14:18:50 +000039
40 __LOGGER_NAME_EXT = ".ssh"
41 __FUNCTION_MAP_POS = 1
42
sousaedu80135b92021-02-17 15:05:18 +010043 def __init__(
44 self, username, password, wim_url, wim_port, network, auth_data, logger_name
45 ):
fantom36068fd2019-11-29 14:18:50 +000046 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:
sousaedu80135b92021-02-17 15:05:18 +010067 - session_id need only be unique per ssh session, thus is currently safe if
fantom36068fd2019-11-29 14:18:50 +000068 ro is restarted
69 """
70 self._check_connection()
sousaedu80135b92021-02-17 15:05:18 +010071
tierno1ec592d2020-06-16 15:29:47 +000072 if data is None:
fantom36068fd2019-11-29 14:18:50 +000073 data = {}
sousaedu80135b92021-02-17 15:05:18 +010074
75 url_ext_info = url_params.split("/")
76
fantom36068fd2019-11-29 14:18:50 +000077 for i in range(0, len(url_ext_info)):
78 if url_ext_info[i] == "service":
sousaedu80135b92021-02-17 15:05:18 +010079 data["service-id"] = int(url_ext_info[i + 1])
80
fantom36068fd2019-11-29 14:18:50 +000081 data["type"] = function[self.__FUNCTION_MAP_POS]
82 data = {
83 "session": self.__session_id,
sousaedu80135b92021-02-17 15:05:18 +010084 "content": data,
fantom36068fd2019-11-29 14:18:50 +000085 }
86 self.__session_id += 1
87
88 try:
89 data = json.dumps(data).encode("utf-8")
sousaedu80135b92021-02-17 15:05:18 +010090 data_packed = struct.pack(">I" + str(len(data)) + "s", len(data), data)
fantom36068fd2019-11-29 14:18:50 +000091 self.__stdin.write(data_packed)
92 self.logger.debug("Data sent to DPB via SSH")
93 except Exception as e:
sousaedu80135b92021-02-17 15:05:18 +010094 raise SdnConnectorError("Failed to write via SSH | text: {}".format(e), 500)
fantom36068fd2019-11-29 14:18:50 +000095
96 try:
97 data_len = struct.unpack(">I", self.__stdout.read(4))[0]
sousaedu80135b92021-02-17 15:05:18 +010098 data = struct.unpack(str(data_len) + "s", self.__stdout.read(data_len))[0]
99
fantom36068fd2019-11-29 14:18:50 +0000100 return json.loads(data).get("content", {})
101 except Exception as e:
102 raise SdnConnectorError(
sousaedu80135b92021-02-17 15:05:18 +0100103 "Could not get response from WIM | text: {}".format(e), 500
104 )
fantom36068fd2019-11-29 14:18:50 +0000105
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())
sousaedu80135b92021-02-17 15:05:18 +0100112
fantom36068fd2019-11-29 14:18:50 +0000113 return ssh_client
114
115 def __connect(self):
116 private_key = None
117 password = None
sousaedu80135b92021-02-17 15:05:18 +0100118
fantom36068fd2019-11-29 14:18:50 +0000119 if self.__auth_data.get("auth_type", "PASS") == "KEY":
120 private_key = self.__build_private_key_obj()
sousaedu80135b92021-02-17 15:05:18 +0100121
fantom36068fd2019-11-29 14:18:50 +0000122 if self.__auth_data.get("auth_type", "PASS") == "PASS":
123 password = self.__password
124
125 try:
sousaedu80135b92021-02-17 15:05:18 +0100126 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 )
fantom36068fd2019-11-29 14:18:50 +0000135 stdin, stdout, stderr = self.__ssh_client.exec_command(
sousaedu80135b92021-02-17 15:05:18 +0100136 command=self.__network
137 )
fantom36068fd2019-11-29 14:18:50 +0000138 except paramiko.BadHostKeyException as e:
139 raise SdnConnectorError(
sousaedu80135b92021-02-17 15:05:18 +0100140 "Could not add SSH host key | text: {}".format(e), 500
141 )
fantom36068fd2019-11-29 14:18:50 +0000142 except paramiko.AuthenticationException as e:
143 raise SdnConnectorError(
sousaedu80135b92021-02-17 15:05:18 +0100144 "Could not authorize SSH connection | text: {}".format(e), 400
145 )
fantom36068fd2019-11-29 14:18:50 +0000146 except paramiko.SSHException as e:
147 raise SdnConnectorError(
sousaedu80135b92021-02-17 15:05:18 +0100148 "Could not establish the SSH connection | text: {}".format(e), 500
149 )
fantom36068fd2019-11-29 14:18:50 +0000150 except Exception as e:
151 raise SdnConnectorError(
sousaedu80135b92021-02-17 15:05:18 +0100152 "Unknown error occurred when connecting via SSH | text: {}".format(e),
153 500,
154 )
fantom36068fd2019-11-29 14:18:50 +0000155
156 try:
157 data_len = struct.unpack(">I", stdout.read(4))[0]
sousaedu80135b92021-02-17 15:05:18 +0100158 data = json.loads(
159 struct.unpack(str(data_len) + "s", stdout.read(data_len))[0]
160 )
fantom36068fd2019-11-29 14:18:50 +0000161 except Exception as e:
162 raise SdnConnectorError(
sousaedu80135b92021-02-17 15:05:18 +0100163 "Failed to get response from DPB | text: {}".format(e), 500
164 )
165
fantom36068fd2019-11-29 14:18:50 +0000166 if "error" in data:
sousaedu80135b92021-02-17 15:05:18 +0100167 raise SdnConnectorError(data.get("msg", data.get("error", "ERROR")), 500)
168
fantom36068fd2019-11-29 14:18:50 +0000169 self.logger.info("SSH connection to DPB established OK")
sousaedu80135b92021-02-17 15:05:18 +0100170
fantom36068fd2019-11-29 14:18:50 +0000171 return stdin, stdout
172
173 def __build_private_key_obj(self):
174 try:
sousaedu80135b92021-02-17 15:05:18 +0100175 with open(self.__auth_data.get("key_file"), "r") as key_file:
fantom36068fd2019-11-29 14:18:50 +0000176 if self.__auth_data.get("key_type") == "RSA":
sousaedu80135b92021-02-17 15:05:18 +0100177 return paramiko.RSAKey.from_private_key(
178 key_file, password=self.__auth_data.get("key_pass", None)
179 )
fantom36068fd2019-11-29 14:18:50 +0000180 elif self.__auth_data.get("key_type") == "ECDSA":
sousaedu80135b92021-02-17 15:05:18 +0100181 return paramiko.ECDSAKey.from_private_key(
182 key_file, password=self.__auth_data.get("key_pass", None)
183 )
fantom36068fd2019-11-29 14:18:50 +0000184 else:
185 raise SdnConnectorError("Key type not supported", 400)
186 except Exception as e:
187 raise SdnConnectorError(
sousaedu80135b92021-02-17 15:05:18 +0100188 "Could not load private SSH key | text: {}".format(e), 500
189 )
fantom36068fd2019-11-29 14:18:50 +0000190
191
sousaedu80135b92021-02-17 15:05:18 +0100192class DpbRestInterface:
garciadeblasdfad9cd2021-05-14 17:22:01 +0200193 """Communicate with the DPB via the REST API"""
fantom36068fd2019-11-29 14:18:50 +0000194
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(
sousaedu80135b92021-02-17 15:05:18 +0100201 wim_url, str(wim_port), network
202 )
fantom36068fd2019-11-29 14:18:50 +0000203 self.logger.info("REST defined OK")
204
205 def post(self, function, url_params="", data=None, get_response=True):
sousaedu80135b92021-02-17 15:05:18 +0100206 url = self.__base_url + url_params + "/" + function[self.__FUNCTION_MAP_POS]
207
fantom36068fd2019-11-29 14:18:50 +0000208 try:
209 self.logger.info(data)
210 response = requests.post(url, json=data)
sousaedu80135b92021-02-17 15:05:18 +0100211
fantom36068fd2019-11-29 14:18:50 +0000212 if response.status_code != 200:
213 raise SdnConnectorError(
sousaedu80135b92021-02-17 15:05:18 +0100214 "REST request failed (status code: {})".format(response.status_code)
215 )
216
fantom36068fd2019-11-29 14:18:50 +0000217 if get_response:
218 return response.json()
219 except Exception as e:
sousaedu80135b92021-02-17 15:05:18 +0100220 raise SdnConnectorError("REST request failed | text: {}".format(e), 500)
fantom36068fd2019-11-29 14:18:50 +0000221
222 def get(self, function, url_params=""):
223 url = self.__base_url + url_params + function[self.__FUNCTION_MAP_POS]
sousaedu80135b92021-02-17 15:05:18 +0100224
fantom36068fd2019-11-29 14:18:50 +0000225 try:
226 return requests.get(url)
227 except Exception as e:
sousaedu80135b92021-02-17 15:05:18 +0100228 raise SdnConnectorError("REST request failed | text: {}".format(e), 500)
fantom36068fd2019-11-29 14:18:50 +0000229
230
231class DpbConnector(SdnConnectorBase):
garciadeblasdfad9cd2021-05-14 17:22:01 +0200232 """Use the DPB to establish multipoint connections"""
fantom36068fd2019-11-29 14:18:50 +0000233
sousaedue493e9b2021-02-09 15:30:01 +0100234 __LOGGER_NAME = "ro.sdn.dpb"
fantom36068fd2019-11-29 14:18:50 +0000235 __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"]
sousaedu80135b92021-02-17 15:05:18 +0100239 __STATUS_MAP = {"ACTIVE": "ACTIVE", "ACTIVATING": "BUILD", "FAILED": "ERROR"}
fantom36068fd2019-11-29 14:18:50 +0000240 __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"),
sousaedu80135b92021-02-17 15:05:18 +0100248 "RESET": ("reset", "NOT IMPLEMENTED"),
fantom36068fd2019-11-29 14:18:50 +0000249 }
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", "")
sousaedu80135b92021-02-17 15:05:18 +0100263 self.__connection_type = self.__cli_config.get("connection_type", "REST")
fantom36068fd2019-11-29 14:18:50 +0000264 self.__port = self.__cli_config.get(
sousaedu80135b92021-02-17 15:05:18 +0100265 "port", (80 if self.__connection_type == "REST" else 22)
266 )
fantom36068fd2019-11-29 14:18:50 +0000267 self.__ssh_auth = self.__cli_config.get("ssh_auth", None)
268
269 if self.__connection_type == "SSH":
sousaedu80135b92021-02-17 15:05:18 +0100270 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 )
fantom36068fd2019-11-29 14:18:50 +0000279 elif self.__connection_type == "REST":
sousaedu80135b92021-02-17 15:05:18 +0100280 interface = DpbRestInterface(
281 self.__url, self.__port, self.__network, self.__LOGGER_NAME
282 )
fantom36068fd2019-11-29 14:18:50 +0000283 else:
284 raise SdnConnectorError(
sousaedu80135b92021-02-17 15:05:18 +0100285 "Connection type not supported (must be SSH or REST)", 400
286 )
287
fantom36068fd2019-11-29 14:18:50 +0000288 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")
sousaedu80135b92021-02-17 15:05:18 +0100294
fantom36068fd2019-11-29 14:18:50 +0000295 try:
296 response = self.__post(self.__ACTIONS_MAP.get("CREATE"))
sousaedu80135b92021-02-17 15:05:18 +0100297
fantom36068fd2019-11-29 14:18:50 +0000298 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(
sousaedu80135b92021-02-17 15:05:18 +0100303 "Invalid create service response (could be an issue with the DPB)",
304 500,
305 )
306
fantom36068fd2019-11-29 14:18:50 +0000307 data = {"segment": []}
sousaedu80135b92021-02-17 15:05:18 +0100308
fantom36068fd2019-11-29 14:18:50 +0000309 for point in connection_points:
sousaedu80135b92021-02-17 15:05:18 +0100310 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 )
fantom36068fd2019-11-29 14:18:50 +0000322 # "ingress-bw": (bandwidth.get(point.get("service_endpoint_id"))).get("ingress"),
323 # "egress-bw": (bandwidth.get(point.get("service_endpoint_id"))).get("egress")}
fantom36068fd2019-11-29 14:18:50 +0000324
sousaedu80135b92021-02-17 15:05:18 +0100325 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
fantom36068fd2019-11-29 14:18:50 +0000338 return (str(service_id), None)
339 except Exception as e:
340 raise SdnConnectorError(
sousaedu80135b92021-02-17 15:05:18 +0100341 "Connectivity service could not be made | text: {}".format(e), 500
342 )
fantom36068fd2019-11-29 14:18:50 +0000343
344 def get_connectivity_service_status(self, service_uuid, conn_info=None):
345 self.logger.info(
sousaedu80135b92021-02-17 15:05:18 +0100346 "Checking connectivity service status id:{}".format(service_uuid)
347 )
348 data = {"timeout-millis": 10000, "acceptable": ["ACTIVE", "FAILED"]}
349
fantom36068fd2019-11-29 14:18:50 +0000350 try:
sousaedu80135b92021-02-17 15:05:18 +0100351 response = self.__post(
352 self.__ACTIONS_MAP.get("CHECK"),
353 "/service/" + service_uuid,
354 data,
355 )
356
fantom36068fd2019-11-29 14:18:50 +0000357 if "status" in response:
358 status = response.get("status", None)
359 self.logger.info("CHECKED CONNECTIVITY SERVICE STATUS")
sousaedu80135b92021-02-17 15:05:18 +0100360
fantom36068fd2019-11-29 14:18:50 +0000361 return {"wim_status": self.__STATUS_MAP.get(status)}
362 else:
363 raise SdnConnectorError(
sousaedu80135b92021-02-17 15:05:18 +0100364 "Invalid status check response (could be an issue with the DPB)",
365 500,
366 )
fantom36068fd2019-11-29 14:18:50 +0000367 except Exception as e:
368 raise SdnConnectorError(
sousaedu80135b92021-02-17 15:05:18 +0100369 "Failed to check service status | text: {}".format(e), 500
370 )
fantom36068fd2019-11-29 14:18:50 +0000371
372 def delete_connectivity_service(self, service_uuid, conn_info=None):
sousaedu80135b92021-02-17 15:05:18 +0100373 self.logger.info("Deleting connectivity service id: {}".format(service_uuid))
374
fantom36068fd2019-11-29 14:18:50 +0000375 try:
sousaedu80135b92021-02-17 15:05:18 +0100376 self.__post(
377 self.__ACTIONS_MAP.get("RELEASE"),
378 "/service/" + service_uuid,
379 get_response=False,
380 )
tierno1ec592d2020-06-16 15:29:47 +0000381 except Exception as e:
fantom36068fd2019-11-29 14:18:50 +0000382 raise SdnConnectorError(
sousaedu80135b92021-02-17 15:05:18 +0100383 "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
fantom36068fd2019-11-29 14:18:50 +0000391 return None
392
sousaedu80135b92021-02-17 15:05:18 +0100393 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
fantom36068fd2019-11-29 14:18:50 +0000399 try:
sousaedu80135b92021-02-17 15:05:18 +0100400 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
fantom36068fd2019-11-29 14:18:50 +0000411 if "status" in response:
sousaedu80135b92021-02-17 15:05:18 +0100412 self.logger.debug("Connectivity service {} reset".format(service_uuid))
fantom36068fd2019-11-29 14:18:50 +0000413 else:
414 raise SdnConnectorError(
sousaedu80135b92021-02-17 15:05:18 +0100415 "Invalid status check response (could be an issue with the DPB)",
416 500,
417 )
fantom36068fd2019-11-29 14:18:50 +0000418 except Exception as e:
sousaedu80135b92021-02-17 15:05:18 +0100419 raise SdnConnectorError("Failed to reset service | text: {}".format(e), 500)
420
fantom36068fd2019-11-29 14:18:50 +0000421 try:
422 data = {"segment": []}
sousaedu80135b92021-02-17 15:05:18 +0100423
fantom36068fd2019-11-29 14:18:50 +0000424 for point in connection_points:
sousaedu80135b92021-02-17 15:05:18 +0100425 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 )
fantom36068fd2019-11-29 14:18:50 +0000437 # "ingress-bw": (bandwidth.get(point.get("service_endpoint_id"))).get("ingress"),
438 # "egress-bw": (bandwidth.get(point.get("service_endpoint_id"))).get("egress")}
sousaedu80135b92021-02-17 15:05:18 +0100439
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 )
fantom36068fd2019-11-29 14:18:50 +0000451 except Exception as e:
452 raise SdnConnectorError(
sousaedu80135b92021-02-17 15:05:18 +0100453 "Failed to edit connectivity service | text: {}".format(e), 500
454 )
455
456 self.logger.debug("Edited connectivity service {}".format(service_uuid))
457
fantom36068fd2019-11-29 14:18:50 +0000458 return conn_info
459
460 def __check_service(self, serv_type, points, kwargs):
tierno1ec592d2020-06-16 15:29:47 +0000461 if serv_type not in self.__SUPPORTED_SERV_TYPES:
fantom36068fd2019-11-29 14:18:50 +0000462 raise SdnConnectorError("Service type no supported", 400)
463 # Future: BW Checks here