RIFT OSM R1 Initial Submission
[osm/SO.git] / rwlaunchpad / plugins / rwimagemgr / rift / tasklets / rwimagemgr / glance_proxy_server.py
diff --git a/rwlaunchpad/plugins/rwimagemgr/rift/tasklets/rwimagemgr/glance_proxy_server.py b/rwlaunchpad/plugins/rwimagemgr/rift/tasklets/rwimagemgr/glance_proxy_server.py
new file mode 100644 (file)
index 0000000..9b3972e
--- /dev/null
@@ -0,0 +1,276 @@
+
+# 
+#   Copyright 2016 RIFT.IO Inc
+#
+#   Licensed under the Apache License, Version 2.0 (the "License");
+#   you may not use this file except in compliance with the License.
+#   You may obtain a copy of the License at
+#
+#       http://www.apache.org/licenses/LICENSE-2.0
+#
+#   Unless required by applicable law or agreed to in writing, software
+#   distributed under the License is distributed on an "AS IS" BASIS,
+#   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+#   See the License for the specific language governing permissions and
+#   limitations under the License.
+#
+
+import json
+
+from .lib import quickproxy
+import rift.tasklets.tornado
+
+
+class GlanceConfig(object):
+    DEFAULT_HOST = "127.0.0.1"
+    DEFAULT_PORT = 9292
+    DEFAULT_TOKEN = "test"
+
+    def __init__(self, host=DEFAULT_HOST, port=DEFAULT_PORT, token=DEFAULT_TOKEN):
+        self.host = host
+        self.port = port
+        self.token = token
+
+
+class GlanceImageCreateRequest(object):
+    def __init__(self, name, size, checksum, disk_format, container_format):
+        self.name = name
+        self.size = size
+        self.checksum = checksum
+        self.disk_format = disk_format
+        self.container_format = container_format
+
+    def __repr__(self):
+        return "{}({})".format(
+                self.__class__.__name__,
+                dict(
+                    name=self.name,
+                    size=self.size,
+                    checksum=self.checksum,
+                    )
+                )
+
+    @classmethod
+    def from_header_dict(cls, header_dict):
+        """
+        curl -i -X POST -H 'x-image-meta-container_format: bare' -H
+        'Transfer-Encoding: chunked' -H 'User-Agent: python-glanceclient' -H
+        'x-image-meta-size: 13167616' -H 'x-image-meta-is_public: False' -H
+        'X-Auth-Token: test' -H 'Content-Type: application/octet-stream' -H
+        'x-image-meta-checksum: 64d7c1cd2b6f60c92c14662941cb7913' -H
+        'x-image-meta-disk_format: raw' -H 'x-image-meta-name:
+        cirros-0.3.2-x86_64-disk.img'
+        """
+
+        name = header_dict["x-image-meta-name"]
+        try:
+            size = int(header_dict["x-image-meta-size"])
+        except KeyError:
+            size = None
+
+        try:
+            checksum = header_dict["x-image-meta-checksum"]
+        except KeyError:
+            checksum = None
+
+        disk_format = header_dict["x-image-meta-disk_format"]
+        container_format = header_dict["x-image-meta-container_format"]
+
+        return cls(name=name, size=size, checksum=checksum,
+                   disk_format=disk_format, container_format=container_format)
+
+
+class GlanceImageCreateResponse(object):
+    def __init__(self, id, name, status, size, checksum):
+        self.id = id
+        self.name = name
+        self.status = status
+        self.size = size
+        self.checksum = checksum
+
+    def __repr__(self):
+        return "{}({})".format(
+                self.__class__.__name__,
+                dict(
+                    id=self.id,
+                    name=self.name,
+                    status=self.status,
+                    checksum=self.checksum,
+                    )
+                )
+
+    @classmethod
+    def from_response_body(cls, response_body):
+        """
+        {"image": {"status": "active", "deleted": false, "container_format":
+        "bare", "min_ram": 0, "updated_at": "2016-06-24T14:41:38.598199",
+        "owner": null, "min_disk": 0, "is_public": false, "deleted_at": null,
+        "id": "5903cb2d-53db-4343-b055-586475a077f5", "size": 13167616, "name":
+        "cirros-0.3.2-x86_64-disk.img", "checksum":
+        "64d7c1cd2b6f60c92c14662941cb7913", "created_at":
+        "2016-06-24T14:41:38.207356", "disk_format": "raw",
+        "properties": {}, "protected": false}}
+        """
+
+        response_dict = json.loads(response_body.decode())
+        image = response_dict["image"]
+
+        id = image["id"]
+        name = image["name"]
+        status = image["status"]
+        size = image["size"]
+        checksum = image["checksum"]
+
+        return cls(
+                id=id, name=name, status=status,
+                size=size, checksum=checksum
+                )
+
+
+class GlanceHTTPMockProxy(object):
+    def __init__(self, log, loop, on_http_request, on_http_response):
+        self._log = log
+        self._loop = loop
+        self._on_http_request = on_http_request
+        self._on_http_response = on_http_response
+
+    def start(self):
+        pass
+
+    def stop(self):
+        pass
+
+
+class QuickProxyServer(object):
+    """ This class implements a HTTP Proxy server
+    """
+    DEFAULT_PROXY_PORT = 9999
+    DEBUG_LEVEL = 0
+
+    def __init__(self, log, loop, proxy_port=DEFAULT_PROXY_PORT):
+        self._log = log
+        self._loop = loop
+        self._proxy_port = proxy_port
+
+        self._proxy_server = None
+
+    def __repr__(self):
+        return "{}(port={})".format(self.__class__.__name__, self._proxy_port)
+
+    def start(self, on_http_request, on_http_response):
+        """ Start the proxy server
+
+        Arguments:
+            on_http_request - A callback when a http request is initiated
+            on_http_response - A callback when a http response is initiated
+
+        """
+        self._log.debug("Starting %s", self)
+        io_loop = rift.tasklets.tornado.TaskletAsyncIOLoop(
+                asyncio_loop=self._loop
+                )
+
+        self._proxy_server = quickproxy.run_proxy(
+                port=self._proxy_port,
+                req_callback=on_http_request,
+                resp_callback=on_http_response,
+                io_loop=io_loop,
+                debug_level=QuickProxyServer.DEBUG_LEVEL
+                )
+
+    def stop(self):
+        """ Stop the proxy server """
+        if self._proxy_server is None:
+            self._log.warning("%s already stopped")
+            return
+
+        self._log.debug("Stopping %s", self)
+        self._proxy_server.stop()
+        self._proxy_server = None
+
+
+class GlanceHTTPProxyServer(object):
+    """ This class implements a HTTP Proxy server
+
+    Proxying requests to glance has the following high-level advantages:
+       - Allows us to intercept HTTP requests and responses to hook in functionality
+       - Allows us to configure the glance catalog server and keep the endpoint the same
+    """
+
+    DEFAULT_GLANCE_CONFIG = GlanceConfig()
+
+    def __init__(self, log, loop,
+                 http_proxy_server,
+                 glance_config=DEFAULT_GLANCE_CONFIG,
+                 on_create_image_request=None,
+                 on_create_image_response=None,
+                 ):
+
+        self._log = log
+        self._loop = loop
+        self._http_proxy_server = http_proxy_server
+        self._glance_config = glance_config
+
+        self._on_create_image_request = on_create_image_request
+        self._on_create_image_response = on_create_image_response
+
+    def _handle_create_image_request(self, request):
+        image_request = GlanceImageCreateRequest.from_header_dict(request.headers)
+        self._log.debug("Parsed image request: %s", image_request)
+        if self._on_create_image_request is not None:
+            self._on_create_image_request(image_request)
+
+        # Store the GlanceImageCreateRequest in the request context so it
+        # is available in the response
+        request.context["image_request"] = image_request
+
+        return request
+
+    def _handle_create_image_response(self, response):
+        image_request = response.context["image_request"]
+
+        self._log.debug("Got response body: %s", response.body)
+        image_response = GlanceImageCreateResponse.from_response_body(response.body)
+        self._log.debug("Parsed image response: %s", image_response)
+        if self._on_create_image_response is not None:
+            response = self._on_create_image_response(image_response, image_request)
+
+        return response
+
+    def start(self):
+        """ Start the glance proxy server """
+        def request_callback(request):
+            # Redirect the request to the actual glance server
+            self._log.debug("Proxying request to glance (path: %s, method: %s)",
+                            request.path, request.method)
+
+            # Save the path and method to detect whether the response for
+            # for a create_image request
+            request.context["path"] = request.path
+            request.context["method"] = request.method
+
+            if request.path.endswith("images") and request.method == "POST":
+                request = self._handle_create_image_request(request)
+
+            # Redirect the request to the actual glance server
+            request.host = self._glance_config.host
+            request.port = self._glance_config.port
+
+            return request
+
+        def response_callback(response):
+            self._log.debug("Got glance request response: %s", response)
+
+            if response.context["path"].endswith("images") and response.context["method"] == "POST":
+                response = self._handle_create_image_response(response)
+
+            return response
+
+        self._http_proxy_server.start(
+                on_http_request=request_callback,
+                on_http_response=response_callback
+                )
+
+    def stop(self):
+        """ Stop the glance proxy server """
+        self._http_proxy_server.stop()