Coverage for RO-SDN-juniper_contrail/osm_rosdn_juniper_contrail/sdn_assist_juniper_contrail.py: 18%

380 statements  

« prev     ^ index     » next       coverage.py v7.3.1, created at 2024-07-02 09:11 +0000

1# -*- coding: utf-8 -*- 

2 

3# Copyright 2020 ETSI OSM 

4# 

5# All Rights Reserved. 

6# 

7# Licensed under the Apache License, Version 2.0 (the "License"); you may 

8# not use this file except in compliance with the License. You may obtain 

9# a copy of the License at 

10# 

11# http://www.apache.org/licenses/LICENSE-2.0 

12# 

13# Unless required by applicable law or agreed to in writing, software 

14# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 

15# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 

16# License for the specific language governing permissions and limitations 

17# under the License. 

18# 

19 

20import logging 

21import random 

22 

23from osm_ro_plugin.sdnconn import SdnConnectorBase, SdnConnectorError 

24from osm_rosdn_juniper_contrail.rest_lib import DuplicateFound 

25from osm_rosdn_juniper_contrail.rest_lib import HttpException 

26from osm_rosdn_juniper_contrail.sdn_api import UnderlayApi 

27import yaml 

28 

29 

30class JuniperContrail(SdnConnectorBase): 

31 """ 

32 Juniper Contrail SDN plugin. The plugin interacts with Juniper Contrail Controller, 

33 whose API details can be found in these links: 

34 

35 - https://github.com/tonyliu0592/contrail/wiki/API-Configuration-REST 

36 - https://www.juniper.net/documentation/en_US/contrail19/information-products/pathway-pages/api-guide-1910/ 

37 tutorial_with_rest.html 

38 - https://github.com/tonyliu0592/contrail-toolbox/blob/master/sriov/sriov 

39 """ 

40 

41 _WIM_LOGGER = "ro.sdn.junipercontrail" 

42 

43 def __init__(self, wim, wim_account, config=None, logger=None): 

44 """ 

45 

46 :param wim: (dict). Contains among others 'wim_url' 

47 :param wim_account: (dict). Contains among others 'uuid' (internal id), 'name', 

48 'sdn' (True if is intended for SDN-assist or False if intended for WIM), 'user', 'password'. 

49 :param config: (dict or None): Particular information of plugin. These keys if present have a common meaning: 

50 'mapping_not_needed': (bool) False by default or if missing, indicates that mapping is not needed. 

51 'service_endpoint_mapping': (list) provides the internal endpoint mapping. The meaning is: 

52 KEY meaning for WIM meaning for SDN assist 

53 -------- -------- -------- 

54 device_id pop_switch_dpid compute_id 

55 device_interface_id pop_switch_port compute_pci_address 

56 service_endpoint_id wan_service_endpoint_id SDN_service_endpoint_id 

57 service_mapping_info wan_service_mapping_info SDN_service_mapping_info 

58 contains extra information if needed. Text in Yaml format 

59 switch_dpid wan_switch_dpid SDN_switch_dpid 

60 switch_port wan_switch_port SDN_switch_port 

61 datacenter_id vim_account vim_account 

62 id: (internal, do not use) 

63 wim_id: (internal, do not use) 

64 :param logger (logging.Logger): optional logger object. If none is passed 'ro.sdn.sdnconn' is used. 

65 """ 

66 self.logger = logger or logging.getLogger(self._WIM_LOGGER) 

67 self.logger.debug( 

68 "wim: {}, wim_account: {}, config: {}".format(wim, wim_account, config) 

69 ) 

70 super().__init__(wim, wim_account, config, logger) 

71 

72 self.user = wim_account.get("user") 

73 self.password = wim_account.get("password") 

74 

75 url = wim.get("wim_url") # underlay url 

76 auth_url = None 

77 self.project = None 

78 self.domain = None 

79 self.asn = None 

80 self.fabric = None 

81 overlay_url = None 

82 self.vni_range = None 

83 self.verify = True 

84 

85 if config: 

86 auth_url = config.get("auth_url") 

87 self.project = config.get("project") 

88 self.domain = config.get("domain") 

89 self.asn = config.get("asn") 

90 self.fabric = config.get("fabric") 

91 self.overlay_url = config.get("overlay_url") 

92 self.vni_range = config.get("vni_range") 

93 

94 if config.get("insecure") and config.get("ca_cert"): 

95 raise SdnConnectorError( 

96 "options insecure and ca_cert are mutually exclusive" 

97 ) 

98 

99 if config.get("ca_cert"): 

100 self.verify = config.get("ca_cert") 

101 

102 elif config.get("insecure"): 

103 self.verify = False 

104 

105 else: 

106 raise SdnConnectorError( 

107 "certificate should provided or ssl verification should be " 

108 "disabled by setting insecure as True in sdn/wim config." 

109 ) 

110 

111 if not url: 

112 raise SdnConnectorError("'url' must be provided") 

