Coverage for n2vc/n2vc_juju_conn.py: 51%

490 statements  

« prev     ^ index     » next       coverage.py v7.6.12, created at 2025-05-07 06:04 +0000

1## 

2# Copyright 2019 Telefonica Investigacion y Desarrollo, S.A.U. 

3# This file is part of OSM 

4# All Rights Reserved. 

5# 

6# Licensed under the Apache License, Version 2.0 (the "License"); 

7# you may not use this file except in compliance with the License. 

8# You may obtain a copy of the License at 

9# 

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

11# 

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

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

14# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 

15# implied. 

16# See the License for the specific language governing permissions and 

17# limitations under the License. 

18# 

19# For those usages not covered by the Apache License, Version 2.0 please 

20# contact with: nfvlabs@tid.es 

21## 

22 

23import asyncio 

24import logging 

25 

26from n2vc.config import EnvironConfig 

27from n2vc.definitions import RelationEndpoint 

28from n2vc.exceptions import ( 

29 N2VCBadArgumentsException, 

30 N2VCException, 

31 N2VCConnectionException, 

32 N2VCExecutionException, 

33 N2VCApplicationExists, 

34 JujuApplicationExists, 

35 # N2VCNotFound, 

36 MethodNotImplemented, 

37) 

38from n2vc.n2vc_conn import N2VCConnector 

39from n2vc.n2vc_conn import obj_to_dict, obj_to_yaml 

40from n2vc.libjuju import Libjuju, retry_callback 

41from n2vc.store import MotorStore 

42from n2vc.utils import get_ee_id_components, generate_random_alfanum_string 

43from n2vc.vca.connection import get_connection 

44from retrying_async import retry 

45from typing import Tuple 

46 

47 

48class N2VCJujuConnector(N2VCConnector): 

49 

50 """ 

51 #################################################################################### 

52 ################################### P U B L I C #################################### 

53 #################################################################################### 

54 """ 

55 

56 BUILT_IN_CLOUDS = ["localhost", "microk8s"] 

57 libjuju = None 

58 

59 def __init__( 

60 self, 

61 db: object, 

62 fs: object, 

63 log: object = None, 

64 on_update_db=None, 

65 ): 

66 """ 

67 Constructor 

68 

69 :param: db: Database object from osm_common 

70 :param: fs: Filesystem object from osm_common 

71 :param: log: Logger 

72 :param: on_update_db: Callback function to be called for updating the database. 

73 """ 

74 

75 # parent class constructor 

76 N2VCConnector.__init__(self, db=db, fs=fs, log=log, on_update_db=on_update_db) 

77 

78 # silence websocket traffic log 

79 logging.getLogger("websockets.protocol").setLevel(logging.INFO) 

80 logging.getLogger("juju.client.connection").setLevel(logging.WARN) 

81 logging.getLogger("model").setLevel(logging.WARN) 

82 

83 self.log.info("Initializing N2VC juju connector...") 

84 

85 db_uri = EnvironConfig(prefixes=["OSMLCM_", "OSMMON_"]).get("database_uri") 

86 self._store = MotorStore(db_uri) 

87 self.loading_libjuju = asyncio.Lock() 

88 self.delete_namespace_locks = {} 

89 self.log.info("N2VC juju connector initialized") 

90 

91 async def get_status( 

92 self, namespace: str, yaml_format: bool = True, vca_id: str = None 

93 ): 

94 """ 

95 Get status from all juju models from a VCA 

96 

97 :param namespace: we obtain ns from namespace 

98 :param yaml_format: returns a yaml string 

99 :param: vca_id: VCA ID from which the status will be retrieved. 

100 """ 

101 # TODO: Review where is this function used. It is not optimal at all to get the status 

102 # from all the juju models of a particular VCA. Additionally, these models might 

103 # not have been deployed by OSM, in that case we are getting information from 

104 # deployments outside of OSM's scope. 

105 

106 # self.log.info('Getting NS status. namespace: {}'.format(namespace)) 

107 libjuju = await self._get_libjuju(vca_id) 

108 

109 _nsi_id, ns_id, _vnf_id, _vdu_id, _vdu_count = self._get_namespace_components( 

110 namespace=namespace 

111 ) 

112 # model name is ns_id 

113 model_name = ns_id 

114 if model_name is None: 

115 msg = "Namespace {} not valid".format(namespace) 

116 self.log.error(msg) 

117 raise N2VCBadArgumentsException(msg, ["namespace"]) 

118 

119 status = {} 

120 models = await libjuju.list_models(contains=ns_id) 

121 

122 for m in models: 

123 status[m] = await libjuju.get_model_status(m) 

124 

125 if yaml_format: 

126 return obj_to_yaml(status) 

127 else: 

128 return obj_to_dict(status) 

129 

130 async def update_vca_status(self, vcastatus: dict, vca_id: str = None): 

131 """ 

132 Add all configs, actions, executed actions of all applications in a model to vcastatus dict. 

133 

134 :param vcastatus: dict containing vcaStatus 

135 :param: vca_id: VCA ID 

136 

137 :return: None 

138 """ 

139 try: 

140 libjuju = await self._get_libjuju(vca_id) 

141 for model_name in vcastatus: 

142 # Adding executed actions 

143 vcastatus[model_name][ 

144 "executedActions" 

145 ] = await libjuju.get_executed_actions(model_name) 

146 for application in vcastatus[model_name]["applications"]: 

147 # Adding application actions 

148 vcastatus[model_name]["applications"][application][ 

149 "actions" 

150 ] = await libjuju.get_actions(application, model_name) 

151 # Adding application configs 

152 vcastatus[model_name]["applications"][application][ 

153 "configs" 

154 ] = await libjuju.get_application_configs(model_name, application) 

155 except Exception as e: 

156 self.log.debug("Error in updating vca status: {}".format(str(e))) 

157 

158 async def create_execution_environment( 

159 self, 

160 namespace: str, 

161 db_dict: dict, 

162 reuse_ee_id: str = None, 

163 progress_timeout: float = None, 

164 total_timeout: float = None, 

165 vca_id: str = None, 

166 ) -> (str, dict): 

167 """ 

168 Create an Execution Environment. Returns when it is created or raises an 

169 exception on failing 

170 

171 :param: namespace: Contains a dot separate string. 

172 LCM will use: [<nsi-id>].<ns-id>.<vnf-id>.<vdu-id>[-<count>] 

173 :param: db_dict: where to write to database when the status changes. 

174 It contains a dictionary with {collection: str, filter: {}, path: str}, 

175 e.g. {collection: "nsrs", filter: {_id: <nsd-id>, path: 

176 "_admin.deployed.VCA.3"} 

177 :param: reuse_ee_id: ee id from an older execution. It allows us to reuse an 

178 older environment 

179 :param: progress_timeout: Progress timeout 

180 :param: total_timeout: Total timeout 

181 :param: vca_id: VCA ID 

182 

183 :returns: id of the new execution environment and credentials for it 

184 (credentials can contains hostname, username, etc depending on underlying cloud) 

185 """ 

186 

187 self.log.info( 

188 "Creating execution environment. namespace: {}, reuse_ee_id: {}".format( 

189 namespace, reuse_ee_id 

190 ) 

191 ) 

