def __init__(self, url, prefix="", features=None, **kwargs): """ Init. :param url: url of RESTful API :type url: str :param prefix: URL prefix :type prefix: str :param features: limitation anf filter parameters for the Adapter class :type features: dict :return: None """ AbstractRESTAdapter.__init__(self, base_url=url, prefix=prefix, **kwargs) AbstractESCAPEAdapter.__init__(self, **kwargs) log.debug("Init %s - type: %s, domain: %s, URL: %s" % (self.__class__.__name__, self.type, self.domain_name, url)) # Converter object self.converter = NFFGConverter( unique_bb_id=CONFIG.ensure_unique_bisbis_id(), unique_nf_id=CONFIG.ensure_unique_vnf_id(), domain=self.domain_name, logger=log) self.features = features if features is not None else {} # Cache for parsed Virtualizer self.__last_virtualizer = None self.__last_request = None self.__original_virtualizer = None
def __init__ (self, url=None, rpc=None, timeout=DEFAULT_TIMEOUT, instance_id=None): """ Init. :param url: URL of the remote server :type url: str :param rpc: RPC name :type rpc: str :param timeout: connections timeout :type timeout: int :param instance_id: additional id to join to the end of the id :type instance_id: str :return: None """ super(RemoteVisualizer, self).__init__() self.log = core.getLogger("visualizer") if url is None: url = CONFIG.get_visualization_url() if rpc is None: rpc = CONFIG.get_visualization_rpc() self._url = urlparse.urljoin(url, rpc) if self._url is None: raise RuntimeError("Missing URL from %s" % self.__class__.__name__) self._timeout = timeout if instance_id is None: self.instance_id = CONFIG.get_visualization_instance_id() self.log.info("Setup remote Visualizer with URL: %s" % self._url) # Store the last request self._response = None self.converter = NFFGConverter(domain="ESCAPE", logger=log, unique_bb_id=CONFIG.ensure_unique_bisbis_id(), unique_nf_id=CONFIG.ensure_unique_vnf_id()) # Suppress low level logging self.__suppress_requests_logging()
def initialize(self): """ .. seealso:: :func:`AbstractAPI.initialize() <escape.util.api.AbstractAPI.initialize>` """ log.debug("Initializing Service Layer...") self.__sid = CONFIG.get_service_layer_id() if self.__sid is not None: log.debug("Setup ID for Service Layer: %s" % self.__sid) else: self.__sid = self.LAYER_ID log.error( "Missing ID of Service Layer from config. Using default value: %s" % self.__sid) # Set element manager self.elementManager = ClickManager() # Init central object of Service layer self.service_orchestrator = ServiceOrchestrator(self) # Read input from file if it's given and initiate SG if self._sg_file: try: stats.init_request_measurement(request_id=self._sg_file) service_request = self._read_data_from_file(self._sg_file) log.info("Graph representation is loaded successfully!") if service_request.startswith('{'): log.debug( "Detected format: JSON - Parsing from NFFG format...") nffg = NFFG.parse(raw_data=service_request) elif service_request.startswith('<'): log.debug( "Detected format: XML - Parsing from Virtualizer format..." ) converter = NFFGConverter(domain="INTERNAL", logger=log) nffg = converter.parse_from_Virtualizer( vdata=service_request) else: log.warning("Detected unexpected format...") return if nffg.mode is not None: log.info('Detected mapping mode in NFFG: %s' % nffg.mode) else: nffg.mode = NFFG.MODE_ADD log.info("No mapping mode has been detected in NFFG! " "Set default mode: %s" % nffg.mode) log.info("Schedule service request delayed by %d seconds..." % SCHEDULED_SERVICE_REQUEST_DELAY) stats.set_request_id(request_id=nffg.id) self.api_sas_sg_request_delayed(service_nffg=nffg) except (ValueError, IOError, TypeError) as e: log.error("Can't load service request from file because of: " + str(e)) quit_with_error(msg=str(e), logger=log) else: # Init REST-API if no input file is given self._initiate_rest_api() # Init GUI if self._gui: self._initiate_gui() log.info("Service Layer has been initialized!")
def get_config(self): """ Response configuration. :return: None """ self.log.info("Call %s function: get-config" % self.LOGGER_NAME) # Forward call to main layer class config = self._proceed_API_call(self.API_CALL_GET_CONFIG) if config is None: self.send_error(404, message="Resource info is missing!") return # Setup OK status for HTTP response self.send_response(200) # Global resource has not changed -> respond with the cached topo if config is False: self.log.info( "Global resource has not changed! Respond with cached topology..." ) if self.virtualizer_format_enabled: data = self.server.last_response.xml() else: data = self.server.last_response.dump() else: # Convert required NFFG if needed if self.virtualizer_format_enabled: self.log.debug("Convert internal NFFG to Virtualizer...") converter = NFFGConverter(domain=None, logger=log) v_topology = converter.dump_to_Virtualizer(nffg=config) # Cache converted data for edit-config patching self.log.debug("Cache converted topology...") self.server.last_response = v_topology # Dump to plain text format data = v_topology.xml() # Setup HTTP response format else: self.log.debug("Cache acquired topology...") self.server.last_response = config data = config.dump() if self.virtualizer_format_enabled: self.send_header('Content-Type', 'application/xml') else: self.send_header('Content-Type', 'application/json') self.log.log(VERBOSE, "Responded topology for 'get-config':\n%s" % data) # Setup length for HTTP response self.send_header('Content-Length', len(data)) self.end_headers() self.log.info("Send back topology description...") self.wfile.write(data) self.log.debug("%s function: get-config ended!" % self.LOGGER_NAME)
def topology (self): """ Provide internal topology description Same functionality as "get-config" in UNIFY interface. :return: None """ self.log.info("Call %s function: topology" % self.LOGGER_NAME) # Forward call to main layer class topology = self._proceed_API_call('api_sas_get_topology') if topology is None: self.send_error(404, message="Resource info is missing!") return # Setup OK status for HTTP response self.send_response(200) if topology is False: self.log.info( "Requested resource has not changed! Respond with cached topology...") if self.virtualizer_format_enabled: data = self.server.last_response.xml() else: data = self.server.last_response.dump() else: if self.virtualizer_format_enabled: self.log.debug("Convert internal NFFG to Virtualizer...") converter = NFFGConverter(domain=None, logger=log) # Dump to plain text format v_topology = converter.dump_to_Virtualizer(nffg=topology) # Cache converted data for edit-config patching self.log.debug("Cache converted topology...") self.server.last_response = v_topology # Dump to plain text format data = v_topology.xml() # Setup HTTP response format else: self.log.debug("Cache converted topology...") self.server.last_response = topology data = topology.dump() if self.virtualizer_format_enabled: self.send_header('Content-Type', 'application/xml') else: self.send_header('Content-Type', 'application/json') self.log.log(VERBOSE, "Responded topology for 'get-config':\n%s" % data) # Setup length for HTTP response self.send_header('Content-Length', len(data)) self.end_headers() self.log.info("Send back topology description...") self.wfile.write(data) self.log.debug("%s function: get-config ended!" % self.LOGGER_NAME)
def __init__(self, domain_name=None, path=None, diff=None, **kwargs): """ Init. :param path: file path offered as the domain topology :type path: str :return: None """ log.debug( "Init %s - type: %s, domain: %s, path: %s, diff: %s" % (self.__class__.__name__, self.type, domain_name, path, diff)) # Converter object self.converter = NFFGConverter( unique_bb_id=CONFIG.ensure_unique_bisbis_id(), unique_nf_id=CONFIG.ensure_unique_vnf_id(), domain=domain_name, logger=log) super(VirtualizerBasedStaticFileAdapter, self).__init__(domain_name=domain_name, path=path, **kwargs) self.diff = diff
def sg (self): """ Main API function for Service Graph initiation. Same functionality as "get-config" in UNIFY interface. Bounded to POST HTTP verb. :return: None """ self.log.debug("Called REST-API function: sg") self.log.info(int(round(time.time() * 1000))) # Obtain NFFG from request body log.debug("Detected response format: %s" % self.headers.get("Content-Type")) body = self._get_body() # log.getChild("REST-API").debug("Request body:\n%s" % body) if body is None or not body: log.warning("Received data is empty!") self.send_error(400, "Missing body!") return # Expect XML format --> need to convert first if self.virtualizer_format_enabled: if self.headers.get("Content-Type") != "application/xml" or \ not body.startswith("<?xml version="): log.error("Received data is not in XML format despite of the UNIFY " "interface is enabled!") self.send_error(415) return # Convert response's body to NFFG nffg = NFFGConverter(domain="INTERNAL", logger=log).parse_from_Virtualizer(vdata=body) else: try: nffg = NFFG.parse(body) # Initialize NFFG from JSON representation except Exception as e: self.log.error( "Abort request! Received exception during payload parsing: %s" % e) return self.log.debug("Parsed service request: %s" % nffg) self._proceed_API_call('api_sas_sg_request', nffg) self.send_acknowledge() self.log.debug("%s function: get-config ended!" % self.LOGGER_NAME)
def edit_config(self): """ Receive configuration and initiate orchestration. :return: None """ self.log.info("Call %s function: edit-config" % self.LOGGER_NAME) # Obtain NFFG from request body self.log.debug("Detected response format: %s" % self.headers.get("Content-Type")) raw_body = self._get_body() # log.getChild("REST-API").debug("Request body:\n%s" % body) if raw_body is None or not raw_body: log.warning("Received data is empty!") self.send_error(400, "Missing body!") return # Expect XML format --> need to convert first if self.virtualizer_format_enabled: if self.headers.get("Content-Type") != "application/xml" and \ not raw_body.startswith("<?xml version="): self.log.error( "Received data is not in XML format despite of the " "UNIFY interface is enabled!") self.send_error(415) return # Get received Virtualizer received_cfg = Virtualizer.parse_from_text(text=raw_body) self.log.log(VERBOSE, "Received request for 'edit-config':\n%s" % raw_body) # If there was not get-config request so far if self.DEFAULT_DIFF: if self.server.last_response is None: self.log.info( "Missing cached Virtualizer! Acquiring topology now..." ) config = self._proceed_API_call(self.API_CALL_GET_CONFIG) if config is None: self.log.error("Requested resource info is missing!") self.send_error(404, message="Resource info is missing!") return elif config is False: self.log.warning( "Requested info is unchanged but has not found!") self.send_error(404, message="Resource info is missing!") else: # Convert required NFFG if needed if self.virtualizer_format_enabled: self.log.debug( "Convert internal NFFG to Virtualizer...") converter = NFFGConverter(domain=None, logger=log) v_topology = converter.dump_to_Virtualizer( nffg=config) # Cache converted data for edit-config patching self.log.debug("Cache converted topology...") self.server.last_response = v_topology else: self.log.debug("Cache acquired topology...") self.server.last_response = config # Perform patching full_cfg = self.__recreate_full_request(diff=received_cfg) else: full_cfg = received_cfg self.log.log(VERBOSE, "Generated request:\n%s" % full_cfg.xml()) # Convert response's body to NFFG self.log.info("Converting full request data...") converter = NFFGConverter(domain="REMOTE", logger=log) nffg = converter.parse_from_Virtualizer(vdata=full_cfg) else: if self.headers.get("Content-Type") != "application/json": self.log.error( "Received data is not in JSON format despite of the " "UNIFY interface is disabled!") self.send_error(415) return # Initialize NFFG from JSON representation self.log.info("Parsing request into internal NFFG format...") nffg = NFFG.parse(raw_body) self.log.debug("Parsed NFFG install request: %s" % nffg) self._proceed_API_call(self.API_CALL_EDIT_CONFIG, nffg) self.send_acknowledge() self.log.debug("%s function: edit-config ended!" % self.LOGGER_NAME)
class RemoteVisualizer(Session): """ Main object for remote Visualization. """ # Singleton __metaclass__ = Singleton """Singleton""" # name form POXCore _core_name = "visualizer" """Name form POXCore""" # Bindings of Layer IDs ID_MAPPER = { SERVICE: "ESCAPE-SERVICE", ORCHEST: "ESCAPE-ORCHESTRATION", ADAPT: "ESCAPE-ADAPTATION" } """Bindings of Layer IDs""" # Basic HTTP headers basic_headers = { 'User-Agent': "ESCAPE/" + __version__, 'Content-Type': "application/xml" } """Basic HTTP headers""" # Default timeout value in sec DEFAULT_TIMEOUT = 1 """Default timeout value in sec""" def __init__(self, url=None, rpc=None, timeout=DEFAULT_TIMEOUT, instance_id=None): """ Init. :param url: URL of the remote server :type url: str :param rpc: RPC name :type rpc: str :param timeout: connections timeout :type timeout: int :param instance_id: additional id to join to the end of the id :type instance_id: str :return: None """ super(RemoteVisualizer, self).__init__() self.log = core.getLogger("visualizer") if url is None: url = CONFIG.get_visualization_url() if rpc is None: rpc = CONFIG.get_visualization_rpc() self._url = urlparse.urljoin(url, rpc) if self._url is None: raise RuntimeError("Missing URL from %s" % self.__class__.__name__) self._timeout = timeout if instance_id is None: self.instance_id = CONFIG.get_visualization_instance_id() self.log.info("Setup remote Visualizer with URL: %s" % self._url) # Store the last request self._response = None self.converter = NFFGConverter(domain="ESCAPE", logger=self.log) # Suppress low level logging self.__suppress_requests_logging() @staticmethod def __suppress_requests_logging(level=None): """ Suppress annoying and detailed logging of `requests` and `urllib3` packages. :param level: level of logging (default: WARNING) :type level: str :return: None """ level = level if level is not None else logging.WARNING logging.getLogger("requests").setLevel(level) logging.getLogger("urllib3").setLevel(level) def send_notification(self, data, url=None, **kwargs): """ Send given data to a remote server for visualization. Convert given NFFG into Virtualizer format if needed. :param data: topology description need to send :type data: :class:`NFFG` or Virtualizer :param id: id of the data, needs for the remote server :type id: str :param url: additional URL (optional) :type url: str :param kwargs: additional params to request :type kwargs: dict :return: response text :rtype: str """ if url is None: url = self._url if url is None: self.log.error( "Missing URL for remote visualizer! Skip notification...") return if 'timeout' not in kwargs: kwargs['timeout'] = self._timeout self.log.debug("Send visualization notification to %s" % self._url) try: if data is None: self.log.warning( "Missing data! Skip notifying remote visualizer.") return False elif isinstance(data, NFFG): data = self.converter.dump_to_Virtualizer(nffg=data) elif not isinstance(data, Virtualizer.Virtualizer): self.log.warning( "Unsupported data type: %s! Skip notification..." % type(data)) return # If additional params is not empty dict -> override the basic params if 'headers' in kwargs: kwargs['headers'].update(self.basic_headers) else: kwargs['headers'] = self.basic_headers.copy() kwargs['headers'].update(CONFIG.get_visualization_headers()) if "params" in kwargs: kwargs['params'].update(CONFIG.get_visualization_params()) else: kwargs['params'] = CONFIG.get_visualization_params() self.log.debug("Sending visualization notification...") self._response = self.request(method='POST', url=url, data=data.xml(), **kwargs) self._response.raise_for_status() return self._response.text except (ConnectionError, HTTPError, KeyboardInterrupt) as e: self.log.warning( "Got exception during notifying remote Visualizer: %s!" % e) return False except Timeout: self.log.warning( "Got timeout(%ss) during notify remote Visualizer!" % kwargs['timeout']) return True
class UnifyRESTAdapter(AbstractRESTAdapter, AbstractESCAPEAdapter, DefaultUnifyDomainAPI): """ Implement the unified way to communicate with "Unify" domains which are using REST-API and the "Virtualizer" XML-based format. """ # Set custom header custom_headers = { 'User-Agent': "ESCAPE/" + __version__, # XML-based Virtualizer format 'Accept': "application/xml" } # Adapter name used in CONFIG and ControllerAdapter class name = "UNIFY-REST" # type of the Adapter class - use this name for searching Adapter config type = AbstractESCAPEAdapter.TYPE_REMOTE MESSAGE_ID_NAME = "message-id" CALLBACK_NAME = "call-back" SKIPPED_REQUEST = 0 def __init__(self, url, prefix="", features=None, **kwargs): """ Init. :param url: url of RESTful API :type url: str :param prefix: URL prefix :type prefix: str :param features: limitation anf filter parameters for the Adapter class :type features: dict :return: None """ AbstractRESTAdapter.__init__(self, base_url=url, prefix=prefix, **kwargs) AbstractESCAPEAdapter.__init__(self, **kwargs) log.debug("Init %s - type: %s, domain: %s, URL: %s" % (self.__class__.__name__, self.type, self.domain_name, url)) # Converter object self.converter = NFFGConverter( unique_bb_id=CONFIG.ensure_unique_bisbis_id(), unique_nf_id=CONFIG.ensure_unique_vnf_id(), domain=self.domain_name, logger=log) self.features = features if features is not None else {} # Cache for parsed Virtualizer self.__last_virtualizer = None self.__last_request = None self.__original_virtualizer = None @property def last_virtualizer(self): """ :return: Return the last received topology. :rtype: :class:`Virtualizer` """ return self.__last_virtualizer @property def last_request(self): """ :return: Return the last sent request . :rtype: :class:`Virtualizer` """ return self.__last_request def ping(self): """ Call the ping RPC. :return: response text (should be: 'OK') :rtype: str """ try: log.log(VERBOSE, "Send ping request to remote agent: %s" % self._base_url) return self.send_quietly(self.GET, 'ping') except RequestException: # Any exception is bad news -> return None return None def get_config(self, filter=None): """ Queries the infrastructure view with a netconf-like "get-config" command. Remote domains always send full-config if ``filter`` is not set. Return topology description in the original format. :param filter: request a filtered description instead of full :type filter: str :return: infrastructure view in the original format :rtype: :class:`Virtualizer` """ log.debug("Send get-config request to remote agent: %s" % self._base_url) # Get topology from remote agent handling every exception data = self.send_no_error(self.POST, 'get-config') if data: # Got data log.debug("Received config from remote %s domain agent at %s" % (self.domain_name, self._base_url)) log.debug( "Detected response format: %s" % self.get_last_response_headers().get("Content-Type", "None")) log.debug("Parse and load received data...") # Try to define content type from HTTP header or the first line of body if not self.is_content_type("xml") and \ not data.startswith("<?xml version="): log.error("Received data is not in XML format!") return virt = Virtualizer.parse_from_text(text=data) log.log( VERBOSE, "Received message to 'get-config' request:\n%s" % virt.xml()) self.__cache_topology(virt) return virt else: log.error("No data has been received from remote agent at %s!" % self._base_url) return def edit_config(self, data, diff=False, message_id=None, callback=None, full_conversion=False): """ Send the requested configuration with a netconf-like "edit-config" command. Remote domains always expect diff of mapping changes. :param data: whole domain view :type data: :class:`NFFG` :param diff: send the diff of the mapping request (default: False) :param diff: bool :param message_id: optional message id :type message_id: str :param callback: callback URL :type callback: str :param full_conversion: do full conversion instead of adapt (default: False) :type full_conversion: bool :return: response data / empty str if request was successful else None :rtype: str """ log.debug("Prepare edit-config request for remote agent at: %s" % self._base_url) if isinstance(data, Virtualizer): # Nothing to do vdata = data elif isinstance(data, NFFG): log.debug("Convert NFFG to XML/Virtualizer format...") if self.last_virtualizer is None: log.debug("Missing last received Virtualizer! " "Updating last topology view now...") self.get_config() if self.last_virtualizer is None: return False stats.add_measurement_start_entry(type=stats.TYPE_CONVERSION, info="%s-deploy" % self.domain_name) if full_conversion: vdata = self.converter.dump_to_Virtualizer(nffg=data) else: vdata = self.converter.adapt_mapping_into_Virtualizer( virtualizer=self.last_virtualizer, nffg=data, reinstall=diff) stats.add_measurement_end_entry(type=stats.TYPE_CONVERSION, info="%s-deploy" % self.domain_name) log.log(VERBOSE, "Adapted Virtualizer:\n%s" % vdata.xml()) else: raise RuntimeError( "Not supported config format: %s for 'edit-config'!" % type(data)) self.__cache_request(data=vdata) if diff: log.debug( "DIFF is enabled. Calculating difference of mapping changes..." ) stats.add_measurement_start_entry(type=stats.TYPE_PROCESSING, info="%s-DIFF" % self.domain_name) vdata = self.__calculate_diff(vdata) stats.add_measurement_end_entry(type=stats.TYPE_PROCESSING, info="%s-DIFF" % self.domain_name) else: log.debug("Using given Virtualizer as full mapping request") plain_data = vdata.xml() log.log(VERBOSE, "Generated Virtualizer:\n%s" % plain_data) if is_empty(virtualizer=vdata): log.info("Generated edit-config request is empty! Skip sending...") return self.SKIPPED_REQUEST log.debug("Send request to %s domain agent at %s..." % (self.domain_name, self._base_url)) params = {} if message_id is not None: params[self.MESSAGE_ID_NAME] = message_id log.debug("Using explicit message-id: %s" % message_id) if callback is not None: params[self.CALLBACK_NAME] = callback log.debug("Using explicit callback: %s" % callback) MessageDumper().dump_to_file(data=plain_data, unique="%s-edit-config" % self.domain_name) try: response = self.send_with_timeout(method=self.POST, url='edit-config', body=plain_data, params=params) except Timeout: log.warning( "Reached timeout(%ss) while waiting for 'edit-config' response!" " Ignore exception..." % self.CONNECTION_TIMEOUT) # Ignore exception - assume the request was successful -> return True return True if response is not None: log.debug("Deploy request has been sent successfully!") return response def get_last_message_id(self): """ :return: Return the last sent request id :rtype: str or int """ if self._response is not None: return self._response.headers.get(self.MESSAGE_ID_NAME, None) else: return None def info(self, info, callback=None, message_id=None): """ Send the given Info request to the domain orchestrator. :param info: Info object :type info: :class:`Info` :param callback: callback URL :type callback: str :param message_id: optional message id :type message_id: str :return: status code or the returned message-id if it is set :rtype: str """ log.log(VERBOSE, "Generated Info:\n%s" % info.xml()) params = {} if message_id is not None: params[self.MESSAGE_ID_NAME] = message_id log.debug("Using explicit message-id: %s" % message_id) if callback is not None: params[self.CALLBACK_NAME] = callback log.debug("Using explicit callback: %s" % callback) MessageDumper().dump_to_file(data=info.xml(), unique="%s-info" % self.domain_name) try: status = self.send_with_timeout(method=self.POST, url='info', body=info.xml(), params=params) except Timeout: log.warning( "Reached timeout(%ss) while waiting for 'info' response!" " Ignore exception..." % self.CONNECTION_TIMEOUT) # Ignore exception - assume the request was successful -> return True return True if status is not None: log.debug("Info request has been sent successfully!") return status def get_original_topology(self): """ Return the original topology as a Virtualizer. :return: the original topology :rtype: :class:`Virtualizer` """ return self.__original_virtualizer def check_domain_reachable(self): """ Checker function for domain polling. Check the remote domain agent is reachable. Use the ping RPC call. :return: the remote domain is detected or not :rtype: bool """ return self.ping() is not None def get_topology_resource(self): """ Return with the topology description as an :class:`NFFG`. :return: the topology description of the remote domain :rtype: :class:`NFFG` """ # Get full topology as a Virtualizer virt = self.get_config() # If not data is received or converted return with None if virt is None: return MessageDumper().dump_to_file(data=virt.xml(), unique="%s-get-config" % self.domain_name) # Convert from XML-based Virtualizer to NFFG nffg = self.converter.parse_from_Virtualizer(vdata=virt) self.__process_features(nffg=nffg) log.log(VERBOSE, "Converted NFFG of 'get-config' response:\n%s" % nffg.dump()) # If first get-config if self.__original_virtualizer is None: log.debug( "Store Virtualizer(id: %s, name: %s) as the original domain " "config..." % (virt.id.get_value(), virt.name.get_value())) # self.__original_virtualizer = virt.full_copy() # Copy reference instead of full_copy to avoid overhead self.__original_virtualizer = virt log.debug("Used domain name for conversion: %s" % self.domain_name) return nffg def __process_features(self, nffg): """ Process features config and transform collected topo according to these. :param nffg: base NFFG :type nffg: :class:`NFFG` :return: None """ if self.features: self.log.debug("Adding features: %s to infra nodes..." % self.features) for infra in nffg.infras: infra.mapping_features.update(self.features) def check_topology_changed(self): """ Check the last received topology and return ``False`` if there was no changes, ``None`` if domain was unreachable and the converted topology if the domain changed. Detection of changes is based on the ``reduce()`` function of the ``Virtualizer``. :return: the received topology is different from cached one :rtype: bool or None or :class:`NFFG` """ # Get full topology as a Virtualizer data = self.send_no_error(self.POST, 'get-config') # Got data if data: # Check the content type or try to recognize the standard XML opening tag if not self.is_content_type("xml") and \ not data.startswith("<?xml version="): log.error("Received data is not in XML format!") return None virt = Virtualizer.parse_from_text(text=data) else: # If no data is received, exception was raised or converted return with # None. log.warning( "No data is received or parsed into Virtualizer during " "topology change detection!") return None if self.last_virtualizer is None: log.warning("Missing last received Virtualizer!") return None # Get the changes happened since the last get-config if not self.__is_changed(virt): return False else: log.info("Received changed topology from domain: %s" % self.domain_name) log.log( VERBOSE, "Changed domain topology from: %s:\n%s" % (self.domain_name, virt.xml())) MessageDumper().dump_to_file(data=virt.xml(), unique="%s-get-config-changed" % self.domain_name) # Cache new topo self.__cache_topology(virt) # Return with the changed topo in NFFG changed_topo = self.converter.parse_from_Virtualizer(vdata=virt) self.__process_features(nffg=changed_topo) return changed_topo def __cache_topology(self, data): """ Cache last received Virtualizer topology. :param data: received Virtualizer :type data: :class:`Virtualizer` :return: None """ log.debug("Cache received 'get-config' response...") # self.__last_virtualizer = data.full_copy() # Copy reference instead of full_copy to avoid overhead self.__last_virtualizer = data def get_topo_cache(self): return self.__last_virtualizer def __cache_request(self, data): """ Cache calculated and converted request. :param data: request Virtualizer :type data: :class:`Virtualizer` :return: None """ log.debug("Cache generated 'edit-config' request...") # self.__last_request = data.full_copy() # Copy reference instead of full_copy to avoid overhead self.__last_request = data def __is_changed(self, new_data): """ Return True if the given ``new_data`` is different compared to cached ``last_virtualizer``. :param new_data: new Virtualizer object :type new_data: :class:`Virtualizer` :return: is different or not :rtype: bool """ return not is_identical(base=self.last_virtualizer, new=new_data) def __calculate_diff(self, changed): """ Calculate the difference of the given Virtualizer compared to the most recent Virtualizer acquired by get-config. :param changed: Virtualizer containing new install :type changed: :class:`Virtualizer` :return: the difference :rtype: :class:`Virtualizer` """ # base = Virtualizer.parse_from_text(text=self.last_virtualizer.xml()) base = self.last_virtualizer base.bind(relative=True) changed.bind(relative=True) # Use fail-safe workaround of diff to avoid bugs in Virtualizer library # diff = base.diff(changed) diff = base.diff_failsafe(changed) return diff
class VirtualizerBasedStaticFileAdapter(StaticFileAdapter): """ Adapter class to return the topology description parsed from a static file in Virtualizer format. """ name = "STATIC-VIRTUALIZER-TOPO" type = AbstractESCAPEAdapter.TYPE_TOPOLOGY def __init__(self, domain_name=None, path=None, diff=None, **kwargs): """ Init. :param path: file path offered as the domain topology :type path: str :return: None """ log.debug( "Init %s - type: %s, domain: %s, path: %s, diff: %s" % (self.__class__.__name__, self.type, domain_name, path, diff)) # Converter object self.converter = NFFGConverter( unique_bb_id=CONFIG.ensure_unique_bisbis_id(), unique_nf_id=CONFIG.ensure_unique_vnf_id(), domain=domain_name, logger=log) super(VirtualizerBasedStaticFileAdapter, self).__init__(domain_name=domain_name, path=path, **kwargs) self.diff = diff def _read_topo_from_file(self, path): """ Load a pre-defined topology from an NFFG stored in a file. The file path is searched in CONFIG with tha name ``SDN-TOPO``. :param path: additional file path :type path: str :return: None """ try: path = os.path.join(PROJECT_ROOT, path) log.debug("Load topology from file: %s" % path) self.virtualizer = Virtualizer.parse_from_file(filename=path) log.log(VERBOSE, "Loaded topology:\n%s" % self.virtualizer.xml()) nffg = self.converter.parse_from_Virtualizer( vdata=self.virtualizer) self.topo = self.rewrite_domain(nffg) log.log(VERBOSE, "Converted topology:\n%s" % self.topo.dump()) except IOError: log.warning("Topology file not found: %s" % path) except ValueError as e: log.error("An error occurred when load topology from file: %s" % e.message) raise TopologyLoadException("File parsing error!") def __calculate_diff(self, changed): """ Calculate the difference of the given Virtualizer compared to the most recent Virtualizer acquired by get-config. :param changed: Virtualizer containing new install :type changed: :class:`Virtualizer` :return: the difference :rtype: :class:`Virtualizer` """ # base = Virtualizer.parse_from_text(text=self.last_virtualizer.xml()) base = self.virtualizer base.bind(relative=True) changed.bind(relative=True) # Use fail-safe workaround of diff to avoid bugs in Virtualizer library # diff = base.diff(changed) diff = base.diff_failsafe(changed) return diff def dump_to_file(self, nffg): """ Dump received :class:`NFFG` into a file. :param nffg: received NFFG need to be deployed :type nffg: :class:`NFFG` :return: successful install (True) :rtype: bool """ vdata = self.converter.adapt_mapping_into_Virtualizer( virtualizer=self.virtualizer, nffg=nffg, reinstall=self.diff) if self.diff: log.debug( "DIFF is enabled. Calculating difference of mapping changes..." ) vdata = self.__calculate_diff(vdata) return self._dump_to_file(file_name='out-%s-edit_config.xml' % self.domain_name, data=vdata.xml())
def __init__(self, **kwargs): self.converter = NFFGConverter(**kwargs) self.request_cache = RequestCache() self.topology_revision = None self.last_response = None
def _deploy_flowrules (self, nffg_part): """ Install the flowrules given in the NFFG. If a flowrule is already defined it will be updated. :param nffg_part: NF-FG need to be deployed :type nffg_part: :any:`NFFG` :return: deploy was successful or not :rtype: bool """ log.debug("Deploy flowrules into the domain: %s..." % self.domain_name) result = True # Remove unnecessary SG and Requirement links to avoid mess up port # definition of NFs nffg_part.clear_links(NFFG.TYPE_LINK_SG) nffg_part.clear_links(NFFG.TYPE_LINK_REQUIREMENT) # Get physical topology description from POX adapter topo = self.topoAdapter.get_topology_resource() if topo is None: log.warning("Missing topology description from %s domain! " "Skip deploying flowrules..." % self.domain_name) return False # Iter through the container INFRAs in the given mapped NFFG part for infra in nffg_part.infras: if infra.infra_type not in ( NFFG.TYPE_INFRA_EE, NFFG.TYPE_INFRA_STATIC_EE, NFFG.TYPE_INFRA_SDN_SW): log.debug("Infrastructure Node: %s (type: %s) is not Switch or " "Container type! Continue to next Node..." % (infra.id, infra.infra_type)) continue # If the actual INFRA isn't in the topology(NFFG) of this domain -> skip if infra.id not in (n.id for n in topo.infras): log.error("Infrastructure Node: %s is not found in the %s domain! Skip " "flowrule install on this Node..." % (infra.id, self.domain_name)) result = False continue # Check the OF connection is alive try: dpid = self.controlAdapter.infra_to_dpid[infra.id] except KeyError as e: log.warning("Missing DPID for Infra(id: %s)! " "Skip deploying flowrules for Infra" % e) result = False continue if self.controlAdapter.openflow.getConnection(dpid) is None: log.warning("Skipping INSTALL flowrule! Cause: connection for %s - " "DPID: %s is not found!" % (infra, dpid_to_str(dpid))) result = False continue for port in infra.ports: for flowrule in port.flowrules: try: match = NFFGConverter.field_splitter(type="MATCH", field=flowrule.match) if "in_port" not in match: log.warning("Missing in_port field from match field! " "Using container port number...") match["in_port"] = port.id action = NFFGConverter.field_splitter(type="ACTION", field=flowrule.action) except RuntimeError as e: log.warning("Wrong format in match/action field: %s" % e) result = False continue log.debug("Assemble OpenFlow flowrule from: %s" % flowrule) self.controlAdapter.install_flowrule(infra.id, match=match, action=action) log.debug("Flowrule deploy result: %s" % ("SUCCESS" if result else "FAILURE")) return result