423ceff30271bb10d6391a54a56d1549d60a98ae
[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 # import sys
34 from osm_ro_plugin.sdnconn import SdnConnectorBase, SdnConnectorError
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__(self, username, password, wim_url, wim_port, network, auth_data, logger_name):
44 self.logger = logging.getLogger(logger_name + self.__LOGGER_NAME_EXT)
45 self.__username = username
46 self.__password = password
47 self.__url = wim_url
48 self.__port = wim_port
49 self.__network = network
50 self.__auth_data = auth_data
51 self.__session_id = 1
52 self.__ssh_client = self.__create_client()
53 self.__stdin = None
54 self.__stdout = None
55 self.logger.info("SSH connection to DPB defined")
56
57 def _check_connection(self):
58 if not (self.__stdin and self.__stdout):
59 self.__stdin, self.__stdout = self.__connect()
60
61 def post(self, function, url_params="", data=None, get_response=True):
62 """post request to dpb via ssh
63
64 notes:
65 - session_id need only be unique per ssh session, thus is currently safe if
66 ro is restarted
67 """
68 self._check_connection()
69 if data is None:
70 data = {}
71 url_ext_info = url_params.split('/')
72 for i in range(0, len(url_ext_info)):
73 if url_ext_info[i] == "service":
74 data["service-id"] = int(url_ext_info[i+1])
75 data["type"] = function[self.__FUNCTION_MAP_POS]
76 data = {
77 "session": self.__session_id,
78 "content": data
79 }
80 self.__session_id += 1
81
82 try:
83 data = json.dumps(data).encode("utf-8")
84 data_packed = struct.pack(
85 ">I" + str(len(data)) + "s", len(data), data)
86 self.__stdin.write(data_packed)
87 self.logger.debug("Data sent to DPB via SSH")
88 except Exception as e:
89 raise SdnConnectorError(
90 "Failed to write via SSH | text: {}".format(e), 500)
91
92 try:
93 data_len = struct.unpack(">I", self.__stdout.read(4))[0]
94 data = struct.unpack(str(data_len) + "s",
95 self.__stdout.read(data_len))[0]
96 return json.loads(data).get("content", {})
97 except Exception as e:
98 raise SdnConnectorError(
99 "Could not get response from WIM | text: {}".format(e), 500)
100
101 def get(self, function, url_params=""):
102 raise SdnConnectorError("SSH Get not implemented", 500)
103
104 def __create_client(self):
105 ssh_client = paramiko.SSHClient()
106 ssh_client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
107 return ssh_client
108
109 def __connect(self):
110 private_key = None
111 password = None
112 if self.__auth_data.get("auth_type", "PASS") == "KEY":
113 private_key = self.__build_private_key_obj()
114 if self.__auth_data.get("auth_type", "PASS") == "PASS":
115 password = self.__password
116
117 try:
118 self.__ssh_client.connect(hostname=self.__url,
119 port=self.__port,
120 username=self.__username,
121 password=password,
122 pkey=private_key,
123 look_for_keys=False,
124 compress=False)
125 stdin, stdout, stderr = self.__ssh_client.exec_command(
126 command=self.__network)
127 except paramiko.BadHostKeyException as e:
128 raise SdnConnectorError(
129 "Could not add SSH host key | text: {}".format(e), 500)
130 except paramiko.AuthenticationException as e:
131 raise SdnConnectorError(
132 "Could not authorize SSH connection | text: {}".format(e), 400)
133 except paramiko.SSHException as e:
134 raise SdnConnectorError(
135 "Could not establish the SSH connection | text: {}".format(e), 500)
136 except Exception as e:
137 raise SdnConnectorError(
138 "Unknown error occurred when connecting via SSH | text: {}".format(e), 500)
139
140 try:
141 data_len = struct.unpack(">I", stdout.read(4))[0]
142 data = json.loads(struct.unpack(
143 str(data_len) + "s", stdout.read(data_len))[0])
144 except Exception as e:
145 raise SdnConnectorError(
146 "Failed to get response from DPB | text: {}".format(e), 500)
147 if "error" in data:
148 raise SdnConnectorError(
149 data.get("msg", data.get("error", "ERROR")), 500)
150 self.logger.info("SSH connection to DPB established OK")
151 return stdin, stdout
152
153 def __build_private_key_obj(self):
154 try:
155 with open(self.__auth_data.get("key_file"), 'r') as key_file:
156 if self.__auth_data.get("key_type") == "RSA":
157 return paramiko.RSAKey.from_private_key(key_file,
158 password=self.__auth_data.get("key_pass", None))
159 elif self.__auth_data.get("key_type") == "ECDSA":
160 return paramiko.ECDSAKey.from_private_key(key_file,
161 password=self.__auth_data.get("key_pass", None))
162 else:
163 raise SdnConnectorError("Key type not supported", 400)
164 except Exception as e:
165 raise SdnConnectorError(
166 "Could not load private SSH key | text: {}".format(e), 500)
167
168
169 class DpbRestInterface():
170 """ Communicate with the DPB via the REST API """
171
172 __LOGGER_NAME_EXT = ".rest"
173 __FUNCTION_MAP_POS = 0
174
175 def __init__(self, wim_url, wim_port, network, logger_name):
176 self.logger = logging.getLogger(logger_name + self.__LOGGER_NAME_EXT)
177 self.__base_url = "http://{}:{}/network/{}".format(
178 wim_url, str(wim_port), network)
179 self.logger.info("REST defined OK")
180
181 def post(self, function, url_params="", data=None, get_response=True):
182 url = self.__base_url + url_params + \
183 "/" + function[self.__FUNCTION_MAP_POS]
184 try:
185 self.logger.info(data)
186 response = requests.post(url, json=data)
187 if response.status_code != 200:
188 raise SdnConnectorError(
189 "REST request failed (status code: {})".format(response.status_code))
190 if get_response:
191 return response.json()
192 except Exception as e:
193 raise SdnConnectorError(
194 "REST request failed | text: {}".format(e), 500)
195
196 def get(self, function, url_params=""):
197 url = self.__base_url + url_params + function[self.__FUNCTION_MAP_POS]
198 try:
199 return requests.get(url)
200 except Exception as e:
201 raise SdnConnectorError(
202 "REST request failed | text: {}".format(e), 500)
203
204
205 class DpbConnector(SdnConnectorBase):
206 """ Use the DPB to establish multipoint connections """
207
208 __LOGGER_NAME = "openmano.rosdnconn.dpb"
209 __SUPPORTED_SERV_TYPES = ["ELAN (L2)", "ELINE (L2)"]
210 __SUPPORTED_CONNECTION_TYPES = ["REST", "SSH"]
211 __SUPPORTED_SSH_AUTH_TYPES = ["KEY", "PASS"]
212 __SUPPORTED_SSH_KEY_TYPES = ["ECDSA", "RSA"]
213 __STATUS_MAP = {
214 "ACTIVE": "ACTIVE",
215 "ACTIVATING": "BUILD",
216 "FAILED": "ERROR"}
217 __ACTIONS_MAP = {
218 "CREATE": ("create-service", "new-service"),
219 "DEFINE": ("define", "define-service"),
220 "ACTIVATE": ("activate", "activate-service"),
221 "RELEASE": ("release", "release-service"),
222 "DEACTIVATE": ("deactivate", "deactivate-service"),
223 "CHECK": ("await-status", "await-service-status"),
224 "GET": ("services", "NOT IMPLEMENTED"),
225 "RESET": ("reset", "NOT IMPLEMENTED")
226 }
227
228 def __init__(self, wim, wim_account, config):
229 self.logger = logging.getLogger(self.__LOGGER_NAME)
230
231 self.__wim = wim
232 self.__account = wim_account
233 self.__config = config
234 self.__cli_config = self.__account.pop("config", None)
235
236 self.__url = self.__wim.get("wim_url", "")
237 self.__password = self.__account.get("passwd", "")
238 self.__username = self.__account.get("user", "")
239 self.__network = self.__cli_config.get("network", "")
240 self.__connection_type = self.__cli_config.get(
241 "connection_type", "REST")
242 self.__port = self.__cli_config.get(
243 "port", (80 if self.__connection_type == "REST" else 22))
244 self.__ssh_auth = self.__cli_config.get("ssh_auth", None)
245
246 if self.__connection_type == "SSH":
247 interface = DpbSshInterface(self.__username,
248 self.__password,
249 self.__url,
250 self.__port,
251 self.__network,
252 self.__ssh_auth,
253 self.__LOGGER_NAME)
254 elif self.__connection_type == "REST":
255 interface = DpbRestInterface(self.__url,
256 self.__port,
257 self.__network,
258 self.__LOGGER_NAME)
259 else:
260 raise SdnConnectorError(
261 "Connection type not supported (must be SSH or REST)", 400)
262 self.__post = interface.post
263 self.__get = interface.get
264 self.logger.info("DPB WimConn Init OK")
265
266 def create_connectivity_service(self, service_type, connection_points, **kwargs):
267 self.logger.info("Creating a connectivity service")
268 try:
269 response = self.__post(self.__ACTIONS_MAP.get("CREATE"))
270 if "service-id" in response:
271 service_id = int(response.get("service-id"))
272 self.logger.debug("created service id {}".format(service_id))
273 else:
274 raise SdnConnectorError(
275 "Invalid create service response (could be an issue with the DPB)", 500)
276 data = {"segment": []}
277 for point in connection_points:
278 data["segment"].append({
279 "terminal-name": point.get("service_endpoint_id"),
280 "label": int((point.get("service_endpoint_encapsulation_info")).get("vlan")),
281 "ingress-bw": 10.0,
282 "egress-bw": 10.0})
283 # "ingress-bw": (bandwidth.get(point.get("service_endpoint_id"))).get("ingress"),
284 # "egress-bw": (bandwidth.get(point.get("service_endpoint_id"))).get("egress")}
285 self.__post(self.__ACTIONS_MAP.get("DEFINE"),
286 "/service/"+str(service_id), data, get_response=False)
287
288 self.__post(self.__ACTIONS_MAP.get("ACTIVATE"),
289 "/service/"+str(service_id), get_response=False)
290 self.logger.debug(
291 "Created connectivity service id:{}".format(service_id))
292 return (str(service_id), None)
293 except Exception as e:
294 raise SdnConnectorError(
295 "Connectivity service could not be made | text: {}".format(e), 500)
296
297 def get_connectivity_service_status(self, service_uuid, conn_info=None):
298 self.logger.info(
299 "Checking connectivity service status id:{}".format(service_uuid))
300 data = {
301 "timeout-millis": 10000,
302 "acceptable": ["ACTIVE", "FAILED"]
303 }
304 try:
305 response = self.__post(self.__ACTIONS_MAP.get(
306 "CHECK"), "/service/"+service_uuid, data)
307 if "status" in response:
308 status = response.get("status", None)
309 self.logger.info("CHECKED CONNECTIVITY SERVICE STATUS")
310 return {"wim_status": self.__STATUS_MAP.get(status)}
311 else:
312 raise SdnConnectorError(
313 "Invalid status check response (could be an issue with the DPB)", 500)
314 except Exception as e:
315 raise SdnConnectorError(
316 "Failed to check service status | text: {}".format(e), 500)
317
318 def delete_connectivity_service(self, service_uuid, conn_info=None):
319 self.logger.info(
320 "Deleting connectivity service id: {}".format(service_uuid))
321 try:
322 self.__post(self.__ACTIONS_MAP.get("RELEASE"),
323 "/service/"+service_uuid, get_response=False)
324 except Exception as e:
325 raise SdnConnectorError(
326 "Could not delete service id:{} (could be an issue with the DPB): {}".format(service_uuid, e), 500)
327 self.logger.debug(
328 "Deleted connectivity service id:{}".format(service_uuid))
329 return None
330
331 def edit_connectivity_service(self, service_uuid, conn_info=None, connection_points=None, **kwargs):
332 self.logger.info(
333 "Editing connectivity service id: {}".format(service_uuid))
334 data = {
335 "timeout-millis": 10000,
336 "acceptable": ["DORMANT"]
337 }
338 try:
339 self.__post(self.__ACTIONS_MAP.get("RESET"),
340 "/service/"+service_uuid, get_response=False)
341 response = self.__post(self.__ACTIONS_MAP.get(
342 "CHECK"), "/service/"+service_uuid, data)
343 if "status" in response:
344 self.logger.debug(
345 "Connectivity service {} reset".format(service_uuid))
346 else:
347 raise SdnConnectorError(
348 "Invalid status check response (could be an issue with the DPB)", 500)
349 except Exception as e:
350 raise SdnConnectorError(
351 "Failed to reset service | text: {}".format(e), 500)
352 try:
353 data = {"segment": []}
354 for point in connection_points:
355 data["segment"].append({
356 "terminal-name": point.get("service_endpoint_id"),
357 "label": int((point.get("service_endpoint_encapsulation_info")).get("vlan")),
358 "ingress-bw": 10.0,
359 "egress-bw": 10.0})
360 # "ingress-bw": (bandwidth.get(point.get("service_endpoint_id"))).get("ingress"),
361 # "egress-bw": (bandwidth.get(point.get("service_endpoint_id"))).get("egress")}
362 self.__post(self.__ACTIONS_MAP.get("DEFINE"), "/service/" +
363 str(service_uuid), data, get_response=False)
364 self.__post(self.__ACTIONS_MAP.get("ACTIVATE"),
365 "/service/"+str(service_uuid), get_response=False)
366 except Exception as e:
367 raise SdnConnectorError(
368 "Failed to edit connectivity service | text: {}".format(e), 500)
369 self.logger.debug(
370 "Edited connectivity service {}".format(service_uuid))
371 return conn_info
372
373 def __check_service(self, serv_type, points, kwargs):
374 if serv_type not in self.__SUPPORTED_SERV_TYPES:
375 raise SdnConnectorError("Service type no supported", 400)
376 # Future: BW Checks here