192 libjuju = await self._get_libjuju(vca_id) 

193 

194 machine_id = None 

195 if reuse_ee_id: 

196 model_name, application_name, machine_id = self._get_ee_id_components( 

197 ee_id=reuse_ee_id 

198 ) 

199 else: 

200 ( 

201 _nsi_id, 

202 ns_id, 

203 _vnf_id, 

204 _vdu_id, 

205 _vdu_count, 

206 ) = self._get_namespace_components(namespace=namespace) 

207 # model name is ns_id 

208 model_name = ns_id 

209 # application name 

210 application_name = self._get_application_name(namespace=namespace) 

211 

212 self.log.debug( 

213 "model name: {}, application name: {}, machine_id: {}".format( 

214 model_name, application_name, machine_id 

215 ) 

216 ) 

217 

218 # create or reuse a new juju machine 

219 try: 

220 if not await libjuju.model_exists(model_name): 

221 await libjuju.add_model(model_name, libjuju.vca_connection.lxd_cloud) 

222 machine, new = await libjuju.create_machine( 

223 model_name=model_name, 

224 machine_id=machine_id, 

225 db_dict=db_dict, 

226 progress_timeout=progress_timeout, 

227 total_timeout=total_timeout, 

228 ) 

229 # id for the execution environment 

230 ee_id = N2VCJujuConnector._build_ee_id( 

231 model_name=model_name, 

232 application_name=application_name, 

233 machine_id=str(machine.entity_id), 

234 ) 

235 self.log.debug("ee_id: {}".format(ee_id)) 

236 

237 if new: 

238 # write ee_id in database 

239 self._write_ee_id_db(db_dict=db_dict, ee_id=ee_id) 

240 

241 except Exception as e: 

242 message = "Error creating machine on juju: {}".format(e) 

243 self.log.error(message) 

244 raise N2VCException(message=message) 

245 

246 # new machine credentials 

247 credentials = {"hostname": machine.dns_name} 

248 

249 self.log.info( 

250 "Execution environment created. ee_id: {}, credentials: {}".format( 

251 ee_id, credentials 

252 ) 

253 ) 

254 

255 return ee_id, credentials 

256 

257 async def register_execution_environment( 

258 self, 

259 namespace: str, 

260 credentials: dict, 

261 db_dict: dict, 

262 progress_timeout: float = None, 

263 total_timeout: float = None, 

264 vca_id: str = None, 

265 ) -> str: 

266 """ 

267 Register an existing execution environment at the VCA 

268 

269 :param: namespace: Contains a dot separate string. 

270 LCM will use: [<nsi-id>].<ns-id>.<vnf-id>.<vdu-id>[-<count>] 

271 :param: credentials: credentials to access the existing execution environment 

272 (it can contains hostname, username, path to private key, 

273 etc depending on underlying cloud) 

274 :param: db_dict: where to write to database when the status changes. 

275 It contains a dictionary with {collection: str, filter: {}, path: str}, 

276 e.g. {collection: "nsrs", filter: {_id: <nsd-id>, path: 

277 "_admin.deployed.VCA.3"} 

278 :param: reuse_ee_id: ee id from an older execution. It allows us to reuse an 

279 older environment 

280 :param: progress_timeout: Progress timeout 

281 :param: total_timeout: Total timeout 

282 :param: vca_id: VCA ID 

283 

284 :returns: id of the execution environment 

285 """ 

286 self.log.info( 

287 "Registering execution environment. namespace={}, credentials={}".format( 

288 namespace, credentials 

289 ) 

290 ) 

291 libjuju = await self._get_libjuju(vca_id) 

292 

293 if credentials is None: 

294 raise N2VCBadArgumentsException( 

295 message="credentials are mandatory", bad_args=["credentials"] 

296 ) 

297 if credentials.get("hostname"): 

298 hostname = credentials["hostname"] 

299 else: 

300 raise N2VCBadArgumentsException( 

301 message="hostname is mandatory", bad_args=["credentials.hostname"] 

302 ) 

303 if credentials.get("username"): 

304 username = credentials["username"] 

305 else: 

306 raise N2VCBadArgumentsException( 

307 message="username is mandatory", bad_args=["credentials.username"] 

308 ) 

309 if "private_key_path" in credentials: 

310 private_key_path = credentials["private_key_path"] 

311 else: 

312 # if not passed as argument, use generated private key path 

313 private_key_path = self.private_key_path 

314 

315 _nsi_id, ns_id, _vnf_id, _vdu_id, _vdu_count = self._get_namespace_components( 

316 namespace=namespace 

317 ) 

318 

319 # model name 

320 model_name = ns_id 

321 # application name 

322 application_name = self._get_application_name(namespace=namespace) 

323 

324 # register machine on juju 

325 try: 

326 if not await libjuju.model_exists(model_name): 

327 await libjuju.add_model(model_name, libjuju.vca_connection.lxd_cloud) 

328 machine_id = await libjuju.provision_machine( 

329 model_name=model_name, 

330 hostname=hostname, 

331 username=username, 

332 private_key_path=private_key_path, 

333 db_dict=db_dict, 

334 progress_timeout=progress_timeout, 

335 total_timeout=total_timeout, 

336 ) 

337 except Exception as e: 

338 self.log.error("Error registering machine: {}".format(e)) 

339 raise N2VCException( 

340 message="Error registering machine on juju: {}".format(e) 

341 ) 

342 

343 self.log.info("Machine registered: {}".format(machine_id)) 

344 

345 # id for the execution environment 

346 ee_id = N2VCJujuConnector._build_ee_id( 

347 model_name=model_name, 

348 application_name=application_name, 

349 machine_id=str(machine_id), 

350 ) 

351 

352 self.log.info("Execution environment registered. ee_id: {}".format(ee_id)) 

353 

354 return ee_id 

355 

356 # In case of native_charm is being deployed, if JujuApplicationExists error happens 

357 # it will try to add_unit 

358 @retry( 

359 attempts=3, 

360 delay=5, 

361 retry_exceptions=(N2VCApplicationExists,), 

362 timeout=None, 

363 callback=retry_callback, 

364 ) 

365 async def install_configuration_sw( 

366 self, 

367 ee_id: str, 

368 artifact_path: str, 

369 db_dict: dict, 

370 progress_timeout: float = None, 

371 total_timeout: float = None, 

372 config: dict = None, 

373 num_units: int = 1, 

374 vca_id: str = None, 

375 scaling_out: bool = False, 

376 vca_type: str = None, 

377 ): 

378 """ 

379 Install the software inside the execution environment identified by ee_id 

380 

381 :param: ee_id: the id of the execution environment returned by 

382 create_execution_environment or register_execution_environment 

383 :param: artifact_path: where to locate the artifacts (parent folder) using 

384 the self.fs 

385 the final artifact path will be a combination of this 

386 artifact_path and additional string from the config_dict 

387 (e.g. charm name) 

388 :param: db_dict: where to write into database when the status changes. 

389 It contains a dict with 

390 {collection: <str>, filter: {}, path: <str>}, 

391 e.g. {collection: "nsrs", filter: 

392 {_id: <nsd-id>, path: "_admin.deployed.VCA.3"} 

393 :param: progress_timeout: Progress timeout 

394 :param: total_timeout: Total timeout 

395 :param: config: Dictionary with deployment config information. 

396 :param: num_units: Number of units to deploy of a particular charm. 

397 :param: vca_id: VCA ID 

398 :param: scaling_out: Boolean to indicate if it is a scaling out operation 

399 :param: vca_type: VCA type 

400 """ 