113 

114 if not url.startswith("http"): 

115 url = "http://" + url 

116 

117 if not url.endswith("/"): 

118 url = url + "/" 

119 

120 self.url = url 

121 

122 if not self.vni_range: 

123 self.vni_range = ["1000001-2000000"] 

124 self.logger.info("No vni_range was provided. Using ['1000001-2000000']") 

125 

126 self.used_vni = set() 

127 

128 if auth_url: 

129 if not auth_url.startswith("http"): 

130 auth_url = "http://" + auth_url 

131 

132 if not auth_url.endswith("/"): 

133 auth_url = auth_url + "/" 

134 

135 self.auth_url = auth_url 

136 

137 if overlay_url: 

138 if not overlay_url.startswith("http"): 

139 overlay_url = "http://" + overlay_url 

140 

141 if not overlay_url.endswith("/"): 

142 overlay_url = overlay_url + "/" 

143 

144 self.overlay_url = overlay_url 

145 

146 if not self.project: 

147 raise SdnConnectorError("'project' must be provided") 

148 

149 if not self.asn: 

150 # TODO: Get ASN from controller config; otherwise raise ERROR for the moment 

151 raise SdnConnectorError( 

152 "'asn' was not provided and it was not possible to obtain it" 

153 ) 

154 

155 if not self.fabric: 

156 # TODO: Get FABRIC from controller config; otherwise raise ERROR for the moment 

157 raise SdnConnectorError( 

158 "'fabric' was not provided and was not possible to obtain it" 

159 ) 

160 

161 if not self.domain: 

162 self.domain = "default-domain" 

163 self.logger.info("No domain was provided. Using 'default-domain'") 

164 

165 underlay_api_config = { 

166 "auth_url": self.auth_url, 

167 "project": self.project, 

168 "domain": self.domain, 

169 "asn": self.asn, 

170 "fabric": self.fabric, 

171 "verify": self.verify, 

172 } 

173 self.underlay_api = UnderlayApi( 

174 url, 

175 underlay_api_config, 

176 user=self.user, 

177 password=self.password, 

178 logger=logger, 

179 ) 

180 

181 self._max_duplicate_retry = 2 

182 self.logger.info("Juniper Contrail Connector Initialized.") 

183 

184 def _generate_vni(self): 

185 """ 

186 Method to get unused VxLAN Network Identifier (VNI) 

187 Args: 

188 None 

189 Returns: 

190 VNI 

191 """ 

192 # find unused VLAN ID 

193 for vlanID_range in self.vni_range: 

194 try: 

195 start_vni, end_vni = map(int, vlanID_range.replace(" ", "").split("-")) 

196 

197 for i in range(start_vni, end_vni + 1): 

198 vni = random.randrange(start_vni, end_vni, 1) 

199 

200 if vni not in self.used_vni: 

201 return vni 

202 except Exception as exp: 

203 raise SdnConnectorError( 

204 "Exception {} occurred while searching a free VNI.".format(exp) 

205 ) 

206 else: 

207 raise SdnConnectorError( 

208 "Unable to create the virtual network." 

209 " All VNI in VNI range {} are in use.".format(self.vni_range) 

210 ) 

211 

212 # Aux functions for testing 

213 def get_url(self): 

214 return self.url 

215 

216 def _create_port(self, switch_id, switch_port, network, vlan): 

217 """ 

218 1 - Look for virtual port groups for provided switch_id, switch_port using name 

219 2 - It the virtual port group does not exist, create it 

220 3 - Create virtual machine interface for the indicated network and vlan 

221 """ 

222 self.logger.debug( 

223 "create_port: switch_id: {}, switch_port: {}, network: {}, vlan: {}".format( 

224 switch_id, switch_port, network, vlan 

225 ) 

226 ) 

227 

228 # 1 - Check if the vpg exists 

229 vpg_name = self.underlay_api.get_vpg_name(switch_id, switch_port) 

230 vpg = self.underlay_api.get_vpg_by_name(vpg_name) 

231 

232 if not vpg: 

233 # 2 - If it does not exist create it 

234 vpg_id, _ = self.underlay_api.create_vpg(switch_id, switch_port) 

235 else: 

236 # Assign vpg_id from vpg 

237 vpg_id = vpg.get("uuid") 

238 

239 # 3 - Check if the vmi alreaady exists 

240 vmi_id, _ = self.underlay_api.create_vmi(switch_id, switch_port, network, vlan) 

241 self.logger.debug("port created") 

242 

243 return vpg_id, vmi_id 

244 

245 def _delete_port(self, switch_id, switch_port, vlan): 

246 self.logger.debug( 

247 "delete port, switch_id: {}, switch_port: {}, vlan: {}".format( 

248 switch_id, switch_port, vlan 

249 ) 

250 ) 

251 

252 vpg_name = self.underlay_api.get_vpg_name(switch_id, switch_port) 

