update from RIFT as of 696b75d2fe9fb046261b08c616f1bcf6c0b54a9b second try
[osm/SO.git] / rwlaunchpad / plugins / rwimagemgr / rift / tasklets / rwimagemgr / glance_proxy_server.py
1
2 #
3 # Copyright 2016 RIFT.IO Inc
4 #
5 # Licensed under the Apache License, Version 2.0 (the "License");
6 # you may not use this file except in compliance with the License.
7 # You may obtain a copy of the License at
8 #
9 # http://www.apache.org/licenses/LICENSE-2.0
10 #
11 # Unless required by applicable law or agreed to in writing, software
12 # distributed under the License is distributed on an "AS IS" BASIS,
13 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 # See the License for the specific language governing permissions and
15 # limitations under the License.
16 #
17
18 import json
19
20 from .lib import quickproxy
21 import rift.tasklets.tornado
22
23
24 class GlanceConfig(object):
25 DEFAULT_HOST = "127.0.0.1"
26 DEFAULT_PORT = 9292
27 DEFAULT_TOKEN = "test"
28
29 def __init__(self, host=DEFAULT_HOST, port=DEFAULT_PORT, token=DEFAULT_TOKEN):
30 self.host = host
31 self.port = port
32 self.token = token
33
34
35 class GlanceImageCreateRequest(object):
36 def __init__(self, name, size, checksum, disk_format, container_format):
37 self.name = name
38 self.size = size
39 self.checksum = checksum
40 self.disk_format = disk_format
41 self.container_format = container_format
42
43 def __repr__(self):
44 return "{}({})".format(
45 self.__class__.__name__,
46 dict(
47 name=self.name,
48 size=self.size,
49 checksum=self.checksum,
50 )
51 )
52
53 @classmethod
54 def from_header_dict(cls, header_dict):
55 """
56 curl -i -X POST -H 'x-image-meta-container_format: bare' -H
57 'Transfer-Encoding: chunked' -H 'User-Agent: python-glanceclient' -H
58 'x-image-meta-size: 13167616' -H 'x-image-meta-is_public: False' -H
59 'X-Auth-Token: test' -H 'Content-Type: application/octet-stream' -H
60 'x-image-meta-checksum: 64d7c1cd2b6f60c92c14662941cb7913' -H
61 'x-image-meta-disk_format: raw' -H 'x-image-meta-name:
62 cirros-0.3.2-x86_64-disk.img'
63 """
64
65 name = header_dict["x-image-meta-name"]
66 try:
67 size = int(header_dict["x-image-meta-size"])
68 except KeyError:
69 size = None
70
71 try:
72 checksum = header_dict["x-image-meta-checksum"]
73 except KeyError:
74 checksum = None
75
76 disk_format = header_dict["x-image-meta-disk_format"]
77 container_format = header_dict["x-image-meta-container_format"]
78
79 return cls(name=name, size=size, checksum=checksum,
80 disk_format=disk_format, container_format=container_format)
81
82
83 class GlanceImageCreateResponse(object):
84 def __init__(self, id, name, status, size, checksum):
85 self.id = id
86 self.name = name
87 self.status = status
88 self.size = size
89 self.checksum = checksum
90
91 def __repr__(self):
92 return "{}({})".format(
93 self.__class__.__name__,
94 dict(
95 id=self.id,
96 name=self.name,
97 status=self.status,
98 checksum=self.checksum,
99 )
100 )
101
102 @classmethod
103 def from_response_body(cls, response_body):
104 """
105 {"image": {"status": "active", "deleted": false, "container_format":
106 "bare", "min_ram": 0, "updated_at": "2016-06-24T14:41:38.598199",
107 "owner": null, "min_disk": 0, "is_public": false, "deleted_at": null,
108 "id": "5903cb2d-53db-4343-b055-586475a077f5", "size": 13167616, "name":
109 "cirros-0.3.2-x86_64-disk.img", "checksum":
110 "64d7c1cd2b6f60c92c14662941cb7913", "created_at":
111 "2016-06-24T14:41:38.207356", "disk_format": "raw",
112 "properties": {}, "protected": false}}
113 """
114
115 response_dict = json.loads(response_body.decode())
116 image = response_dict["image"]
117
118 id = image["id"]
119 name = image["name"]
120 status = image["status"]
121 size = image["size"]
122 checksum = image["checksum"]
123
124 return cls(
125 id=id, name=name, status=status,
126 size=size, checksum=checksum
127 )
128
129
130 class GlanceHTTPMockProxy(object):
131 def __init__(self, log, loop, on_http_request, on_http_response):
132 self._log = log
133 self._loop = loop
134 self._on_http_request = on_http_request
135 self._on_http_response = on_http_response
136
137 def start(self):
138 pass
139
140 def stop(self):
141 pass
142
143
144 class QuickProxyServer(object):
145 """ This class implements a HTTP Proxy server
146 """
147 DEFAULT_PROXY_PORT = 9999
148 DEBUG_LEVEL = 0
149
150 def __init__(self, log, loop, proxy_port=DEFAULT_PROXY_PORT):
151 self._log = log
152 self._loop = loop
153 self._proxy_port = proxy_port
154
155 self._proxy_server = None
156
157 def __repr__(self):
158 return "{}(port={})".format(self.__class__.__name__, self._proxy_port)
159
160 def start(self, on_http_request, on_http_response):
161 """ Start the proxy server
162
163 Arguments:
164 on_http_request - A callback when a http request is initiated
165 on_http_response - A callback when a http response is initiated
166
167 """
168 self._log.debug("Starting %s", self)
169 io_loop = rift.tasklets.tornado.TaskletAsyncIOLoop(
170 asyncio_loop=self._loop
171 )
172
173 self._proxy_server = quickproxy.run_proxy(
174 port=self._proxy_port,
175 req_callback=on_http_request,
176 resp_callback=on_http_response,
177 io_loop=io_loop,
178 debug_level=QuickProxyServer.DEBUG_LEVEL,
179 address="127.0.0.1",
180 )
181
182 def stop(self):
183 """ Stop the proxy server """
184 if self._proxy_server is None:
185 self._log.warning("%s already stopped")
186 return
187
188 self._log.debug("Stopping %s", self)
189 self._proxy_server.stop()
190 self._proxy_server = None
191
192
193 class GlanceHTTPProxyServer(object):
194 """ This class implements a HTTP Proxy server
195
196 Proxying requests to glance has the following high-level advantages:
197 - Allows us to intercept HTTP requests and responses to hook in functionality
198 - Allows us to configure the glance catalog server and keep the endpoint the same
199 """
200
201 DEFAULT_GLANCE_CONFIG = GlanceConfig()
202
203 def __init__(self, log, loop,
204 http_proxy_server,
205 glance_config=DEFAULT_GLANCE_CONFIG,
206 on_create_image_request=None,
207 on_create_image_response=None,
208 ):
209
210 self._log = log
211 self._loop = loop
212 self._http_proxy_server = http_proxy_server
213 self._glance_config = glance_config
214
215 self._on_create_image_request = on_create_image_request
216 self._on_create_image_response = on_create_image_response
217
218 def _handle_create_image_request(self, request):
219 image_request = GlanceImageCreateRequest.from_header_dict(request.headers)
220 self._log.debug("Parsed image request: %s", image_request)
221 if self._on_create_image_request is not None:
222 self._on_create_image_request(image_request)
223
224 # Store the GlanceImageCreateRequest in the request context so it
225 # is available in the response
226 request.context["image_request"] = image_request
227
228 return request
229
230 def _handle_create_image_response(self, response):
231 image_request = response.context["image_request"]
232
233 self._log.debug("Got response body: %s", response.body)
234 image_response = GlanceImageCreateResponse.from_response_body(response.body)
235 self._log.debug("Parsed image response: %s", image_response)
236 if self._on_create_image_response is not None:
237 response = self._on_create_image_response(image_response, image_request)
238
239 return response
240
241 def start(self):
242 """ Start the glance proxy server """
243 def request_callback(request):
244 # Redirect the request to the actual glance server
245 self._log.debug("Proxying request to glance (path: %s, method: %s)",
246 request.path, request.method)
247
248 # Save the path and method to detect whether the response for
249 # for a create_image request
250 request.context["path"] = request.path
251 request.context["method"] = request.method
252
253 if request.path.endswith("images") and request.method == "POST":
254 request = self._handle_create_image_request(request)
255
256 # Redirect the request to the actual glance server
257 request.host = self._glance_config.host
258 request.port = self._glance_config.port
259
260 return request
261
262 def response_callback(response):
263 self._log.debug("Got glance request response: %s", response)
264
265 if response.context["path"].endswith("images") and response.context["method"] == "POST":
266 response = self._handle_create_image_response(response)
267
268 return response
269
270 self._http_proxy_server.start(
271 on_http_request=request_callback,
272 on_http_response=response_callback
273 )
274
275 def stop(self):
276 """ Stop the glance proxy server """
277 self._http_proxy_server.stop()