401 

402 self.log.info( 

403 ( 

404 "Installing configuration sw on ee_id: {}, " 

405 "artifact path: {}, db_dict: {}" 

406 ).format(ee_id, artifact_path, db_dict) 

407 ) 

408 libjuju = await self._get_libjuju(vca_id) 

409 

410 # check arguments 

411 if ee_id is None or len(ee_id) == 0: 

412 raise N2VCBadArgumentsException( 

413 message="ee_id is mandatory", bad_args=["ee_id"] 

414 ) 

415 if artifact_path is None or len(artifact_path) == 0: 

416 raise N2VCBadArgumentsException( 

417 message="artifact_path is mandatory", bad_args=["artifact_path"] 

418 ) 

419 if db_dict is None: 

420 raise N2VCBadArgumentsException( 

421 message="db_dict is mandatory", bad_args=["db_dict"] 

422 ) 

423 

424 try: 

425 ( 

426 model_name, 

427 application_name, 

428 machine_id, 

429 ) = N2VCJujuConnector._get_ee_id_components(ee_id=ee_id) 

430 self.log.debug( 

431 "model: {}, application: {}, machine: {}".format( 

432 model_name, application_name, machine_id 

433 ) 

434 ) 

435 except Exception: 

436 raise N2VCBadArgumentsException( 

437 message="ee_id={} is not a valid execution environment id".format( 

438 ee_id 

439 ), 

440 bad_args=["ee_id"], 

441 ) 

442 

443 # remove // in charm path 

444 while artifact_path.find("//") >= 0: 

445 artifact_path = artifact_path.replace("//", "/") 

446 

447 # check charm path 

448 if not self.fs.file_exists(artifact_path): 

449 msg = "artifact path does not exist: {}".format(artifact_path) 

450 raise N2VCBadArgumentsException(message=msg, bad_args=["artifact_path"]) 

451 

452 if artifact_path.startswith("/"): 

453 full_path = self.fs.path + artifact_path 

454 else: 

455 full_path = self.fs.path + "/" + artifact_path 

456 

457 try: 

458 if vca_type == "native_charm" and await libjuju.check_application_exists( 

459 model_name, application_name 

460 ): 

461 await libjuju.add_unit( 

462 application_name=application_name, 

463 model_name=model_name, 

464 machine_id=machine_id, 

465 db_dict=db_dict, 

466 progress_timeout=progress_timeout, 

467 total_timeout=total_timeout, 

468 ) 

469 else: 

470 await libjuju.deploy_charm( 

471 model_name=model_name, 

472 application_name=application_name, 

473 path=full_path, 

474 machine_id=machine_id, 

475 db_dict=db_dict, 

476 progress_timeout=progress_timeout, 

477 total_timeout=total_timeout, 

478 config=config, 

479 num_units=num_units, 

480 ) 

481 except JujuApplicationExists as e: 

482 raise N2VCApplicationExists( 

483 message="Error deploying charm into ee={} : {}".format(ee_id, e.message) 

484 ) 

485 except Exception as e: 

486 raise N2VCException( 

487 message="Error deploying charm into ee={} : {}".format(ee_id, e) 

488 ) 

489 

490 self.log.info("Configuration sw installed") 

491 

492 async def install_k8s_proxy_charm( 

493 self, 

494 charm_name: str, 

495 namespace: str, 

496 artifact_path: str, 

497 db_dict: dict, 

498 progress_timeout: float = None, 

499 total_timeout: float = None, 

500 config: dict = None, 

501 vca_id: str = None, 

502 ) -> str: 

503 """ 

504 Install a k8s proxy charm 

505 

506 :param charm_name: Name of the charm being deployed 

507 :param namespace: collection of all the uuids related to the charm. 

508 :param str artifact_path: where to locate the artifacts (parent folder) using 

509 the self.fs 

510 the final artifact path will be a combination of this artifact_path and 

511 additional string from the config_dict (e.g. charm name) 

512 :param dict db_dict: where to write into database when the status changes. 

513 It contains a dict with 

514 {collection: <str>, filter: {}, path: <str>}, 

515 e.g. {collection: "nsrs", filter: 

516 {_id: <nsd-id>, path: "_admin.deployed.VCA.3"} 

517 :param: progress_timeout: Progress timeout 

518 :param: total_timeout: Total timeout 

519 :param config: Dictionary with additional configuration 

520 :param vca_id: VCA ID 

521 

522 :returns ee_id: execution environment id. 

523 """ 

524 self.log.info( 

525 "Installing k8s proxy charm: {}, artifact path: {}, db_dict: {}".format( 

526 charm_name, artifact_path, db_dict 

527 ) 

528 ) 

529 libjuju = await self._get_libjuju(vca_id) 

530 

531 if artifact_path is None or len(artifact_path) == 0: 

532 raise N2VCBadArgumentsException( 

533 message="artifact_path is mandatory", bad_args=["artifact_path"] 

534 ) 

535 if db_dict is None: 

536 raise N2VCBadArgumentsException( 

537 message="db_dict is mandatory", bad_args=["db_dict"] 

538 ) 

539 

540 # remove // in charm path 

541 while artifact_path.find("//") >= 0: 

542 artifact_path = artifact_path.replace("//", "/") 

543 

544 # check charm path 

545 if not self.fs.file_exists(artifact_path): 

546 msg = "artifact path does not exist: {}".format(artifact_path) 

547 raise N2VCBadArgumentsException(message=msg, bad_args=["artifact_path"]) 

548 

549 if artifact_path.startswith("/"): 

550 full_path = self.fs.path + artifact_path 

551 else: 

552 full_path = self.fs.path + "/" + artifact_path 

553 

554 _, ns_id, _, _, _ = self._get_namespace_components(namespace=namespace) 

555 model_name = "{}-k8s".format(ns_id) 

556 if not await libjuju.model_exists(model_name): 

557 await libjuju.add_model(model_name, libjuju.vca_connection.k8s_cloud) 

558 application_name = self._get_application_name(namespace) 

559 

560 try: 

561 await libjuju.deploy_charm( 

562 model_name=model_name, 

563 application_name=application_name, 

564 path=full_path, 

565 machine_id=None, 

566 db_dict=db_dict, 

567 progress_timeout=progress_timeout, 

568 total_timeout=total_timeout, 

569 config=config, 

570 ) 

571 except Exception as e: 

572 raise N2VCException(message="Error deploying charm: {}".format(e)) 

573 

574 self.log.info("K8s proxy charm installed") 

575 ee_id = N2VCJujuConnector._build_ee_id( 

576 model_name=model_name, application_name=application_name, machine_id="k8s" 

577 ) 

578 

579 self._write_ee_id_db(db_dict=db_dict, ee_id=ee_id) 

580 

581 return ee_id 

582 

583 async def get_ee_ssh_public__key( 

584 self, 

585 ee_id: str, 

586 db_dict: dict, 

587 progress_timeout: float = None, 

588 total_timeout: float = None, 

589 vca_id: str = None, 

590 ) -> str: 

