bug 425 fix SR-IOV PCI-PASSTHROUGH interfaces
[osm/RO.git] / osm_ro / vimconn.py
index 8b6f80e..bdfcb15 100644 (file)
@@ -29,6 +29,13 @@ __author__="Alfonso Tierno, Igor D.C."
 __date__ ="$14-aug-2017 23:59:59$"
 
 import logging
 __date__ ="$14-aug-2017 23:59:59$"
 
 import logging
+import paramiko
+import socket
+import StringIO
+import yaml
+import sys
+from email.mime.multipart import MIMEMultipart
+from email.mime.text import MIMEText
 
 #Error variables 
 HTTP_Bad_Request = 400
 
 #Error variables 
 HTTP_Bad_Request = 400
@@ -164,7 +171,116 @@ class vimconnector():
             self.url_admin = value
         else:
             raise KeyError("Invalid key '%s'" %str(index))
             self.url_admin = value
         else:
             raise KeyError("Invalid key '%s'" %str(index))
-        
+
+    @staticmethod
+    def _create_mimemultipart(content_list):
+        """Creates a MIMEmultipart text combining the content_list
+        :param content_list: list of text scripts to be combined
+        :return: str of the created MIMEmultipart. If the list is empty returns None, if the list contains only one
+        element MIMEmultipart is not created and this content is returned
+        """
+        if not content_list:
+            return None
+        elif len(content_list) == 1:
+            return content_list[0]
+        combined_message = MIMEMultipart()
+        for content in content_list:
+            if content.startswith('#include'):
+                format = 'text/x-include-url'
+            elif content.startswith('#include-once'):
+                format = 'text/x-include-once-url'
+            elif content.startswith('#!'):
+                format = 'text/x-shellscript'
+            elif content.startswith('#cloud-config'):
+                format = 'text/cloud-config'
+            elif content.startswith('#cloud-config-archive'):
+                format = 'text/cloud-config-archive'
+            elif content.startswith('#upstart-job'):
+                format = 'text/upstart-job'
+            elif content.startswith('#part-handler'):
+                format = 'text/part-handler'
+            elif content.startswith('#cloud-boothook'):
+                format = 'text/cloud-boothook'
+            else:  # by default
+                format = 'text/x-shellscript'
+            sub_message = MIMEText(content, format, sys.getdefaultencoding())
+            combined_message.attach(sub_message)
+        return combined_message.as_string()
+
+    def _create_user_data(self, cloud_config):
+        """
+        Creates a script user database on cloud_config info
+        :param cloud_config: dictionary with
+            'key-pairs': (optional) list of strings with the public key to be inserted to the default user
+            'users': (optional) list of users to be inserted, each item is a dict with:
+                'name': (mandatory) user name,
+                'key-pairs': (optional) list of strings with the public key to be inserted to the user
+            'user-data': (optional) can be a string with the text script to be passed directly to cloud-init,
+                or a list of strings, each one contains a script to be passed, usually with a MIMEmultipart file
+            'config-files': (optional). List of files to be transferred. Each item is a dict with:
+                'dest': (mandatory) string with the destination absolute path
+                'encoding': (optional, by default text). Can be one of:
+                    'b64', 'base64', 'gz', 'gz+b64', 'gz+base64', 'gzip+b64', 'gzip+base64'
+                'content' (mandatory): string with the content of the file
+                'permissions': (optional) string with file permissions, typically octal notation '0644'
+                'owner': (optional) file owner, string with the format 'owner:group'
+            'boot-data-drive': boolean to indicate if user-data must be passed using a boot drive (hard disk)
+        :return: config_drive, userdata. The first is a boolean or None, the second a string or None
+        """
+        config_drive = None
+        userdata = None
+        userdata_list = []
+        if isinstance(cloud_config, dict):
+            if cloud_config.get("user-data"):
+                if isinstance(cloud_config["user-data"], str):
+                    userdata_list.append(cloud_config["user-data"])
+                else:
+                    for u in cloud_config["user-data"]:
+                        userdata_list.append(u)
+            if cloud_config.get("boot-data-drive") != None:
+                config_drive = cloud_config["boot-data-drive"]
+            if cloud_config.get("config-files") or cloud_config.get("users") or cloud_config.get("key-pairs"):
+                userdata_dict = {}
+                # default user
+                if cloud_config.get("key-pairs"):
+                    userdata_dict["ssh-authorized-keys"] = cloud_config["key-pairs"]
+                    userdata_dict["users"] = [{"default": None, "ssh-authorized-keys": cloud_config["key-pairs"]}]
+                if cloud_config.get("users"):
+                    if "users" not in userdata_dict:
+                        userdata_dict["users"] = ["default"]
+                    for user in cloud_config["users"]:
+                        user_info = {
+                            "name": user["name"],
+                            "sudo": "ALL = (ALL)NOPASSWD:ALL"
+                        }
+                        if "user-info" in user:
+                            user_info["gecos"] = user["user-info"]
+                        if user.get("key-pairs"):
+                            user_info["ssh-authorized-keys"] = user["key-pairs"]
+                        userdata_dict["users"].append(user_info)
+
+                if cloud_config.get("config-files"):
+                    userdata_dict["write_files"] = []
+                    for file in cloud_config["config-files"]:
+                        file_info = {
+                            "path": file["dest"],
+                            "content": file["content"]
+                        }
+                        if file.get("encoding"):
+                            file_info["encoding"] = file["encoding"]
+                        if file.get("permissions"):
+                            file_info["permissions"] = file["permissions"]
+                        if file.get("owner"):
+                            file_info["owner"] = file["owner"]
+                        userdata_dict["write_files"].append(file_info)
+                userdata_list.append("#cloud-config\n" + yaml.safe_dump(userdata_dict, indent=4,
+                                                                        default_flow_style=False))
+            userdata = self._create_mimemultipart(userdata_list)
+            self.logger.debug("userdata: %s", userdata)
+        elif isinstance(cloud_config, str):
+            userdata = cloud_config
+        return config_drive, userdata
+
     def check_vim_connectivity(self):
         """Checks VIM can be reached and user credentials are ok.
         Returns None if success or raised vimconnConnectionException, vimconnAuthException, ...
     def check_vim_connectivity(self):
         """Checks VIM can be reached and user credentials are ok.
         Returns None if success or raised vimconnConnectionException, vimconnAuthException, ...