253 vmi_name = self.underlay_api.get_vmi_name(switch_id, switch_port, vlan) 

254 

255 # 1 - Obtain vpg by id (if not vpg_id must have been error creating ig, nothing to be done) 

256 vpg_fqdn = ["default-global-system-config", self.fabric, vpg_name] 

257 vpg = self.underlay_api.get_by_fq_name("virtual-port-group", vpg_fqdn) 

258 

259 if not vpg: 

260 self.logger.warning("vpg: {} to be deleted not found".format(vpg_name)) 

261 else: 

262 # 2 - Get vmi interfaces from vpg 

263 vmi_list = vpg.get("virtual_machine_interface_refs") 

264 

265 if not vmi_list: 

266 # must have been an error during port creation when vmi is created 

267 # may happen if there has been an error during creation 

268 self.logger.warning( 

269 "vpg: {} has not vmi, will delete nothing".format(vpg) 

270 ) 

271 else: 

272 num_vmis = len(vmi_list) 

273 

274 for vmi in vmi_list: 

275 fqdn = vmi.get("to") 

276 # check by name 

277 

278 if fqdn[2] == vmi_name: 

279 self.underlay_api.unref_vmi_vpg( 

280 vpg.get("uuid"), vmi.get("uuid"), fqdn 

281 ) 

282 self.underlay_api.delete_vmi(vmi.get("uuid")) 

283 num_vmis = num_vmis - 1 

284 

285 # 3 - If there are no more vmi delete the vpg 

286 if not vmi_list or num_vmis == 0: 

287 self.underlay_api.delete_vpg(vpg.get("uuid")) 

288 

289 def check_credentials(self): 

290 """Check if the connector itself can access the SDN/WIM with the provided url (wim.wim_url), 

291 user (wim_account.user), and password (wim_account.password) 

292 

293 Raises: 

294 SdnConnectorError: Issues regarding authorization, access to 

295 external URLs, etc are detected. 

296 """ 

297 self.logger.debug("") 

298 

299 try: 

300 resp = self.underlay_api.check_auth() 

301 if not resp: 

302 raise SdnConnectorError("Empty response") 

303 except Exception as e: 

304 self.logger.error("Error checking credentials") 

305 

306 raise SdnConnectorError("Error checking credentials: {}".format(str(e))) 

307 

308 def get_connectivity_service_status(self, service_uuid, conn_info=None): 

309 """Monitor the status of the connectivity service established 

310 

311 Arguments: 

312 service_uuid (str): UUID of the connectivity service 

313 conn_info (dict or None): Information returned by the connector 

314 during the service creation/edition and subsequently stored in 

315 the database. 

316 

317 Returns: 

318 dict: JSON/YAML-serializable dict that contains a mandatory key 

319 ``sdn_status`` associated with one of the following values:: 

320 

321 {'sdn_status': 'ACTIVE'} 

322 # The service is up and running. 

323 

324 {'sdn_status': 'INACTIVE'} 

325 # The service was created, but the connector 

326 # cannot determine yet if connectivity exists 

327 # (ideally, the caller needs to wait and check again). 

328 

329 {'sdn_status': 'DOWN'} 

330 # Connection was previously established, 

331 # but an error/failure was detected. 

332 

333 {'sdn_status': 'ERROR'} 

334 # An error occurred when trying to create the service/ 

335 # establish the connectivity. 

336 

337 {'sdn_status': 'BUILD'} 

338 # Still trying to create the service, the caller 

339 # needs to wait and check again. 

340 

341 Additionally ``error_msg``(**str**) and ``sdn_info``(**dict**) 

342 keys can be used to provide additional status explanation or 

343 new information available for the connectivity service. 

344 """ 

345 self.logger.debug("") 

346 

347 try: 

348 resp = self.underlay_api.get_virtual_network(service_uuid) 

349 if not resp: 

350 raise SdnConnectorError("Empty response") 

351 

352 if resp: 

353 vnet_info = resp 

354 

355 # Check if conn_info reports error 

356 if conn_info.get("sdn_status") == "ERROR": 

357 return {"sdn_status": "ERROR", "sdn_info": conn_info} 

358 else: 

359 return {"sdn_status": "ACTIVE", "sdn_info": vnet_info} 

360 else: 

361 return {"sdn_status": "ERROR", "sdn_info": "not found"} 

362 except SdnConnectorError: 

363 raise 

364 except HttpException as e: 

365 self.logger.error("Error getting connectivity service: {}".format(e)) 

366 

367 raise SdnConnectorError( 

368 "Exception deleting connectivity service: {}".format(str(e)) 

369 ) 

370 except Exception as e: 

371 self.logger.error( 

372 "Exception getting connectivity service info: %s", e, exc_info=True 

373 ) 

374 

375 return {"sdn_status": "ERROR", "error_msg": str(e)} 

376 

377 def create_connectivity_service(self, service_type, connection_points, **kwargs): 