591 """ 

592 Get Execution environment ssh public key 

593 

594 :param: ee_id: the id of the execution environment returned by 

595 create_execution_environment or register_execution_environment 

596 :param: db_dict: where to write into database when the status changes. 

597 It contains a dict with 

598 {collection: <str>, filter: {}, path: <str>}, 

599 e.g. {collection: "nsrs", filter: 

600 {_id: <nsd-id>, path: "_admin.deployed.VCA.3"} 

601 :param: progress_timeout: Progress timeout 

602 :param: total_timeout: Total timeout 

603 :param vca_id: VCA ID 

604 :returns: public key of the execution environment 

605 For the case of juju proxy charm ssh-layered, it is the one 

606 returned by 'get-ssh-public-key' primitive. 

607 It raises a N2VC exception if fails 

608 """ 

609 

610 self.log.info( 

611 ( 

612 "Generating priv/pub key pair and get pub key on ee_id: {}, db_dict: {}" 

613 ).format(ee_id, db_dict) 

614 ) 

615 libjuju = await self._get_libjuju(vca_id) 

616 

617 # check arguments 

618 if ee_id is None or len(ee_id) == 0: 

619 raise N2VCBadArgumentsException( 

620 message="ee_id is mandatory", bad_args=["ee_id"] 

621 ) 

622 if db_dict is None: 

623 raise N2VCBadArgumentsException( 

624 message="db_dict is mandatory", bad_args=["db_dict"] 

625 ) 

626 

627 try: 

628 ( 

629 model_name, 

630 application_name, 

631 machine_id, 

632 ) = N2VCJujuConnector._get_ee_id_components(ee_id=ee_id) 

633 self.log.debug( 

634 "model: {}, application: {}, machine: {}".format( 

635 model_name, application_name, machine_id 

636 ) 

637 ) 

638 except Exception: 

639 raise N2VCBadArgumentsException( 

640 message="ee_id={} is not a valid execution environment id".format( 

641 ee_id 

642 ), 

643 bad_args=["ee_id"], 

644 ) 

645 

646 # try to execute ssh layer primitives (if exist): 

647 # generate-ssh-key 

648 # get-ssh-public-key 

649 

650 output = None 

651 

652 application_name = N2VCJujuConnector._format_app_name(application_name) 

653 

654 # execute action: generate-ssh-key 

655 try: 

656 output, _status = await libjuju.execute_action( 

657 model_name=model_name, 

658 application_name=application_name, 

659 action_name="generate-ssh-key", 

660 db_dict=db_dict, 

661 progress_timeout=progress_timeout, 

662 total_timeout=total_timeout, 

663 ) 

664 except Exception as e: 

665 self.log.info( 

666 "Skipping exception while executing action generate-ssh-key: {}".format( 

667 e 

668 ) 

669 ) 

670 

671 # execute action: get-ssh-public-key 

672 try: 

673 output, _status = await libjuju.execute_action( 

674 model_name=model_name, 

675 application_name=application_name, 

676 action_name="get-ssh-public-key", 

677 db_dict=db_dict, 

678 progress_timeout=progress_timeout, 

679 total_timeout=total_timeout, 

680 ) 

681 except Exception as e: 

682 msg = "Cannot execute action get-ssh-public-key: {}\n".format(e) 

683 self.log.info(msg) 

684 raise N2VCExecutionException(e, primitive_name="get-ssh-public-key") 

685 

686 # return public key if exists 

687 return output["pubkey"] if "pubkey" in output else output 

688 

689 async def get_metrics( 

690 self, model_name: str, application_name: str, vca_id: str = None 

691 ) -> dict: 

692 """ 

693 Get metrics from application 

694 

695 :param: model_name: Model name 

696 :param: application_name: Application name 

697 :param: vca_id: VCA ID 

698 

699 :return: Dictionary with obtained metrics 

700 """ 

701 libjuju = await self._get_libjuju(vca_id) 

702 return await libjuju.get_metrics(model_name, application_name) 

703 

704 async def add_relation( 

705 self, provider: RelationEndpoint, requirer: RelationEndpoint 

706 ): 

707 """ 

708 Add relation between two charmed endpoints 

709 

710 :param: provider: Provider relation endpoint 

711 :param: requirer: Requirer relation endpoint 

712 """ 

713 self.log.debug(f"adding new relation between {provider} and {requirer}") 

714 cross_model_relation = ( 

715 provider.model_name != requirer.model_name 

716 or provider.vca_id != requirer.vca_id 

717 ) 

718 try: 

719 if cross_model_relation: 

720 # Cross-model relation 

721 provider_libjuju = await self._get_libjuju(provider.vca_id) 

722 requirer_libjuju = await self._get_libjuju(requirer.vca_id) 

723 offer = await provider_libjuju.offer(provider) 

724 if offer: 

725 saas_name = await requirer_libjuju.consume( 

726 requirer.model_name, offer, provider_libjuju 

727 ) 

728 await requirer_libjuju.add_relation( 

729 requirer.model_name, requirer.endpoint, saas_name 

730 ) 

731 else: 

732 # Standard relation 

733 vca_id = provider.vca_id 

734 model = provider.model_name 

735 libjuju = await self._get_libjuju(vca_id) 

736 # add juju relations between two applications 

737 await libjuju.add_relation( 

738 model_name=model, 

739 endpoint_1=provider.endpoint, 

740 endpoint_2=requirer.endpoint, 

741 ) 

742 except Exception as e: 

743 message = f"Error adding relation between {provider} and {requirer}: {e}" 

744 self.log.error(message) 

745 raise N2VCException(message=message) 

746 

747 async def remove_relation(self): 

748 # TODO 

749 self.log.info("Method not implemented yet") 

750 raise MethodNotImplemented() 

751 

752 async def deregister_execution_environments(self): 

753 self.log.info("Method not implemented yet") 

754 raise MethodNotImplemented() 

755 

756 async def delete_namespace( 

757 self, 

758 namespace: str, 

759 db_dict: dict = None, 

760 total_timeout: float = None, 

761 vca_id: str = None, 

762 ): 

763 """ 

764 Remove a network scenario and its execution environments 

765 :param: namespace: [<nsi-id>].<ns-id> 

766 :param: db_dict: where to write into database when the status changes. 

767 It contains a dict with 

768 {collection: <str>, filter: {}, path: <str>}, 

769 e.g. {collection: "nsrs", filter: 

770 {_id: <nsd-id>, path: "_admin.deployed.VCA.3"} 

771 :param: total_timeout: Total timeout 

772 :param: vca_id: VCA ID 

773 """ 

774 self.log.info("Deleting namespace={}".format(namespace)) 

775 will_not_delete = False 

776 if namespace not in self.delete_namespace_locks: 

777 self.delete_namespace_locks[namespace] = asyncio.Lock() 

778 delete_lock = self.delete_namespace_locks[namespace] 

779 

780 while delete_lock.locked(): 

781 will_not_delete = True 

782 await asyncio.sleep(0.1) 

783 

784 if will_not_delete: 

785 self.log.info("Namespace {} deleted by another worker.".format(namespace)) 

786 return 

787 