@@ -371,9 +487,9 @@ class vimconnector():
                     the VLAN tag to be used. In case net_id is provided, the internal network vlan is used for tagging VF
                 'type': (mandatory) can be one of:
                     'virtual', in this case always connected to a network of type 'net_type=bridge'
                     the VLAN tag to be used. In case net_id is provided, the internal network vlan is used for tagging VF
                 'type': (mandatory) can be one of:
                     'virtual', in this case always connected to a network of type 'net_type=bridge'
-                     'PF' (passthrough): depending on VIM capabilities it can be connected to a data/ptp network ot it
+                     'PCI-PASSTHROUGH' or 'PF' (passthrough): depending on VIM capabilities it can be connected to a data/ptp network ot it
                            can created unconnected
                            can created unconnected
-                     'VF' (SRIOV with VLAN tag): same as PF for network connectivity.
+                     'SR-IOV' or 'VF' (SRIOV with VLAN tag): same as PF for network connectivity.
                      'VFnotShared'(SRIOV without VLAN tag) same as PF for network connectivity. VF where no other VFs
                             are allocated on the same physical NIC
                 'bw': (optional) only for PF/VF/VFnotShared. Minimal Bandwidth required for the interface in GBPS
                      'VFnotShared'(SRIOV without VLAN tag) same as PF for network connectivity. VF where no other VFs
                             are allocated on the same physical NIC
                 'bw': (optional) only for PF/VF/VFnotShared. Minimal Bandwidth required for the interface in GBPS
@@ -403,7 +519,11 @@ class vimconnector():
             availability_zone_index: Index of availability_zone_list to use for this this VM. None if not AV required
             availability_zone_list: list of availability zones given by user in the VNFD descriptor.  Ignore if
                 availability_zone_index is None
             availability_zone_index: Index of availability_zone_list to use for this this VM. None if not AV required
             availability_zone_list: list of availability zones given by user in the VNFD descriptor.  Ignore if
                 availability_zone_index is None
-        Returns the instance identifier or raises an exception on error
+        Returns a tuple with the instance identifier and created_items or raises an exception on error
+            created_items can be None or a dictionary where this method can include key-values that will be passed to
+            the method delete_vminstance and action_vminstance. Can be used to store created ports, volumes, etc.
+            Format is vimconnector dependent, but do not use nested dictionaries and a value of None should be the same
+            as not present.
         """
         raise vimconnNotImplemented( "Should have implemented this" )
         
         """
         raise vimconnNotImplemented( "Should have implemented this" )
         
@@ -411,9 +531,14 @@ class vimconnector():
         """Returns the VM instance information from VIM"""
         raise vimconnNotImplemented( "Should have implemented this" )
         
         """Returns the VM instance information from VIM"""
         raise vimconnNotImplemented( "Should have implemented this" )
         
