From 8af176ef1a053a394ce2d3acb94dc6b564049c09 Mon Sep 17 00:00:00 2001 From: Philip Joseph Date: Thu, 5 Jan 2017 08:10:20 +0000 Subject: [PATCH] New feature: Support for ping pong charm Signed-off-by: Philip Joseph --- common/python/CMakeLists.txt | 1 + common/python/rift/mano/utils/ssh_keys.py | 147 +++++++++ .../rift/mano/examples/ping_pong_nsd.py | 310 ++++++++++++++++-- models/plugins/yang/nsr.yang | 12 + models/plugins/yang/vnfd.yang | 7 + models/plugins/yang/vnfr.yang | 12 + .../rift/tasklets/rwconmantasklet/jujuconf.py | 25 +- .../rwconmantasklet/riftcm_config_plugin.py | 6 + .../rwconmantasklet/rwconman_config.py | 25 +- .../rwnsm/rift/tasklets/rwnsmtasklet/cloud.py | 7 +- .../tasklets/rwnsmtasklet/openmano_nsm.py | 32 +- .../tasklets/rwnsmtasklet/rwnsm_conman.py | 3 +- .../rift/tasklets/rwnsmtasklet/rwnsmplugin.py | 8 +- .../tasklets/rwnsmtasklet/rwnsmtasklet.py | 100 +++++- .../tasklets/rwvnfmtasklet/rwvnfmtasklet.py | 49 ++- 15 files changed, 670 insertions(+), 74 deletions(-) create mode 100644 common/python/rift/mano/utils/ssh_keys.py diff --git a/common/python/CMakeLists.txt b/common/python/CMakeLists.txt index 85ead68d..46b30a1b 100644 --- a/common/python/CMakeLists.txt +++ b/common/python/CMakeLists.txt @@ -108,6 +108,7 @@ rift_python_install_tree( rift/mano/utils/__init.py__ rift/mano/utils/compare_desc.py rift/mano/utils/juju_api.py + rift/mano/utils/ssh_keys.py COMPONENT ${PKG_LONG_NAME} PYTHON3_ONLY ) diff --git a/common/python/rift/mano/utils/ssh_keys.py b/common/python/rift/mano/utils/ssh_keys.py new file mode 100644 index 00000000..6453f886 --- /dev/null +++ b/common/python/rift/mano/utils/ssh_keys.py @@ -0,0 +1,147 @@ +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +# Copyright 2016 RIFT.io Inc + + +import argparse +import logging +import os +import socket +import stat +import sys +import tempfile + +from Crypto.PublicKey import RSA + + +class ManoSshKey(object): + ''' + Generate a SSH key pair and store them in a file + ''' + + def __init__(self, log, size=2048): + self._log = log + self._size = size + + self._key = None + self._key_pem = None + self._pub_ssh = None + self._key_file = None + self._pub_file = None + + @property + def log(self): + return self._log + + @property + def size(self): + return self._size + + @property + def private_key(self): + if self._key is None: + self._gen_keys() + return self._key_pem + + @property + def public_key(self): + if self._key is None: + self._gen_keys() + return self._pub_ssh + + @property + def private_key_file(self): + return self._key_file + + @property + def public_key_file(self): + return self._pub_file + + def _gen_keys(self): + if self._key: + return + + self.log.info("Generating key of size: {}".format(self.size)) + + self._key = RSA.generate(self.size, os.urandom) + self._key_pem = self._key.exportKey('PEM').decode('utf-8') + self.log.debug("Private key PEM: {}".format(self._key_pem)) + + # Public key export as 'OpenSSH' has a bug + # (https://github.com/dlitz/pycrypto/issues/99) + + username = None + try: + username = os.getlogin() + hostname = socket.getfqdn() + except OSError: + pass + + pub = self._key.publickey().exportKey('OpenSSH').decode('utf-8') + if username: + self._pub_ssh = '{} {}@{}'.format(pub, username, hostname) + else: + self._pub_ssh = pub + self.log.debug("Public key SSH: {}".format(self._pub_ssh)) + + def write_to_disk(self, + name="id_rsa", + directory="."): + if self._key is None: + self._gen_keys() + + path = os.path.abspath(directory) + self._pub_file = "{}/{}.pub".format(path, name) + self._key_file = "{}/{}.key".format(path, name) + + with open(self._key_file, 'w') as content_file: + content_file.write(self.private_key) + os.chmod(self._key_file, stat.S_IREAD|stat.S_IWRITE) + + with open(self._pub_file, 'w') as content_file: + content_file.write(self.public_key) + +if __name__ == "__main__": + parser = argparse.ArgumentParser(description='Generate SSH key pair') + parser.add_argument("-s", "--size", type=int, default=2048, help="Key size") + parser.add_argument("-d", "--directory", help="Directory to store the keys") + parser.add_argument("-n", "--name", help="Name for the key file") + parser.add_argument("--debug", help="Enable debug logging", + action="store_true") + args = parser.parse_args() + + fmt = logging.Formatter( + '%(asctime)-23s %(levelname)-5s (%(name)s@%(process)d:' \ + '%(filename)s:%(lineno)d) - %(message)s') + stderr_handler = logging.StreamHandler(stream=sys.stderr) + stderr_handler.setFormatter(fmt) + if args.debug: + logging.basicConfig(level=logging.DEBUG) + else: + logging.basicConfig(level=logging.INFO) + log = logging.getLogger('rw-mano-ssh-keys') + log.addHandler(stderr_handler) + + log.info("Args passed: {}".format(args)) + if args.directory: + path = args.directory + else: + path = tempfile.mkdtemp() + + kp = ManoSshKey(log, size=args.size) + kp.write_to_disk(directory=path) + log.info("Private Key: {}".format(kp.private_key)) + log.info("Public key: {}".format(kp.public_key)) + log.info("Key file: {}, Public file: {}".format(kp.private_key_file, + kp.public_key_file)) diff --git a/examples/ping_pong_ns/rift/mano/examples/ping_pong_nsd.py b/examples/ping_pong_ns/rift/mano/examples/ping_pong_nsd.py index eb6add4b..8999507f 100755 --- a/examples/ping_pong_ns/rift/mano/examples/ping_pong_nsd.py +++ b/examples/ping_pong_ns/rift/mano/examples/ping_pong_nsd.py @@ -118,6 +118,10 @@ class VirtualNetworkFunction(ManoDescriptor): def __init__(self, name, instance_count=1): self.vnfd_catalog = None self.vnfd = None + self.mano_ut = False + self.use_ns_init_conf = False + self.use_vca_conf = False + self.use_charm = False self.instance_count = instance_count self._placement_groups = [] super(VirtualNetworkFunction, self).__init__(name) @@ -125,7 +129,116 @@ class VirtualNetworkFunction(ManoDescriptor): def add_placement_group(self, group): self._placement_groups.append(group) - def add_vnf_access_point(self, mano_ut=False): + def add_vnf_conf_param_charm(self): + vnfd = self.descriptor.vnfd[0] + confparam = vnfd.config_parameter + + src = confparam.create_config_parameter_source() + src.from_dict({ + "name": "mgmt_ip", + "description": "Management IP address", + "attribute": "../../../mgmt-interface, ip-address", + "parameter" : [{ + "config_primitive_name_ref": "config", + "config_primitive_parameter_ref": "ssh-hostname" + }] + }) + confparam.config_parameter_source.append(src) + + src = confparam.create_config_parameter_source() + src.from_dict({ + "name": "username", + "description": "SSH username", + "value": "fedora", + "parameter" : [{ + "config_primitive_name_ref": "config", + "config_primitive_parameter_ref": "ssh-username" + }] + }) + confparam.config_parameter_source.append(src) + + src = confparam.create_config_parameter_source() + src.from_dict({ + "name": "ssh_key", + "description": "SSH private key file", + "attribute": "../../../mgmt-interface/ssh-key, private-key-file", + "parameter" : [{ + "config_primitive_name_ref": "config", + "config_primitive_parameter_ref": "ssh-private-key" + }] + }) + confparam.config_parameter_source.append(src) + + # Check if pong + if 'pong_' in self.name: + src = confparam.create_config_parameter_source() + src.from_dict({ + "name": "service_ip", + "description": "IP on which Pong service is listening", + "attribute": "../../../connection-point[name='pong_vnfd/cp0'], ip-address", + "parameter" : [ + { + "config_primitive_name_ref": "set-server", + "config_primitive_parameter_ref": "server-ip" + }, + ] + }) + confparam.config_parameter_source.append(src) + src = confparam.create_config_parameter_source() + src.from_dict({ + "name": "service_port", + "description": "Port on which server listens for incoming data packets", + "value": "5555", + "parameter" : [ + { + "config_primitive_name_ref": "set-server", + "config_primitive_parameter_ref": "server-port" + }, + ] + }) + confparam.config_parameter_source.append(src) + + else: + src = confparam.create_config_parameter_source() + src.from_dict({ + "name": "rate", + "description": "Rate of packet generation", + "value": "5", + "parameter" : [ + { + "config_primitive_name_ref": "set-rate", + "config_primitive_parameter_ref": "rate" + }, + ] + }) + confparam.config_parameter_source.append(src) + + req = confparam.create_config_parameter_request() + req.from_dict({ + "name": "pong_ip", + "description": "IP on which Pong service is listening", + "parameter" : [ + { + "config_primitive_name_ref": "set-server", + "config_primitive_parameter_ref": "server-ip" + }, + ] + }) + confparam.config_parameter_request.append(req) + req = confparam.create_config_parameter_request() + req.from_dict({ + "name": "pong_port", + "description": "Port on which Pong service is listening", + "parameter" : [ + { + "config_primitive_name_ref": "set-server", + "config_primitive_parameter_ref": "server-port" + }, + ] + }) + confparam.config_parameter_request.append(req) + + def add_vnf_conf_param(self): vnfd = self.descriptor.vnfd[0] confparam = vnfd.config_parameter @@ -261,7 +374,7 @@ class VirtualNetworkFunction(ManoDescriptor): }) confparam.config_parameter_request.append(req) - def add_ping_config(self, mano_ut=False, use_ns_init_conf=False): + def add_ping_config(self): vnfd = self.descriptor.vnfd[0] # Add vnf configuration vnf_config = vnfd.vnf_configuration @@ -334,7 +447,7 @@ class VirtualNetworkFunction(ManoDescriptor): ) vnf_config.initial_config_primitive.append(init_config) - if use_ns_init_conf is False: + if self.use_ns_init_conf is False: init_config = VnfdYang.InitialConfigPrimitive.from_dict( { "seq": 3, @@ -343,7 +456,7 @@ class VirtualNetworkFunction(ManoDescriptor): ) vnf_config.initial_config_primitive.append(init_config) - def add_pong_config(self, mano_ut=False, use_ns_init_conf=False): + def add_pong_config(self): vnfd = self.descriptor.vnfd[0] # Add vnf configuration vnf_config = vnfd.vnf_configuration @@ -391,7 +504,7 @@ class VirtualNetworkFunction(ManoDescriptor): ) vnf_config.initial_config_primitive.append(init_config) - if use_ns_init_conf is False: + if self.use_ns_init_conf is False: init_config = VnfdYang.InitialConfigPrimitive.from_dict( { "seq": 2, @@ -400,10 +513,131 @@ class VirtualNetworkFunction(ManoDescriptor): ) vnf_config.initial_config_primitive.append(init_config) + def add_charm_config(self): + vnfd = self.descriptor.vnfd[0] + # Add vnf configuration + vnf_config = vnfd.vnf_configuration + + if 'pong_' in self.name: + mode = "pong" + else: + mode = "ping" + + # Select "script" configuration + vnf_config.juju.charm = 'pingpong' + + # Add config primitive + vnf_config.create_config_primitive() + prim = VnfdYang.ConfigPrimitive.from_dict({ + "name": "start", + }) + vnf_config.config_primitive.append(prim) + + prim = VnfdYang.ConfigPrimitive.from_dict({ + "name": "stop", + }) + vnf_config.config_primitive.append(prim) + + prim = VnfdYang.ConfigPrimitive.from_dict({ + "name": "restart", + }) + vnf_config.config_primitive.append(prim) + + prim = VnfdYang.ConfigPrimitive.from_dict({ + "name": "config", + "parameter": [ + {"name": "ssh-hostname", "data_type": "STRING"}, + {"name": "ssh-username", "data_type": "STRING"}, + {"name": "ssh-private-key", "data_type": "STRING"}, + {"name": "mode", "data_type": "STRING", + "default_value": "{}".format(mode), + "read_only": "true"}, + ], + }) + vnf_config.config_primitive.append(prim) + + prim = VnfdYang.ConfigPrimitive.from_dict({ + "name": "set-server", + "parameter": [ + {"name": "server-ip", "data_type": "STRING"}, + {"name": "server-port", "data_type": "INTEGER"}, + ], + }) + vnf_config.config_primitive.append(prim) + + if mode == 'ping': + prim = VnfdYang.ConfigPrimitive.from_dict({ + "name": "set-rate", + "parameter": [ + {"name": "rate", "data_type": "INTEGER", + "default_value": "5"}, + ], + }) + vnf_config.config_primitive.append(prim) + + prim = VnfdYang.ConfigPrimitive.from_dict({ + "name": "start-traffic", + }) + vnf_config.config_primitive.append(prim) + + prim = VnfdYang.ConfigPrimitive.from_dict({ + "name": "stop-traffic", + }) + vnf_config.config_primitive.append(prim) + + # Add initial config primitive + vnf_config.create_initial_config_primitive() + init_config = VnfdYang.InitialConfigPrimitive.from_dict( + { + "seq": 1, + "config_primitive_ref": "config", + } + ) + vnf_config.initial_config_primitive.append(init_config) + + init_config = VnfdYang.InitialConfigPrimitive.from_dict( + { + "seq": 2, + "config_primitive_ref": "start", + } + ) + vnf_config.initial_config_primitive.append(init_config) + + init_config = VnfdYang.InitialConfigPrimitive.from_dict( + { + "seq": 3, + "config_primitive_ref": "set-server", + }, + ) + vnf_config.initial_config_primitive.append(init_config) + + if mode == 'ping': + init_config = VnfdYang.InitialConfigPrimitive.from_dict( + { + "seq": 4, + "config_primitive_ref": "set-rate", + }, + ) + vnf_config.initial_config_primitive.append(init_config) + + if self.use_ns_init_conf is False: + init_config = VnfdYang.InitialConfigPrimitive.from_dict( + { + "seq": 5, + "config_primitive_ref": "start-traffic", + }, + ) + vnf_config.initial_config_primitive.append(init_config) + def compose(self, image_name, cloud_init="", cloud_init_file="", endpoint=None, mon_params=[], mon_port=8888, mgmt_port=8888, num_vlr_count=1, num_ivlr_count=1, num_vms=1, image_md5sum=None, mano_ut=False, use_ns_init_conf=False, - use_vca_conf=False): + use_vca_conf=False, use_charm=False): + self.mano_ut = mano_ut + self.use_ns_init_conf = use_ns_init_conf + self.use_vca_conf = use_vca_conf + self.use_charm = use_charm + self.descriptor = RwVnfdYang.YangData_Vnfd_VnfdCatalog() self.id = str(uuid.uuid1()) vnfd = self.descriptor.vnfd.add() @@ -471,22 +705,28 @@ class VirtualNetworkFunction(ManoDescriptor): mgmt_intf.dashboard_params.path = endpoint mgmt_intf.dashboard_params.port = mgmt_port - if cloud_init_file and len(cloud_init_file): - vdu.cloud_init_file = cloud_init_file - else: - vdu.cloud_init = cloud_init - if aws: - vdu.cloud_init += " - [ systemctl, restart, --no-block, elastic-network-interfaces.service ]\n" + if use_charm: + mgmt_intf.ssh_key = True + + if not self.use_charm: + if cloud_init_file and len(cloud_init_file): + vdu.cloud_init_file = cloud_init_file + else: + vdu.cloud_init = cloud_init + if aws: + vdu.cloud_init += " - [ systemctl, restart, --no-block, elastic-network-interfaces.service ]\n" # Add VNF access point if use_vca_conf: - self.add_vnf_access_point(mano_ut=mano_ut) - if 'pong_' in self.name: - self.add_pong_config(mano_ut=mano_ut, - use_ns_init_conf=use_ns_init_conf) + if use_charm: + self.add_vnf_conf_param_charm() + self.add_charm_config() else: - self.add_ping_config(mano_ut=mano_ut, - use_ns_init_conf=use_ns_init_conf) + self.add_vnf_conf_param() + if 'pong_' in self.name: + self.add_pong_config() + else: + self.add_ping_config() # sepcify the guest EPA if use_epa: @@ -583,14 +823,14 @@ class VirtualNetworkFunction(ManoDescriptor): member_vdu.member_vdu_ref = vdu.id - def write_to_file(self, outdir, output_format, use_vca_conf=False): + def write_to_file(self, outdir, output_format): dirpath = "%s/%s" % (outdir, self.name) if not os.path.exists(dirpath): os.makedirs(dirpath) super(VirtualNetworkFunction, self).write_to_file(['vnfd', 'rw-vnfd'], dirpath, output_format) - self.add_scripts(outdir, use_vca_conf=use_vca_conf) + self.add_scripts(outdir) def add_cloud_init(self, outdir): script_dir = os.path.join(outdir, self.name, 'cloud_init') @@ -610,9 +850,11 @@ class VirtualNetworkFunction(ManoDescriptor): with open(script_file, "w") as f: f.write("{}".format(cfg)) - def add_scripts(self, outdir, use_vca_conf=False): - self.add_cloud_init(outdir) - if use_vca_conf: + def add_scripts(self, outdir): + if not self.use_charm: + self.add_cloud_init(outdir) + + if self.use_vca_conf and not self.use_charm: self.add_vca_scripts(outdir) def add_vca_scripts(self, outdir): @@ -927,7 +1169,7 @@ exit 0 self.nsd.monitoring_param.append(nsd_monp) param_id += 1 - def add_confparam_map(self): + def add_conf_param_map(self): nsd = self.nsd confparam_map = nsd.config_parameter_map.add() @@ -1051,7 +1293,7 @@ exit 0 # self.create_mon_params(vnfd_list) if use_vca_conf: - self.add_confparam_map() + self.add_conf_param_map() def write_config(self, outdir, vnfds): @@ -1227,6 +1469,7 @@ def generate_ping_pong_descriptors(fmt="json", use_placement_group=True, use_ns_init_conf=True, use_vca_conf=True, + use_charm=False, ): # List of connection point groups # Each connection point group refers to a virtual link @@ -1273,6 +1516,7 @@ def generate_ping_pong_descriptors(fmt="json", mano_ut=mano_ut, use_ns_init_conf=use_ns_init_conf, use_vca_conf=use_vca_conf, + use_charm=use_charm, ) pong = VirtualNetworkFunction("pong_vnfd%s" % (suffix)) @@ -1315,6 +1559,7 @@ def generate_ping_pong_descriptors(fmt="json", mano_ut=mano_ut, use_ns_init_conf=use_ns_init_conf, use_vca_conf=use_vca_conf, + use_charm=use_charm, ) # Initialize the member VNF index @@ -1385,10 +1630,8 @@ def generate_ping_pong_descriptors(fmt="json", ) if write_to_file: - ping.write_to_file(out_dir, ping_fmt if ping_fmt is not None else fmt, - use_vca_conf=use_vca_conf) - pong.write_to_file(out_dir, pong_fmt if ping_fmt is not None else fmt, - use_vca_conf=use_vca_conf) + ping.write_to_file(out_dir, ping_fmt if ping_fmt is not None else fmt) + pong.write_to_file(out_dir, pong_fmt if ping_fmt is not None else fmt) nsd_catalog.write_config(out_dir, vnfd_list) nsd_catalog.write_to_file(out_dir, ping_fmt if nsd_fmt is not None else fmt) @@ -1407,6 +1650,8 @@ def main(argv=sys.argv[1:]): parser.add_argument('--pong-image-md5') parser.add_argument('--ping-cloud-init', default=None) parser.add_argument('--pong-cloud-init', default=None) + parser.add_argument('--charm', action="store_true", default=False) + args = parser.parse_args() outdir = args.outdir output_format = args.format @@ -1417,9 +1662,12 @@ def main(argv=sys.argv[1:]): use_pong_cloud_init_file = args.pong_cloud_init generate_ping_pong_descriptors(args.format, True, args.outdir, pingcount, - ping_md5sum=args.ping_image_md5, pong_md5sum=args.pong_image_md5, + ping_md5sum=args.ping_image_md5, + pong_md5sum=args.pong_image_md5, mano_ut=False, - use_scale_group=False,) + use_scale_group=False, + use_charm=args.charm, + ) if __name__ == "__main__": main() diff --git a/models/plugins/yang/nsr.yang b/models/plugins/yang/nsr.yang index e8b65ae9..0ded043d 100644 --- a/models/plugins/yang/nsr.yang +++ b/models/plugins/yang/nsr.yang @@ -621,6 +621,18 @@ module nsr type uint32; } + container ssh-key-generated { + description "SSH key pair generated for this NS"; + leaf public-key { + description "Public key generated"; + type string; + } + leaf private-key-file { + description "Path to the private key file"; + type string; + } + } + list connection-point { description "List for external connection points. diff --git a/models/plugins/yang/vnfd.yang b/models/plugins/yang/vnfd.yang index 0806bf19..e5058045 100644 --- a/models/plugins/yang/vnfd.yang +++ b/models/plugins/yang/vnfd.yang @@ -342,6 +342,13 @@ module vnfd type inet:port-number; } + leaf ssh-key { + description + "Whether SSH keys need to be generated and passed + to the RO and VCA during instantiation."; + type boolean; + } + container dashboard-params { description "Parameters for the VNF dashboard"; diff --git a/models/plugins/yang/vnfr.yang b/models/plugins/yang/vnfr.yang index 69d56913..73ca1521 100644 --- a/models/plugins/yang/vnfr.yang +++ b/models/plugins/yang/vnfr.yang @@ -223,6 +223,18 @@ module vnfr leaf port { type inet:port-number; } + + container ssh-key { + description "SSH key pair used for this VNF"; + leaf public-key { + description "Public key configured on this VNF"; + type string; + } + leaf private-key-file { + description "Path to the private key file"; + type string; + } + } } list internal-vlr { diff --git a/rwcm/plugins/rwconman/rift/tasklets/rwconmantasklet/jujuconf.py b/rwcm/plugins/rwconman/rift/tasklets/rwconmantasklet/jujuconf.py index 73a00051..016f712a 100644 --- a/rwcm/plugins/rwconman/rift/tasklets/rwconmantasklet/jujuconf.py +++ b/rwcm/plugins/rwconman/rift/tasklets/rwconmantasklet/jujuconf.py @@ -299,7 +299,7 @@ class JujuConfigPlugin(riftcm_config_plugin.RiftCMConfigPluginBase): @asyncio.coroutine def _vnf_config_primitive(self, nsr_id, vnfr_id, primitive, - vnf_config=None): + vnf_config=None, wait=False): self._log.debug("jujuCA: VNF config primitive {} for nsr {}, " "vnfr_id {}". format(primitive, nsr_id, vnfr_id)) @@ -370,11 +370,10 @@ class JujuConfigPlugin(riftcm_config_plugin.RiftCMConfigPluginBase): rc = yield from self.api.apply_config( params, service=service, - wait=False) + wait=True) if rc: - # Mark as pending and check later for the status - rc = "pending" + rc = "completed" self._log.debug("jujuCA: applied config {} " "on {}".format(params, service)) else: @@ -401,7 +400,7 @@ class JujuConfigPlugin(riftcm_config_plugin.RiftCMConfigPluginBase): if resp: if 'error' in resp: - details = resp['error']['Message'] + details = resp['error']['message'] else: exec_id = resp['action']['tag'] rc = resp['status'] @@ -423,10 +422,17 @@ class JujuConfigPlugin(riftcm_config_plugin.RiftCMConfigPluginBase): except KeyError as e: msg = "VNF %s does not have config primitives, e=%s", \ - vnfr_msg.name, e - self._log.error(msg) + vnfr_id, e + self._log.exception(msg) raise ValueError(msg) + while wait and (rc in ['pending', 'running']): + self._log.debug("JujuCA: action {}, rc {}". + format(exec_id, rc)) + yield from asyncio.sleep(0.2, loop=self._loop) + status = yield from self.api.get_action_status(exec_id) + rc = status['status'] + return rc, exec_id, details @asyncio.coroutine @@ -590,7 +596,7 @@ class JujuConfigPlugin(riftcm_config_plugin.RiftCMConfigPluginBase): for primitive in primitives: self._log.debug("(%s) Initial config primitive %s", - vnfr['vnf_juju_name'], primitive) + vnfr['vnf_juju_name'], primitive.as_dict()) if primitive.config_primitive_ref: # Reference to a primitive in config primitive class Primitive: @@ -604,7 +610,8 @@ class JujuConfigPlugin(riftcm_config_plugin.RiftCMConfigPluginBase): agent_nsr.id, agent_vnfr.id, prim, - vnf_config) + vnf_config, + wait=True) if rc == "failed": msg = "Error executing initial config primitive" \ diff --git a/rwcm/plugins/rwconman/rift/tasklets/rwconmantasklet/riftcm_config_plugin.py b/rwcm/plugins/rwconman/rift/tasklets/rwconmantasklet/riftcm_config_plugin.py index 78f0aa0d..e289cf46 100644 --- a/rwcm/plugins/rwconman/rift/tasklets/rwconmantasklet/riftcm_config_plugin.py +++ b/rwcm/plugins/rwconman/rift/tasklets/rwconmantasklet/riftcm_config_plugin.py @@ -18,6 +18,7 @@ import asyncio import abc import os import tempfile +from urllib.parse import urlparse import yaml import gi @@ -325,6 +326,11 @@ class RiftCMConfigPluginBase(object): def convert_value(self, value, type_='STRING'): if type_ == 'STRING': + if value.startswith('file://'): + p = urlparse(value) + with open(p[2], 'r') as f: + val = f.read() + return(val) return str(value) if type_ == 'INTEGER': diff --git a/rwcm/plugins/rwconman/rift/tasklets/rwconmantasklet/rwconman_config.py b/rwcm/plugins/rwconman/rift/tasklets/rwconmantasklet/rwconman_config.py index 6944d48a..4ea516d6 100644 --- a/rwcm/plugins/rwconman/rift/tasklets/rwconmantasklet/rwconman_config.py +++ b/rwcm/plugins/rwconman/rift/tasklets/rwconmantasklet/rwconman_config.py @@ -468,11 +468,12 @@ class ConfigManagerConfig(object): prims = vnfd.vnf_configuration.config_primitive if not prims: self._log.debug("VNFR {} with VNFD {} has no config primitives defined". - format(vnfr.name, vnfd.name)) + format(vnfr['name'], vnfd.name)) return except AttributeError as e: self._log.error("No config primitives found on VNFR {} ({})". - format(vnfr.name, vnfd.name)) + format(vnfr['name'], vnfd.name)) + continue cm_state = nsr_obj.find_vnfr_cm_state(vnfr['id']) srcs = cm_state['config_parameter']['config_parameter_source'] @@ -482,6 +483,9 @@ class ConfigManagerConfig(object): vnf_configuration['config_primitive'] = [] for prim in prims: confp = prim.as_dict() + if 'parameter' not in confp: + continue + for param in confp['parameter']: # First check the param in capabilities found = False @@ -1055,12 +1059,15 @@ class ConfigManagerConfig(object): ) v['vdur'] = [] - vdu_data = [(vdu['name'], vdu['management_ip'], vdu['vm_management_ip'], vdu['id']) - for vdu in vnfr['vdur']] + vdu_data = [] + for vdu in vnfr['vdur']: + d = {} + for k in ['name', 'management_ip', 'vm_management_ip', 'id']: + if k in vdu: + d[k] = vdu[k] + vdu_data.append(d) - for data in vdu_data: - data = dict(zip(['name', 'management_ip', 'vm_management_ip', 'id'] , data)) - v['vdur'].append(data) + v['vdur'].append(vdu_data) inp['vnfr'][vnfr['member_vnf_index_ref']] = v @@ -1152,7 +1159,7 @@ class ConfigManagerConfig(object): stderr=asyncio.subprocess.PIPE) yield from process.wait() if process.returncode: - script_out, script_err = yield from proc.communicate() + script_out, script_err = yield from process.communicate() msg = "NSR {} initial config using {} failed with {}". \ format(nsr_name, script, process.returncode) self._log.error(msg) @@ -1163,7 +1170,7 @@ class ConfigManagerConfig(object): os.remove(inp_file) except KeyError as e: - self._log.debug("Did not find initial config {}". + self._log.debug("Did not find initial config: {}". format(e)) diff --git a/rwlaunchpad/plugins/rwnsm/rift/tasklets/rwnsmtasklet/cloud.py b/rwlaunchpad/plugins/rwnsm/rift/tasklets/rwnsmtasklet/cloud.py index 32efff2a..63c804c8 100644 --- a/rwlaunchpad/plugins/rwnsm/rift/tasklets/rwnsmtasklet/cloud.py +++ b/rwlaunchpad/plugins/rwnsm/rift/tasklets/rwnsmtasklet/cloud.py @@ -40,7 +40,7 @@ class RwNsPlugin(rwnsmplugin.NsmPluginBase): self._log = log self._loop = loop - def create_nsr(self, nsr_msg, nsd,key_pairs=None): + def create_nsr(self, nsr_msg, nsd, key_pairs=None, ssh_key=None): """ Create Network service record """ @@ -92,6 +92,11 @@ class RwNsPlugin(rwnsmplugin.NsmPluginBase): """ yield from vlr.terminate() + @asyncio.coroutine + def update_vnfr(self, vnfr): + """ Update the virtual network function record """ + yield from vnfr.update_vnfm() + class NsmPlugins(object): """ NSM Plugins """ diff --git a/rwlaunchpad/plugins/rwnsm/rift/tasklets/rwnsmtasklet/openmano_nsm.py b/rwlaunchpad/plugins/rwnsm/rift/tasklets/rwnsmtasklet/openmano_nsm.py index 6c189464..31b05448 100644 --- a/rwlaunchpad/plugins/rwnsm/rift/tasklets/rwnsmtasklet/openmano_nsm.py +++ b/rwlaunchpad/plugins/rwnsm/rift/tasklets/rwnsmtasklet/openmano_nsm.py @@ -153,7 +153,7 @@ class VnfrConsoleOperdataDtsHandler(object): class OpenmanoVnfr(object): - def __init__(self, log, loop, cli_api, vnfr, nsd): + def __init__(self, log, loop, cli_api, vnfr, nsd, ssh_key=None): self._log = log self._loop = loop self._cli_api = cli_api @@ -165,6 +165,7 @@ class OpenmanoVnfr(object): self._created = False self.nsd = nsd + self._ssh_key = ssh_key @property def vnfd(self): @@ -260,7 +261,8 @@ class OpenmanoNSRecordState(Enum): class OpenmanoNsr(object): TIMEOUT_SECS = 300 - def __init__(self, dts, log, loop, publisher, cli_api, http_api, nsd_msg, nsr_config_msg,key_pairs): + def __init__(self, dts, log, loop, publisher, cli_api, http_api, nsd_msg, + nsr_config_msg, key_pairs, ssh_key): self._dts = dts self._log = log self._loop = loop @@ -275,6 +277,7 @@ class OpenmanoNsr(object): self._vnfrs = [] self._vdur_console_handler = {} self._key_pairs = key_pairs + self._ssh_key = ssh_key self._nsd_uuid = None self._nsr_uuid = None @@ -320,6 +323,10 @@ class OpenmanoNsr(object): self._log.debug("Key pair NSD is %s",authorized_key) key_pairs.append(authorized_key.key) + if self._ssh_key: + self._log.debug("Pub key NSD is %s", self._ssh_key['public_key']) + key_pairs.append(self._ssh_key['public_key']) + if key_pairs: cloud_config["key-pairs"] = key_pairs @@ -434,7 +441,8 @@ class OpenmanoNsr(object): @asyncio.coroutine def add_vnfr(self, vnfr): - vnfr = OpenmanoVnfr(self._log, self._loop, self._cli_api, vnfr, nsd=self.nsd) + vnfr = OpenmanoVnfr(self._log, self._loop, self._cli_api, vnfr, + nsd=self.nsd, ssh_key=self._ssh_key) yield from vnfr.create() self._vnfrs.append(vnfr) @@ -635,8 +643,13 @@ class OpenmanoNsr(object): self._log.debug("All VMs in VNF are active. Marking as running.") vnfr_msg.operational_status = "running" - self._log.debug("Got VNF ip address: %s, mac-address: %s", vnf_ip_address, vnf_mac_address) + self._log.debug("Got VNF ip address: %s, mac-address: %s", + vnf_ip_address, vnf_mac_address) vnfr_msg.mgmt_interface.ip_address = vnf_ip_address + vnfr_msg.mgmt_interface.ssh_key.public_key = \ + vnfr._ssh_key['public_key'] + vnfr_msg.mgmt_interface.ssh_key.private_key_file = \ + vnfr._ssh_key['private_key'] vnfr_msg.vnf_configuration.config_access.mgmt_ip_address = vnf_ip_address @@ -815,7 +828,7 @@ class OpenmanoNsPlugin(rwnsmplugin.NsmPluginBase): ro_account.openmano.tenant_id, ) - def create_nsr(self, nsr_config_msg, nsd_msg, key_pairs=None): + def create_nsr(self, nsr_config_msg, nsd_msg, key_pairs=None, ssh_key=None): """ Create Network service record """ @@ -828,7 +841,8 @@ class OpenmanoNsPlugin(rwnsmplugin.NsmPluginBase): self._http_api, nsd_msg, nsr_config_msg, - key_pairs + key_pairs, + ssh_key, ) self._openmano_nsrs[nsr_config_msg.id] = openmano_nsr @@ -865,6 +879,12 @@ class OpenmanoNsPlugin(rwnsmplugin.NsmPluginBase): self._log.debug("Creating a task to update uptime for vnfr: %s", vnfr.id) self._vnfr_uptime_tasks[vnfr.id] = self._loop.create_task(self.vnfr_uptime_update(vnfr)) + def update_vnfr(self, vnfr): + vnfr_msg = vnfr.vnfr_msg.deep_copy() + self._log.debug("Attempting to publish openmano vnf: %s", vnfr_msg) + with self._dts.transaction() as xact: + yield from self._publisher.publish_vnfr(xact, vnfr_msg) + def vnfr_uptime_update(self, vnfr): try: vnfr_ = RwVnfrYang.YangData_Vnfr_VnfrCatalog_Vnfr.from_dict({'id': vnfr.id}) diff --git a/rwlaunchpad/plugins/rwnsm/rift/tasklets/rwnsmtasklet/rwnsm_conman.py b/rwlaunchpad/plugins/rwnsm/rift/tasklets/rwnsmtasklet/rwnsm_conman.py index 8073c4cf..a237c3fb 100644 --- a/rwlaunchpad/plugins/rwnsm/rift/tasklets/rwnsmtasklet/rwnsm_conman.py +++ b/rwlaunchpad/plugins/rwnsm/rift/tasklets/rwnsmtasklet/rwnsm_conman.py @@ -101,7 +101,8 @@ class ROConfigManager(object): yield from \ self.nsm.vnfrs[vnfrid].update_config_primitives( - vnfr.vnf_configuration) + vnfr.vnf_configuration, + self.nsm.nsrs[nsrid]) # Update the NSR's config status new_status = ROConfigManager.map_config_status(cm_nsr.state) diff --git a/rwlaunchpad/plugins/rwnsm/rift/tasklets/rwnsmtasklet/rwnsmplugin.py b/rwlaunchpad/plugins/rwnsm/rift/tasklets/rwnsmtasklet/rwnsmplugin.py index ec162597..3febfd12 100755 --- a/rwlaunchpad/plugins/rwnsm/rift/tasklets/rwnsmtasklet/rwnsmplugin.py +++ b/rwlaunchpad/plugins/rwnsm/rift/tasklets/rwnsmtasklet/rwnsmplugin.py @@ -48,7 +48,7 @@ class NsmPluginBase(object): def nsm(self): return self._nsm - def create_nsr(self, nsr): + def create_nsr(self, nsr, nsd, key_pairs=None, ssh_key=None): """ Create an NSR """ pass @@ -75,6 +75,12 @@ class NsmPluginBase(object): """ Instantiate the virtual link""" pass + @abc.abstractmethod + @asyncio.coroutine + def update_vnfr(self, vnfr): + """ Update the virtual network function record """ + pass + @abc.abstractmethod @asyncio.coroutine def get_nsr(self, nsr_path): diff --git a/rwlaunchpad/plugins/rwnsm/rift/tasklets/rwnsmtasklet/rwnsmtasklet.py b/rwlaunchpad/plugins/rwnsm/rift/tasklets/rwnsmtasklet/rwnsmtasklet.py index dce9ebe6..c9f7bb9e 100755 --- a/rwlaunchpad/plugins/rwnsm/rift/tasklets/rwnsmtasklet/rwnsmtasklet.py +++ b/rwlaunchpad/plugins/rwnsm/rift/tasklets/rwnsmtasklet/rwnsmtasklet.py @@ -27,7 +27,7 @@ import uuid import yaml import requests import json - +from urllib.parse import urlparse from collections import deque from collections import defaultdict @@ -59,10 +59,11 @@ from gi.repository import ( ProtobufC, ) -import rift.tasklets +from rift.mano.utils.ssh_keys import ManoSshKey import rift.mano.ncclient import rift.mano.config_data.config import rift.mano.dts as mano_dts +import rift.tasklets from . import rwnsm_conman as conman from . import cloud @@ -963,8 +964,9 @@ class VirtualNetworkFunctionRecord(object): vnfr = RwVnfrYang.YangData_Vnfr_VnfrCatalog_Vnfr.from_dict(vnfr_dict) - vnfr.vnfd = VnfrYang.YangData_Vnfr_VnfrCatalog_Vnfr_Vnfd.from_dict(self.vnfd.as_dict(), - ignore_missing_keys=True) + vnfr.vnfd = VnfrYang.YangData_Vnfr_VnfrCatalog_Vnfr_Vnfd.from_dict( + self.vnfd.as_dict(), + ignore_missing_keys=True) vnfr.member_vnf_index_ref = self.member_vnf_index vnfr.vnf_configuration.from_dict(self._vnfd.vnf_configuration.as_dict()) @@ -1049,7 +1051,7 @@ class VirtualNetworkFunctionRecord(object): return False @asyncio.coroutine - def update_config_primitives(self, vnf_config): + def update_config_primitives(self, vnf_config, nsr): # Update only after we are configured if self._config_status == NsrYang.ConfigStates.INIT: return @@ -1084,7 +1086,7 @@ class VirtualNetworkFunctionRecord(object): self._vnfr_msg = self.create_vnfr_msg() try: - yield from self.update_vnfm() + yield from nsr.nsm_plugin.update_vnfr(self) except Exception as e: self._log.error("Exception updating VNFM with new config " "primitive for VNFR {}: {}". @@ -1296,6 +1298,8 @@ class NetworkServiceRecord(object): self._nsr_msg = None self._nsr_regh = None self._key_pairs = key_pairs + self._ssh_key_file = None + self._ssh_pub_key = None self._vlrs = [] self._vnfrs = {} self._vnfds = {} @@ -1431,6 +1435,14 @@ class NetworkServiceRecord(object): """ Config status for NSR """ return self._config_status + @property + def public_key(self): + return self._ssh_pub_key + + @property + def private_key(self): + return self._ssh_key_file + def resolve_placement_group_cloud_construct(self, input_group): """ Returns the cloud specific construct for placement group @@ -1495,6 +1507,33 @@ class NetworkServiceRecord(object): self._log.exception(e) return "Unknown trigger" + @asyncio.coroutine + def generate_ssh_key_pair(self, config_xact): + '''Generate a ssh key pair if required''' + if self._ssh_key_file: + self._log.debug("Key pair already generated") + return + + gen_key = False + for cv in self.nsd_msg.constituent_vnfd: + vnfd = self._get_vnfd(cv.vnfd_id_ref, config_xact) + if vnfd and vnfd.mgmt_interface.ssh_key: + gen_key = True + break + + if not gen_key: + return + + try: + key = ManoSshKey(self._log) + path = tempfile.mkdtemp() + key.write_to_disk(name=self.id, directory=path) + self._ssh_key_file = "file://{}".format(key.private_key_file) + self._ssh_pub_key = key.public_key + except Exception as e: + self._log.exception("Error generating ssh key for {}: {}". + format(self.nsr_cfg_msg.name, e)) + @asyncio.coroutine def instantiate_vls(self): """ @@ -2498,6 +2537,15 @@ class NetworkServiceRecord(object): yield from self.nsm_plugin.terminate_ns(self) + # Remove the generated SSH key + if self._ssh_key_file: + p = urlparse(self._ssh_key_file) + if p[0] == 'file': + path = os.path.dirname(p[2]) + self._log.debug("NSR {}: Removing keys in {}".format(self.name, + path)) + shutil.rmtree(path, ignore_errors=True) + # Move the state to TERMINATED self.set_state(NetworkServiceRecordState.TERMINATED) event_descr = "Terminated NS Id:%s" % self.id @@ -2544,6 +2592,11 @@ class NetworkServiceRecord(object): nsr.create_time = self._create_time nsr.uptime = int(time.time()) - self._create_time + # Generated SSH key + if self._ssh_pub_key: + nsr.ssh_key_generated.private_key_file = self._ssh_key_file + nsr.ssh_key_generated.public_key = self._ssh_pub_key + for cfg_prim in self.nsd_msg.service_primitive: cfg_prim = NsrYang.YangData_Nsr_NsInstanceOpdata_Nsr_ServicePrimitive.from_dict( cfg_prim.as_dict()) @@ -3283,6 +3336,7 @@ class NsrDtsHandler(object): self._log.debug("Got nsr apply (xact: %s) (action: %s)(scr: %s)", xact, action, scratch) + @asyncio.coroutine def handle_create_nsr(msg, key_pairs=None, restart_mode=False): # Handle create nsr requests """ # Do some validations @@ -3293,7 +3347,10 @@ class NsrDtsHandler(object): self._log.debug("Creating NetworkServiceRecord %s from nsr config %s", msg.id, msg.as_dict()) - nsr = self.nsm.create_nsr(msg, key_pairs=key_pairs, restart_mode=restart_mode) + nsr = self.nsm.create_nsr(msg, + xact, + key_pairs=key_pairs, + restart_mode=restart_mode) return nsr def handle_delete_nsr(msg): @@ -3320,6 +3377,12 @@ class NsrDtsHandler(object): self._log.info("Beginning NS instantiation: %s", nsr.id) yield from self._nsm.instantiate_ns(nsr.id, xact) + @asyncio.coroutine + def instantiate_ns(msg, key_pairs, restart_mode=False): + nsr = yield from handle_create_nsr(msg, key_pairs, + restart_mode=restart_mode) + yield from begin_instantiation(nsr) + self._log.debug("Got nsr apply (xact: %s) (action: %s)(scr: %s)", xact, action, scratch) @@ -3328,9 +3391,8 @@ class NsrDtsHandler(object): for element in self._key_pair_regh.elements: key_pairs.append(element) for element in self._nsr_regh.elements: - nsr = handle_create_nsr(element, key_pairs, restart_mode=True) - self._loop.create_task(begin_instantiation(nsr)) - + self._loop.create_task(instantiate_ns(element, key_pairs, + restart_mode=True)) (added_msgs, deleted_msgs, updated_msgs) = get_add_delete_update_cfgs(self._nsr_regh, xact, @@ -3343,8 +3405,7 @@ class NsrDtsHandler(object): if msg.id not in self._nsm.nsrs: self._log.info("Create NSR received in on_apply to instantiate NS:%s", msg.id) key_pairs = get_nsr_key_pairs(self._key_pair_regh, xact) - nsr = handle_create_nsr(msg,key_pairs) - self._loop.create_task(begin_instantiation(nsr)) + self._loop.create_task(instantiate_ns(msg, key_pairs)) for msg in deleted_msgs: self._log.info("Delete NSR received in on_apply to terminate NS:%s", msg.id) @@ -3884,7 +3945,7 @@ class NsManager(object): # Not calling in a separate task as this is called from a separate task yield from nsr.delete_vl_instance(vld) - def create_nsr(self, nsr_msg, key_pairs=None,restart_mode=False): + def create_nsr(self, nsr_msg, config_xact, key_pairs=None, restart_mode=False): """ Create an NSR instance """ if nsr_msg.id in self._nsrs: msg = "NSR id %s already exists" % nsr_msg.id @@ -3910,7 +3971,18 @@ class NsManager(object): vlr_handler=self._ro_plugin_selector._records_publisher._vlr_pub_hdlr ) self._nsrs[nsr_msg.id] = nsr - nsm_plugin.create_nsr(nsr_msg, nsr_msg.nsd, key_pairs) + + # Generate ssh key pair if required + yield from nsr.generate_ssh_key_pair(config_xact) + + self._log.debug("NSR {}: SSh key generated: {}".format(nsr_msg.name, + nsr.public_key)) + + ssh_key = {'private_key': nsr.private_key, + 'public_key': nsr.public_key + } + + nsm_plugin.create_nsr(nsr_msg, nsr_msg.nsd, key_pairs, ssh_key=ssh_key) return nsr diff --git a/rwlaunchpad/plugins/rwvnfm/rift/tasklets/rwvnfmtasklet/rwvnfmtasklet.py b/rwlaunchpad/plugins/rwvnfm/rift/tasklets/rwvnfmtasklet/rwvnfmtasklet.py index f456b168..2958cd9f 100755 --- a/rwlaunchpad/plugins/rwvnfm/rift/tasklets/rwvnfmtasklet/rwvnfmtasklet.py +++ b/rwlaunchpad/plugins/rwvnfm/rift/tasklets/rwvnfmtasklet/rwvnfmtasklet.py @@ -502,7 +502,20 @@ class VirtualDeploymentUnitRecord(object): def vdud_cloud_init(self): """ Return the cloud-init contents for the VDU """ if self._vdud_cloud_init is None: - self._vdud_cloud_init = self.cloud_init() + ci = self.cloud_init() + + # VNFR ssh public key, if available + if self._vnfr.public_key: + if not ci: + ci = "#cloud-config" + self._vdud_cloud_init = """{} +ssh_authorized_keys: + - {}""". \ + format(ci, self._vnfr.public_key) + else: + self._vdud_cloud_init = ci + + self._log.debug("Cloud init: {}".format(self._vdud_cloud_init)) return self._vdud_cloud_init @@ -1159,6 +1172,9 @@ class VirtualNetworkFunctionRecord(object): self._rw_vnfd = None self._vnfd_ref_count = 0 + self._ssh_pub_key = None + self._ssh_key_file = None + def _get_vdur_from_vdu_id(self, vdu_id): self._log.debug("Finding vdur for vdu_id %s", vdu_id) self._log.debug("Searching through vdus: %s", self._vdus) @@ -1254,6 +1270,10 @@ class VirtualNetworkFunctionRecord(object): """ Config agent status for this VNFR """ return self._config_status + @property + def public_key(self): + return self._ssh_pub_key + def component_by_name(self, component_name): """ Find a component by name in the inventory list""" mangled_name = VcsComponent.mangle_name(component_name, @@ -1278,6 +1298,22 @@ class VirtualNetworkFunctionRecord(object): return nsr return None + @asyncio.coroutine + def get_nsr_opdata(self): + """ NSR opdata associated with this VNFR """ + xpath = "D,/nsr:ns-instance-opdata/nsr:nsr" \ + "[nsr:ns-instance-config-ref = '{}']". \ + format(self._vnfr_msg.nsr_id_ref) + + results = yield from self._dts.query_read(xpath, rwdts.XactFlag.MERGE) + + for result in results: + entry = yield from result + nsr_op = entry.result + return nsr_op + + return None + @asyncio.coroutine def start_component(self, component_name, ip_addr): """ Start a component in the VNFR by name """ @@ -1325,6 +1361,10 @@ class VirtualNetworkFunctionRecord(object): if port is not None: mgmt_intf.port = port + if self._ssh_pub_key: + mgmt_intf.ssh_key.public_key = self._ssh_pub_key + mgmt_intf.ssh_key.private_key_file = self._ssh_key_file + vnfr_dict = {"id": self._vnfr_id, "nsr_id_ref": self._vnfr_msg.nsr_id_ref, "name": self.name, @@ -1388,7 +1428,7 @@ class VirtualNetworkFunctionRecord(object): self._vnfr = RwVnfrYang.YangData_Vnfr_VnfrCatalog_Vnfr.from_dict( msg.as_dict()) self._log.debug("VNFR msg config: {}". - format(self.msg.vnf_configuration.as_dict())) + format(self._vnfr.as_dict())) yield from self.publish(xact) @property @@ -1803,6 +1843,11 @@ class VirtualNetworkFunctionRecord(object): self.set_state(VirtualNetworkFunctionRecordState.VL_INIT_PHASE) self._rw_vnfd = yield from self._vnfm.fetch_vnfd(self._vnfd_id) + nsr_op = yield from self.get_nsr_opdata() + if nsr_op: + self._ssh_key_file = nsr_op.ssh_key_generated.private_key_file + self._ssh_pub_key = nsr_op.ssh_key_generated.public_key + @asyncio.coroutine def fetch_vlrs(): """ Fetch VLRs """ -- 2.25.1