788 try: 

789 async with delete_lock: 

790 libjuju = await self._get_libjuju(vca_id) 

791 

792 # check arguments 

793 if namespace is None: 

794 raise N2VCBadArgumentsException( 

795 message="namespace is mandatory", bad_args=["namespace"] 

796 ) 

797 

798 ( 

799 _nsi_id, 

800 ns_id, 

801 _vnf_id, 

802 _vdu_id, 

803 _vdu_count, 

804 ) = self._get_namespace_components(namespace=namespace) 

805 if ns_id is not None: 

806 try: 

807 models = await libjuju.list_models(contains=ns_id) 

808 for model in models: 

809 await libjuju.destroy_model( 

810 model_name=model, total_timeout=total_timeout 

811 ) 

812 except Exception as e: 

813 self.log.error(f"Error deleting namespace {namespace} : {e}") 

814 raise N2VCException( 

815 message="Error deleting namespace {} : {}".format( 

816 namespace, e 

817 ) 

818 ) 

819 else: 

820 raise N2VCBadArgumentsException( 

821 message="only ns_id is permitted to delete yet", 

822 bad_args=["namespace"], 

823 ) 

824 except Exception as e: 

825 self.log.error(f"Error deleting namespace {namespace} : {e}") 

826 raise e 

827 finally: 

828 self.delete_namespace_locks.pop(namespace) 

829 self.log.info("Namespace {} deleted".format(namespace)) 

830 

831 async def delete_execution_environment( 

832 self, 

833 ee_id: str, 

834 db_dict: dict = None, 

835 total_timeout: float = None, 

836 scaling_in: bool = False, 

837 vca_type: str = None, 

838 vca_id: str = None, 

839 application_to_delete: str = None, 

840 ): 

841 """ 

842 Delete an execution environment 

843 :param str ee_id: id of the execution environment to delete 

844 :param dict db_dict: where to write into database when the status changes. 

845 It contains a dict with 

846 {collection: <str>, filter: {}, path: <str>}, 

847 e.g. {collection: "nsrs", filter: 

848 {_id: <nsd-id>, path: "_admin.deployed.VCA.3"} 

849 :param total_timeout: Total timeout 

850 :param scaling_in: Boolean to indicate if it is a scaling in operation 

851 :param vca_type: VCA type 

852 :param vca_id: VCA ID 

853 :param application_to_delete: name of the single application to be deleted 

854 """ 

855 self.log.info("Deleting execution environment ee_id={}".format(ee_id)) 

856 libjuju = await self._get_libjuju(vca_id) 

857 

858 # check arguments 

859 if ee_id is None: 

860 raise N2VCBadArgumentsException( 

861 message="ee_id is mandatory", bad_args=["ee_id"] 

862 ) 

863 

864 model_name, application_name, machine_id = self._get_ee_id_components( 

865 ee_id=ee_id 

866 ) 

867 try: 

868 if application_to_delete == application_name: 

869 # destroy the application 

870 await libjuju.destroy_application( 

871 model_name=model_name, 

872 application_name=application_name, 

873 total_timeout=total_timeout, 

874 ) 

875 # if model is empty delete it 

876 controller = await libjuju.get_controller() 

877 model = await libjuju.get_model( 

878 controller=controller, 

879 model_name=model_name, 

880 ) 

881 if not model.applications: 

882 self.log.info("Model {} is empty, deleting it".format(model_name)) 

883 await libjuju.destroy_model( 

884 model_name=model_name, 

885 total_timeout=total_timeout, 

886 ) 

887 elif not scaling_in: 

888 # destroy the model 

889 await libjuju.destroy_model( 

890 model_name=model_name, total_timeout=total_timeout 

891 ) 

892 elif vca_type == "native_charm" and scaling_in: 

893 # destroy the unit in the application 

894 await libjuju.destroy_unit( 

895 application_name=application_name, 

896 model_name=model_name, 

897 machine_id=machine_id, 

898 total_timeout=total_timeout, 

899 ) 

900 else: 

901 # destroy the application 

902 await libjuju.destroy_application( 

903 model_name=model_name, 

904 application_name=application_name, 

905 total_timeout=total_timeout, 

906 ) 

907 except Exception as e: 

908 raise N2VCException( 

909 message=( 

910 "Error deleting execution environment {} (application {}) : {}" 

911 ).format(ee_id, application_name, e) 

912 ) 

913 

914 self.log.info("Execution environment {} deleted".format(ee_id)) 

915 

916 async def exec_primitive( 

917 self, 

918 ee_id: str, 

919 primitive_name: str, 

920 params_dict: dict, 

921 db_dict: dict = None, 

922 progress_timeout: float = None, 

923 total_timeout: float = None, 

924 vca_id: str = None, 

925 vca_type: str = None, 

926 ) -> str: 

927 """ 

928 Execute a primitive in the execution environment 

929 

930 :param: ee_id: the one returned by create_execution_environment or 

931 register_execution_environment 

932 :param: primitive_name: must be one defined in the software. There is one 

933 called 'config', where, for the proxy case, the 'credentials' of VM are 

934 provided 

935 :param: params_dict: parameters of the action 

936 :param: db_dict: where to write into database when the status changes. 

937 It contains a dict with 

938 {collection: <str>, filter: {}, path: <str>}, 

939 e.g. {collection: "nsrs", filter: 

940 {_id: <nsd-id>, path: "_admin.deployed.VCA.3"} 

941 :param: progress_timeout: Progress timeout 

942 :param: total_timeout: Total timeout 

943 :param: vca_id: VCA ID 

944 :param: vca_type: VCA type 

945 :returns str: primitive result, if ok. It raises exceptions in case of fail 

946 """ 

947 

948 self.log.info( 

949 "Executing primitive: {} on ee: {}, params: {}".format( 

950 primitive_name, ee_id, params_dict 

951 ) 

952 ) 

953 libjuju = await self._get_libjuju(vca_id) 

954 

955 # check arguments 

956 if ee_id is None or len(ee_id) == 0: 

957 raise N2VCBadArgumentsException( 

958 message="ee_id is mandatory", bad_args=["ee_id"] 

959 ) 

960 if primitive_name is None or len(primitive_name) == 0: 

961 raise N2VCBadArgumentsException( 

962 message="action_name is mandatory", bad_args=["action_name"] 

963 ) 

964 if params_dict is None: 

965 params_dict = dict() 

966 

967 try: 

968 ( 

969 model_name, 

970 application_name, 

971 machine_id, 

972 ) = N2VCJujuConnector._get_ee_id_components(ee_id=ee_id) 

973 # To run action on the leader unit in libjuju.execute_action function, 

974 # machine_id must be set to None if vca_type is not native_charm 

975 if vca_type != "native_charm": 

976 machine_id = None 

977 except Exception: 

978 raise N2VCBadArgumentsException( 

979 message="ee_id={} is not a valid execution environment id".format( 

980 ee_id 

981 ), 

982 bad_args=["ee_id"], 

983 ) 

984 

985 if primitive_name == "config": 

986 # Special case: config primitive 

987 try: 

988 await libjuju.configure_application( 

989 model_name=model_name, 

990 application_name=application_name, 

991 config=params_dict, 

992 ) 