378 """ 

379 Establish SDN/WAN connectivity between the endpoints 

380 :param service_type: (str): ``ELINE`` (L2), ``ELAN`` (L2), ``ETREE`` (L2), ``L3``. 

381 :param connection_points: (list): each point corresponds to 

382 an entry point to be connected. For WIM: from the DC to the transport network. 

383 For SDN: Compute/PCI to the transport network. One 

384 connection point serves to identify the specific access and 

385 some other service parameters, such as encapsulation type. 

386 Each item of the list is a dict with: 

387 "service_endpoint_id": (str)(uuid) Same meaning that for 'service_endpoint_mapping' (see __init__) 

388 In case the config attribute mapping_not_needed is True, this value is not relevant. In this case 

389 it will contain the string "device_id:device_interface_id" 

390 "service_endpoint_encapsulation_type": None, "dot1q", ... 

391 "service_endpoint_encapsulation_info": (dict) with: 

392 "vlan": ..., (int, present if encapsulation is dot1q) 

393 "vni": ... (int, present if encapsulation is vxlan), 

394 "peers": [(ipv4_1), (ipv4_2)] (present if encapsulation is vxlan) 

395 "mac": ... 

396 "device_id": ..., same meaning that for 'service_endpoint_mapping' (see __init__) 

397 "device_interface_id": same meaning that for 'service_endpoint_mapping' (see __init__) 

398 "switch_dpid": ..., present if mapping has been found for this device_id,device_interface_id 

399 "switch_port": ... present if mapping has been found for this device_id,device_interface_id 

400 "service_mapping_info": present if mapping has been found for this device_id,device_interface_id 

401 :param kwargs: For future versions: 

402 bandwidth (int): value in kilobytes 

403 latency (int): value in milliseconds 

404 Other QoS might be passed as keyword arguments. 

405 :return: tuple: ``(service_id, conn_info)`` containing: 

406 - *service_uuid* (str): UUID of the established connectivity service 

407 - *conn_info* (dict or None): Information to be stored at the database (or ``None``). 

408 This information will be provided to the :meth:`~.edit_connectivity_service` and :obj:`~.delete`. 

409 **MUST** be JSON/YAML-serializable (plain data structures). 

410 :raises: SdnConnectorException: In case of error. Nothing should be created in this case. 

411 Provide the parameter http_code 

412 """ 

413 # Step 1. Check in the overlay controller the virtual network created by the VIM 

414 # Best option: get network id of the VIM as param (if the VIM already created the network), 

415 # and do a request to the controller of the virtual networks whose VIM network id is the provided 

416 # Next best option: obtain the network by doing a request to the controller 

417 # of the virtual networks using the VLAN ID of any service endpoint. 

418 # 1.1 Read VLAN ID from a service endpoint 

419 # 1.2 Look for virtual networks with "Provider Network" including a VLAN ID. 

420 # 1.3 If more than one, ERROR 

421 # Step 2. Modify the existing virtual network in the overlay controller 

422 # 2.1 Add VNI (VxLAN Network Identifier - one free from the provided range) 

423 # 2.2 Add RouteTarget (RT) ('ASN:VNI', ASN = Autonomous System Number, provided as param or read from 

424 # controller config) 

425 # Step 3. Create a virtual network in the underlay controller 

426 # 3.1 Create virtual network (name, VNI, RT) 

427 # If the network already existed in the overlay controller, we should use the same name 

428 # name = 'osm-plugin-' + overlay_name 

429 # Else: 

430 # name = 'osm-plugin-' + VNI 

431 self.logger.info( 

432 "create_connectivity_service, service_type: {}, connection_points: {}".format( 

433 service_type, connection_points 

434 ) 

435 ) 

436 

437 if service_type.lower() != "elan": 

438 raise SdnConnectorError( 

439 "Only ELAN network type is supported by Juniper Contrail." 

440 ) 

441 

442 try: 

443 # Initialize data 

444 conn_info = None 

445 

446 # 1 - Filter connection_points (transform cp to a dictionary with no duplicates) 

447 # This data will be returned even if no cp can be created if something is created 

448 work_cps = {} 

449 for cp in connection_points: 

450 switch_id = cp.get("service_endpoint_encapsulation_info").get( 

451 "switch_dpid" 

452 ) 

453 switch_port = cp.get("service_endpoint_encapsulation_info").get( 

454 "switch_port" 

455 ) 

456 service_endpoint_id = cp.get("service_endpoint_id") 

457 cp_name = self.underlay_api.get_vpg_name(switch_id, switch_port) 

458 add_cp = work_cps.get(cp_name) 

459 

460 if not add_cp: 

461 # check cp has vlan 

462 vlan = cp.get("service_endpoint_encapsulation_info").get("vlan") 

463 

464 if vlan: 

465 # add cp to dict 

466 service_endpoint_ids = [] 

467 service_endpoint_ids.append(service_endpoint_id) 

