Ejemplo n.º 1
0
    def version(self, params):
        """
    Return with version

    :return: None
    """
        self.log.debug("Call REST-API function: version")
        self.send_json_response({
            "name": __project__,
            "version": get_escape_version()
        })
Ejemplo n.º 2
0
    def version(self, params):
        """
    Return with version

    :param params: additional params (skipped)
    :return: None
    """
        self.log.debug("Call REST-API function: version")
        body = {"name": __project__, "version": get_escape_version()}
        self.send_json_response(body)
        self.log.debug("Sent response: %s" % body)
Ejemplo n.º 3
0
def main():
    # Implement parser options
    parser = argparse.ArgumentParser(
        description="ESCAPEv2: Extensible Service ChAin Prototyping Environment "
        "using Mininet, Click, NETCONF and POX",
        add_help=True,
        version=get_escape_version(),
        prefix_chars="-+")
    # Add optional arguments
    escape = parser.add_argument_group("ESCAPEv2 arguments")
    escape.add_argument(
        "-a",
        "--agent",
        action="store_true",
        default=False,
        help="run in AGENT mode: start the infrastructure layer "
        "with the ROS REST-API (without the Service "
        "sublayer (SAS))")
    escape.add_argument(
        "-b",
        "--bypassapi",
        action="store_true",
        default=False,
        help="start the REST-API to bypass embedding and access "
        "to the DoV directly")
    escape.add_argument("-c",
                        "--config",
                        metavar="path",
                        type=str,
                        help="use external config file to extend the default "
                        "configuration")
    escape.add_argument("-d",
                        "--debug",
                        action="count",
                        default=0,
                        help="run the ESCAPE in debug mode (can use multiple "
                        "times for more verbose logging)")
    escape.add_argument("-e",
                        "--environment",
                        action="store_true",
                        default=False,
                        help="run ESCAPEv2 in the pre-defined virtualenv")
    escape.add_argument("-f",
                        "--full",
                        action="store_true",
                        default=False,
                        help="run the infrastructure layer also")
    escape.add_argument(
        "-g",
        "--gui",
        action="store_true",
        default=False,
        help="(OBSOLETE) initiate the graph-viewer GUI app which "
        "automatically connects to the ROS REST-API")
    escape.add_argument("-i",
                        "--interactive",
                        action="store_true",
                        default=False,
                        help="run an interactive shell for observing internal "
                        "states")
    escape.add_argument("-l",
                        "--log",
                        metavar="file",
                        type=str,
                        help="define log file path explicitly "
                        "(default: log/escape.log)")
    escape.add_argument(
        "--log_folder",
        metavar="path",
        type=str,
        help="define log folder path explicitly (default: log/)")
    escape.add_argument("-m",
                        "--mininet",
                        metavar="file",
                        type=str,
                        help="read the Mininet topology from the given file")
    escape.add_argument("-n",
                        "--nosignal",
                        action="store_true",
                        default=False,
                        help="run ESCAPE in a sub-shell that prevents "
                        "propagation of received SIGNALs")
    escape.add_argument("-o",
                        "--openflow",
                        action="store",
                        type=int,
                        nargs="?",
                        const=6633,
                        metavar="port",
                        help="initiate internal OpenFlow module with given "
                        "listening port (default: 6633)")
    escape.add_argument("-q",
                        "--quit",
                        action="store_true",
                        default=False,
                        help="quit right after the first service request has "
                        "processed")
    escape.add_argument("+q",
                        "++quit",
                        action="store_false",
                        default=False,
                        help="explicitly disable quit mode")
    escape.add_argument("-r",
                        "--rosapi",
                        action="store_true",
                        default=False,
                        help="start REST-API for the Resource Orchestration "
                        "sublayer (ROS)")
    escape.add_argument("-s",
                        "--service",
                        metavar="file",
                        type=str,
                        help="skip the SAS REST-API initiation and read the "
                        "service request from the given file")
    escape.add_argument("-t",
                        "--test",
                        action="store_true",
                        default=False,
                        help="run in test mode")
    escape.add_argument(
        "-x",
        "--clean",
        action="store_true",
        default=False,
        help="run the cleanup task standalone and kill remained "
        "programs, interfaces, veth parts and junk files")
    escape.add_argument("-V",
                        "--visualization",
                        action="store_true",
                        default=False,
                        help="run the visualization module to send data to a "
                        "remote server")
    # Add remaining POX modules
    escape.add_argument("modules",
                        metavar="...",
                        nargs=argparse.REMAINDER,
                        help="optional POX modules")
    # Parsing arguments
    args = parser.parse_args()
    if args.clean:
        return clean()
    # Construct POX init command according to argument basic command
    cmd = [os.path.join(PROJECT_ROOT, "pox/pox.py"), MAIN_CONTAINER_LAYER_NAME]

    # Run ESCAPE in VERBOSE logging level if it is needed
    if args.debug == 1:
        # Set logging level to DEBUG
        cmd.append("--loglevel=DEBUG")
    elif args.debug > 1:
        # Setup logging level to specific VERBOSE
        cmd.append("--loglevel=VERBOSE")
    else:
        # Use default loglevel: INFO
        pass

    # Run the Infrastructure Layer with the required root privilege
    if args.full:
        cmd.insert(0, "sudo")
        cmd.append("--full")

    if args.test:
        cmd.append("--test")

    if args.log:
        cmd.append("--log=%s" % args.log)

    if args.log_folder:
        cmd.append("--log_folder=%s" % args.log_folder)

    if args.quit:
        cmd.append("--quit")

    # Initiate the rudimentary GUI
    if args.gui:
        cmd.append("--gui")

    # Read the service request NFFG from a file and start the mapping process
    if args.service:
        cmd.append("--sg_file=%s" % os.path.abspath(args.service))

    # Override optional external config file
    if args.config:
        cmd.append("--config=%s" % os.path.abspath(args.config))

    # Skip the Service Layer initiation and start the ROS agent REST-API
    if args.agent:
        cmd.append("--agent")
        # AGENT mode is only useful with --full -> initiate infrastructure if needed
        if not args.full:
            cmd.insert(0, "sudo")
            cmd.append("--full")

    # Start the REST-API for the ROS layer
    if args.rosapi or args.gui:
        cmd.append("--rosapi")

    # Start an REST-API for the Adaptation Layer
    if args.bypassapi:
        cmd.append("--dovapi")

    # Enable Visualization
    if args.visualization:
        cmd.append("--visualization")

    # Add topology file if --full is set
    if args.mininet:
        if args.full or args.agent:
            cmd.append("--mininet=%s" % os.path.abspath(args.mininet))
        else:
            parser.error(
                message=
                "-m/--mininet can be used only with Infrastructure layer! "
                "(with -f/--full or -a/--agent flag)")

    # Add the interactive shell if needed
    if args.interactive:
        cmd.append("py")
        cmd.append("--completion")

    if args.openflow:
        cmd.extend(("openflow.of_01", "--port=%s" % args.openflow))

    # Add optional POX modules
    cmd.extend(args.modules)

    def __activate_virtualenv():
        """
    Activate virtualenv based on activate script under bin/

    :return: None
    """
        try:
            activate_this = PROJECT_ROOT + '/bin/activate_this.py'
            execfile(activate_this, dict(__file__=activate_this))
        except IOError as e:
            print "Virtualenv is not set properly:\n%s" % e
            print "Remove the '.set_virtualenv' file or configure the virtualenv!"
            exit(os.EX_DATAERR)

    # Activate virtual environment if necessary
    if args.environment:
        __activate_virtualenv()
    else:
        for entry in os.listdir(PROJECT_ROOT):
            if entry.upper().startswith(".USE_VIRTUALENV"):
                __activate_virtualenv()
                break

    # Starting ESCAPEv2 (as a POX module)
    print "Starting %s..." % parser.description

    if args.nosignal:
        # Create command
        cmd = " ".join(cmd)
        if args.debug:
            print "Command: %s" % cmd
        try:
            return os.system(cmd)
        except KeyboardInterrupt:
            # Catch KeyboardInterrupt generated by own SIGINT signal
            pass
    else:
        # sys.path[0] = pox_base
        pox_params = cmd[2:] if cmd[0] == "sudo" else cmd[1:]
        if args.debug:
            print "POX parameters: %s" % pox_params
        # Start POX
        from pox.boot import boot
        return boot(argv=pox_params)