993 actions = await libjuju.get_actions( 

994 application_name=application_name, model_name=model_name 

995 ) 

996 self.log.debug( 

997 "Application {} has these actions: {}".format( 

998 application_name, actions 

999 ) 

1000 ) 

1001 if "verify-ssh-credentials" in actions: 

1002 # execute verify-credentials 

1003 num_retries = 20 

1004 retry_timeout = 15.0 

1005 for _ in range(num_retries): 

1006 try: 

1007 self.log.debug("Executing action verify-ssh-credentials...") 

1008 output, ok = await libjuju.execute_action( 

1009 model_name=model_name, 

1010 application_name=application_name, 

1011 action_name="verify-ssh-credentials", 

1012 db_dict=db_dict, 

1013 progress_timeout=progress_timeout, 

1014 total_timeout=total_timeout, 

1015 ) 

1016 

1017 if ok == "failed": 

1018 self.log.debug( 

1019 "Error executing verify-ssh-credentials: {}. Retrying..." 

1020 ) 

1021 await asyncio.sleep(retry_timeout) 

1022 

1023 continue 

1024 self.log.debug("Result: {}, output: {}".format(ok, output)) 

1025 break 

1026 except asyncio.CancelledError: 

1027 raise 

1028 else: 

1029 self.log.error( 

1030 "Error executing verify-ssh-credentials after {} retries. ".format( 

1031 num_retries 

1032 ) 

1033 ) 

1034 else: 

1035 msg = "Action verify-ssh-credentials does not exist in application {}".format( 

1036 application_name 

1037 ) 

1038 self.log.debug(msg=msg) 

1039 except Exception as e: 

1040 self.log.error("Error configuring juju application: {}".format(e)) 

1041 raise N2VCExecutionException( 

1042 message="Error configuring application into ee={} : {}".format( 

1043 ee_id, e 

1044 ), 

1045 primitive_name=primitive_name, 

1046 ) 

1047 return "CONFIG OK" 

1048 else: 

1049 try: 

1050 output, status = await libjuju.execute_action( 

1051 model_name=model_name, 

1052 application_name=application_name, 

1053 action_name=primitive_name, 

1054 db_dict=db_dict, 

1055 machine_id=machine_id, 

1056 progress_timeout=progress_timeout, 

1057 total_timeout=total_timeout, 

1058 **params_dict, 

1059 ) 

1060 if status == "completed": 

1061 return output 

1062 else: 

1063 if "output" in output: 

1064 raise Exception(f'{status}: {output["output"]}') 

1065 else: 

1066 raise Exception( 

1067 f"{status}: No further information received from action" 

1068 ) 

1069 

1070 except Exception as e: 

1071 self.log.error(f"Error executing primitive {primitive_name}: {e}") 

1072 raise N2VCExecutionException( 

1073 message=f"Error executing primitive {primitive_name} in ee={ee_id}: {e}", 

1074 primitive_name=primitive_name, 

1075 ) 

1076 

1077 async def upgrade_charm( 

1078 self, 

1079 ee_id: str = None, 

1080 path: str = None, 

1081 charm_id: str = None, 

1082 charm_type: str = None, 

1083 timeout: float = None, 

1084 ) -> str: 

1085 """This method upgrade charms in VNFs 

1086 

1087 Args: 

1088 ee_id: Execution environment id 

1089 path: Local path to the charm 

1090 charm_id: charm-id 

1091 charm_type: Charm type can be lxc-proxy-charm, native-charm or k8s-proxy-charm 

1092 timeout: (Float) Timeout for the ns update operation 

1093 

1094 Returns: 

1095 The output of the update operation if status equals to "completed" 

1096 

1097 """ 

1098 self.log.info("Upgrading charm: {} on ee: {}".format(path, ee_id)) 

1099 libjuju = await self._get_libjuju(charm_id) 

1100 

1101 # check arguments 

1102 if ee_id is None or len(ee_id) == 0: 

1103 raise N2VCBadArgumentsException( 

1104 message="ee_id is mandatory", bad_args=["ee_id"] 

1105 ) 

1106 try: 

1107 ( 

1108 model_name, 

1109 application_name, 

1110 machine_id, 

1111 ) = N2VCJujuConnector._get_ee_id_components(ee_id=ee_id) 

1112 

1113 except Exception: 

1114 raise N2VCBadArgumentsException( 

1115 message="ee_id={} is not a valid execution environment id".format( 

1116 ee_id 

1117 ), 

1118 bad_args=["ee_id"], 

1119 ) 

1120 

1121 try: 

1122 await libjuju.upgrade_charm( 

1123 application_name=application_name, 

1124 path=path, 

1125 model_name=model_name, 

1126 total_timeout=timeout, 

1127 ) 

1128 

1129 return f"Charm upgraded with application name {application_name}" 

1130 

1131 except Exception as e: 

1132 self.log.error("Error upgrading charm {}: {}".format(path, e)) 

1133 

1134 raise N2VCException( 

1135 message="Error upgrading charm {} in ee={} : {}".format(path, ee_id, e) 

1136 ) 

1137 

1138 async def disconnect(self, vca_id: str = None): 

1139 """ 

1140 Disconnect from VCA 

1141 

1142 :param: vca_id: VCA ID 

1143 """ 

1144 self.log.info("closing juju N2VC...") 

1145 libjuju = await self._get_libjuju(vca_id) 

1146 try: 

1147 await libjuju.disconnect() 

1148 except Exception as e: 

1149 raise N2VCConnectionException( 

1150 message="Error disconnecting controller: {}".format(e), 

1151 url=libjuju.vca_connection.data.endpoints, 

1152 ) 

1153 

1154 """ 

1155#################################################################################### 

1156################################### P R I V A T E ################################## 

1157#################################################################################### 

1158 """ 

1159 

1160 async def _get_libjuju(self, vca_id: str = None) -> Libjuju: 

1161 """ 

1162 Get libjuju object 

1163 

1164 :param: vca_id: VCA ID 

1165 If None, get a libjuju object with a Connection to the default VCA 

1166 Else, geta libjuju object with a Connection to the specified VCA 

1167 """ 

1168 if not vca_id: 

1169 while self.loading_libjuju.locked(): 

1170 await asyncio.sleep(0.1) 

1171 if not self.libjuju: 

1172 async with self.loading_libjuju: 

1173 vca_connection = await get_connection(self._store) 

1174 self.libjuju = Libjuju(vca_connection, log=self.log) 

1175 return self.libjuju 

1176 else: 

1177 vca_connection = await get_connection(self._store, vca_id) 

1178 return Libjuju(vca_connection, log=self.log, n2vc=self) 

1179 

1180 def _write_ee_id_db(self, db_dict: dict, ee_id: str): 

1181 # write ee_id to database: _admin.deployed.VCA.x 

1182 try: 

1183 the_table = db_dict["collection"] 

1184 the_filter = db_dict["filter"] 

1185 the_path = db_dict["path"] 

1186 if not the_path[-1] == ".": 

1187 the_path = the_path + "." 

1188 update_dict = {the_path + "ee_id": ee_id} 

1189 # self.log.debug('Writing ee_id to database: {}'.format(the_path)) 