468 add_cp = { 

469 "service_endpoint_ids": service_endpoint_ids, 

470 "switch_dpid": switch_id, 

471 "switch_port": switch_port, 

472 "vlan": vlan, 

473 } 

474 work_cps[cp_name] = add_cp 

475 else: 

476 self.logger.warning( 

477 "cp service_endpoint_id : {} has no vlan, ignore".format( 

478 service_endpoint_id 

479 ) 

480 ) 

481 else: 

482 # add service_endpoint_id to list 

483 service_endpoint_ids = add_cp["service_endpoint_ids"] 

484 service_endpoint_ids.append(service_endpoint_id) 

485 

486 # 2 - Obtain free VNI 

487 vni = self._generate_vni() 

488 self.logger.debug("VNI: {}".format(vni)) 

489 

490 # 3 - Create virtual network (name, VNI, RT), by the moment the name will use VNI 

491 retry = 0 

492 while retry < self._max_duplicate_retry: 

493 try: 

494 vnet_name = "osm-plugin-" + str(vni) 

495 vnet_id, _ = self.underlay_api.create_virtual_network( 

496 vnet_name, vni 

497 ) 

498 self.used_vni.add(vni) 

499 break 

500 except DuplicateFound as e: 

501 self.logger.debug( 

502 "Duplicate error for vnet_name: {}".format(vnet_name) 

503 ) 

504 self.used_vni.add(vni) 

505 retry += 1 

506 

507 if retry >= self._max_duplicate_retry: 

508 raise e 

509 else: 

510 # Try to obtain a new vni 

511 vni = self._generate_vni() 

512 continue 

513 

514 conn_info = { 

515 "vnet": { 

516 "uuid": vnet_id, 

517 "name": vnet_name, 

518 }, 

519 "connection_points": work_cps, # dict with port_name as key 

520 } 

521 

522 # 4 - Create a port for each endpoint 

523 for cp in work_cps.values(): 

524 switch_id = cp.get("switch_dpid") 

525 switch_port = cp.get("switch_port") 

526 vlan = cp.get("vlan") 

527 vpg_id, vmi_id = self._create_port( 

528 switch_id, switch_port, vnet_name, vlan 

529 ) 

530 cp["vpg_id"] = vpg_id 

531 cp["vmi_id"] = vmi_id 

532 

533 self.logger.info( 

534 "created connectivity service, uuid: {}, name: {}".format( 

535 vnet_id, vnet_name 

536 ) 

537 ) 

538 

539 return vnet_id, conn_info 

540 except Exception as e: 

541 # Log error 

542 if isinstance(e, SdnConnectorError) or isinstance(e, HttpException): 

543 self.logger.error("Error creating connectivity service: {}".format(e)) 

544 else: 

545 self.logger.error( 

546 "Error creating connectivity service: {}".format(e), exc_info=True 

547 ) 

548 

549 # If nothing is created raise error else return what has been created and mask as error 

550 if not conn_info: 

551 raise SdnConnectorError( 

552 "Exception create connectivity service: {}".format(str(e)) 

553 ) 

554 else: 

555 conn_info["sdn_status"] = "ERROR" 

556 conn_info["sdn_info"] = repr(e) 

557 # iterate over not added connection_points and add but marking them as error 

558 for cp in work_cps.values(): 

559 if not cp.get("vmi_id") or not cp.get("vpg_id"): 

560 cp["sdn_status"] = "ERROR" 

561 

562 return vnet_id, conn_info 

563 

564 def delete_connectivity_service(self, service_uuid, conn_info=None): 

565 """ 

566 Disconnect multi-site endpoints previously connected 

567 

568 :param service_uuid: The one returned by create_connectivity_service 

569 :param conn_info: The one returned by last call to 'create_connectivity_service' or 'edit_connectivity_service' 

570 if they do not return None 

571 :return: None 

572 :raises: SdnConnectorException: In case of error. The parameter http_code must be filled 

573 """ 

574 self.logger.info( 

575 "delete_connectivity_service vnet_name: {}, connection_points: {}".format( 

576 service_uuid, conn_info 

577 ) 

578 ) 

579 

580 try: 

581 vnet_uuid = service_uuid 

582 # vnet_name = conn_info["vnet"]["name"] 

583 # always should exist as the network is the first thing created 

584 work_cps = conn_info["connection_points"] 

585 

586 # 1: For each connection point delete vlan from vpg and it is is the 

587 # last one, delete vpg 

588 for cp in work_cps.values(): 

589 self._delete_port( 

590 cp.get("switch_dpid"), cp.get("switch_port"), cp.get("vlan") 

591 ) 

592 

593 # 2: Delete vnet 

594 self.underlay_api.delete_virtual_network(vnet_uuid) 

595 self.logger.info( 

596 "deleted connectivity_service vnet_uuid: {}, connection_points: {}".format( 

597 service_uuid, conn_info 

598 ) 

599 ) 