Ejemplo n.º 4
0
def get_escape_version():
    from escape.util.misc import get_escape_version
    return get_escape_version()
Ejemplo n.º 5
0
class AbstractRequestHandler(BaseHTTPRequestHandler, object):
    """
  Minimalistic RESTful API for Layer APIs.

  Handle /escape/* URLs.

  Method calling permissions represented in escape_intf dictionary.

  .. warning::
    This class is out of the context of the recoco's co-operative thread
    context! While you don't need to worry much about synchronization between
    recoco tasks, you do need to think about synchronization between recoco task
    and normal threads. Synchronisation is needed to take care manually - use
    relevant helper function of core object: :func:`callLater()`/
    :func:`raiseLater()` or use :func:`schedule_as_coop_task()
    <escape.util.misc.schedule_as_coop_task>` decorator defined in
    :mod:`escape.util.misc` on the called function!
  """
    # For HTTP Response messages
    server_version = "ESCAPE/" + get_escape_version()
    """server version for HTTP Response messages"""
    static_prefix = "escape"
    # Bound HTTP verbs to UNIFY's API functions
    request_perm = {
        'GET': ('ping', 'version', 'operations'),
        'POST': ('ping', )
    }
    """Bound HTTP verbs to UNIFY's API functions"""
    # Name of the layer API to which the server bounded
    bounded_layer = None
    """Name of the layer API to which the server bounded"""
    # Name mapper to avoid Python naming constraint (dict: rpc-name: mapped name)
    rpc_mapper = None
    """Name mapper to avoid Python naming constraint"""
    # Logger name
    LOGGER_NAME = "REST-API"
    """Logger name"""
    # Logger. Should be overrided in child classes
    log = core.getLogger("[%s]" % LOGGER_NAME)
    # Use Virtualizer format
    virtualizer_format_enabled = False
    """Use Virtualizer format"""
    # Default communication approach
    format = "FULL"
    """Default communication approach"""
    MESSAGE_ID_NAME = "message-id"

    def do_GET(self):
        """
    Get information about an entity. R for CRUD convention.
    """
        self._process_url()

    def do_POST(self):
        """
    Create an entity. C for CRUD convention.

    :return: None
    """
        self._process_url()

    def do_PUT(self):
        """
    Update an entity. U for CRUD convention.

    :return: None
    """
        self._process_url()

    def do_DELETE(self):
        """
    Delete an entity. D for CRUD convention.

    :return: None
    """
        self._process_url()

    # Unsupported HTTP verbs

    def do_OPTIONS(self):
        """
    Handling unsupported HTTP verbs.

    :return: None
    """
        self.send_error(httplib.NOT_IMPLEMENTED)
        self.wfile.close()

    def do_HEAD(self):
        """
    Handling unsupported HTTP verbs.

    :return: None
    """
        # self.send_error(501)
        self.wfile.close()

    def do_TRACE(self):
        """
    Handling unsupported HTTP verbs.

    :return: None
    """
        self.send_error(httplib.NOT_IMPLEMENTED)
        self.wfile.close()

    def do_CONNECT(self):
        """
    Handling unsupported HTTP verbs.

    :return: None
    """
        self.send_error(httplib.NOT_IMPLEMENTED)
        self.wfile.close()

    def get_request_params(self):
        params = {}
        query = urlparse.urlparse(self.path).query
        if query:
            query = query.split('&')
            for param in query:
                if '=' in param:
                    name, value = param.split('=', 1)
                    params[name] = value
                else:
                    params[param] = True
        # Check message-id in headers as backup
        if self.MESSAGE_ID_NAME not in params:
            if self.MESSAGE_ID_NAME in self.headers:
                params[self.MESSAGE_ID_NAME] = self.headers[
                    self.MESSAGE_ID_NAME]
                self.log.debug("Detected message id: %s" %
                               params[self.MESSAGE_ID_NAME])
            else:
                params[self.MESSAGE_ID_NAME] = str(uuid.uuid1())
                self.log.debug("No message-id! Generated id: %s" %
                               params[self.MESSAGE_ID_NAME])
        else:
            self.log.debug("Detected message id: %s" %
                           params[self.MESSAGE_ID_NAME])
        self.log.debug("Detected request parameters: %s" % params)
        return params

    def _process_url(self):
        """
    Split HTTP path and call the carved function if it is defined in this class
    and in request_perm.

    :return: None
    """
        self.log.debug(">>> Got HTTP request: %s" %
                       str(self.raw_requestline).rstrip())
        http_method = self.command.upper()
        real_path = urlparse.urlparse(self.path).path
        try:
            prefix = '/%s/' % self.static_prefix
            if real_path.startswith(prefix):
                self.func_name = real_path[len(prefix):].split('/')[0]
                if self.rpc_mapper:
                    try:
                        self.func_name = self.rpc_mapper[self.func_name]
                    except KeyError:
                        # No need for RPC name mapping, continue
                        pass
                if http_method in self.request_perm:
                    if self.func_name in self.request_perm[http_method]:
                        if hasattr(self, self.func_name):
                            # Response is assembled, and sent back by handler functions
                            params = self.get_request_params()
                            getattr(self, self.func_name)(params=params)
                        else:
                            self.send_error(
                                httplib.INTERNAL_SERVER_ERROR,
                                "Missing handler for actual request!")
                    else:
                        self.send_error(httplib.NOT_ACCEPTABLE)
                else:
                    self.send_error(httplib.NOT_IMPLEMENTED)
            else:
                self.send_error(httplib.NOT_FOUND,
                                "URL path is not valid or misconfigured!")
        except RESTError as e:
            # Handle all the errors
            if e.code:
                self.send_error(e.code, e.msg)
            else:
                self.send_error(httplib.INTERNAL_SERVER_ERROR, e.msg)
        except:
            # Got unexpected exception
            self.send_error(httplib.INTERNAL_SERVER_ERROR)
            raise
        finally:
            self.func_name = None
            self.wfile.flush()
            self.wfile.close()
        self.log.debug(">>> HTTP request: %s ended!" %
                       str(self.raw_requestline).rstrip())

    def _get_body(self):
        """
    Parse HTTP request body as a plain text.

    .. note::

      Call only once by HTTP request.

    .. note::

      Parsed JSON object is Unicode.

    GET, DELETE messages don't have body - return empty dict by default.

    :return: request body in str format
    :rtype: str
    """
        charset = 'utf-8'
        # json.loads returns an empty dict if it's called with an empty string
        # but this check we can avoid to respond with unnecessary missing
        # content-* error
        if self.command.upper() in ('GET', 'DELETE'):
            return {}
        try:
            splitted_type = self.headers['Content-Type'].split('charset=')
            if len(splitted_type) > 1:
                charset = splitted_type[1]
            content_len = int(self.headers['Content-Length'])
            raw_data = self.rfile.read(size=content_len).encode(charset)
            # Avoid missing param exception by hand over an empty json data
            return raw_data if content_len else "{}"
        except KeyError as e:
            # Content-Length header is not defined
            # or charset is not defined in Content-Type header.
            if e.args[0] == 'Content-Type':
                self.log.warning("Missing header from request: %s" % e.args[0])
            if e.args[0] == 'Content-Length':
                # 411: ('Length Required', 'Client must specify Content-Length.'),
                raise RESTError(code=httplib.LENGTH_REQUIRED)
            else:
                raise RESTError(code=httplib.PRECONDITION_FAILED,
                                msg="Missing header from request: %s" %
                                e.args[0])
        except ValueError as e:
            # Failed to parse request body to JSON
            self.log_error("Request parsing failed: %s", e)
            raise RESTError(code=httplib.UNSUPPORTED_MEDIA_TYPE,
                            msg="Request parsing failed: %s" % e)

    def send_REST_headers(self):
        """
    Set the allowed REST verbs as an HTTP header (Allow).

    :return: None
    """
        try:
            if self.func_name:
                self.send_header(
                    'Allow', ','.join([
                        str(verbs)
                        for verbs, f in self.request_perm.iteritems()
                        if self.func_name in f
                    ]))
        except KeyError:
            pass

    def send_acknowledge(self, code=None, message_id=None):
        """
    Send back acknowledge message.

    :param message_id: response body
    :param message_id: dict
    :return: None
    """
        if code:
            self.send_response(int(code))
        else:
            self.send_response(httplib.ACCEPTED)
        if message_id:
            self.send_header('message-id', message_id)
        self.send_REST_headers()
        self.end_headers()

    def send_raw_response(self,
                          raw_data,
                          code=None,
                          content="text/plain",
                          encoding='utf-8'):
        """
    Send requested data.

    :param raw_data: data in JSON format
    :type raw_data: dict
    :param encoding: Set data encoding (optional)
    :type encoding: str
    :return: None
    """
        if code:
            self.send_response(int(code))
        else:
            self.send_response(httplib.OK)
        self.send_header('Content-Type',
                         '%s; charset=%s' % (content, encoding))
        self.send_header('Content-Length', len(raw_data))
        self.send_REST_headers()
        self.end_headers()
        self.wfile.write(raw_data)

    def send_json_response(self, data, code=None, encoding='utf-8'):
        """
    Send requested data in JSON format.

    :param data: data in JSON format
    :type data: dict
    :param encoding: Set data encoding (optional)
    :type encoding: str
    :return: None
    """
        response_body = json.dumps(data, encoding=encoding)
        return self.send_raw_response(raw_data=response_body,
                                      code=code,
                                      content="application/json",
                                      encoding=encoding)

    error_content_type = "text/json"
    """Content-Type for error responses"""

    def send_error(self, code, message=None):
        """
    Override original function to send back allowed HTTP verbs and set format
    to JSON.

    :param code: error code
    :type code: int
    :param message: error message
    :type message: str
    :return: None
    """
        try:
            short, long = self.responses[code]
        except KeyError:
            short, long = '???', '???'
        if message is None:
            message = short
        explain = long
        self.log_error("code %d, message %s", code, message)
        # using _quote_html to prevent Cross Site Scripting attacks (see bug
        # #1100201)
        content = {
            "title": "Error response",
            'Error code': code,
            'Message': message,
            'Explanation': explain
        }
        self.send_response(code, message)
        self.send_header("Content-Type", self.error_content_type)
        self.send_header('Connection', 'close')
        # self.send_REST_headers()
        self.end_headers()
        if self.command != 'HEAD' and code >= 200 and code not in (204, 304):
            self.wfile.write(json.dumps(content))

    def log_error(self, mformat, *args):
        """
    Overwritten to use POX logging mechanism.

    :param mformat: message format
    :type mformat: str
    :return: None
    """
        self.log.warning(
            "%s - - [%s] %s" % (self.client_address[0],
                                self.log_date_time_string(), mformat % args))

    def log_message(self, mformat, *args):
        """
    Disable logging of incoming messages.

    :param mformat: message format
    :type mformat: str
    :return: None
    """
        pass

    def log_full_message(self, mformat, *args):
        """
    Overwritten to use POX logging mechanism.

    :param mformat: message format
    :type mformat: str
    :return: None
    """
        self.log.debug(
            "%s - - [%s] %s" % (self.client_address[0],
                                self.log_date_time_string(), mformat % args))

    def _proceed_API_call(self, function, *args, **kwargs):
        """
    Fail-safe method to call API function.

    The cooperative micro-task context is handled by actual APIs.

    Should call this with params, not directly the function of actual API.

    :param function: function name
    :type function: str
    :param args: Optional params
    :type args: tuple
    :param kwargs: Optional named params
    :type kwargs: dict
    :return: None
    """
        if core.core.hasComponent(self.bounded_layer):
            layer = core.components[self.bounded_layer]
            if hasattr(layer, function):
                return getattr(layer, function)(*args, **kwargs)
            else:
                self.log.warning(
                    'Mistyped or not implemented API function call: %s ' %
                    function)
                raise RESTError(
                    msg='Mistyped or not implemented API function call: %s ' %
                    function)
        else:
            self.log.error(
                'Error: No component has registered with the name: %s, '
                'ABORT function call!' % self.bounded_layer)

    ##############################################################################
    # Basic REST-API functions
    ##############################################################################

    def ping(self, params):
        """
    For testing REST API aliveness and reachability.

    :return: None
    """
        response_body = "OK"
        self.send_response(httplib.OK)
        self.send_header('Content-Type', 'text/plain')
        self.send_header('Content-Length', len(response_body))
        self.send_REST_headers()
        self.end_headers()
        self.wfile.write(response_body)

    def version(self, params):
        """
    Return with version

    :return: None
    """
        self.log.debug("Call REST-API function: version")
        self.send_json_response({
            "name": __project__,
            "version": get_escape_version()
        })

    def operations(self, params):
        """
    Return with allowed operations

    :return: None
    """
        self.log.debug("Call REST-API function: operations")
        self.send_json_response(self.request_perm)
Ejemplo n.º 6
0
 def version():
     return Response(get_escape_version() + "\n")