1190 self.db.set_one( 

1191 table=the_table, 

1192 q_filter=the_filter, 

1193 update_dict=update_dict, 

1194 fail_on_empty=True, 

1195 ) 

1196 except asyncio.CancelledError: 

1197 raise 

1198 except Exception as e: 

1199 self.log.error("Error writing ee_id to database: {}".format(e)) 

1200 

1201 @staticmethod 

1202 def _build_ee_id(model_name: str, application_name: str, machine_id: str): 

1203 """ 

1204 Build an execution environment id form model, application and machine 

1205 :param model_name: 

1206 :param application_name: 

1207 :param machine_id: 

1208 :return: 

1209 """ 

1210 # id for the execution environment 

1211 return "{}.{}.{}".format(model_name, application_name, machine_id) 

1212 

1213 @staticmethod 

1214 def _get_ee_id_components(ee_id: str) -> (str, str, str): 

1215 """ 

1216 Get model, application and machine components from an execution environment id 

1217 :param ee_id: 

1218 :return: model_name, application_name, machine_id 

1219 """ 

1220 

1221 return get_ee_id_components(ee_id) 

1222 

1223 @staticmethod 

1224 def _find_charm_level(vnf_id: str, vdu_id: str) -> str: 

1225 """Decides the charm level. 

1226 Args: 

1227 vnf_id (str): VNF id 

1228 vdu_id (str): VDU id 

1229 

1230 Returns: 

1231 charm_level (str): ns-level or vnf-level or vdu-level 

1232 """ 

1233 if vdu_id and not vnf_id: 

1234 raise N2VCException(message="If vdu-id exists, vnf-id should be provided.") 

1235 if vnf_id and vdu_id: 

1236 return "vdu-level" 

1237 if vnf_id and not vdu_id: 

1238 return "vnf-level" 

1239 if not vnf_id and not vdu_id: 

1240 return "ns-level" 

1241 

1242 @staticmethod 

1243 def _generate_backward_compatible_application_name( 

1244 vnf_id: str, vdu_id: str, vdu_count: str 

1245 ) -> str: 

1246 """Generate backward compatible application name 

1247 by limiting the app name to 50 characters. 

1248 

1249 Args: 

1250 vnf_id (str): VNF ID 

1251 vdu_id (str): VDU ID 

1252 vdu_count (str): vdu-count-index 

1253 

1254 Returns: 

1255 application_name (str): generated application name 

1256 

1257 """ 

1258 if vnf_id is None or len(vnf_id) == 0: 

1259 vnf_id = "" 

1260 else: 

1261 # Shorten the vnf_id to its last twelve characters 

1262 vnf_id = "vnf-" + vnf_id[-12:] 

1263 

1264 if vdu_id is None or len(vdu_id) == 0: 

1265 vdu_id = "" 

1266 else: 

1267 # Shorten the vdu_id to its last twelve characters 

1268 vdu_id = "-vdu-" + vdu_id[-12:] 

1269 

1270 if vdu_count is None or len(vdu_count) == 0: 

1271 vdu_count = "" 

1272 else: 

1273 vdu_count = "-cnt-" + vdu_count 

1274 

1275 # Generate a random suffix with 5 characters (the default size used by K8s) 

1276 random_suffix = generate_random_alfanum_string(size=5) 

1277 

1278 application_name = "app-{}{}{}-{}".format( 

1279 vnf_id, vdu_id, vdu_count, random_suffix 

1280 ) 

1281 return application_name 

1282 

1283 @staticmethod 

1284 def _get_vca_record(search_key: str, vca_records: list, vdu_id: str) -> dict: 

1285 """Get the correct VCA record dict depending on the search key 

1286 

1287 Args: 

1288 search_key (str): keyword to find the correct VCA record 

1289 vca_records (list): All VCA records as list 

1290 vdu_id (str): VDU ID 

1291 

1292 Returns: 

1293 vca_record (dict): Dictionary which includes the correct VCA record 

1294 

1295 """ 

1296 return next( 

1297 filter(lambda record: record[search_key] == vdu_id, vca_records), {} 

1298 ) 

1299 

1300 @staticmethod 

1301 def _generate_application_name( 

1302 charm_level: str, 

1303 vnfrs: dict, 

1304 vca_records: list, 

1305 vnf_count: str = None, 

1306 vdu_id: str = None, 

1307 vdu_count: str = None, 

1308 ) -> str: 

1309 """Generate application name to make the relevant charm of VDU/KDU 

1310 in the VNFD descriptor become clearly visible. 

1311 Limiting the app name to 50 characters. 

1312 

1313 Args: 

1314 charm_level (str): level of charm 

1315 vnfrs (dict): vnf record dict 

1316 vca_records (list): db_nsr["_admin"]["deployed"]["VCA"] as list 

1317 vnf_count (str): vnf count index 

1318 vdu_id (str): VDU ID 

1319 vdu_count (str): vdu count index 

1320 

1321 Returns: 

1322 application_name (str): generated application name 

1323 

1324 """ 

1325 application_name = "" 

1326 if charm_level == "ns-level": 

1327 if len(vca_records) != 1: 

1328 raise N2VCException(message="One VCA record is expected.") 

1329 # Only one VCA record is expected if it's ns-level charm. 

1330 # Shorten the charm name to its first 40 characters. 

1331 charm_name = vca_records[0]["charm_name"][:40] 

1332 if not charm_name: 

1333 raise N2VCException(message="Charm name should be provided.") 

1334 application_name = charm_name + "-ns" 

1335 

1336 elif charm_level == "vnf-level": 

1337 if len(vca_records) < 1: 

1338 raise N2VCException(message="One or more VCA record is expected.") 

1339 # If VNF is scaled, more than one VCA record may be included in vca_records 

1340 # but ee_descriptor_id is same. 

1341 # Shorten the ee_descriptor_id and member-vnf-index-ref 

1342 # to first 12 characters. 

1343 application_name = ( 

1344 vca_records[0]["ee_descriptor_id"][:12] 

1345 + "-" 

1346 + vnf_count 

1347 + "-" 

1348 + vnfrs["member-vnf-index-ref"][:12] 

1349 + "-vnf" 

1350 ) 

1351 elif charm_level == "vdu-level": 

1352 if len(vca_records) < 1: 

1353 raise N2VCException(message="One or more VCA record is expected.") 

1354 

1355 # Charms are also used for deployments with Helm charts. 

1356 # If deployment unit is a Helm chart/KDU, 

1357 # vdu_profile_id and vdu_count will be empty string. 

1358 if vdu_count is None: 

1359 vdu_count = "" 

1360 

1361 # If vnf/vdu is scaled, more than one VCA record may be included in vca_records 

1362 # but ee_descriptor_id is same. 

1363 # Shorten the ee_descriptor_id, member-vnf-index-ref and vdu_profile_id 

1364 # to first 12 characters. 

1365 if not vdu_id: 

1366 raise N2VCException(message="vdu-id should be provided.") 

1367 

1368 vca_record = N2VCJujuConnector._get_vca_record( 

1369 "vdu_id", vca_records, vdu_id 

1370 ) 

1371 

1372 if not vca_record: 

1373 vca_record = N2VCJujuConnector._get_vca_record( 

1374 "kdu_name", vca_records, vdu_id 

1375 ) 

1376 

