e08cdfdf7cf3cd247c8b41bdce837eb735f75609
[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 paramiko
31 import requests
32 import struct
33
34 # import sys
35 from osm_ro_plugin.sdnconn import SdnConnectorBase, SdnConnectorError
36
37
38 class DpbSshInterface:
39 """ Communicate with the DPB via SSH """
40
41 __LOGGER_NAME_EXT = ".ssh"
42 __FUNCTION_MAP_POS = 1
43
44 def __init__(
45 self, username, password, wim_url, wim_port, network, auth_data, logger_name
46 ):
47 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:
68 - session_id need only be unique per ssh session, thus is currently safe if
69 ro is restarted
70 """
71 self._check_connection()
72
73 if data is None:
74 data = {}
75
76 url_ext_info = url_params.split("/")
77
78 for i in range(0, len(url_ext_info)):
79 if url_ext_info[i] == "service":
80 data["service-id"] = int(url_ext_info[i + 1])
81
82 data["type"] = function[self.__FUNCTION_MAP_POS]
83 data = {
84 "session": self.__session_id,
85 "content": data,
86 }
87 self.__session_id += 1
88
89 try:
90 data = json.dumps(data).encode("utf-8")
91 data_packed = struct.pack(">I" + str(len(data)) + "s", len(data), data)
92 self.__stdin.write(data_packed)
93 self.logger.debug("Data sent to DPB via SSH")
94 except Exception as e:
95 raise SdnConnectorError("Failed to write via SSH | text: {}".format(e), 500)
96
97 try:
98 data_len = struct.unpack(">I", self.__stdout.read(4))[0]
99 data = struct.unpack(str(data_len) + "s", self.__stdout.read(data_len))[0]
100
101 return json.loads(data).get("content", {})
102 except Exception as e:
103 raise SdnConnectorError(
104 "Could not get response from WIM | text: {}".format(e), 500
105 )
106
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())
113
114 return ssh_client
115
116 def __connect(self):
117 private_key = None
118 password = None
119
120 if self.__auth_data.get("auth_type", "PASS") == "KEY":
121 private_key = self.__build_private_key_obj()
122
123 if self.__auth_data.get("auth_type", "PASS") == "PASS":
124 password = self.__password
125
126 try:
127 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 )
136 stdin, stdout, stderr = self.__ssh_client.exec_command(
137 command=self.__network
138 )
139 except paramiko.BadHostKeyException as e:
140 raise SdnConnectorError(
141 "Could not add SSH host key | text: {}".format(e), 500
142 )
143 except paramiko.AuthenticationException as e:
144 raise SdnConnectorError(
145 "Could not authorize SSH connection | text: {}".format(e), 400
146 )
147 except paramiko.SSHException as e:
148 raise SdnConnectorError(
149 "Could not establish the SSH connection | text: {}".format(e), 500
150 )
151 except Exception as e:
152 raise SdnConnectorError(
153 "Unknown error occurred when connecting via SSH | text: {}".format(e),
154 500,
155 )
156
157 try:
158 data_len = struct.unpack(">I", stdout.read(4))[0]
159 data = json.loads(
160 struct.unpack(str(data_len) + "s", stdout.read(data_len))[0]
161 )
162 except Exception as e:
163 raise SdnConnectorError(
164 "Failed to get response from DPB | text: {}".format(e), 500
165 )
166
167 if "error" in data:
168 raise SdnConnectorError(data.get("msg", data.get("error", "ERROR")), 500)
169
170 self.logger.info("SSH connection to DPB established OK")
171
172 return stdin, stdout
173
174 def __build_private_key_obj(self):
175 try:
176 with open(self.__auth_data.get("key_file"), "r") as key_file:
177 if self.__auth_data.get("key_type") == "RSA":
178 return paramiko.RSAKey.from_private_key(
179 key_file, password=self.__auth_data.get("key_pass", None)
180 )
181 elif self.__auth_data.get("key_type") == "ECDSA":
182 return paramiko.ECDSAKey.from_private_key(
183 key_file, password=self.__auth_data.get("key_pass", None)
184 )
185 else:
186 raise SdnConnectorError("Key type not supported", 400)
187 except Exception as e:
188 raise SdnConnectorError(
189 "Could not load private SSH key | text: {}".format(e), 500
190 )
191
192
193 class DpbRestInterface:
194 """ 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(
202 wim_url, str(wim_port), network
203 )
204 self.logger.info("REST defined OK")
205
206 def post(self, function, url_params="", data=None, get_response=True):
207 url = self.__base_url + url_params + "/" + function[self.__FUNCTION_MAP_POS]
208
209 try:
210 self.logger.info(data)
211 response = requests.post(url, json=data)
212
213 if response.status_code != 200:
214 raise SdnConnectorError(
215 "REST request failed (status code: {})".format(response.status_code)
216 )
217
218 if get_response:
219 return response.json()
220 except Exception as e:
221 raise SdnConnectorError("REST request failed | text: {}".format(e), 500)
222
223 def get(self, function, url_params=""):
224 url = self.__base_url + url_params + function[self.__FUNCTION_MAP_POS]
225
226 try:
227 return requests.get(url)
228 except Exception as e:
229 raise SdnConnectorError("REST request failed | text: {}".format(e), 500)
230
231
232 class DpbConnector(SdnConnectorBase):
233 """ Use the DPB to establish multipoint connections """
234
235 __LOGGER_NAME = "ro.sdn.dpb"
236 __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"]
240 __STATUS_MAP = {"ACTIVE": "ACTIVE", "ACTIVATING": "BUILD", "FAILED": "ERROR"}
241 __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"),
249 "RESET": ("reset", "NOT IMPLEMENTED"),
250 }
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", "")
264 self.__connection_type = self.__cli_config.get("connection_type", "REST")
265 self.__port = self.__cli_config.get(
266 "port", (80 if self.__connection_type == "REST" else 22)
267 )
268 self.__ssh_auth = self.__cli_config.get("ssh_auth", None)
269
270 if self.__connection_type == "SSH":
271 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 )
280 elif self.__connection_type == "REST":
281 interface = DpbRestInterface(
282 self.__url, self.__port, self.__network, self.__LOGGER_NAME
283 )
284 else:
285 raise SdnConnectorError(
286 "Connection type not supported (must be SSH or REST)", 400
287 )
288
289 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")
295
296 try:
297 response = self.__post(self.__ACTIONS_MAP.get("CREATE"))
298
299 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(
304 "Invalid create service response (could be an issue with the DPB)",
305 500,
306 )
307
308 data = {"segment": []}
309
310 for point in connection_points:
311 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 )
323 # "ingress-bw": (bandwidth.get(point.get("service_endpoint_id"))).get("ingress"),
324 # "egress-bw": (bandwidth.get(point.get("service_endpoint_id"))).get("egress")}
325
326 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
339 return (str(service_id), None)
340 except Exception as e:
341 raise SdnConnectorError(
342 "Connectivity service could not be made | text: {}".format(e), 500
343 )
344
345 def get_connectivity_service_status(self, service_uuid, conn_info=None):
346 self.logger.info(
347 "Checking connectivity service status id:{}".format(service_uuid)
348 )
349 data = {"timeout-millis": 10000, "acceptable": ["ACTIVE", "FAILED"]}
350
351 try:
352 response = self.__post(
353 self.__ACTIONS_MAP.get("CHECK"),
354 "/service/" + service_uuid,
355 data,
356 )
357
358 if "status" in response:
359 status = response.get("status", None)
360 self.logger.info("CHECKED CONNECTIVITY SERVICE STATUS")
361
362 return {"wim_status": self.__STATUS_MAP.get(status)}
363 else:
364 raise SdnConnectorError(
365 "Invalid status check response (could be an issue with the DPB)",
366 500,
367 )
368 except Exception as e:
369 raise SdnConnectorError(
370 "Failed to check service status | text: {}".format(e), 500
371 )
372
373 def delete_connectivity_service(self, service_uuid, conn_info=None):
374 self.logger.info("Deleting connectivity service id: {}".format(service_uuid))
375
376 try:
377 self.__post(
378 self.__ACTIONS_MAP.get("RELEASE"),
379 "/service/" + service_uuid,
380 get_response=False,
381 )
382 except Exception as e:
383 raise SdnConnectorError(
384 "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
392 return None
393
394 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
400 try:
401 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
412 if "status" in response:
413 self.logger.debug("Connectivity service {} reset".format(service_uuid))
414 else:
415 raise SdnConnectorError(
416 "Invalid status check response (could be an issue with the DPB)",
417 500,
418 )
419 except Exception as e:
420 raise SdnConnectorError("Failed to reset service | text: {}".format(e), 500)
421
422 try:
423 data = {"segment": []}
424
425 for point in connection_points:
426 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 )
438 # "ingress-bw": (bandwidth.get(point.get("service_endpoint_id"))).get("ingress"),
439 # "egress-bw": (bandwidth.get(point.get("service_endpoint_id"))).get("egress")}
440
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 )
452 except Exception as e:
453 raise SdnConnectorError(
454 "Failed to edit connectivity service | text: {}".format(e), 500
455 )
456
457 self.logger.debug("Edited connectivity service {}".format(service_uuid))
458
459 return conn_info
460
461 def __check_service(self, serv_type, points, kwargs):
462 if serv_type not in self.__SUPPORTED_SERV_TYPES:
463 raise SdnConnectorError("Service type no supported", 400)
464 # Future: BW Checks here