blob: e08cdfdf7cf3cd247c8b41bdce837eb735f75609 [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
30import paramiko
31import requests
32import struct
sousaedu80135b92021-02-17 15:05:18 +010033
tierno1ec592d2020-06-16 15:29:47 +000034# import sys
tierno72774862020-05-04 11:44:15 +000035from osm_ro_plugin.sdnconn import SdnConnectorBase, SdnConnectorError
fantom36068fd2019-11-29 14:18:50 +000036
37
sousaedu80135b92021-02-17 15:05:18 +010038class DpbSshInterface:
fantom36068fd2019-11-29 14:18:50 +000039 """ Communicate with the DPB via SSH """
40
41 __LOGGER_NAME_EXT = ".ssh"
42 __FUNCTION_MAP_POS = 1
43
sousaedu80135b92021-02-17 15:05:18 +010044 def __init__(
45 self, username, password, wim_url, wim_port, network, auth_data, logger_name
46 ):
fantom36068fd2019-11-29 14:18:50 +000047 self.logger = logging.getLogger(logger_name + self.__LOGGER_NAME_EXT)
48 self.__username = username
49 self.__password = password
50 self.__url = wim_url
51 self.__port = wim_port
52 self.__network = network
53 self.__auth_data = auth_data
54 self.__session_id = 1
55 self.__ssh_client = self.__create_client()
56 self.__stdin = None
57 self.__stdout = None
58 self.logger.info("SSH connection to DPB defined")
59
60 def _check_connection(self):
61 if not (self.__stdin and self.__stdout):
62 self.__stdin, self.__stdout = self.__connect()
63
64 def post(self, function, url_params="", data=None, get_response=True):
65 """post request to dpb via ssh
66
67 notes:
sousaedu80135b92021-02-17 15:05:18 +010068 - session_id need only be unique per ssh session, thus is currently safe if
fantom36068fd2019-11-29 14:18:50 +000069 ro is restarted
70 """
71 self._check_connection()
sousaedu80135b92021-02-17 15:05:18 +010072
tierno1ec592d2020-06-16 15:29:47 +000073 if data is None:
fantom36068fd2019-11-29 14:18:50 +000074 data = {}
sousaedu80135b92021-02-17 15:05:18 +010075
76 url_ext_info = url_params.split("/")
77
fantom36068fd2019-11-29 14:18:50 +000078 for i in range(0, len(url_ext_info)):
79 if url_ext_info[i] == "service":
sousaedu80135b92021-02-17 15:05:18 +010080 data["service-id"] = int(url_ext_info[i + 1])
81
fantom36068fd2019-11-29 14:18:50 +000082 data["type"] = function[self.__FUNCTION_MAP_POS]
83 data = {
84 "session": self.__session_id,
sousaedu80135b92021-02-17 15:05:18 +010085 "content": data,
fantom36068fd2019-11-29 14:18:50 +000086 }
87 self.__session_id += 1
88
89 try:
90 data = json.dumps(data).encode("utf-8")
sousaedu80135b92021-02-17 15:05:18 +010091 data_packed = struct.pack(">I" + str(len(data)) + "s", len(data), data)
fantom36068fd2019-11-29 14:18:50 +000092 self.__stdin.write(data_packed)
93 self.logger.debug("Data sent to DPB via SSH")
94 except Exception as e:
sousaedu80135b92021-02-17 15:05:18 +010095 raise SdnConnectorError("Failed to write via SSH | text: {}".format(e), 500)
fantom36068fd2019-11-29 14:18:50 +000096
97 try:
98 data_len = struct.unpack(">I", self.__stdout.read(4))[0]
sousaedu80135b92021-02-17 15:05:18 +010099 data = struct.unpack(str(data_len) + "s", self.__stdout.read(data_len))[0]
100
fantom36068fd2019-11-29 14:18:50 +0000101 return json.loads(data).get("content", {})
102 except Exception as e:
103 raise SdnConnectorError(
sousaedu80135b92021-02-17 15:05:18 +0100104 "Could not get response from WIM | text: {}".format(e), 500
105 )
fantom36068fd2019-11-29 14:18:50 +0000106
107 def get(self, function, url_params=""):
108 raise SdnConnectorError("SSH Get not implemented", 500)
109
110 def __create_client(self):
111 ssh_client = paramiko.SSHClient()
112 ssh_client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
sousaedu80135b92021-02-17 15:05:18 +0100113
fantom36068fd2019-11-29 14:18:50 +0000114 return ssh_client
115
116 def __connect(self):
117 private_key = None
118 password = None
sousaedu80135b92021-02-17 15:05:18 +0100119
fantom36068fd2019-11-29 14:18:50 +0000120 if self.__auth_data.get("auth_type", "PASS") == "KEY":
121 private_key = self.__build_private_key_obj()
sousaedu80135b92021-02-17 15:05:18 +0100122
fantom36068fd2019-11-29 14:18:50 +0000123 if self.__auth_data.get("auth_type", "PASS") == "PASS":
124 password = self.__password
125
126 try:
sousaedu80135b92021-02-17 15:05:18 +0100127 self.__ssh_client.connect(
128 hostname=self.__url,
129 port=self.__port,
130 username=self.__username,
131 password=password,
132 pkey=private_key,
133 look_for_keys=False,
134 compress=False,
135 )
fantom36068fd2019-11-29 14:18:50 +0000136 stdin, stdout, stderr = self.__ssh_client.exec_command(
sousaedu80135b92021-02-17 15:05:18 +0100137 command=self.__network
138 )
fantom36068fd2019-11-29 14:18:50 +0000139 except paramiko.BadHostKeyException as e:
140 raise SdnConnectorError(
sousaedu80135b92021-02-17 15:05:18 +0100141 "Could not add SSH host key | text: {}".format(e), 500
142 )
fantom36068fd2019-11-29 14:18:50 +0000143 except paramiko.AuthenticationException as e:
144 raise SdnConnectorError(
sousaedu80135b92021-02-17 15:05:18 +0100145 "Could not authorize SSH connection | text: {}".format(e), 400
146 )
fantom36068fd2019-11-29 14:18:50 +0000147 except paramiko.SSHException as e:
148 raise SdnConnectorError(
sousaedu80135b92021-02-17 15:05:18 +0100149 "Could not establish the SSH connection | text: {}".format(e), 500
150 )
fantom36068fd2019-11-29 14:18:50 +0000151 except Exception as e:
152 raise SdnConnectorError(
sousaedu80135b92021-02-17 15:05:18 +0100153 "Unknown error occurred when connecting via SSH | text: {}".format(e),
154 500,
155 )
fantom36068fd2019-11-29 14:18:50 +0000156
157 try:
158 data_len = struct.unpack(">I", stdout.read(4))[0]
sousaedu80135b92021-02-17 15:05:18 +0100159 data = json.loads(
160 struct.unpack(str(data_len) + "s", stdout.read(data_len))[0]
161 )
fantom36068fd2019-11-29 14:18:50 +0000162 except Exception as e:
163 raise SdnConnectorError(
sousaedu80135b92021-02-17 15:05:18 +0100164 "Failed to get response from DPB | text: {}".format(e), 500
165 )
166
fantom36068fd2019-11-29 14:18:50 +0000167 if "error" in data:
sousaedu80135b92021-02-17 15:05:18 +0100168 raise SdnConnectorError(data.get("msg", data.get("error", "ERROR")), 500)
169
fantom36068fd2019-11-29 14:18:50 +0000170 self.logger.info("SSH connection to DPB established OK")
sousaedu80135b92021-02-17 15:05:18 +0100171
fantom36068fd2019-11-29 14:18:50 +0000172 return stdin, stdout
173
174 def __build_private_key_obj(self):
175 try:
sousaedu80135b92021-02-17 15:05:18 +0100176 with open(self.__auth_data.get("key_file"), "r") as key_file:
fantom36068fd2019-11-29 14:18:50 +0000177 if self.__auth_data.get("key_type") == "RSA":
sousaedu80135b92021-02-17 15:05:18 +0100178 return paramiko.RSAKey.from_private_key(
179 key_file, password=self.__auth_data.get("key_pass", None)
180 )
fantom36068fd2019-11-29 14:18:50 +0000181 elif self.__auth_data.get("key_type") == "ECDSA":
sousaedu80135b92021-02-17 15:05:18 +0100182 return paramiko.ECDSAKey.from_private_key(
183 key_file, password=self.__auth_data.get("key_pass", None)
184 )
fantom36068fd2019-11-29 14:18:50 +0000185 else:
186 raise SdnConnectorError("Key type not supported", 400)
187 except Exception as e:
188 raise SdnConnectorError(
sousaedu80135b92021-02-17 15:05:18 +0100189 "Could not load private SSH key | text: {}".format(e), 500
190 )
fantom36068fd2019-11-29 14:18:50 +0000191
192
sousaedu80135b92021-02-17 15:05:18 +0100193class DpbRestInterface:
fantom36068fd2019-11-29 14:18:50 +0000194 """ Communicate with the DPB via the REST API """
195
196 __LOGGER_NAME_EXT = ".rest"
197 __FUNCTION_MAP_POS = 0
198
199 def __init__(self, wim_url, wim_port, network, logger_name):
200 self.logger = logging.getLogger(logger_name + self.__LOGGER_NAME_EXT)
201 self.__base_url = "http://{}:{}/network/{}".format(
sousaedu80135b92021-02-17 15:05:18 +0100202 wim_url, str(wim_port), network
203 )
fantom36068fd2019-11-29 14:18:50 +0000204 self.logger.info("REST defined OK")
205
206 def post(self, function, url_params="", data=None, get_response=True):
sousaedu80135b92021-02-17 15:05:18 +0100207 url = self.__base_url + url_params + "/" + function[self.__FUNCTION_MAP_POS]
208
fantom36068fd2019-11-29 14:18:50 +0000209 try:
210 self.logger.info(data)
211 response = requests.post(url, json=data)
sousaedu80135b92021-02-17 15:05:18 +0100212
fantom36068fd2019-11-29 14:18:50 +0000213 if response.status_code != 200:
214 raise SdnConnectorError(
sousaedu80135b92021-02-17 15:05:18 +0100215 "REST request failed (status code: {})".format(response.status_code)
216 )
217
fantom36068fd2019-11-29 14:18:50 +0000218 if get_response:
219 return response.json()
220 except Exception as e:
sousaedu80135b92021-02-17 15:05:18 +0100221 raise SdnConnectorError("REST request failed | text: {}".format(e), 500)
fantom36068fd2019-11-29 14:18:50 +0000222
223 def get(self, function, url_params=""):
224 url = self.__base_url + url_params + function[self.__FUNCTION_MAP_POS]
sousaedu80135b92021-02-17 15:05:18 +0100225
fantom36068fd2019-11-29 14:18:50 +0000226 try:
227 return requests.get(url)
228 except Exception as e:
sousaedu80135b92021-02-17 15:05:18 +0100229 raise SdnConnectorError("REST request failed | text: {}".format(e), 500)
fantom36068fd2019-11-29 14:18:50 +0000230
231
232class DpbConnector(SdnConnectorBase):
233 """ Use the DPB to establish multipoint connections """
234
sousaedue493e9b2021-02-09 15:30:01 +0100235 __LOGGER_NAME = "ro.sdn.dpb"
fantom36068fd2019-11-29 14:18:50 +0000236 __SUPPORTED_SERV_TYPES = ["ELAN (L2)", "ELINE (L2)"]
237 __SUPPORTED_CONNECTION_TYPES = ["REST", "SSH"]
238 __SUPPORTED_SSH_AUTH_TYPES = ["KEY", "PASS"]
239 __SUPPORTED_SSH_KEY_TYPES = ["ECDSA", "RSA"]
sousaedu80135b92021-02-17 15:05:18 +0100240 __STATUS_MAP = {"ACTIVE": "ACTIVE", "ACTIVATING": "BUILD", "FAILED": "ERROR"}
fantom36068fd2019-11-29 14:18:50 +0000241 __ACTIONS_MAP = {
242 "CREATE": ("create-service", "new-service"),
243 "DEFINE": ("define", "define-service"),
244 "ACTIVATE": ("activate", "activate-service"),
245 "RELEASE": ("release", "release-service"),
246 "DEACTIVATE": ("deactivate", "deactivate-service"),
247 "CHECK": ("await-status", "await-service-status"),
248 "GET": ("services", "NOT IMPLEMENTED"),
sousaedu80135b92021-02-17 15:05:18 +0100249 "RESET": ("reset", "NOT IMPLEMENTED"),
fantom36068fd2019-11-29 14:18:50 +0000250 }
251
252 def __init__(self, wim, wim_account, config):
253 self.logger = logging.getLogger(self.__LOGGER_NAME)
254
255 self.__wim = wim
256 self.__account = wim_account
257 self.__config = config
258 self.__cli_config = self.__account.pop("config", None)
259
260 self.__url = self.__wim.get("wim_url", "")
261 self.__password = self.__account.get("passwd", "")
262 self.__username = self.__account.get("user", "")
263 self.__network = self.__cli_config.get("network", "")
sousaedu80135b92021-02-17 15:05:18 +0100264 self.__connection_type = self.__cli_config.get("connection_type", "REST")
fantom36068fd2019-11-29 14:18:50 +0000265 self.__port = self.__cli_config.get(
sousaedu80135b92021-02-17 15:05:18 +0100266 "port", (80 if self.__connection_type == "REST" else 22)
267 )
fantom36068fd2019-11-29 14:18:50 +0000268 self.__ssh_auth = self.__cli_config.get("ssh_auth", None)
269
270 if self.__connection_type == "SSH":
sousaedu80135b92021-02-17 15:05:18 +0100271 interface = DpbSshInterface(
272 self.__username,
273 self.__password,
274 self.__url,
275 self.__port,
276 self.__network,
277 self.__ssh_auth,
278 self.__LOGGER_NAME,
279 )
fantom36068fd2019-11-29 14:18:50 +0000280 elif self.__connection_type == "REST":
sousaedu80135b92021-02-17 15:05:18 +0100281 interface = DpbRestInterface(
282 self.__url, self.__port, self.__network, self.__LOGGER_NAME
283 )
fantom36068fd2019-11-29 14:18:50 +0000284 else:
285 raise SdnConnectorError(
sousaedu80135b92021-02-17 15:05:18 +0100286 "Connection type not supported (must be SSH or REST)", 400
287 )
288
fantom36068fd2019-11-29 14:18:50 +0000289 self.__post = interface.post
290 self.__get = interface.get
291 self.logger.info("DPB WimConn Init OK")
292
293 def create_connectivity_service(self, service_type, connection_points, **kwargs):
294 self.logger.info("Creating a connectivity service")
sousaedu80135b92021-02-17 15:05:18 +0100295
fantom36068fd2019-11-29 14:18:50 +0000296 try:
297 response = self.__post(self.__ACTIONS_MAP.get("CREATE"))
sousaedu80135b92021-02-17 15:05:18 +0100298
fantom36068fd2019-11-29 14:18:50 +0000299 if "service-id" in response:
300 service_id = int(response.get("service-id"))
301 self.logger.debug("created service id {}".format(service_id))
302 else:
303 raise SdnConnectorError(
sousaedu80135b92021-02-17 15:05:18 +0100304 "Invalid create service response (could be an issue with the DPB)",
305 500,
306 )
307
fantom36068fd2019-11-29 14:18:50 +0000308 data = {"segment": []}
sousaedu80135b92021-02-17 15:05:18 +0100309
fantom36068fd2019-11-29 14:18:50 +0000310 for point in connection_points:
sousaedu80135b92021-02-17 15:05:18 +0100311 data["segment"].append(
312 {
313 "terminal-name": point.get("service_endpoint_id"),
314 "label": int(
315 (point.get("service_endpoint_encapsulation_info")).get(
316 "vlan"
317 )
318 ),
319 "ingress-bw": 10.0,
320 "egress-bw": 10.0,
321 }
322 )
fantom36068fd2019-11-29 14:18:50 +0000323 # "ingress-bw": (bandwidth.get(point.get("service_endpoint_id"))).get("ingress"),
324 # "egress-bw": (bandwidth.get(point.get("service_endpoint_id"))).get("egress")}
fantom36068fd2019-11-29 14:18:50 +0000325
sousaedu80135b92021-02-17 15:05:18 +0100326 self.__post(
327 self.__ACTIONS_MAP.get("DEFINE"),
328 "/service/" + str(service_id),
329 data,
330 get_response=False,
331 )
332 self.__post(
333 self.__ACTIONS_MAP.get("ACTIVATE"),
334 "/service/" + str(service_id),
335 get_response=False,
336 )
337 self.logger.debug("Created connectivity service id:{}".format(service_id))
338
fantom36068fd2019-11-29 14:18:50 +0000339 return (str(service_id), None)
340 except Exception as e:
341 raise SdnConnectorError(
sousaedu80135b92021-02-17 15:05:18 +0100342 "Connectivity service could not be made | text: {}".format(e), 500
343 )
fantom36068fd2019-11-29 14:18:50 +0000344
345 def get_connectivity_service_status(self, service_uuid, conn_info=None):
346 self.logger.info(
sousaedu80135b92021-02-17 15:05:18 +0100347 "Checking connectivity service status id:{}".format(service_uuid)
348 )
349 data = {"timeout-millis": 10000, "acceptable": ["ACTIVE", "FAILED"]}
350
fantom36068fd2019-11-29 14:18:50 +0000351 try:
sousaedu80135b92021-02-17 15:05:18 +0100352 response = self.__post(
353 self.__ACTIONS_MAP.get("CHECK"),
354 "/service/" + service_uuid,
355 data,
356 )
357
fantom36068fd2019-11-29 14:18:50 +0000358 if "status" in response:
359 status = response.get("status", None)
360 self.logger.info("CHECKED CONNECTIVITY SERVICE STATUS")
sousaedu80135b92021-02-17 15:05:18 +0100361
fantom36068fd2019-11-29 14:18:50 +0000362 return {"wim_status": self.__STATUS_MAP.get(status)}
363 else:
364 raise SdnConnectorError(
sousaedu80135b92021-02-17 15:05:18 +0100365 "Invalid status check response (could be an issue with the DPB)",
366 500,
367 )
fantom36068fd2019-11-29 14:18:50 +0000368 except Exception as e:
369 raise SdnConnectorError(
sousaedu80135b92021-02-17 15:05:18 +0100370 "Failed to check service status | text: {}".format(e), 500
371 )
fantom36068fd2019-11-29 14:18:50 +0000372
373 def delete_connectivity_service(self, service_uuid, conn_info=None):
sousaedu80135b92021-02-17 15:05:18 +0100374 self.logger.info("Deleting connectivity service id: {}".format(service_uuid))
375
fantom36068fd2019-11-29 14:18:50 +0000376 try:
sousaedu80135b92021-02-17 15:05:18 +0100377 self.__post(
378 self.__ACTIONS_MAP.get("RELEASE"),
379 "/service/" + service_uuid,
380 get_response=False,
381 )
tierno1ec592d2020-06-16 15:29:47 +0000382 except Exception as e:
fantom36068fd2019-11-29 14:18:50 +0000383 raise SdnConnectorError(
sousaedu80135b92021-02-17 15:05:18 +0100384 "Could not delete service id:{} (could be an issue with the DPB): {}".format(
385 service_uuid, e
386 ),
387 500,
388 )
389
390 self.logger.debug("Deleted connectivity service id:{}".format(service_uuid))
391
fantom36068fd2019-11-29 14:18:50 +0000392 return None
393
sousaedu80135b92021-02-17 15:05:18 +0100394 def edit_connectivity_service(
395 self, service_uuid, conn_info=None, connection_points=None, **kwargs
396 ):
397 self.logger.info("Editing connectivity service id: {}".format(service_uuid))
398 data = {"timeout-millis": 10000, "acceptable": ["DORMANT"]}
399
fantom36068fd2019-11-29 14:18:50 +0000400 try:
sousaedu80135b92021-02-17 15:05:18 +0100401 self.__post(
402 self.__ACTIONS_MAP.get("RESET"),
403 "/service/" + service_uuid,
404 get_response=False,
405 )
406 response = self.__post(
407 self.__ACTIONS_MAP.get("CHECK"),
408 "/service/" + service_uuid,
409 data,
410 )
411
fantom36068fd2019-11-29 14:18:50 +0000412 if "status" in response:
sousaedu80135b92021-02-17 15:05:18 +0100413 self.logger.debug("Connectivity service {} reset".format(service_uuid))
fantom36068fd2019-11-29 14:18:50 +0000414 else:
415 raise SdnConnectorError(
sousaedu80135b92021-02-17 15:05:18 +0100416 "Invalid status check response (could be an issue with the DPB)",
417 500,
418 )
fantom36068fd2019-11-29 14:18:50 +0000419 except Exception as e:
sousaedu80135b92021-02-17 15:05:18 +0100420 raise SdnConnectorError("Failed to reset service | text: {}".format(e), 500)
421
fantom36068fd2019-11-29 14:18:50 +0000422 try:
423 data = {"segment": []}
sousaedu80135b92021-02-17 15:05:18 +0100424
fantom36068fd2019-11-29 14:18:50 +0000425 for point in connection_points:
sousaedu80135b92021-02-17 15:05:18 +0100426 data["segment"].append(
427 {
428 "terminal-name": point.get("service_endpoint_id"),
429 "label": int(
430 (point.get("service_endpoint_encapsulation_info")).get(
431 "vlan"
432 )
433 ),
434 "ingress-bw": 10.0,
435 "egress-bw": 10.0,
436 }
437 )
fantom36068fd2019-11-29 14:18:50 +0000438 # "ingress-bw": (bandwidth.get(point.get("service_endpoint_id"))).get("ingress"),
439 # "egress-bw": (bandwidth.get(point.get("service_endpoint_id"))).get("egress")}
sousaedu80135b92021-02-17 15:05:18 +0100440
441 self.__post(
442 self.__ACTIONS_MAP.get("DEFINE"),
443 "/service/" + str(service_uuid),
444 data,
445 get_response=False,
446 )
447 self.__post(
448 self.__ACTIONS_MAP.get("ACTIVATE"),
449 "/service/" + str(service_uuid),
450 get_response=False,
451 )
fantom36068fd2019-11-29 14:18:50 +0000452 except Exception as e:
453 raise SdnConnectorError(
sousaedu80135b92021-02-17 15:05:18 +0100454 "Failed to edit connectivity service | text: {}".format(e), 500
455 )
456
457 self.logger.debug("Edited connectivity service {}".format(service_uuid))
458
fantom36068fd2019-11-29 14:18:50 +0000459 return conn_info
460
461 def __check_service(self, serv_type, points, kwargs):
tierno1ec592d2020-06-16 15:29:47 +0000462 if serv_type not in self.__SUPPORTED_SERV_TYPES:
fantom36068fd2019-11-29 14:18:50 +0000463 raise SdnConnectorError("Service type no supported", 400)
464 # Future: BW Checks here