1377 application_name = ( 

1378 vca_record["ee_descriptor_id"][:12] 

1379 + "-" 

1380 + vnf_count 

1381 + "-" 

1382 + vnfrs["member-vnf-index-ref"][:12] 

1383 + "-" 

1384 + vdu_id[:12] 

1385 + "-" 

1386 + vdu_count 

1387 + "-vdu" 

1388 ) 

1389 

1390 return application_name 

1391 

1392 def _get_vnf_count_and_record( 

1393 self, charm_level: str, vnf_id_and_count: str 

1394 ) -> Tuple[str, dict]: 

1395 """Get the vnf count and VNF record depend on charm level 

1396 

1397 Args: 

1398 charm_level (str) 

1399 vnf_id_and_count (str) 

1400 

1401 Returns: 

1402 (vnf_count (str), db_vnfr(dict)) as Tuple 

1403 

1404 """ 

1405 vnf_count = "" 

1406 db_vnfr = {} 

1407 

1408 if charm_level in ("vnf-level", "vdu-level"): 

1409 vnf_id = "-".join(vnf_id_and_count.split("-")[:-1]) 

1410 vnf_count = vnf_id_and_count.split("-")[-1] 

1411 db_vnfr = self.db.get_one("vnfrs", {"_id": vnf_id}) 

1412 

1413 # If the charm is ns level, it returns empty vnf_count and db_vnfr 

1414 return vnf_count, db_vnfr 

1415 

1416 @staticmethod 

1417 def _get_vca_records(charm_level: str, db_nsr: dict, db_vnfr: dict) -> list: 

1418 """Get the VCA records from db_nsr dict 

1419 

1420 Args: 

1421 charm_level (str): level of charm 

1422 db_nsr (dict): NS record from database 

1423 db_vnfr (dict): VNF record from database 

1424 

1425 Returns: 

1426 vca_records (list): List of VCA record dictionaries 

1427 

1428 """ 

1429 vca_records = {} 

1430 if charm_level == "ns-level": 

1431 vca_records = list( 

1432 filter( 

1433 lambda vca_record: vca_record["target_element"] == "ns", 

1434 db_nsr["_admin"]["deployed"]["VCA"], 

1435 ) 

1436 ) 

1437 elif charm_level in ["vnf-level", "vdu-level"]: 

1438 vca_records = list( 

1439 filter( 

1440 lambda vca_record: vca_record["member-vnf-index"] 

1441 == db_vnfr["member-vnf-index-ref"], 

1442 db_nsr["_admin"]["deployed"]["VCA"], 

1443 ) 

1444 ) 

1445 

1446 return vca_records 

1447 

1448 def _get_application_name(self, namespace: str) -> str: 

1449 """Build application name from namespace 

1450 

1451 Application name structure: 

1452 NS level: <charm-name>-ns 

1453 VNF level: <ee-name>-z<vnf-ordinal-scale-number>-<vnf-profile-id>-vnf 

1454 VDU level: <ee-name>-z<vnf-ordinal-scale-number>-<vnf-profile-id>- 

1455 <vdu-profile-id>-z<vdu-ordinal-scale-number>-vdu 

1456 

1457 Application naming for backward compatibility (old structure): 

1458 NS level: app-<random_value> 

1459 VNF level: app-vnf-<vnf-id>-z<ordinal-scale-number>-<random_value> 

1460 VDU level: app-vnf-<vnf-id>-z<vnf-ordinal-scale-number>-vdu- 

1461 <vdu-id>-cnt-<vdu-count>-z<vdu-ordinal-scale-number>-<random_value> 

1462 

1463 Args: 

1464 namespace (str) 

1465 

1466 Returns: 

1467 application_name (str) 

1468 

1469 """ 

1470 # split namespace components 

1471 ( 

1472 nsi_id, 

1473 ns_id, 

1474 vnf_id_and_count, 

1475 vdu_id, 

1476 vdu_count, 

1477 ) = self._get_namespace_components(namespace=namespace) 

1478 

1479 if not ns_id: 

1480 raise N2VCException(message="ns-id should be provided.") 

1481 

1482 charm_level = self._find_charm_level(vnf_id_and_count, vdu_id) 

1483 db_nsr = self.db.get_one("nsrs", {"_id": ns_id}) 

1484 vnf_count, db_vnfr = self._get_vnf_count_and_record( 

1485 charm_level, vnf_id_and_count 

1486 ) 

1487 vca_records = self._get_vca_records(charm_level, db_nsr, db_vnfr) 

1488 

1489 if all("charm_name" in vca_record.keys() for vca_record in vca_records): 

1490 application_name = self._generate_application_name( 

1491 charm_level, 

1492 db_vnfr, 

1493 vca_records, 

1494 vnf_count=vnf_count, 

1495 vdu_id=vdu_id, 

1496 vdu_count=vdu_count, 

1497 ) 

1498 else: 

1499 application_name = self._generate_backward_compatible_application_name( 

1500 vnf_id_and_count, vdu_id, vdu_count 

1501 ) 

1502 

1503 return N2VCJujuConnector._format_app_name(application_name) 

1504 

1505 @staticmethod 

1506 def _format_model_name(name: str) -> str: 

1507 """Format the name of the model. 

1508 

1509 Model names may only contain lowercase letters, digits and hyphens 

1510 """ 

1511 

1512 return name.replace("_", "-").replace(" ", "-").lower() 

1513 

1514 @staticmethod 

1515 def _format_app_name(name: str) -> str: 

1516 """Format the name of the application (in order to assure valid application name). 

1517 

1518 Application names have restrictions (run juju deploy --help): 

1519 - contains lowercase letters 'a'-'z' 

1520 - contains numbers '0'-'9' 

1521 - contains hyphens '-' 

1522 - starts with a lowercase letter 

1523 - not two or more consecutive hyphens 

1524 - after a hyphen, not a group with all numbers 

1525 """ 

1526 

1527 def all_numbers(s: str) -> bool: 

1528 for c in s: 

1529 if not c.isdigit(): 

1530 return False 

1531 return True 

1532 

1533 new_name = name.replace("_", "-") 

1534 new_name = new_name.replace(" ", "-") 

1535 new_name = new_name.lower() 

1536 while new_name.find("--") >= 0: 

1537 new_name = new_name.replace("--", "-") 

1538 groups = new_name.split("-") 

1539 

1540 # find 'all numbers' groups and prefix them with a letter 

1541 app_name = "" 

1542 for i in range(len(groups)): 

1543 group = groups[i] 

1544 if all_numbers(group): 

1545 group = "z" + group 

1546 if i > 0: 

1547 app_name += "-" 

1548 app_name += group 

1549 

1550 if app_name[0].isdigit(): 

1551 app_name = "z" + app_name 

1552 

1553 return app_name 

1554 

1555 async def validate_vca(self, vca_id: str): 

1556 """ 

1557 Validate a VCA by connecting/disconnecting to/from it 

1558 

1559 :param: vca_id: VCA ID 

1560 """ 

1561 vca_connection = await get_connection(self._store, vca_id=vca_id) 

1562 libjuju = Libjuju(vca_connection, log=self.log, n2vc=self) 

1563 controller = await libjuju.get_controller() 

1564 await libjuju.disconnect_controller(controller)