600 except SdnConnectorError: 

601 raise 

602 except HttpException as e: 

603 self.logger.error("Error deleting connectivity service: {}".format(e)) 

604 

605 raise SdnConnectorError( 

606 "Exception deleting connectivity service: {}".format(str(e)) 

607 ) 

608 except Exception as e: 

609 self.logger.error( 

610 "Error deleting connectivity service: {}".format(e), 

611 exc_info=True, 

612 ) 

613 

614 raise SdnConnectorError( 

615 "Exception deleting connectivity service: {}".format(str(e)) 

616 ) 

617 

618 def edit_connectivity_service( 

619 self, service_uuid, conn_info=None, connection_points=None, **kwargs 

620 ): 

621 """Change an existing connectivity service. 

622 

623 This method's arguments and return value follow the same convention as 

624 :meth:`~.create_connectivity_service`. 

625 

626 :param service_uuid: UUID of the connectivity service. 

627 :param conn_info: (dict or None): Information previously returned by last call to create_connectivity_service 

628 or edit_connectivity_service 

629 :param connection_points: (list): If provided, the old list of connection points will be replaced. 

630 :param kwargs: Same meaning that create_connectivity_service 

631 :return: dict or None: Information to be updated and stored at the database. 

632 When ``None`` is returned, no information should be changed. 

633 When an empty dict is returned, the database record will be deleted. 

634 **MUST** be JSON/YAML-serializable (plain data structures). 

635 Raises: 

636 SdnConnectorException: In case of error. 

637 """ 

638 # 0 - Check if there are connection_points marked as error and delete them 

639 # 1 - Compare conn_info (old connection points) and connection_points (new ones to be applied): 

640 # Obtain list of connection points to be added and to be deleted 

641 # Obtain vlan and check it has not changed 

642 # 2 - Obtain network: Check vnet exists and obtain name 

643 # 3 - Delete unnecesary ports 

644 # 4 - Add new ports 

645 self.logger.info( 

646 "edit connectivity service, service_uuid: {}, conn_info: {}, " 

647 "connection points: {} ".format(service_uuid, conn_info, connection_points) 

648 ) 

649 

650 # conn_info should always exist and have connection_points and vnet elements 

651 old_cp = conn_info.get("connection_points", {}) 

652 

653 # Check if an element of old_cp is marked as error, in case it is delete it 

654 # Not return a new conn_info in this case because it is only partial information 

655 # Current conn_info already marks ports as error 

656 try: 

657 deleted_ports = [] 

658 for cp in old_cp.values(): 

659 if cp.get("sdn_status") == "ERROR": 

660 switch_id = cp.get("switch_dpid") 

661 switch_port = cp.get("switch_port") 

662 old_vlan = cp.get("vlan") 

663 self._delete_port(switch_id, switch_port, old_vlan) 

664 deleted_ports.append( 

665 self.underlay_api.get_vpg_name(switch_id, switch_port) 

666 ) 

667 

668 for port in deleted_ports: 

669 del old_cp[port] 

670 

671 # Delete sdn_status and sdn_info if exists (possibly marked as error) 

672 if conn_info.get("vnet", {}).get("sdn_status"): 

673 del conn_info["vnet"]["sdn_status"] 

674 except HttpException as e: 

675 self.logger.error( 

676 "Error trying to delete old ports marked as error: {}".format(e) 

677 ) 

678 

679 raise SdnConnectorError(e) 

680 except SdnConnectorError as e: 

681 self.logger.error( 

682 "Error trying to delete old ports marked as error: {}".format(e) 

683 ) 

684 

685 raise 

686 except Exception as e: 

687 self.logger.error( 

688 "Error trying to delete old ports marked as error: {}".format(e), 

689 exc_info=True, 

690 ) 

691 

692 raise SdnConnectorError( 

693 "Error trying to delete old ports marked as error: {}".format(e) 

694 ) 

695 

696 if connection_points: 

697 # Check and obtain what should be added and deleted, if there is an error here raise an exception 

698 try: 

699 work_cps = {} 

700 for cp in connection_points: 

701 switch_id = cp.get("service_endpoint_encapsulation_info").get( 

702 "switch_dpid" 

703 ) 

704 switch_port = cp.get("service_endpoint_encapsulation_info").get( 

705 "switch_port" 

706 ) 

707 service_endpoint_id = cp.get("service_endpoint_id") 

708 cp_name = self.underlay_api.get_vpg_name(switch_id, switch_port) 

709 add_cp = work_cps.get(cp_name) 

710 

711 if not add_cp: 

712 # add cp to dict 

713 # check cp has vlan 

714 vlan = cp.get("service_endpoint_encapsulation_info").get("vlan") 

715 

716 if vlan: 

717 service_endpoint_ids = [] 

718 service_endpoint_ids.append(service_endpoint_id) 

