--- /dev/null
+
+#
+# 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()