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
31 from http
import HTTPStatus
32 from n2vc
.loggable
import Loggable
33 from n2vc
.exceptions
import N2VCBadArgumentsException
36 from osm_common
.dbmongo
import DbException
39 class N2VCDeploymentStatus(Enum
):
42 COMPLETED
= 'completed'
47 class N2VCConnector(abc
.ABC
, Loggable
):
48 """Generic N2VC connector
54 ##################################################################################################
55 ########################################## P U B L I C ###########################################
56 ##################################################################################################
70 """Initialize N2VC abstract connector. It defines de API for VCA connectors
72 :param object db: Mongo object managing the MongoDB (repo common DbBase)
73 :param object fs: FileSystem object managing the package artifacts (repo common FsBase)
74 :param object log: the logging object to log to
75 :param object loop: the loop to use for asyncio (default current thread loop)
76 :param str url: a string that how to connect to the VCA (if needed, IP and port can be obtained from there)
77 :param str username: the username to authenticate with VCA
78 :param dict vca_config: Additional parameters for the specific VCA. For example, for juju it will contain:
79 secret: The password to authenticate with
80 public_key: The contents of the juju public SSH key
81 ca_cert str: The CA certificate used to authenticate
82 :param on_update_db: callback called when n2vc connector updates database. Received arguments:
84 filter: e.g. {_id: <nsd-id> }
85 path: e.g. "_admin.deployed.VCA.3."
86 updated_data: e.g. , "{ _admin.deployed.VCA.3.status: 'xxx', etc }"
90 Loggable
.__init
__(self
, log
=log
, log_to_console
=True, prefix
='\nN2VC')
94 raise N2VCBadArgumentsException('Argument db is mandatory', ['db'])
96 raise N2VCBadArgumentsException('Argument fs is mandatory', ['fs'])
98 self
.log
.info('url={}, username={}, vca_config={}'.format(url
, username
, vca_config
))
100 # store arguments into self
103 self
.loop
= loop
or asyncio
.get_event_loop()
105 self
.username
= username
106 self
.vca_config
= vca_config
107 self
.on_update_db
= on_update_db
109 # generate private/public key-pair
110 self
.private_key_path
= None
111 self
.public_key_path
= None
112 self
.get_public_key()
115 async def get_status(self
, namespace
: str, yaml_format
: bool = True):
116 """Get namespace status
118 :param namespace: we obtain ns from namespace
119 :param yaml_format: returns a yaml string
122 # TODO: review which public key
123 def get_public_key(self
) -> str:
124 """Get the VCA ssh-public-key
126 Returns the SSH public key from local mahine, to be injected into virtual machines to
127 be managed by the VCA.
128 First run, a ssh keypair will be created.
129 The public key is injected into a VM so that we can provision the
130 machine with Juju, after which Juju will communicate with the VM
131 directly via the juju agent.
136 # Find the path where we expect our key lives (~/.ssh)
137 homedir
= os
.environ
.get('HOME')
139 self
.warning('No HOME environment variable, using /tmp')
141 sshdir
= "{}/.ssh".format(homedir
)
142 if not os
.path
.exists(sshdir
):
145 self
.private_key_path
= "{}/id_n2vc_rsa".format(sshdir
)
146 self
.public_key_path
= "{}.pub".format(self
.private_key_path
)
148 # If we don't have a key generated, then we have to generate it using ssh-keygen
149 if not os
.path
.exists(self
.private_key_path
):
150 cmd
= "ssh-keygen -t {} -b {} -N '' -f {}".format(
153 self
.private_key_path
155 # run command with arguments
156 subprocess
.check_output(shlex
.split(cmd
))
158 # Read the public key. Only one public key (one line) in the file
159 with
open(self
.public_key_path
, "r") as file:
160 public_key
= file.readline()
165 async def create_execution_environment(
169 reuse_ee_id
: str = None,
170 progress_timeout
: float = None,
171 total_timeout
: float = None
173 """Create an Execution Environment. Returns when it is created or raises an exception on failing
175 :param str namespace: Contains a dot separate string.
176 LCM will use: [<nsi-id>].<ns-id>.<vnf-id>.<vdu-id>[-<count>]
177 :param dict db_dict: where to write to database when the status changes.
178 It contains a dictionary with {collection: str, filter: {}, path: str},
179 e.g. {collection: "nsrs", filter: {_id: <nsd-id>, path: "_admin.deployed.VCA.3"}
180 :param str reuse_ee_id: ee id from an older execution. It allows us to reuse an older environment
181 :param float progress_timeout:
182 :param float total_timeout:
183 :returns str, dict: id of the new execution environment and credentials for it
184 (credentials can contains hostname, username, etc depending on underlying cloud)
188 async def register_execution_environment(
193 progress_timeout
: float = None,
194 total_timeout
: float = None
197 Register an existing execution environment at the VCA
199 :param str namespace: same as create_execution_environment method
200 :param dict credentials: credentials to access the existing execution environment
201 (it can contains hostname, username, path to private key, etc depending on underlying cloud)
202 :param dict db_dict: where to write to database when the status changes.
203 It contains a dictionary with {collection: str, filter: {}, path: str},
204 e.g. {collection: "nsrs", filter: {_id: <nsd-id>, path: "_admin.deployed.VCA.3"}
205 :param float progress_timeout:
206 :param float total_timeout:
207 :returns str: id of the execution environment
211 async def install_configuration_sw(
216 progress_timeout
: float = None,
217 total_timeout
: float = None
220 Install the software inside the execution environment identified by ee_id
222 :param str ee_id: the id of the execution environment returned by create_execution_environment
223 or register_execution_environment
224 :param str artifact_path: where to locate the artifacts (parent folder) using the self.fs
225 the final artifact path will be a combination of this artifact_path and additional string from
226 the config_dict (e.g. charm name)
227 :param dict db_dict: where to write into database when the status changes.
228 It contains a dict with {collection: <str>, filter: {}, path: <str>},
229 e.g. {collection: "nsrs", filter: {_id: <nsd-id>, path: "_admin.deployed.VCA.3"}
230 :param float progress_timeout:
231 :param float total_timeout:
235 async def get_ee_ssh_public__key(
239 progress_timeout
: float = None,
240 total_timeout
: float = None
243 Generate a priv/pub key pair in the execution environment and return the public key
245 :param str ee_id: the id of the execution environment returned by create_execution_environment
246 or register_execution_environment
247 :param dict db_dict: where to write into database when the status changes.
248 It contains a dict with {collection: <str>, filter: {}, path: <str>},
249 e.g. {collection: "nsrs", filter: {_id: <nsd-id>, path: "_admin.deployed.VCA.3"}
250 :param float progress_timeout:
251 :param float total_timeout:
252 :returns: public key of the execution environment
253 For the case of juju proxy charm ssh-layered, it is the one returned by 'get-ssh-public-key'
255 It raises a N2VC exception if fails
259 async def add_relation(
267 Add a relation between two Execution Environments (using their associated endpoints).
269 :param str ee_id_1: The id of the first execution environment
270 :param str ee_id_2: The id of the second execution environment
271 :param str endpoint_1: The endpoint in the first execution environment
272 :param str endpoint_2: The endpoint in the second execution environment
277 async def remove_relation(
285 async def deregister_execution_environments(
292 async def delete_namespace(
295 db_dict
: dict = None,
296 total_timeout
: float = None
299 Remove a network scenario and its execution environments
300 :param namespace: [<nsi-id>].<ns-id>
301 :param dict db_dict: where to write into database when the status changes.
302 It contains a dict with {collection: <str>, filter: {}, path: <str>},
303 e.g. {collection: "nsrs", filter: {_id: <nsd-id>, path: "_admin.deployed.VCA.3"}
304 :param float total_timeout:
308 async def delete_execution_environment(
311 db_dict
: dict = None,
312 total_timeout
: float = None
315 Delete an execution environment
316 :param str ee_id: id of the execution environment to delete
317 :param dict db_dict: where to write into database when the status changes.
318 It contains a dict with {collection: <str>, filter: {}, path: <str>},
319 e.g. {collection: "nsrs", filter: {_id: <nsd-id>, path: "_admin.deployed.VCA.3"}
320 :param float total_timeout:
324 async def exec_primitive(
329 db_dict
: dict = None,
330 progress_timeout
: float = None,
331 total_timeout
: float = None
334 Execute a primitive in the execution environment
336 :param str ee_id: the one returned by create_execution_environment or register_execution_environment
337 :param str primitive_name: must be one defined in the software. There is one called 'config',
338 where, for the proxy case, the 'credentials' of VM are provided
339 :param dict params_dict: parameters of the action
340 :param dict db_dict: where to write into database when the status changes.
341 It contains a dict with {collection: <str>, filter: {}, path: <str>},
342 e.g. {collection: "nsrs", filter: {_id: <nsd-id>, path: "_admin.deployed.VCA.3"}
343 :param float progress_timeout:
344 :param float total_timeout:
345 :returns str: primitive result, if ok. It raises exceptions in case of fail
348 async def disconnect(self
):
354 ##################################################################################################
355 ########################################## P R I V A T E #########################################
356 ##################################################################################################
359 def _get_namespace_components(self
, namespace
: str) -> (str, str, str, str, str):
361 Split namespace components
363 :param namespace: [<nsi-id>].<ns-id>.<vnf-id>.<vdu-id>[-<count>]
364 :return: nsi_id, ns_id, vnf_id, vdu_id, vdu_count
368 if namespace
is None or len(namespace
) == 0:
369 raise N2VCBadArgumentsException('Argument namespace is mandatory', ['namespace'])
371 # split namespace components
372 parts
= namespace
.split('.')
378 if len(parts
) > 0 and len(parts
[0]) > 0:
380 if len(parts
) > 1 and len(parts
[1]) > 0:
382 if len(parts
) > 2 and len(parts
[2]) > 0:
384 if len(parts
) > 3 and len(parts
[3]) > 0:
386 vdu_parts
= parts
[3].split('-')
387 if len(vdu_parts
) > 1:
388 vdu_id
= vdu_parts
[0]
389 vdu_count
= vdu_parts
[1]
391 return nsi_id
, ns_id
, vnf_id
, vdu_id
, vdu_count
393 async def write_app_status_to_db(
396 status
: N2VCDeploymentStatus
,
397 detailed_status
: str,
402 self
.log
.debug('No db_dict => No database write')
405 # self.log.debug('status={} / detailed-status={} / VCA-status={} / entity_type={}'
406 # .format(str(status.value), detailed_status, vca_status, entity_type))
410 the_table
= db_dict
['collection']
411 the_filter
= db_dict
['filter']
412 the_path
= db_dict
['path']
413 if not the_path
[-1] == '.':
414 the_path
= the_path
+ '.'
416 the_path
+ 'status': str(status
.value
),
417 the_path
+ 'detailed-status': detailed_status
,
418 the_path
+ 'VCA-status': vca_status
,
419 the_path
+ 'entity-type': entity_type
,
420 the_path
+ 'status-time': str(time
.time()),
426 update_dict
=update_dict
,
431 if self
.on_update_db
:
432 if asyncio
.iscoroutinefunction(self
.on_update_db
):
433 await self
.on_update_db(the_table
, the_filter
, the_path
, update_dict
)
435 self
.on_update_db(the_table
, the_filter
, the_path
, update_dict
)
437 except DbException
as e
:
438 if e
.http_code
== HTTPStatus
.NOT_FOUND
:
439 self
.log
.error('NOT_FOUND error: Exception writing status to database: {}'.format(e
))
441 self
.log
.info('Exception writing status to database: {}'.format(e
))
444 def juju_status_2_osm_status(type: str, status
: str) -> N2VCDeploymentStatus
:
445 if type == 'application' or type == 'unit':
446 if status
in ['waiting', 'maintenance']:
447 return N2VCDeploymentStatus
.RUNNING
448 if status
in ['error']:
449 return N2VCDeploymentStatus
.FAILED
450 elif status
in ['active']:
451 return N2VCDeploymentStatus
.COMPLETED
452 elif status
in ['blocked']:
453 return N2VCDeploymentStatus
.RUNNING
455 return N2VCDeploymentStatus
.UNKNOWN
456 elif type == 'action':
457 if status
in ['running']:
458 return N2VCDeploymentStatus
.RUNNING
459 elif status
in ['completed']:
460 return N2VCDeploymentStatus
.COMPLETED
462 return N2VCDeploymentStatus
.UNKNOWN
463 elif type == 'machine':
464 if status
in ['pending']:
465 return N2VCDeploymentStatus
.PENDING
466 elif status
in ['started']:
467 return N2VCDeploymentStatus
.COMPLETED
469 return N2VCDeploymentStatus
.UNKNOWN
471 return N2VCDeploymentStatus
.FAILED
474 def obj_to_yaml(obj
: object) -> str:
476 dump_text
= yaml
.dump(obj
, default_flow_style
=False, indent
=2)
478 lines
= dump_text
.splitlines()
479 # remove !!python/object tags
482 index
= line
.find('!!python/object')
485 yaml_text
+= line
+ '\n'
489 def obj_to_dict(obj
: object) -> dict:
490 # convert obj to yaml
491 yaml_text
= obj_to_yaml(obj
)
493 return yaml
.load(yaml_text
, Loader
=yaml
.Loader
)