bug 425 fix SR-IOV PCI-PASSTHROUGH interfaces
[osm/RO.git] / osm_ro / vimconn.py
index a9bd9be..bdfcb15 100644 (file)
 vimconn implement an Abstract class for the vim connector plugins
  with the definition of the method to be implemented.
 """
-__author__="Alfonso Tierno"
-__date__ ="$16-oct-2015 11:09:29$"
+__author__="Alfonso Tierno, Igor D.C."
+__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
@@ -82,6 +89,7 @@ class vimconnNotImplemented(vimconnException):
     def __init__(self, message, http_code=HTTP_Not_Implemented):
         vimconnException.__init__(self, message, http_code)
 
+
 class vimconnector():
     """Abstract base class for all the VIM connector plugins
     These plugins must implement a vimconnector class derived from this 
@@ -115,6 +123,7 @@ class vimconnector():
         self.user      = user
         self.passwd    = passwd
         self.config    = config
+        self.availability_zone = None
         self.logger = logging.getLogger('openmano.vim')
         if log_level:
             self.logger.setLevel( getattr(logging, log_level) )
@@ -162,7 +171,116 @@ class vimconnector():
             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, ...
@@ -232,6 +350,8 @@ class vimconnector():
             'id': (mandatory) VIM network id
             'name': (mandatory) VIM network name
             'status': (mandatory) can be 'ACTIVE', 'INACTIVE', 'DOWN', 'BUILD', 'ERROR', 'VIM_ERROR', 'OTHER'
+            'network_type': (optional) can be 'vxlan', 'vlan' or 'flat'
+            'segmentation_id': (optional) in case network_type is vlan or vxlan this field contains the segmentation id
             'error_msg': (optional) text that explains the ERROR status
             other VIM specific fields: (optional) whenever possible using the same naming of filter_dict param
         List can be empty if no network map the filter_dict. Raise an exception only upon VIM connectivity,
@@ -351,8 +471,8 @@ class vimconnector():
         """
         raise vimconnNotImplemented( "Should have implemented this" )
 
-    def new_vminstance(self, name, description, start, image_id, flavor_id, net_list, cloud_config=None,
-                       disk_list=None):
+    def new_vminstance(self, name, description, start, image_id, flavor_id, net_list, cloud_config=None, disk_list=None,
+        availability_zone_index=None, availability_zone_list=None):
         """Adds a VM instance to VIM
         Params:
             'start': (boolean) indicates if VM must start or created in pause mode.
@@ -367,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'
-                     '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
-                     '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
@@ -383,7 +503,8 @@ class vimconnector():
                 '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) string is a text script to be passed directly to cloud-init
+                '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:
@@ -395,7 +516,14 @@ class vimconnector():
             'disk_list': (optional) list with additional disks to the VM. Each item is a dict with:
                 'image_id': (optional). VIM id of an existing image. If not provided an empty disk must be mounted
                 'size': (mandatory) string with the size of the disk in GB
-        Returns the instance identifier or raises an exception on error
+            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 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" )
         
@@ -403,9 +531,14 @@ class vimconnector():
         """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):
@@ -430,15 +563,24 @@ class vimconnector():
                         vim_net_id:       #network id where this interface is connected, if provided at creation
                         vim_interface_id: #interface/port VIM id
                         ip_address:       #null, or text with IPv4, IPv6 address
-                        physical_compute: #identification of compute node where PF,VF interface is allocated
-                        physical_pci:     #PCI address of the NIC that hosts the PF,VF
-                        physical_vlan:    #physical VLAN used for VF
+                        compute_node:     #identification of compute node where PF,VF interface is allocated
+                        pci:              #PCI address of the NIC that hosts the PF,VF
+                        vlan:             #physical VLAN used for VF
         """
         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"):
