blob: f79ef998c69b96a0d0eec1849ee9390df655356b [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()
garciadeblas60cb82f2024-09-17 18:27:24 +0200111 # Load known host keys
112 ssh_client.load_system_host_keys()
113 # Reject unknown hosts
114 ssh_client.set_missing_host_key_policy(paramiko.RejectPolicy())
sousaedu80135b92021-02-17 15:05:18 +0100115
fantom36068fd2019-11-29 14:18:50 +0000116 return ssh_client
117
118 def __connect(self):
119 private_key = None
120 password = None
sousaedu80135b92021-02-17 15:05:18 +0100121
fantom36068fd2019-11-29 14:18:50 +0000122 if self.__auth_data.get("auth_type", "PASS") == "KEY":
123 private_key = self.__build_private_key_obj()
sousaedu80135b92021-02-17 15:05:18 +0100124
fantom36068fd2019-11-29 14:18:50 +0000125 if self.__auth_data.get("auth_type", "PASS") == "PASS":
126 password = self.__password
127
128 try:
sousaedu80135b92021-02-17 15:05:18 +0100129 self.__ssh_client.connect(
130 hostname=self.__url,
131 port=self.__port,
132 username=self.__username,
133 password=password,
134 pkey=private_key,
135 look_for_keys=False,
136 compress=False,
137 )
garciadeblas60cb82f2024-09-17 18:27:24 +0200138 # TODO: sanitizing commands to be executed
139 # Whitelist of allowed commands
140 # valid_commands = ["command1", "command2", "command3"]
141 # if self.__network not in valid_commands:
142 # raise SdnConnectorError("Invalid command executed", 400)
fantom36068fd2019-11-29 14:18:50 +0000143 stdin, stdout, stderr = self.__ssh_client.exec_command(
sousaedu80135b92021-02-17 15:05:18 +0100144 command=self.__network
145 )
fantom36068fd2019-11-29 14:18:50 +0000146 except paramiko.BadHostKeyException as e:
147 raise SdnConnectorError(
sousaedu80135b92021-02-17 15:05:18 +0100148 "Could not add SSH host key | text: {}".format(e), 500
149 )
fantom36068fd2019-11-29 14:18:50 +0000150 except paramiko.AuthenticationException as e:
151 raise SdnConnectorError(
sousaedu80135b92021-02-17 15:05:18 +0100152 "Could not authorize SSH connection | text: {}".format(e), 400
153 )
fantom36068fd2019-11-29 14:18:50 +0000154 except paramiko.SSHException as e:
155 raise SdnConnectorError(
sousaedu80135b92021-02-17 15:05:18 +0100156 "Could not establish the SSH connection | text: {}".format(e), 500
157 )
fantom36068fd2019-11-29 14:18:50 +0000158 except Exception as e:
159 raise SdnConnectorError(
sousaedu80135b92021-02-17 15:05:18 +0100160 "Unknown error occurred when connecting via SSH | text: {}".format(e),
161 500,
162 )
fantom36068fd2019-11-29 14:18:50 +0000163
164 try:
165 data_len = struct.unpack(">I", stdout.read(4))[0]
sousaedu80135b92021-02-17 15:05:18 +0100166 data = json.loads(
167 struct.unpack(str(data_len) + "s", stdout.read(data_len))[0]
168 )
fantom36068fd2019-11-29 14:18:50 +0000169 except Exception as e:
170 raise SdnConnectorError(
sousaedu80135b92021-02-17 15:05:18 +0100171 "Failed to get response from DPB | text: {}".format(e), 500
172 )
173
fantom36068fd2019-11-29 14:18:50 +0000174 if "error" in data:
sousaedu80135b92021-02-17 15:05:18 +0100175 raise SdnConnectorError(data.get("msg", data.get("error", "ERROR")), 500)
176
fantom36068fd2019-11-29 14:18:50 +0000177 self.logger.info("SSH connection to DPB established OK")
sousaedu80135b92021-02-17 15:05:18 +0100178
fantom36068fd2019-11-29 14:18:50 +0000179 return stdin, stdout
180
181 def __build_private_key_obj(self):
182 try:
sousaedu80135b92021-02-17 15:05:18 +0100183 with open(self.__auth_data.get("key_file"), "r") as key_file:
fantom36068fd2019-11-29 14:18:50 +0000184 if self.__auth_data.get("key_type") == "RSA":
sousaedu80135b92021-02-17 15:05:18 +0100185 return paramiko.RSAKey.from_private_key(
186 key_file, password=self.__auth_data.get("key_pass", None)
187 )
fantom36068fd2019-11-29 14:18:50 +0000188 elif self.__auth_data.get("key_type") == "ECDSA":
sousaedu80135b92021-02-17 15:05:18 +0100189 return paramiko.ECDSAKey.from_private_key(
190 key_file, password=self.__auth_data.get("key_pass", None)
191 )
fantom36068fd2019-11-29 14:18:50 +0000192 else:
193 raise SdnConnectorError("Key type not supported", 400)
194 except Exception as e:
195 raise SdnConnectorError(
sousaedu80135b92021-02-17 15:05:18 +0100196 "Could not load private SSH key | text: {}".format(e), 500
197 )
fantom36068fd2019-11-29 14:18:50 +0000198
199
sousaedu80135b92021-02-17 15:05:18 +0100200class DpbRestInterface:
garciadeblasdfad9cd2021-05-14 17:22:01 +0200201 """Communicate with the DPB via the REST API"""
fantom36068fd2019-11-29 14:18:50 +0000202
203 __LOGGER_NAME_EXT = ".rest"
204 __FUNCTION_MAP_POS = 0
205
206 def __init__(self, wim_url, wim_port, network, logger_name):
207 self.logger = logging.getLogger(logger_name + self.__LOGGER_NAME_EXT)
208 self.__base_url = "http://{}:{}/network/{}".format(
sousaedu80135b92021-02-17 15:05:18 +0100209 wim_url, str(wim_port), network
210 )
fantom36068fd2019-11-29 14:18:50 +0000211 self.logger.info("REST defined OK")
212
213 def post(self, function, url_params="", data=None, get_response=True):
sousaedu80135b92021-02-17 15:05:18 +0100214 url = self.__base_url + url_params + "/" + function[self.__FUNCTION_MAP_POS]
215
fantom36068fd2019-11-29 14:18:50 +0000216 try:
217 self.logger.info(data)
218 response = requests.post(url, json=data)
sousaedu80135b92021-02-17 15:05:18 +0100219
fantom36068fd2019-11-29 14:18:50 +0000220 if response.status_code != 200:
221 raise SdnConnectorError(
sousaedu80135b92021-02-17 15:05:18 +0100222 "REST request failed (status code: {})".format(response.status_code)
223 )
224
fantom36068fd2019-11-29 14:18:50 +0000225 if get_response:
226 return response.json()
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 def get(self, function, url_params=""):
231 url = self.__base_url + url_params + function[self.__FUNCTION_MAP_POS]
sousaedu80135b92021-02-17 15:05:18 +0100232
fantom36068fd2019-11-29 14:18:50 +0000233 try:
234 return requests.get(url)
235 except Exception as e:
sousaedu80135b92021-02-17 15:05:18 +0100236 raise SdnConnectorError("REST request failed | text: {}".format(e), 500)
fantom36068fd2019-11-29 14:18:50 +0000237
238
239class DpbConnector(SdnConnectorBase):
garciadeblasdfad9cd2021-05-14 17:22:01 +0200240 """Use the DPB to establish multipoint connections"""
fantom36068fd2019-11-29 14:18:50 +0000241
sousaedue493e9b2021-02-09 15:30:01 +0100242 __LOGGER_NAME = "ro.sdn.dpb"
fantom36068fd2019-11-29 14:18:50 +0000243 __SUPPORTED_SERV_TYPES = ["ELAN (L2)", "ELINE (L2)"]
244 __SUPPORTED_CONNECTION_TYPES = ["REST", "SSH"]
245 __SUPPORTED_SSH_AUTH_TYPES = ["KEY", "PASS"]
246 __SUPPORTED_SSH_KEY_TYPES = ["ECDSA", "RSA"]
sousaedu80135b92021-02-17 15:05:18 +0100247 __STATUS_MAP = {"ACTIVE": "ACTIVE", "ACTIVATING": "BUILD", "FAILED": "ERROR"}
fantom36068fd2019-11-29 14:18:50 +0000248 __ACTIONS_MAP = {
249 "CREATE": ("create-service", "new-service"),
250 "DEFINE": ("define", "define-service"),
251 "ACTIVATE": ("activate", "activate-service"),
252 "RELEASE": ("release", "release-service"),
253 "DEACTIVATE": ("deactivate", "deactivate-service"),
254 "CHECK": ("await-status", "await-service-status"),
255 "GET": ("services", "NOT IMPLEMENTED"),
sousaedu80135b92021-02-17 15:05:18 +0100256 "RESET": ("reset", "NOT IMPLEMENTED"),
fantom36068fd2019-11-29 14:18:50 +0000257 }
258
259 def __init__(self, wim, wim_account, config):
260 self.logger = logging.getLogger(self.__LOGGER_NAME)
261
262 self.__wim = wim
263 self.__account = wim_account
264 self.__config = config
265 self.__cli_config = self.__account.pop("config", None)
266
267 self.__url = self.__wim.get("wim_url", "")
268 self.__password = self.__account.get("passwd", "")
269 self.__username = self.__account.get("user", "")
270 self.__network = self.__cli_config.get("network", "")
sousaedu80135b92021-02-17 15:05:18 +0100271 self.__connection_type = self.__cli_config.get("connection_type", "REST")
fantom36068fd2019-11-29 14:18:50 +0000272 self.__port = self.__cli_config.get(
sousaedu80135b92021-02-17 15:05:18 +0100273 "port", (80 if self.__connection_type == "REST" else 22)
274 )
fantom36068fd2019-11-29 14:18:50 +0000275 self.__ssh_auth = self.__cli_config.get("ssh_auth", None)
276
277 if self.__connection_type == "SSH":
sousaedu80135b92021-02-17 15:05:18 +0100278 interface = DpbSshInterface(
279 self.__username,
280 self.__password,
281 self.__url,
282 self.__port,
283 self.__network,
284 self.__ssh_auth,
285 self.__LOGGER_NAME,
286 )
fantom36068fd2019-11-29 14:18:50 +0000287 elif self.__connection_type == "REST":
sousaedu80135b92021-02-17 15:05:18 +0100288 interface = DpbRestInterface(
289 self.__url, self.__port, self.__network, self.__LOGGER_NAME
290 )
fantom36068fd2019-11-29 14:18:50 +0000291 else:
292 raise SdnConnectorError(
sousaedu80135b92021-02-17 15:05:18 +0100293 "Connection type not supported (must be SSH or REST)", 400
294 )
295
fantom36068fd2019-11-29 14:18:50 +0000296 self.__post = interface.post
297 self.__get = interface.get
298 self.logger.info("DPB WimConn Init OK")
299
300 def create_connectivity_service(self, service_type, connection_points, **kwargs):
301 self.logger.info("Creating a connectivity service")
sousaedu80135b92021-02-17 15:05:18 +0100302
fantom36068fd2019-11-29 14:18:50 +0000303 try:
304 response = self.__post(self.__ACTIONS_MAP.get("CREATE"))
sousaedu80135b92021-02-17 15:05:18 +0100305
fantom36068fd2019-11-29 14:18:50 +0000306 if "service-id" in response:
307 service_id = int(response.get("service-id"))
308 self.logger.debug("created service id {}".format(service_id))
309 else:
310 raise SdnConnectorError(
sousaedu80135b92021-02-17 15:05:18 +0100311 "Invalid create service response (could be an issue with the DPB)",
312 500,
313 )
314
fantom36068fd2019-11-29 14:18:50 +0000315 data = {"segment": []}
sousaedu80135b92021-02-17 15:05:18 +0100316
fantom36068fd2019-11-29 14:18:50 +0000317 for point in connection_points:
sousaedu80135b92021-02-17 15:05:18 +0100318 data["segment"].append(
319 {
320 "terminal-name": point.get("service_endpoint_id"),
321 "label": int(
322 (point.get("service_endpoint_encapsulation_info")).get(
323 "vlan"
324 )
325 ),
326 "ingress-bw": 10.0,
327 "egress-bw": 10.0,
328 }
329 )
fantom36068fd2019-11-29 14:18:50 +0000330 # "ingress-bw": (bandwidth.get(point.get("service_endpoint_id"))).get("ingress"),
331 # "egress-bw": (bandwidth.get(point.get("service_endpoint_id"))).get("egress")}
fantom36068fd2019-11-29 14:18:50 +0000332
sousaedu80135b92021-02-17 15:05:18 +0100333 self.__post(
334 self.__ACTIONS_MAP.get("DEFINE"),
335 "/service/" + str(service_id),
336 data,
337 get_response=False,
338 )
339 self.__post(
340 self.__ACTIONS_MAP.get("ACTIVATE"),
341 "/service/" + str(service_id),
342 get_response=False,
343 )
344 self.logger.debug("Created connectivity service id:{}".format(service_id))
345
fantom36068fd2019-11-29 14:18:50 +0000346 return (str(service_id), None)
347 except Exception as e:
348 raise SdnConnectorError(
sousaedu80135b92021-02-17 15:05:18 +0100349 "Connectivity service could not be made | text: {}".format(e), 500
350 )
fantom36068fd2019-11-29 14:18:50 +0000351
352 def get_connectivity_service_status(self, service_uuid, conn_info=None):
353 self.logger.info(
sousaedu80135b92021-02-17 15:05:18 +0100354 "Checking connectivity service status id:{}".format(service_uuid)
355 )
356 data = {"timeout-millis": 10000, "acceptable": ["ACTIVE", "FAILED"]}
357
fantom36068fd2019-11-29 14:18:50 +0000358 try:
sousaedu80135b92021-02-17 15:05:18 +0100359 response = self.__post(
360 self.__ACTIONS_MAP.get("CHECK"),
361 "/service/" + service_uuid,
362 data,
363 )
364
fantom36068fd2019-11-29 14:18:50 +0000365 if "status" in response:
366 status = response.get("status", None)
367 self.logger.info("CHECKED CONNECTIVITY SERVICE STATUS")
sousaedu80135b92021-02-17 15:05:18 +0100368
fantom36068fd2019-11-29 14:18:50 +0000369 return {"wim_status": self.__STATUS_MAP.get(status)}
370 else:
371 raise SdnConnectorError(
sousaedu80135b92021-02-17 15:05:18 +0100372 "Invalid status check response (could be an issue with the DPB)",
373 500,
374 )
fantom36068fd2019-11-29 14:18:50 +0000375 except Exception as e:
376 raise SdnConnectorError(
sousaedu80135b92021-02-17 15:05:18 +0100377 "Failed to check service status | text: {}".format(e), 500
378 )
fantom36068fd2019-11-29 14:18:50 +0000379
380 def delete_connectivity_service(self, service_uuid, conn_info=None):
sousaedu80135b92021-02-17 15:05:18 +0100381 self.logger.info("Deleting connectivity service id: {}".format(service_uuid))
382
fantom36068fd2019-11-29 14:18:50 +0000383 try:
sousaedu80135b92021-02-17 15:05:18 +0100384 self.__post(
385 self.__ACTIONS_MAP.get("RELEASE"),
386 "/service/" + service_uuid,
387 get_response=False,
388 )
tierno1ec592d2020-06-16 15:29:47 +0000389 except Exception as e:
fantom36068fd2019-11-29 14:18:50 +0000390 raise SdnConnectorError(
sousaedu80135b92021-02-17 15:05:18 +0100391 "Could not delete service id:{} (could be an issue with the DPB): {}".format(
392 service_uuid, e
393 ),
394 500,
395 )
396
397 self.logger.debug("Deleted connectivity service id:{}".format(service_uuid))
398
fantom36068fd2019-11-29 14:18:50 +0000399 return None
400
sousaedu80135b92021-02-17 15:05:18 +0100401 def edit_connectivity_service(
402 self, service_uuid, conn_info=None, connection_points=None, **kwargs
403 ):
404 self.logger.info("Editing connectivity service id: {}".format(service_uuid))
405 data = {"timeout-millis": 10000, "acceptable": ["DORMANT"]}
406
fantom36068fd2019-11-29 14:18:50 +0000407 try:
sousaedu80135b92021-02-17 15:05:18 +0100408 self.__post(
409 self.__ACTIONS_MAP.get("RESET"),
410 "/service/" + service_uuid,
411 get_response=False,
412 )
413 response = self.__post(
414 self.__ACTIONS_MAP.get("CHECK"),
415 "/service/" + service_uuid,
416 data,
417 )
418
fantom36068fd2019-11-29 14:18:50 +0000419 if "status" in response:
sousaedu80135b92021-02-17 15:05:18 +0100420 self.logger.debug("Connectivity service {} reset".format(service_uuid))
fantom36068fd2019-11-29 14:18:50 +0000421 else:
422 raise SdnConnectorError(
sousaedu80135b92021-02-17 15:05:18 +0100423 "Invalid status check response (could be an issue with the DPB)",
424 500,
425 )
fantom36068fd2019-11-29 14:18:50 +0000426 except Exception as e:
sousaedu80135b92021-02-17 15:05:18 +0100427 raise SdnConnectorError("Failed to reset service | text: {}".format(e), 500)
428
fantom36068fd2019-11-29 14:18:50 +0000429 try:
430 data = {"segment": []}
sousaedu80135b92021-02-17 15:05:18 +0100431
fantom36068fd2019-11-29 14:18:50 +0000432 for point in connection_points:
sousaedu80135b92021-02-17 15:05:18 +0100433 data["segment"].append(
434 {
435 "terminal-name": point.get("service_endpoint_id"),
436 "label": int(
437 (point.get("service_endpoint_encapsulation_info")).get(
438 "vlan"
439 )
440 ),
441 "ingress-bw": 10.0,
442 "egress-bw": 10.0,
443 }
444 )
fantom36068fd2019-11-29 14:18:50 +0000445 # "ingress-bw": (bandwidth.get(point.get("service_endpoint_id"))).get("ingress"),
446 # "egress-bw": (bandwidth.get(point.get("service_endpoint_id"))).get("egress")}
sousaedu80135b92021-02-17 15:05:18 +0100447
448 self.__post(
449 self.__ACTIONS_MAP.get("DEFINE"),
450 "/service/" + str(service_uuid),
451 data,
452 get_response=False,
453 )
454 self.__post(
455 self.__ACTIONS_MAP.get("ACTIVATE"),
456 "/service/" + str(service_uuid),
457 get_response=False,
458 )
fantom36068fd2019-11-29 14:18:50 +0000459 except Exception as e:
460 raise SdnConnectorError(
sousaedu80135b92021-02-17 15:05:18 +0100461 "Failed to edit connectivity service | text: {}".format(e), 500
462 )
463
464 self.logger.debug("Edited connectivity service {}".format(service_uuid))
465
fantom36068fd2019-11-29 14:18:50 +0000466 return conn_info
467
468 def __check_service(self, serv_type, points, kwargs):
tierno1ec592d2020-06-16 15:29:47 +0000469 if serv_type not in self.__SUPPORTED_SERV_TYPES:
fantom36068fd2019-11-29 14:18:50 +0000470 raise SdnConnectorError("Service type no supported", 400)
471 # Future: BW Checks here