719 add_cp = { 

720 "service_endpoint_ids": service_endpoint_ids, 

721 "switch_dpid": switch_id, 

722 "switch_port": switch_port, 

723 "vlan": vlan, 

724 } 

725 work_cps[cp_name] = add_cp 

726 else: 

727 self.logger.warning( 

728 "cp service_endpoint_id : {} has no vlan, ignore".format( 

729 service_endpoint_id 

730 ) 

731 ) 

732 else: 

733 # add service_endpoint_id to list 

734 service_endpoint_ids = add_cp["service_endpoint_ids"] 

735 service_endpoint_ids.append(service_endpoint_id) 

736 

737 old_port_list = list(old_cp.keys()) 

738 port_list = list(work_cps.keys()) 

739 to_delete_ports = list(set(old_port_list) - set(port_list)) 

740 to_add_ports = list(set(port_list) - set(old_port_list)) 

741 self.logger.debug("ports to delete: {}".format(to_delete_ports)) 

742 self.logger.debug("ports to add: {}".format(to_add_ports)) 

743 

744 # Obtain network (check it is correctly created) 

745 vnet = self.underlay_api.get_virtual_network(service_uuid) 

746 if vnet: 

747 vnet_name = vnet["name"] 

748 else: 

749 raise SdnConnectorError( 

750 "vnet uuid: {} not found".format(service_uuid) 

751 ) 

752 except SdnConnectorError: 

753 raise 

754 except Exception as e: 

755 self.logger.error( 

756 "Error edit connectivity service: {}".format(e), exc_info=True 

757 ) 

758 

759 raise SdnConnectorError( 

760 "Exception edit connectivity service: {}".format(str(e)) 

761 ) 

762 

763 # Delete unneeded ports and add new ones: if there is an error return conn_info 

764 try: 

765 # Connection points returned in con_info should reflect what has (and should as ERROR) be done 

766 # Start with old cp dictionary and modify it as we work 

767 conn_info_cp = old_cp 

768 

769 # Delete unneeded ports 

770 deleted_ports = [] 

771 for port_name in conn_info_cp.keys(): 

772 if port_name in to_delete_ports: 

773 cp = conn_info_cp[port_name] 

774 switch_id = cp.get("switch_dpid") 

775 switch_port = cp.get("switch_port") 

776 self.logger.debug( 

777 "delete port switch_id={}, switch_port={}".format( 

778 switch_id, switch_port 

779 ) 

780 ) 

781 self._delete_port(switch_id, switch_port, vlan) 

782 deleted_ports.append(port_name) 

783 

784 # Delete ports 

785 for port_name in deleted_ports: 

786 del conn_info_cp[port_name] 

787 

788 # Add needed ports 

789 for port_name, cp in work_cps.items(): 

790 if port_name in to_add_ports: 

791 switch_id = cp.get("switch_dpid") 

792 switch_port = cp.get("switch_port") 

793 vlan = cp.get("vlan") 

794 self.logger.debug( 

795 "add port switch_id={}, switch_port={}".format( 

796 switch_id, switch_port 

797 ) 

798 ) 

799 vpg_id, vmi_id = self._create_port( 

800 switch_id, switch_port, vnet_name, vlan 

801 ) 

802 cp_added = cp.copy() 

803 cp_added["vpg_id"] = vpg_id 

804 cp_added["vmi_id"] = vmi_id 

805 conn_info_cp[port_name] = cp_added 

806 

807 # replace endpoints in case they have changed 

808 conn_info_cp[port_name]["service_endpoint_ids"] = cp[ 

809 "service_endpoint_ids" 

810 ] 

811 

812 conn_info["connection_points"] = conn_info_cp 

813 return conn_info 

814 

815 except Exception as e: 

816 # Log error 

817 if isinstance(e, SdnConnectorError) or isinstance(e, HttpException): 

818 self.logger.error( 

819 "Error edit connectivity service: {}".format(e), exc_info=True 

820 ) 

821 else: 

822 self.logger.error("Error edit connectivity service: {}".format(e)) 

823 

824 # There has been an error mount conn_info_cp marking as error cp that should 

825 # have been deleted but have not or should have been added 

826 for port_name, cp in conn_info_cp.items(): 

827 if port_name in to_delete_ports: 

828 cp["sdn_status"] = "ERROR" 

829 

830 for port_name, cp in work_cps.items(): 

831 curr_cp = conn_info_cp.get(port_name) 

832 

833 if not curr_cp: 

834 cp_error = work_cps.get(port_name).copy() 

835 cp_error["sdn_status"] = "ERROR" 

836 conn_info_cp[port_name] = cp_error 

837 

838 conn_info_cp[port_name]["service_endpoint_ids"] = cp[ 

839 "service_endpoint_ids" 

840 ] 

841 

842 conn_info["sdn_status"] = "ERROR" 

843 conn_info["sdn_info"] = repr(e) 

844 conn_info["connection_points"] = conn_info_cp 

845 

846 return conn_info 

847 else: 

