2 # Copyright 2019 Telefonica Investigacion y Desarrollo, S.A.U.
3 # This file is part of OSM
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
10 # http://www.apache.org/licenses/LICENSE-2.0
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
16 # See the License for the specific language governing permissions and
17 # limitations under the License.
19 # For those usages not covered by the Apache License, Version 2.0 please
20 # contact with: nfvlabs@tid.es
26 from http
import HTTPStatus
32 from n2vc
.exceptions
import N2VCBadArgumentsException
33 from osm_common
.dbmongo
import DbException
36 from n2vc
.loggable
import Loggable
37 from n2vc
.utils
import EntityType
, JujuStatusToOSM
, N2VCDeploymentStatus
40 class N2VCConnector(abc
.ABC
, Loggable
):
41 """Generic N2VC connector
47 ####################################################################################
48 ################################### P U B L I C ####################################
49 ####################################################################################
63 """Initialize N2VC abstract connector. It defines de API for VCA connectors
65 :param object db: Mongo object managing the MongoDB (repo common DbBase)
66 :param object fs: FileSystem object managing the package artifacts (repo common
68 :param object log: the logging object to log to
69 :param object loop: the loop to use for asyncio (default current thread loop)
70 :param str url: a string that how to connect to the VCA (if needed, IP and port
71 can be obtained from there)
72 :param str username: the username to authenticate with VCA
73 :param dict vca_config: Additional parameters for the specific VCA. For example,
74 for juju it will contain:
75 secret: The password to authenticate with
76 public_key: The contents of the juju public SSH key
77 ca_cert str: The CA certificate used to authenticate
78 :param on_update_db: callback called when n2vc connector updates database.
81 filter: e.g. {_id: <nsd-id> }
82 path: e.g. "_admin.deployed.VCA.3."
83 updated_data: e.g. , "{ _admin.deployed.VCA.3.status: 'xxx', etc }"
87 Loggable
.__init
__(self
, log
=log
, log_to_console
=True, prefix
="\nN2VC")
91 raise N2VCBadArgumentsException("Argument db is mandatory", ["db"])
93 raise N2VCBadArgumentsException("Argument fs is mandatory", ["fs"])
96 "url={}, username={}, vca_config={}".format(
101 for k
, v
in vca_config
.items()
103 not in ("host", "port", "user", "secret", "public_key", "ca_cert")
108 # store arguments into self
111 self
.loop
= loop
or asyncio
.get_event_loop()
113 self
.username
= username
114 self
.vca_config
= vca_config
115 self
.on_update_db
= on_update_db
117 # generate private/public key-pair
118 self
.private_key_path
= None
119 self
.public_key_path
= None
120 self
.get_public_key()
123 async def get_status(self
, namespace
: str, yaml_format
: bool = True):
124 """Get namespace status
126 :param namespace: we obtain ns from namespace
127 :param yaml_format: returns a yaml string
130 # TODO: review which public key
131 def get_public_key(self
) -> str:
132 """Get the VCA ssh-public-key
134 Returns the SSH public key from local mahine, to be injected into virtual
135 machines to be managed by the VCA.
136 First run, a ssh keypair will be created.
137 The public key is injected into a VM so that we can provision the
138 machine with Juju, after which Juju will communicate with the VM
139 directly via the juju agent.
142 # Find the path where we expect our key lives (~/.ssh)
143 homedir
= os
.environ
.get("HOME")
145 self
.warning("No HOME environment variable, using /tmp")
147 sshdir
= "{}/.ssh".format(homedir
)
148 if not os
.path
.exists(sshdir
):
151 self
.private_key_path
= "{}/id_n2vc_rsa".format(sshdir
)
152 self
.public_key_path
= "{}.pub".format(self
.private_key_path
)
154 # If we don't have a key generated, then we have to generate it using ssh-keygen
155 if not os
.path
.exists(self
.private_key_path
):
156 cmd
= "ssh-keygen -t {} -b {} -N '' -f {}".format(
157 "rsa", "4096", self
.private_key_path
159 # run command with arguments
160 subprocess
.check_output(shlex
.split(cmd
))
162 # Read the public key. Only one public key (one line) in the file
163 with
open(self
.public_key_path
, "r") as file:
164 public_key
= file.readline()
169 async def create_execution_environment(
173 reuse_ee_id
: str = None,
174 progress_timeout
: float = None,
175 total_timeout
: float = None,
177 """Create an Execution Environment. Returns when it is created or raises an
180 :param str namespace: Contains a dot separate string.
181 LCM will use: [<nsi-id>].<ns-id>.<vnf-id>.<vdu-id>[-<count>]
182 :param dict db_dict: where to write to database when the status changes.
183 It contains a dictionary with {collection: str, filter: {}, path: str},
184 e.g. {collection: "nsrs", filter: {_id: <nsd-id>, path:
185 "_admin.deployed.VCA.3"}
186 :param str reuse_ee_id: ee id from an older execution. It allows us to reuse an
188 :param float progress_timeout:
189 :param float total_timeout:
190 :returns str, dict: id of the new execution environment and credentials for it
191 (credentials can contains hostname, username, etc depending on
196 async def register_execution_environment(
201 progress_timeout
: float = None,
202 total_timeout
: float = None,
205 Register an existing execution environment at the VCA
207 :param str namespace: same as create_execution_environment method
208 :param dict credentials: credentials to access the existing execution
210 (it can contains hostname, username, path to private key, etc depending on
212 :param dict db_dict: where to write to database when the status changes.
213 It contains a dictionary with {collection: str, filter: {}, path: str},
214 e.g. {collection: "nsrs", filter:
215 {_id: <nsd-id>, path: "_admin.deployed.VCA.3"}
216 :param float progress_timeout:
217 :param float total_timeout:
218 :returns str: id of the execution environment
222 async def install_configuration_sw(
227 progress_timeout
: float = None,
228 total_timeout
: float = None,
231 Install the software inside the execution environment identified by ee_id
233 :param str ee_id: the id of the execution environment returned by
234 create_execution_environment or register_execution_environment
235 :param str artifact_path: where to locate the artifacts (parent folder) using
237 the final artifact path will be a combination of this artifact_path and
238 additional string from the config_dict (e.g. charm name)
239 :param dict db_dict: where to write into database when the status changes.
240 It contains a dict with
241 {collection: <str>, filter: {}, path: <str>},
242 e.g. {collection: "nsrs", filter:
243 {_id: <nsd-id>, path: "_admin.deployed.VCA.3"}
244 :param float progress_timeout:
245 :param float total_timeout:
249 async def get_ee_ssh_public__key(
253 progress_timeout
: float = None,
254 total_timeout
: float = None,
257 Generate a priv/pub key pair in the execution environment and return the public
260 :param str ee_id: the id of the execution environment returned by
261 create_execution_environment or register_execution_environment
262 :param dict db_dict: where to write into database when the status changes.
263 It contains a dict with
264 {collection: <str>, filter: {}, path: <str>},
265 e.g. {collection: "nsrs", filter:
266 {_id: <nsd-id>, path: "_admin.deployed.VCA.3"}
267 :param float progress_timeout:
268 :param float total_timeout:
269 :returns: public key of the execution environment
270 For the case of juju proxy charm ssh-layered, it is the one
271 returned by 'get-ssh-public-key' primitive.
272 It raises a N2VC exception if fails
276 async def add_relation(
277 self
, ee_id_1
: str, ee_id_2
: str, endpoint_1
: str, endpoint_2
: str
280 Add a relation between two Execution Environments (using their associated
283 :param str ee_id_1: The id of the first execution environment
284 :param str ee_id_2: The id of the second execution environment
285 :param str endpoint_1: The endpoint in the first execution environment
286 :param str endpoint_2: The endpoint in the second execution environment
291 async def remove_relation(self
):
297 async def deregister_execution_environments(self
):
302 async def delete_namespace(
303 self
, namespace
: str, db_dict
: dict = None, total_timeout
: float = None
306 Remove a network scenario and its execution environments
307 :param namespace: [<nsi-id>].<ns-id>
308 :param dict db_dict: where to write into database when the status changes.
309 It contains a dict with
310 {collection: <str>, filter: {}, path: <str>},
311 e.g. {collection: "nsrs", filter:
312 {_id: <nsd-id>, path: "_admin.deployed.VCA.3"}
313 :param float total_timeout:
317 async def delete_execution_environment(
318 self
, ee_id
: str, db_dict
: dict = None, total_timeout
: float = None
321 Delete an execution environment
322 :param str ee_id: id of the execution environment to delete
323 :param dict db_dict: where to write into database when the status changes.
324 It contains a dict with
325 {collection: <str>, filter: {}, path: <str>},
326 e.g. {collection: "nsrs", filter:
327 {_id: <nsd-id>, path: "_admin.deployed.VCA.3"}
328 :param float total_timeout:
332 async def exec_primitive(
337 db_dict
: dict = None,
338 progress_timeout
: float = None,
339 total_timeout
: float = None,
342 Execute a primitive in the execution environment
344 :param str ee_id: the one returned by create_execution_environment or
345 register_execution_environment
346 :param str primitive_name: must be one defined in the software. There is one
347 called 'config', where, for the proxy case, the 'credentials' of VM are
349 :param dict params_dict: parameters of the action
350 :param dict db_dict: where to write into database when the status changes.
351 It contains a dict with
352 {collection: <str>, filter: {}, path: <str>},
353 e.g. {collection: "nsrs", filter:
354 {_id: <nsd-id>, path: "_admin.deployed.VCA.3"}
355 :param float progress_timeout:
356 :param float total_timeout:
357 :returns str: primitive result, if ok. It raises exceptions in case of fail
360 async def disconnect(self
):
366 ####################################################################################
367 ################################### P R I V A T E ##################################
368 ####################################################################################
371 def _get_namespace_components(self
, namespace
: str) -> (str, str, str, str, str):
373 Split namespace components
375 :param namespace: [<nsi-id>].<ns-id>.<vnf-id>.<vdu-id>[-<count>]
376 :return: nsi_id, ns_id, vnf_id, vdu_id, vdu_count
380 if namespace
is None or len(namespace
) == 0:
381 raise N2VCBadArgumentsException(
382 "Argument namespace is mandatory", ["namespace"]
385 # split namespace components
386 parts
= namespace
.split(".")
392 if len(parts
) > 0 and len(parts
[0]) > 0:
394 if len(parts
) > 1 and len(parts
[1]) > 0:
396 if len(parts
) > 2 and len(parts
[2]) > 0:
398 if len(parts
) > 3 and len(parts
[3]) > 0:
400 vdu_parts
= parts
[3].split("-")
401 if len(vdu_parts
) > 1:
402 vdu_id
= vdu_parts
[0]
403 vdu_count
= vdu_parts
[1]
405 return nsi_id
, ns_id
, vnf_id
, vdu_id
, vdu_count
407 async def write_app_status_to_db(
410 status
: N2VCDeploymentStatus
,
411 detailed_status
: str,
416 self
.log
.debug("No db_dict => No database write")
419 # self.log.debug('status={} / detailed-status={} / VCA-status={}/entity_type={}'
420 # .format(str(status.value), detailed_status, vca_status, entity_type))
424 the_table
= db_dict
["collection"]
425 the_filter
= db_dict
["filter"]
426 the_path
= db_dict
["path"]
427 if not the_path
[-1] == ".":
428 the_path
= the_path
+ "."
430 the_path
+ "status": str(status
.value
),
431 the_path
+ "detailed-status": detailed_status
,
432 the_path
+ "VCA-status": vca_status
,
433 the_path
+ "entity-type": entity_type
,
434 the_path
+ "status-time": str(time
.time()),
440 update_dict
=update_dict
,
445 if self
.on_update_db
:
446 if asyncio
.iscoroutinefunction(self
.on_update_db
):
447 await self
.on_update_db(
448 the_table
, the_filter
, the_path
, update_dict
451 self
.on_update_db(the_table
, the_filter
, the_path
, update_dict
)
453 except DbException
as e
:
454 if e
.http_code
== HTTPStatus
.NOT_FOUND
:
456 "NOT_FOUND error: Exception writing status to database: {}".format(
461 self
.log
.info("Exception writing status to database: {}".format(e
))
463 def osm_status(self
, entity_type
: EntityType
, status
: str) -> N2VCDeploymentStatus
:
464 if status
not in JujuStatusToOSM
[entity_type
]:
465 self
.log
.warning("Status {} not found in JujuStatusToOSM.")
466 return N2VCDeploymentStatus
.UNKNOWN
467 return JujuStatusToOSM
[entity_type
][status
]
471 def juju_status_2_osm_status(statustype
: str, status
: str) -> N2VCDeploymentStatus
:
472 if statustype
== "application" or statustype
== "unit":
473 if status
in ["waiting", "maintenance"]:
474 return N2VCDeploymentStatus
.RUNNING
475 if status
in ["error"]:
476 return N2VCDeploymentStatus
.FAILED
477 elif status
in ["active"]:
478 return N2VCDeploymentStatus
.COMPLETED
479 elif status
in ["blocked"]:
480 return N2VCDeploymentStatus
.RUNNING
482 return N2VCDeploymentStatus
.UNKNOWN
483 elif statustype
== "action":
484 if status
in ["running"]:
485 return N2VCDeploymentStatus
.RUNNING
486 elif status
in ["completed"]:
487 return N2VCDeploymentStatus
.COMPLETED
489 return N2VCDeploymentStatus
.UNKNOWN
490 elif statustype
== "machine":
491 if status
in ["pending"]:
492 return N2VCDeploymentStatus
.PENDING
493 elif status
in ["started"]:
494 return N2VCDeploymentStatus
.COMPLETED
496 return N2VCDeploymentStatus
.UNKNOWN
498 return N2VCDeploymentStatus
.FAILED
501 def obj_to_yaml(obj
: object) -> str:
503 dump_text
= yaml
.dump(obj
, default_flow_style
=False, indent
=2)
505 lines
= dump_text
.splitlines()
506 # remove !!python/object tags
509 index
= line
.find("!!python/object")
512 yaml_text
+= line
+ "\n"
516 def obj_to_dict(obj
: object) -> dict:
517 # convert obj to yaml
518 yaml_text
= obj_to_yaml(obj
)
520 return yaml
.load(yaml_text
, Loader
=yaml
.Loader
)