@@ -456,8 +598,246 @@ class vimconnector():
                 suffix:   extra text, e.g. the http path and query string   
         """
         raise vimconnNotImplemented( "Should have implemented this" )
-        
-#NOT USED METHODS in current version        
+
+    def new_classification(self, name, ctype, definition):
+        """Creates a traffic classification in the VIM
+        Params:
+            'name': name of this classification
+            'ctype': type of this classification
+            'definition': definition of this classification (type-dependent free-form text)
+        Returns the VIM's classification ID on success or raises an exception on failure
+        """
+        raise vimconnNotImplemented( "SFC support not implemented" )
+
+    def get_classification(self, classification_id):
+        """Obtain classification details of the VIM's classification with ID='classification_id'
+        Return a dict that contains:
+            'id': VIM's classification ID (same as classification_id)
+            'name': VIM's classification name
+            'type': type of this classification
+            'definition': definition of the classification
+            'status': 'ACTIVE', 'INACTIVE', 'DOWN', 'BUILD', 'ERROR', 'VIM_ERROR', 'OTHER'
+            'error_msg': (optional) text that explains the ERROR status
+            other VIM specific fields: (optional) whenever possible
+        Raises an exception upon error or when classification is not found
+        """
+        raise vimconnNotImplemented( "SFC support not implemented" )
+
+    def get_classification_list(self, filter_dict={}):
+        """Obtain classifications from the VIM
+        Params:
+            'filter_dict' (optional): contains the entries to filter the classifications on and only return those that match ALL:
+                id:   string => returns classifications with this VIM's classification ID, which implies a return of one classification at most
+                name: string => returns only classifications with this name
+                type: string => returns classifications of this type
+                definition: string => returns classifications that have this definition
+                tenant_id: string => returns only classifications that belong to this tenant/project
+        Returns a list of classification dictionaries, each dictionary contains:
+            'id': (mandatory) VIM's classification ID
+            'name': (mandatory) VIM's classification name
+            'type': type of this classification
+            'definition': definition of the classification
+            other VIM specific fields: (optional) whenever possible using the same naming of filter_dict param
+        List can be empty if no classification matches the filter_dict. Raise an exception only upon VIM connectivity,
+            authorization, or some other unspecific error
+        """
+        raise vimconnNotImplemented( "SFC support not implemented" )
+
+    def delete_classification(self, classification_id):
+        """Deletes a classification from the VIM
+        Returns the classification ID (classification_id) or raises an exception upon error or when classification is not found
+        """
+        raise vimconnNotImplemented( "SFC support not implemented" )
+
+    def new_sfi(self, name, ingress_ports, egress_ports, sfc_encap=True):
+        """Creates a service function instance in the VIM
+        Params:
+            'name': name of this service function instance
+            'ingress_ports': set of ingress ports (VIM's port IDs)
+            'egress_ports': set of egress ports (VIM's port IDs)
+            'sfc_encap': boolean stating whether this specific instance supports IETF SFC Encapsulation
+        Returns the VIM's service function instance ID on success or raises an exception on failure
+        """
+        raise vimconnNotImplemented( "SFC support not implemented" )
+
+    def get_sfi(self, sfi_id):
+        """Obtain service function instance details of the VIM's service function instance with ID='sfi_id'
+        Return a dict that contains:
+            'id': VIM's sfi ID (same as sfi_id)
+            'name': VIM's sfi name
+            'ingress_ports': set of ingress ports (VIM's port IDs)
+            'egress_ports': set of egress ports (VIM's port IDs)
+            'status': 'ACTIVE', 'INACTIVE', 'DOWN', 'BUILD', 'ERROR', 'VIM_ERROR', 'OTHER'
+            'error_msg': (optional) text that explains the ERROR status
+            other VIM specific fields: (optional) whenever possible
+        Raises an exception upon error or when service function instance is not found
+        """
+        raise vimconnNotImplemented( "SFC support not implemented" )
+
+    def get_sfi_list(self, filter_dict={}):
+        """Obtain service function instances from the VIM
+        Params:
+            'filter_dict' (optional): contains the entries to filter the sfis on and only return those that match ALL:
+                id:   string  => returns sfis with this VIM's sfi ID, which implies a return of one sfi at most
+                name: string  => returns only service function instances with this name
+                tenant_id: string => returns only service function instances that belong to this tenant/project
+        Returns a list of service function instance dictionaries, each dictionary contains:
+            'id': (mandatory) VIM's sfi ID
+            'name': (mandatory) VIM's sfi name
+            'ingress_ports': set of ingress ports (VIM's port IDs)
+            'egress_ports': set of egress ports (VIM's port IDs)
+            other VIM specific fields: (optional) whenever possible using the same naming of filter_dict param
+        List can be empty if no sfi matches the filter_dict. Raise an exception only upon VIM connectivity,
+            authorization, or some other unspecific error
+        """
+        raise vimconnNotImplemented( "SFC support not implemented" )
+
+    def delete_sfi(self, sfi_id):
+        """Deletes a service function instance from the VIM
+        Returns the service function instance ID (sfi_id) or raises an exception upon error or when sfi is not found
+        """
+        raise vimconnNotImplemented( "SFC support not implemented" )
+
+    def new_sf(self, name, sfis, sfc_encap=True):
+        """Creates (an abstract) service function in the VIM
+        Params:
+            'name': name of this service function
+            'sfis': set of service function instances of this (abstract) service function
+            'sfc_encap': boolean stating whether this service function supports IETF SFC Encapsulation
+        Returns the VIM's service function ID on success or raises an exception on failure
+        """
+        raise vimconnNotImplemented( "SFC support not implemented" )
+
+    def get_sf(self, sf_id):
+        """Obtain service function details of the VIM's service function with ID='sf_id'
+        Return a dict that contains:
+            'id': VIM's sf ID (same as sf_id)
+            'name': VIM's sf name
+            'sfis': VIM's sf's set of VIM's service function instance IDs
+            'sfc_encap': boolean stating whether this service function supports IETF SFC Encapsulation
+            'status': 'ACTIVE', 'INACTIVE', 'DOWN', 'BUILD', 'ERROR', 'VIM_ERROR', 'OTHER'
+            'error_msg': (optional) text that explains the ERROR status
+            other VIM specific fields: (optional) whenever possible
+        Raises an exception upon error or when sf is not found
+        """
+
+    def get_sf_list(self, filter_dict={}):
+        """Obtain service functions from the VIM
+        Params:
+            'filter_dict' (optional): contains the entries to filter the sfs on and only return those that match ALL:
+                id:   string  => returns sfs with this VIM's sf ID, which implies a return of one sf at most
+                name: string  => returns only service functions with this name
+                tenant_id: string => returns only service functions that belong to this tenant/project
+        Returns a list of service function dictionaries, each dictionary contains:
+            'id': (mandatory) VIM's sf ID
+            'name': (mandatory) VIM's sf name
+            'sfis': VIM's sf's set of VIM's service function instance IDs
+            'sfc_encap': boolean stating whether this service function supports IETF SFC Encapsulation
+            other VIM specific fields: (optional) whenever possible using the same naming of filter_dict param
+        List can be empty if no sf matches the filter_dict. Raise an exception only upon VIM connectivity,
+            authorization, or some other unspecific error
+        """
+        raise vimconnNotImplemented( "SFC support not implemented" )
+
+    def delete_sf(self, sf_id):
+        """Deletes (an abstract) service function from the VIM
+        Returns the service function ID (sf_id) or raises an exception upon error or when sf is not found
+        """
+        raise vimconnNotImplemented( "SFC support not implemented" )
+
+
+    def new_sfp(self, name, classifications, sfs, sfc_encap=True, spi=None):
+        """Creates a service function path
+        Params:
+            'name': name of this service function path
+            'classifications': set of traffic classifications that should be matched on to get into this sfp
+            'sfs': list of every service function that constitutes this path , from first to last
+            'sfc_encap': whether this is an SFC-Encapsulated chain (i.e using NSH), True by default
+            'spi': (optional) the Service Function Path identifier (SPI: Service Path Identifier) for this path
+        Returns the VIM's sfp ID on success or raises an exception on failure
+        """
+        raise vimconnNotImplemented( "SFC support not implemented" )
+
+    def get_sfp(self, sfp_id):
+        """Obtain service function path details of the VIM's sfp with ID='sfp_id'
+        Return a dict that contains:
+            'id': VIM's sfp ID (same as sfp_id)
+            'name': VIM's sfp name
+            'classifications': VIM's sfp's list of VIM's classification IDs
+            'sfs': VIM's sfp's list of VIM's service function IDs
+            'status': 'ACTIVE', 'INACTIVE', 'DOWN', 'BUILD', 'ERROR', 'VIM_ERROR', 'OTHER'
+            'error_msg': (optional) text that explains the ERROR status
+            other VIM specific fields: (optional) whenever possible
+        Raises an exception upon error or when sfp is not found
+        """
+        raise vimconnNotImplemented( "SFC support not implemented" )
+
+    def get_sfp_list(self, filter_dict={}):
+        """Obtain service function paths from VIM
+        Params:
+            'filter_dict' (optional): contains the entries to filter the sfps on, and only return those that match ALL:
+                id:   string  => returns sfps with this VIM's sfp ID , which implies a return of one sfp at most
+                name: string  => returns only sfps with this name
+                tenant_id: string => returns only sfps that belong to this tenant/project
+        Returns a list of service function path dictionaries, each dictionary contains:
+            'id': (mandatory) VIM's sfp ID
+            'name': (mandatory) VIM's sfp name
+            'classifications': VIM's sfp's list of VIM's classification IDs
+            'sfs': VIM's sfp's list of VIM's service function IDs
+            other VIM specific fields: (optional) whenever possible using the same naming of filter_dict param
+        List can be empty if no sfp matches the filter_dict. Raise an exception only upon VIM connectivity,
+            authorization, or some other unspecific error
+        """
+        raise vimconnNotImplemented( "SFC support not implemented" )
+
+    def delete_sfp(self, sfp_id):
+        """Deletes a service function path from the VIM
+        Returns the sfp ID (sfp_id) or raises an exception upon error or when sf is not found
+        """
+        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):
         """Transform host dictionary from VIM format to GUI format,