848 # Connection points have not changed, so do nothing 

849 self.logger.info("no new connection_points provided, nothing to be done") 

850 

851 return 

852 

853 

854if __name__ == "__main__": 

855 # Init logger 

856 log_format = "%(asctime)s %(levelname)s %(name)s %(filename)s:%(lineno)s %(funcName)s(): %(message)s" 

857 log_formatter = logging.Formatter(log_format, datefmt="%Y-%m-%dT%H:%M:%S") 

858 handler = logging.StreamHandler() 

859 handler.setFormatter(log_formatter) 

860 logger = logging.getLogger("ro.sdn.junipercontrail") 

861 # logger.setLevel(level=logging.ERROR) 

862 # logger.setLevel(level=logging.INFO) 

863 logger.setLevel(level=logging.DEBUG) 

864 logger.addHandler(handler) 

865 

866 # Read config 

867 with open("test.yaml") as f: 

868 config = yaml.safe_load(f.read()) 

869 

870 wim = {"wim_url": config.pop("wim_url")} 

871 wim_account = {"user": config.pop("user"), "password": config.pop("password")} 

872 logger.info("wim: {}, wim_account: {}, config: {}".format(wim, wim_account, config)) 

873 

874 # Init controller 

875 juniper_contrail = JuniperContrail( 

876 wim=wim, wim_account=wim_account, config=config, logger=logger 

877 ) 

878 

879 # Tests 

880 # Generate VNI 

881 for i in range(5): 

882 vni = juniper_contrail._generate_vni() 

883 juniper_contrail.used_vni.add(vni) 

884 

885 print(juniper_contrail.used_vni) 

886 # juniper_contrail.used_vni.remove(1000003) 

887 print(juniper_contrail.used_vni) 

888 

889 for i in range(2): 

890 vni = juniper_contrail._generate_vni() 

891 juniper_contrail.used_vni.add(vni) 

892 

893 print(juniper_contrail.used_vni) 

894 

895 # 0. Check credentials 

896 print("0. Check credentials") 

897 # juniper_contrail.check_credentials() 

898 

899 # 1 - Create and delete connectivity service 

900 conn_point_0 = { 

901 "service_endpoint_id": "0000:83:11.4", 

902 "service_endpoint_encapsulation_type": "dot1q", 

903 "service_endpoint_encapsulation_info": { 

904 "switch_dpid": "LEAF-1", 

905 "switch_port": "xe-0/0/17", 

906 "vlan": "501", 

907 }, 

908 } 

909 conn_point_1 = { 

910 "service_endpoint_id": "0000:81:10.3", 

911 "service_endpoint_encapsulation_type": "dot1q", 

912 "service_endpoint_encapsulation_info": { 

913 "switch_dpid": "LEAF-2", 

914 "switch_port": "xe-0/0/16", 

915 "vlan": "501", 

916 }, 

917 } 

918 conn_point_2 = { 

919 "service_endpoint_id": "0000:08:11.7", 

920 "service_endpoint_encapsulation_type": "dot1q", 

921 "service_endpoint_encapsulation_info": { 

922 "switch_dpid": "LEAF-2", 

923 "switch_port": "xe-0/0/16", 

924 "vlan": "502", 

925 }, 

926 } 

927 conn_point_3 = { 

928 "service_endpoint_id": "0000:83:10.4", 

929 "service_endpoint_encapsulation_type": "dot1q", 

930 "service_endpoint_encapsulation_info": { 

931 "switch_dpid": "LEAF-1", 

932 "switch_port": "xe-0/0/17", 

933 "vlan": "502", 

934 }, 

935 } 

936 

937 # 1 - Define connection points 

938 logger.debug("create first connection service") 

939 print("Create connectivity service") 

940 connection_points = [conn_point_0, conn_point_1] 

941 service_id, conn_info = juniper_contrail.create_connectivity_service( 

942 "ELAN", connection_points 

943 ) 

944 logger.info("Created connectivity service 1") 

945 logger.info(service_id) 

946 logger.info(yaml.safe_dump(conn_info, indent=4, default_flow_style=False)) 

947 

948 logger.debug("create second connection service") 

949 print("Create connectivity service") 

950 connection_points = [conn_point_2, conn_point_3] 

951 service_id2, conn_info2 = juniper_contrail.create_connectivity_service( 

952 "ELAN", connection_points 

953 ) 

954 logger.info("Created connectivity service 2") 

955 logger.info(service_id2) 

956 logger.info(yaml.safe_dump(conn_info2, indent=4, default_flow_style=False)) 

957 

958 logger.debug("Delete connectivity service 1") 

959 juniper_contrail.delete_connectivity_service(service_id, conn_info) 

960 logger.debug("Delete Ok") 

961 

962 logger.debug("Delete connectivity service 2") 

963 juniper_contrail.delete_connectivity_service(service_id2, conn_info2) 

964 logger.debug("Delete Ok")