-    def delete_vminstance(self, vm_id):
-        """Removes a VM instance from VIM
-        Returns the instance identifier"""
+    def delete_vminstance(self, vm_id, created_items=None):
+        """
+        Removes a VM instance from VIM and each associate elements
+        :param vm_id: VIM identifier of the VM, provided by method new_vminstance
+        :param created_items: dictionary with extra items to be deleted. provided by method new_vminstance and/or method
+            action_vminstance
+        :return: None or the same vm_id. Raises an exception on fail
+        """
         raise vimconnNotImplemented( "Should have implemented this" )
 
     def refresh_vms_status(self, vm_list):
         raise vimconnNotImplemented( "Should have implemented this" )
 
     def refresh_vms_status(self, vm_list):
@@ -444,9 +569,18 @@ class vimconnector():
         """
         raise vimconnNotImplemented( "Should have implemented this" )
     
         """
         raise vimconnNotImplemented( "Should have implemented this" )
     
-    def action_vminstance(self, vm_id, action_dict):
-        """Send and action over a VM instance from VIM
-        Returns the vm_id if the action was successfully sent to the VIM"""
+    def action_vminstance(self, vm_id, action_dict, created_items={}):
+        """
+        Send and action over a VM instance. Returns created_items if the action was successfully sent to the VIM.
+        created_items is a dictionary with items that
+        :param vm_id: VIM identifier of the VM, provided by method new_vminstance
+        :param action_dict: dictionary with the action to perform
+        :param created_items: provided by method new_vminstance is a dictionary with key-values that will be passed to
+            the method delete_vminstance. Can be used to store created ports, volumes, etc. Format is vimconnector
+            dependent, but do not use nested dictionaries and a value of None should be the same as not present. This
+            method can modify this value
+        :return: None, or a console dict
+        """
         raise vimconnNotImplemented( "Should have implemented this" )
     
     def get_vminstance_console(self, vm_id, console_type="vnc"):
         raise vimconnNotImplemented( "Should have implemented this" )
     
     def get_vminstance_console(self, vm_id, console_type="vnc"):
@@ -662,6 +796,47 @@ class vimconnector():
         """
         raise vimconnNotImplemented( "SFC support not implemented" )
 
         """
         raise vimconnNotImplemented( "SFC support not implemented" )
 
+    def inject_user_key(self, ip_addr=None, user=None, key=None, ro_key=None, password=None):
+        """
+        Inject a ssh public key in a VM
+        Params:
+            ip_addr: ip address of the VM
+            user: username (default-user) to enter in the VM
+            key: public key to be injected in the VM
+            ro_key: private key of the RO, used to enter in the VM if the password is not provided
+            password: password of the user to enter in the VM
+        The function doesn't return a value:
+        """
+        if not ip_addr or not user:
+            raise vimconnNotSupportedException("All parameters should be different from 'None'")
+        elif not ro_key and not password:
+            raise vimconnNotSupportedException("All parameters should be different from 'None'")
+        else:
+            commands = {'mkdir -p ~/.ssh/', 'echo "%s" >> ~/.ssh/authorized_keys' % key,
+                        'chmod 644 ~/.ssh/authorized_keys', 'chmod 700 ~/.ssh/'}
+            client = paramiko.SSHClient()
+            try:
+                if ro_key:
+                    pkey = paramiko.RSAKey.from_private_key(StringIO.StringIO(ro_key))
+                else:
+                    pkey = None
+                client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
+                client.connect(ip_addr, username=user, password=password, pkey=pkey, timeout=10)
+                for command in commands:
+                    (i, o, e) = client.exec_command(command, timeout=10)
+                    returncode = o.channel.recv_exit_status()
+                    output = o.read()
+                    outerror = e.read()
+                    if returncode != 0:
+                        text = "run_command='{}' Error='{}'".format(command, outerror)
+                        raise vimconnUnexpectedResponse("Cannot inject ssh key in VM: '{}'".format(text))
+                        return
+            except (socket.error, paramiko.AuthenticationException, paramiko.SSHException) as message:
+                raise vimconnUnexpectedResponse(
+                    "Cannot inject ssh key in VM: '{}' - {}".format(ip_addr, str(message)))
+                return
+
+
 #NOT USED METHODS in current version
 
     def host_vim2gui(self, host, server_dict):
 #NOT USED METHODS in current version
 
     def host_vim2gui(self, host, server_dict):
@@ -708,3 +883,4 @@ class vimconnector():
         """Adds a VM instance to VIM"""
         """Returns the instance identifier"""
         raise vimconnNotImplemented( "Should have implemented this" )
         """Adds a VM instance to VIM"""
         """Returns the instance identifier"""
         raise vimconnNotImplemented( "Should have implemented this" )
+