Merge lp:~eday/burrow/prototype-conversion into lp:burrow
- prototype-conversion
- Merge into trunk
Proposed by
Eric Day
Status: | Merged |
---|---|
Approved by: | Eric Day |
Approved revision: | 2 |
Merged at revision: | 2 |
Proposed branch: | lp:~eday/burrow/prototype-conversion |
Merge into: | lp:burrow |
Diff against target: |
1874 lines (+1780/-0) 17 files modified
Authors (+1/-0) ChangeLog (+4/-0) LICENSE (+202/-0) MANIFEST.in (+4/-0) bin/burrow (+18/-0) bin/burrowd (+34/-0) burrow/__init__.py (+19/-0) burrowd/__init__.py (+131/-0) burrowd/backend/__init__.py (+94/-0) burrowd/backend/memory.py (+176/-0) burrowd/backend/sqlite.py (+250/-0) burrowd/config.py (+55/-0) burrowd/frontend/__init__.py (+30/-0) burrowd/frontend/wsgi.py (+276/-0) etc/burrowd.conf (+99/-0) setup.py (+74/-0) test/frontend/test_wsgi.py (+313/-0) |
To merge this branch: | bzr merge lp:~eday/burrow/prototype-conversion |
Related bugs: | |
Related blueprints: |
Core setup and modules for the project
(Essential)
|
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Burrow Core Team | Pending | ||
Review via email:
|
Commit message
Description of the change
Python prototype conversion to get new trunk started.
To post a comment you must log in.
Preview Diff
[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1 | === added file 'Authors' | |||
2 | --- Authors 1970-01-01 00:00:00 +0000 | |||
3 | +++ Authors 2011-03-17 23:47:24 +0000 | |||
4 | @@ -0,0 +1,1 @@ | |||
5 | 1 | Eric Day <eday@oddments.org> | ||
6 | 0 | 2 | ||
7 | === added file 'ChangeLog' | |||
8 | --- ChangeLog 1970-01-01 00:00:00 +0000 | |||
9 | +++ ChangeLog 2011-03-17 23:47:24 +0000 | |||
10 | @@ -0,0 +1,4 @@ | |||
11 | 1 | 2011-03-17 Eric Day <eday@oddments.org> | ||
12 | 2 | |||
13 | 3 | Created new burrow trunk. | ||
14 | 4 | |||
15 | 0 | 5 | ||
16 | === added file 'LICENSE' | |||
17 | --- LICENSE 1970-01-01 00:00:00 +0000 | |||
18 | +++ LICENSE 2011-03-17 23:47:24 +0000 | |||
19 | @@ -0,0 +1,202 @@ | |||
20 | 1 | |||
21 | 2 | Apache License | ||
22 | 3 | Version 2.0, January 2004 | ||
23 | 4 | http://www.apache.org/licenses/ | ||
24 | 5 | |||
25 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION | ||
26 | 7 | |||
27 | 8 | 1. Definitions. | ||
28 | 9 | |||
29 | 10 | "License" shall mean the terms and conditions for use, reproduction, | ||
30 | 11 | and distribution as defined by Sections 1 through 9 of this document. | ||
31 | 12 | |||
32 | 13 | "Licensor" shall mean the copyright owner or entity authorized by | ||
33 | 14 | the copyright owner that is granting the License. | ||
34 | 15 | |||
35 | 16 | "Legal Entity" shall mean the union of the acting entity and all | ||
36 | 17 | other entities that control, are controlled by, or are under common | ||
37 | 18 | control with that entity. For the purposes of this definition, | ||
38 | 19 | "control" means (i) the power, direct or indirect, to cause the | ||
39 | 20 | direction or management of such entity, whether by contract or | ||
40 | 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the | ||
41 | 22 | outstanding shares, or (iii) beneficial ownership of such entity. | ||
42 | 23 | |||
43 | 24 | "You" (or "Your") shall mean an individual or Legal Entity | ||
44 | 25 | exercising permissions granted by this License. | ||
45 | 26 | |||
46 | 27 | "Source" form shall mean the preferred form for making modifications, | ||
47 | 28 | including but not limited to software source code, documentation | ||
48 | 29 | source, and configuration files. | ||
49 | 30 | |||
50 | 31 | "Object" form shall mean any form resulting from mechanical | ||
51 | 32 | transformation or translation of a Source form, including but | ||
52 | 33 | not limited to compiled object code, generated documentation, | ||
53 | 34 | and conversions to other media types. | ||
54 | 35 | |||
55 | 36 | "Work" shall mean the work of authorship, whether in Source or | ||
56 | 37 | Object form, made available under the License, as indicated by a | ||
57 | 38 | copyright notice that is included in or attached to the work | ||
58 | 39 | (an example is provided in the Appendix below). | ||
59 | 40 | |||
60 | 41 | "Derivative Works" shall mean any work, whether in Source or Object | ||
61 | 42 | form, that is based on (or derived from) the Work and for which the | ||
62 | 43 | editorial revisions, annotations, elaborations, or other modifications | ||
63 | 44 | represent, as a whole, an original work of authorship. For the purposes | ||
64 | 45 | of this License, Derivative Works shall not include works that remain | ||
65 | 46 | separable from, or merely link (or bind by name) to the interfaces of, | ||
66 | 47 | the Work and Derivative Works thereof. | ||
67 | 48 | |||
68 | 49 | "Contribution" shall mean any work of authorship, including | ||
69 | 50 | the original version of the Work and any modifications or additions | ||
70 | 51 | to that Work or Derivative Works thereof, that is intentionally | ||
71 | 52 | submitted to Licensor for inclusion in the Work by the copyright owner | ||
72 | 53 | or by an individual or Legal Entity authorized to submit on behalf of | ||
73 | 54 | the copyright owner. For the purposes of this definition, "submitted" | ||
74 | 55 | means any form of electronic, verbal, or written communication sent | ||
75 | 56 | to the Licensor or its representatives, including but not limited to | ||
76 | 57 | communication on electronic mailing lists, source code control systems, | ||
77 | 58 | and issue tracking systems that are managed by, or on behalf of, the | ||
78 | 59 | Licensor for the purpose of discussing and improving the Work, but | ||
79 | 60 | excluding communication that is conspicuously marked or otherwise | ||
80 | 61 | designated in writing by the copyright owner as "Not a Contribution." | ||
81 | 62 | |||
82 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity | ||
83 | 64 | on behalf of whom a Contribution has been received by Licensor and | ||
84 | 65 | subsequently incorporated within the Work. | ||
85 | 66 | |||
86 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of | ||
87 | 68 | this License, each Contributor hereby grants to You a perpetual, | ||
88 | 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable | ||
89 | 70 | copyright license to reproduce, prepare Derivative Works of, | ||
90 | 71 | publicly display, publicly perform, sublicense, and distribute the | ||
91 | 72 | Work and such Derivative Works in Source or Object form. | ||
92 | 73 | |||
93 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of | ||
94 | 75 | this License, each Contributor hereby grants to You a perpetual, | ||
95 | 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable | ||
96 | 77 | (except as stated in this section) patent license to make, have made, | ||
97 | 78 | use, offer to sell, sell, import, and otherwise transfer the Work, | ||
98 | 79 | where such license applies only to those patent claims licensable | ||
99 | 80 | by such Contributor that are necessarily infringed by their | ||
100 | 81 | Contribution(s) alone or by combination of their Contribution(s) | ||
101 | 82 | with the Work to which such Contribution(s) was submitted. If You | ||
102 | 83 | institute patent litigation against any entity (including a | ||
103 | 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work | ||
104 | 85 | or a Contribution incorporated within the Work constitutes direct | ||
105 | 86 | or contributory patent infringement, then any patent licenses | ||
106 | 87 | granted to You under this License for that Work shall terminate | ||
107 | 88 | as of the date such litigation is filed. | ||
108 | 89 | |||
109 | 90 | 4. Redistribution. You may reproduce and distribute copies of the | ||
110 | 91 | Work or Derivative Works thereof in any medium, with or without | ||
111 | 92 | modifications, and in Source or Object form, provided that You | ||
112 | 93 | meet the following conditions: | ||
113 | 94 | |||
114 | 95 | (a) You must give any other recipients of the Work or | ||
115 | 96 | Derivative Works a copy of this License; and | ||
116 | 97 | |||
117 | 98 | (b) You must cause any modified files to carry prominent notices | ||
118 | 99 | stating that You changed the files; and | ||
119 | 100 | |||
120 | 101 | (c) You must retain, in the Source form of any Derivative Works | ||
121 | 102 | that You distribute, all copyright, patent, trademark, and | ||
122 | 103 | attribution notices from the Source form of the Work, | ||
123 | 104 | excluding those notices that do not pertain to any part of | ||
124 | 105 | the Derivative Works; and | ||
125 | 106 | |||
126 | 107 | (d) If the Work includes a "NOTICE" text file as part of its | ||
127 | 108 | distribution, then any Derivative Works that You distribute must | ||
128 | 109 | include a readable copy of the attribution notices contained | ||
129 | 110 | within such NOTICE file, excluding those notices that do not | ||
130 | 111 | pertain to any part of the Derivative Works, in at least one | ||
131 | 112 | of the following places: within a NOTICE text file distributed | ||
132 | 113 | as part of the Derivative Works; within the Source form or | ||
133 | 114 | documentation, if provided along with the Derivative Works; or, | ||
134 | 115 | within a display generated by the Derivative Works, if and | ||
135 | 116 | wherever such third-party notices normally appear. The contents | ||
136 | 117 | of the NOTICE file are for informational purposes only and | ||
137 | 118 | do not modify the License. You may add Your own attribution | ||
138 | 119 | notices within Derivative Works that You distribute, alongside | ||
139 | 120 | or as an addendum to the NOTICE text from the Work, provided | ||
140 | 121 | that such additional attribution notices cannot be construed | ||
141 | 122 | as modifying the License. | ||
142 | 123 | |||
143 | 124 | You may add Your own copyright statement to Your modifications and | ||
144 | 125 | may provide additional or different license terms and conditions | ||
145 | 126 | for use, reproduction, or distribution of Your modifications, or | ||
146 | 127 | for any such Derivative Works as a whole, provided Your use, | ||
147 | 128 | reproduction, and distribution of the Work otherwise complies with | ||
148 | 129 | the conditions stated in this License. | ||
149 | 130 | |||
150 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, | ||
151 | 132 | any Contribution intentionally submitted for inclusion in the Work | ||
152 | 133 | by You to the Licensor shall be under the terms and conditions of | ||
153 | 134 | this License, without any additional terms or conditions. | ||
154 | 135 | Notwithstanding the above, nothing herein shall supersede or modify | ||
155 | 136 | the terms of any separate license agreement you may have executed | ||
156 | 137 | with Licensor regarding such Contributions. | ||
157 | 138 | |||
158 | 139 | 6. Trademarks. This License does not grant permission to use the trade | ||
159 | 140 | names, trademarks, service marks, or product names of the Licensor, | ||
160 | 141 | except as required for reasonable and customary use in describing the | ||
161 | 142 | origin of the Work and reproducing the content of the NOTICE file. | ||
162 | 143 | |||
163 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or | ||
164 | 145 | agreed to in writing, Licensor provides the Work (and each | ||
165 | 146 | Contributor provides its Contributions) on an "AS IS" BASIS, | ||
166 | 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or | ||
167 | 148 | implied, including, without limitation, any warranties or conditions | ||
168 | 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A | ||
169 | 150 | PARTICULAR PURPOSE. You are solely responsible for determining the | ||
170 | 151 | appropriateness of using or redistributing the Work and assume any | ||
171 | 152 | risks associated with Your exercise of permissions under this License. | ||
172 | 153 | |||
173 | 154 | 8. Limitation of Liability. In no event and under no legal theory, | ||
174 | 155 | whether in tort (including negligence), contract, or otherwise, | ||
175 | 156 | unless required by applicable law (such as deliberate and grossly | ||
176 | 157 | negligent acts) or agreed to in writing, shall any Contributor be | ||
177 | 158 | liable to You for damages, including any direct, indirect, special, | ||
178 | 159 | incidental, or consequential damages of any character arising as a | ||
179 | 160 | result of this License or out of the use or inability to use the | ||
180 | 161 | Work (including but not limited to damages for loss of goodwill, | ||
181 | 162 | work stoppage, computer failure or malfunction, or any and all | ||
182 | 163 | other commercial damages or losses), even if such Contributor | ||
183 | 164 | has been advised of the possibility of such damages. | ||
184 | 165 | |||
185 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing | ||
186 | 167 | the Work or Derivative Works thereof, You may choose to offer, | ||
187 | 168 | and charge a fee for, acceptance of support, warranty, indemnity, | ||
188 | 169 | or other liability obligations and/or rights consistent with this | ||
189 | 170 | License. However, in accepting such obligations, You may act only | ||
190 | 171 | on Your own behalf and on Your sole responsibility, not on behalf | ||
191 | 172 | of any other Contributor, and only if You agree to indemnify, | ||
192 | 173 | defend, and hold each Contributor harmless for any liability | ||
193 | 174 | incurred by, or claims asserted against, such Contributor by reason | ||
194 | 175 | of your accepting any such warranty or additional liability. | ||
195 | 176 | |||
196 | 177 | END OF TERMS AND CONDITIONS | ||
197 | 178 | |||
198 | 179 | APPENDIX: How to apply the Apache License to your work. | ||
199 | 180 | |||
200 | 181 | To apply the Apache License to your work, attach the following | ||
201 | 182 | boilerplate notice, with the fields enclosed by brackets "[]" | ||
202 | 183 | replaced with your own identifying information. (Don't include | ||
203 | 184 | the brackets!) The text should be enclosed in the appropriate | ||
204 | 185 | comment syntax for the file format. We also recommend that a | ||
205 | 186 | file or class name and description of purpose be included on the | ||
206 | 187 | same "printed page" as the copyright notice for easier | ||
207 | 188 | identification within third-party archives. | ||
208 | 189 | |||
209 | 190 | Copyright [yyyy] [name of copyright owner] | ||
210 | 191 | |||
211 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); | ||
212 | 193 | you may not use this file except in compliance with the License. | ||
213 | 194 | You may obtain a copy of the License at | ||
214 | 195 | |||
215 | 196 | http://www.apache.org/licenses/LICENSE-2.0 | ||
216 | 197 | |||
217 | 198 | Unless required by applicable law or agreed to in writing, software | ||
218 | 199 | distributed under the License is distributed on an "AS IS" BASIS, | ||
219 | 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
220 | 201 | See the License for the specific language governing permissions and | ||
221 | 202 | limitations under the License. | ||
222 | 0 | 203 | ||
223 | === added file 'MANIFEST.in' | |||
224 | --- MANIFEST.in 1970-01-01 00:00:00 +0000 | |||
225 | +++ MANIFEST.in 2011-03-17 23:47:24 +0000 | |||
226 | @@ -0,0 +1,4 @@ | |||
227 | 1 | graft etc | ||
228 | 2 | include Authors | ||
229 | 3 | include ChangeLog | ||
230 | 4 | include LICENSE | ||
231 | 0 | 5 | ||
232 | === added directory 'bin' | |||
233 | === added file 'bin/burrow' | |||
234 | --- bin/burrow 1970-01-01 00:00:00 +0000 | |||
235 | +++ bin/burrow 2011-03-17 23:47:24 +0000 | |||
236 | @@ -0,0 +1,18 @@ | |||
237 | 1 | #!/usr/bin/env python | ||
238 | 2 | # Copyright (C) 2011 OpenStack LLC. | ||
239 | 3 | # | ||
240 | 4 | # Licensed under the Apache License, Version 2.0 (the "License"); | ||
241 | 5 | # you may not use this file except in compliance with the License. | ||
242 | 6 | # You may obtain a copy of the License at | ||
243 | 7 | # | ||
244 | 8 | # http://www.apache.org/licenses/LICENSE-2.0 | ||
245 | 9 | # | ||
246 | 10 | # Unless required by applicable law or agreed to in writing, software | ||
247 | 11 | # distributed under the License is distributed on an "AS IS" BASIS, | ||
248 | 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
249 | 13 | # See the License for the specific language governing permissions and | ||
250 | 14 | # limitations under the License. | ||
251 | 15 | |||
252 | 16 | ''' | ||
253 | 17 | Burrow command line client. | ||
254 | 18 | ''' | ||
255 | 0 | 19 | ||
256 | === added file 'bin/burrowd' | |||
257 | --- bin/burrowd 1970-01-01 00:00:00 +0000 | |||
258 | +++ bin/burrowd 2011-03-17 23:47:24 +0000 | |||
259 | @@ -0,0 +1,34 @@ | |||
260 | 1 | #!/usr/bin/env python | ||
261 | 2 | # Copyright (C) 2011 OpenStack LLC. | ||
262 | 3 | # | ||
263 | 4 | # Licensed under the Apache License, Version 2.0 (the "License"); | ||
264 | 5 | # you may not use this file except in compliance with the License. | ||
265 | 6 | # You may obtain a copy of the License at | ||
266 | 7 | # | ||
267 | 8 | # http://www.apache.org/licenses/LICENSE-2.0 | ||
268 | 9 | # | ||
269 | 10 | # Unless required by applicable law or agreed to in writing, software | ||
270 | 11 | # distributed under the License is distributed on an "AS IS" BASIS, | ||
271 | 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
272 | 13 | # See the License for the specific language governing permissions and | ||
273 | 14 | # limitations under the License. | ||
274 | 15 | |||
275 | 16 | ''' | ||
276 | 17 | Burrow server. | ||
277 | 18 | ''' | ||
278 | 19 | |||
279 | 20 | import os | ||
280 | 21 | import sys | ||
281 | 22 | |||
282 | 23 | # If ../burrowd/__init__.py exists, add ../ to the Python search path so | ||
283 | 24 | # that it will override whatever may be installed in the default Python | ||
284 | 25 | # search path. | ||
285 | 26 | BASE_DIRECTORY = os.path.join(os.path.abspath(__file__), os.pardir, os.pardir) | ||
286 | 27 | BASE_DIRECTORY = os.path.normpath(BASE_DIRECTORY) | ||
287 | 28 | if os.path.exists(os.path.join(BASE_DIRECTORY, 'burrowd', '__init__.py')): | ||
288 | 29 | sys.path.insert(0, BASE_DIRECTORY) | ||
289 | 30 | |||
290 | 31 | import burrowd | ||
291 | 32 | |||
292 | 33 | if __name__ == '__main__': | ||
293 | 34 | burrowd.Burrowd(sys.argv[1:]).run() | ||
294 | 0 | 35 | ||
295 | === added directory 'burrow' | |||
296 | === added file 'burrow/__init__.py' | |||
297 | --- burrow/__init__.py 1970-01-01 00:00:00 +0000 | |||
298 | +++ burrow/__init__.py 2011-03-17 23:47:24 +0000 | |||
299 | @@ -0,0 +1,19 @@ | |||
300 | 1 | # Copyright (C) 2011 OpenStack LLC. | ||
301 | 2 | # | ||
302 | 3 | # Licensed under the Apache License, Version 2.0 (the "License"); | ||
303 | 4 | # you may not use this file except in compliance with the License. | ||
304 | 5 | # You may obtain a copy of the License at | ||
305 | 6 | # | ||
306 | 7 | # http://www.apache.org/licenses/LICENSE-2.0 | ||
307 | 8 | # | ||
308 | 9 | # Unless required by applicable law or agreed to in writing, software | ||
309 | 10 | # distributed under the License is distributed on an "AS IS" BASIS, | ||
310 | 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
311 | 12 | # See the License for the specific language governing permissions and | ||
312 | 13 | # limitations under the License. | ||
313 | 14 | |||
314 | 15 | '''Main client module for burrow.''' | ||
315 | 16 | |||
316 | 17 | |||
317 | 18 | class Burrow(object): | ||
318 | 19 | pass | ||
319 | 0 | 20 | ||
320 | === added directory 'burrowd' | |||
321 | === added file 'burrowd/__init__.py' | |||
322 | --- burrowd/__init__.py 1970-01-01 00:00:00 +0000 | |||
323 | +++ burrowd/__init__.py 2011-03-17 23:47:24 +0000 | |||
324 | @@ -0,0 +1,131 @@ | |||
325 | 1 | # Copyright (C) 2011 OpenStack LLC. | ||
326 | 2 | # | ||
327 | 3 | # Licensed under the Apache License, Version 2.0 (the "License"); | ||
328 | 4 | # you may not use this file except in compliance with the License. | ||
329 | 5 | # You may obtain a copy of the License at | ||
330 | 6 | # | ||
331 | 7 | # http://www.apache.org/licenses/LICENSE-2.0 | ||
332 | 8 | # | ||
333 | 9 | # Unless required by applicable law or agreed to in writing, software | ||
334 | 10 | # distributed under the License is distributed on an "AS IS" BASIS, | ||
335 | 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
336 | 12 | # See the License for the specific language governing permissions and | ||
337 | 13 | # limitations under the License. | ||
338 | 14 | |||
339 | 15 | '''Main server module for burrow.''' | ||
340 | 16 | |||
341 | 17 | import ConfigParser | ||
342 | 18 | import gettext | ||
343 | 19 | import logging | ||
344 | 20 | import logging.config | ||
345 | 21 | import sys | ||
346 | 22 | |||
347 | 23 | import eventlet | ||
348 | 24 | |||
349 | 25 | import burrowd.config | ||
350 | 26 | |||
351 | 27 | # This installs the _(...) function as a built-in so all other modules | ||
352 | 28 | # don't need to. | ||
353 | 29 | gettext.install('burrowd') | ||
354 | 30 | |||
355 | 31 | # Default configuration values for this module. | ||
356 | 32 | DEFAULT_BACKEND = 'burrowd.backend.sqlite' | ||
357 | 33 | DEFAULT_FRONTENDS = 'burrowd.frontend.wsgi' | ||
358 | 34 | DEFAULT_THREAD_POOL_SIZE = 1000 | ||
359 | 35 | |||
360 | 36 | |||
361 | 37 | class Burrowd(object): | ||
362 | 38 | '''Server class for burrow.''' | ||
363 | 39 | |||
364 | 40 | def __init__(self, config_files=[], add_default_log_handler=True): | ||
365 | 41 | '''Initialize a server using the config files from the given | ||
366 | 42 | list. This is passed directly to ConfigParser.read(), so | ||
367 | 43 | files should be in ConfigParser format. This will load all | ||
368 | 44 | frontend and backend classes from the configuration.''' | ||
369 | 45 | if len(config_files) > 0: | ||
370 | 46 | logging.config.fileConfig(config_files) | ||
371 | 47 | self._config = ConfigParser.ConfigParser() | ||
372 | 48 | self._config.read(config_files) | ||
373 | 49 | self.config = burrowd.config.Config(self._config, 'burrowd') | ||
374 | 50 | self.log = get_logger(self.config) | ||
375 | 51 | if add_default_log_handler: | ||
376 | 52 | self._add_default_log_handler() | ||
377 | 53 | self._import_backend() | ||
378 | 54 | self._import_frontends() | ||
379 | 55 | |||
380 | 56 | def _add_default_log_handler(self): | ||
381 | 57 | '''Add a default log handler it one has not been set.''' | ||
382 | 58 | root_log = logging.getLogger() | ||
383 | 59 | if len(root_log.handlers) > 0 or len(self.log.handlers) > 0: | ||
384 | 60 | return | ||
385 | 61 | handler = logging.StreamHandler() | ||
386 | 62 | handler.setLevel(logging.DEBUG) | ||
387 | 63 | log_format = '%(asctime)s - %(name)s - %(levelname)s - %(message)s' | ||
388 | 64 | handler.setFormatter(logging.Formatter(log_format)) | ||
389 | 65 | root_log.addHandler(handler) | ||
390 | 66 | |||
391 | 67 | def _import_backend(self): | ||
392 | 68 | '''Load backend given in the 'backend' option.''' | ||
393 | 69 | backend = self.config.get('backend', DEFAULT_BACKEND) | ||
394 | 70 | config = (self._config, backend) | ||
395 | 71 | self.backend = import_class(backend, 'Backend')(config) | ||
396 | 72 | |||
397 | 73 | def _import_frontends(self): | ||
398 | 74 | '''Load frontends given in the 'frontends' option.''' | ||
399 | 75 | self.frontends = [] | ||
400 | 76 | frontends = self.config.get('frontends', DEFAULT_FRONTENDS) | ||
401 | 77 | for frontend in frontends.split(','): | ||
402 | 78 | frontend = frontend.split(':') | ||
403 | 79 | if len(frontend) == 1: | ||
404 | 80 | frontend.append(None) | ||
405 | 81 | config = (self._config, frontend[0], frontend[1]) | ||
406 | 82 | frontend = import_class(frontend[0], 'Frontend') | ||
407 | 83 | frontend = frontend(config, self.backend) | ||
408 | 84 | self.frontends.append(frontend) | ||
409 | 85 | |||
410 | 86 | def run(self): | ||
411 | 87 | '''Create the thread pool and start the main server loop. Wait | ||
412 | 88 | for the pool to complete, but possibly run forever if the | ||
413 | 89 | frontends and backend never remove threads.''' | ||
414 | 90 | thread_pool_size = self.config.getint('thread_pool_size', | ||
415 | 91 | DEFAULT_THREAD_POOL_SIZE) | ||
416 | 92 | thread_pool = eventlet.GreenPool(size=int(thread_pool_size)) | ||
417 | 93 | self.backend.run(thread_pool) | ||
418 | 94 | for frontend in self.frontends: | ||
419 | 95 | frontend.run(thread_pool) | ||
420 | 96 | self.log.info(_('Waiting for all threads to exit')) | ||
421 | 97 | try: | ||
422 | 98 | thread_pool.waitall() | ||
423 | 99 | except KeyboardInterrupt: | ||
424 | 100 | pass | ||
425 | 101 | |||
426 | 102 | |||
427 | 103 | class Module(object): | ||
428 | 104 | '''Common module class for burrow.''' | ||
429 | 105 | |||
430 | 106 | def __init__(self, config): | ||
431 | 107 | self.config = burrowd.config.Config(*config) | ||
432 | 108 | self.log = get_logger(self.config) | ||
433 | 109 | self.log.debug(_('Module created')) | ||
434 | 110 | |||
435 | 111 | |||
436 | 112 | def get_logger(config): | ||
437 | 113 | '''Create a logger from the given config.''' | ||
438 | 114 | log = logging.getLogger(config.section) | ||
439 | 115 | log_level = config.get('log_level', 'DEBUG') | ||
440 | 116 | log_level = logging.getLevelName(log_level) | ||
441 | 117 | if isinstance(log_level, int): | ||
442 | 118 | log.setLevel(log_level) | ||
443 | 119 | return log | ||
444 | 120 | |||
445 | 121 | |||
446 | 122 | def import_class(module_name, class_name=None): | ||
447 | 123 | '''Import a class given a full module.class name.''' | ||
448 | 124 | if class_name is None: | ||
449 | 125 | module_name, _separator, class_name = module_name.rpartition('.') | ||
450 | 126 | try: | ||
451 | 127 | __import__(module_name) | ||
452 | 128 | return getattr(sys.modules[module_name], class_name) | ||
453 | 129 | except (ImportError, ValueError, AttributeError), exception: | ||
454 | 130 | raise ImportError(_('Class %s.%s cannot be found (%s)') % | ||
455 | 131 | (module_name, class_name, exception)) | ||
456 | 0 | 132 | ||
457 | === added directory 'burrowd/backend' | |||
458 | === added file 'burrowd/backend/__init__.py' | |||
459 | --- burrowd/backend/__init__.py 1970-01-01 00:00:00 +0000 | |||
460 | +++ burrowd/backend/__init__.py 2011-03-17 23:47:24 +0000 | |||
461 | @@ -0,0 +1,94 @@ | |||
462 | 1 | # Copyright (C) 2011 OpenStack LLC. | ||
463 | 2 | # | ||
464 | 3 | # Licensed under the Apache License, Version 2.0 (the "License"); | ||
465 | 4 | # you may not use this file except in compliance with the License. | ||
466 | 5 | # You may obtain a copy of the License at | ||
467 | 6 | # | ||
468 | 7 | # http://www.apache.org/licenses/LICENSE-2.0 | ||
469 | 8 | # | ||
470 | 9 | # Unless required by applicable law or agreed to in writing, software | ||
471 | 10 | # distributed under the License is distributed on an "AS IS" BASIS, | ||
472 | 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
473 | 12 | # See the License for the specific language governing permissions and | ||
474 | 13 | # limitations under the License. | ||
475 | 14 | |||
476 | 15 | '''Backends for the burrow server.''' | ||
477 | 16 | |||
478 | 17 | import eventlet | ||
479 | 18 | |||
480 | 19 | import burrowd | ||
481 | 20 | |||
482 | 21 | |||
483 | 22 | class Backend(burrowd.Module): | ||
484 | 23 | '''Interface that backend implementations must provide.''' | ||
485 | 24 | |||
486 | 25 | def __init__(self, config): | ||
487 | 26 | super(Backend, self).__init__(config) | ||
488 | 27 | self.queues = {} | ||
489 | 28 | |||
490 | 29 | def run(self, thread_pool): | ||
491 | 30 | thread_pool.spawn_n(self._clean) | ||
492 | 31 | |||
493 | 32 | def _clean(self): | ||
494 | 33 | while True: | ||
495 | 34 | self.clean() | ||
496 | 35 | eventlet.sleep(1) | ||
497 | 36 | |||
498 | 37 | def delete_accounts(self): | ||
499 | 38 | pass | ||
500 | 39 | |||
501 | 40 | def get_accounts(self): | ||
502 | 41 | return [] | ||
503 | 42 | |||
504 | 43 | def delete_account(self, account): | ||
505 | 44 | pass | ||
506 | 45 | |||
507 | 46 | def get_queues(self, account): | ||
508 | 47 | return [] | ||
509 | 48 | |||
510 | 49 | def queue_exists(self, account, queue): | ||
511 | 50 | return False | ||
512 | 51 | |||
513 | 52 | def delete_messages(self, account, queue, limit, marker, match_hidden): | ||
514 | 53 | return [] | ||
515 | 54 | |||
516 | 55 | def get_messages(self, account, queue, limit, marker, match_hidden): | ||
517 | 56 | return [] | ||
518 | 57 | |||
519 | 58 | def update_messages(self, account, queue, limit, marker, match_hidden, ttl, | ||
520 | 59 | hide): | ||
521 | 60 | return [] | ||
522 | 61 | |||
523 | 62 | def delete_message(self, account, queue, message_id): | ||
524 | 63 | return None | ||
525 | 64 | |||
526 | 65 | def get_message(self, account, queue, message_id): | ||
527 | 66 | return None | ||
528 | 67 | |||
529 | 68 | def put_message(self, account, queue, message_id, ttl, hide, body): | ||
530 | 69 | return True | ||
531 | 70 | |||
532 | 71 | def update_message(self, account, queue, message_id, ttl, hide): | ||
533 | 72 | return None | ||
534 | 73 | |||
535 | 74 | def clean(self): | ||
536 | 75 | '''This method should remove all messages with an expired | ||
537 | 76 | TTL and make hidden messages that have an expired hide time | ||
538 | 77 | visible again.''' | ||
539 | 78 | pass | ||
540 | 79 | |||
541 | 80 | def notify(self, account, queue): | ||
542 | 81 | queue = '%s/%s' % (account, queue) | ||
543 | 82 | if queue in self.queues: | ||
544 | 83 | self.queues[queue].put(0) | ||
545 | 84 | |||
546 | 85 | def wait(self, account, queue, seconds): | ||
547 | 86 | queue = '%s/%s' % (account, queue) | ||
548 | 87 | if queue not in self.queues: | ||
549 | 88 | self.queues[queue] = eventlet.Queue() | ||
550 | 89 | try: | ||
551 | 90 | self.queues[queue].get(timeout=seconds) | ||
552 | 91 | except Exception: | ||
553 | 92 | pass | ||
554 | 93 | if self.queues[queue].getting() == 0: | ||
555 | 94 | del self.queues[queue] | ||
556 | 0 | 95 | ||
557 | === added file 'burrowd/backend/memory.py' | |||
558 | --- burrowd/backend/memory.py 1970-01-01 00:00:00 +0000 | |||
559 | +++ burrowd/backend/memory.py 2011-03-17 23:47:24 +0000 | |||
560 | @@ -0,0 +1,176 @@ | |||
561 | 1 | # Copyright (C) 2011 OpenStack LLC. | ||
562 | 2 | # | ||
563 | 3 | # Licensed under the Apache License, Version 2.0 (the "License"); | ||
564 | 4 | # you may not use this file except in compliance with the License. | ||
565 | 5 | # You may obtain a copy of the License at | ||
566 | 6 | # | ||
567 | 7 | # http://www.apache.org/licenses/LICENSE-2.0 | ||
568 | 8 | # | ||
569 | 9 | # Unless required by applicable law or agreed to in writing, software | ||
570 | 10 | # distributed under the License is distributed on an "AS IS" BASIS, | ||
571 | 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
572 | 12 | # See the License for the specific language governing permissions and | ||
573 | 13 | # limitations under the License. | ||
574 | 14 | |||
575 | 15 | '''Memory backend for the burrow server.''' | ||
576 | 16 | |||
577 | 17 | import time | ||
578 | 18 | |||
579 | 19 | import burrowd.backend | ||
580 | 20 | |||
581 | 21 | |||
582 | 22 | class Backend(burrowd.backend.Backend): | ||
583 | 23 | |||
584 | 24 | def __init__(self, config): | ||
585 | 25 | super(Backend, self).__init__(config) | ||
586 | 26 | self.accounts = {} | ||
587 | 27 | |||
588 | 28 | def delete_accounts(self): | ||
589 | 29 | self.accounts.clear() | ||
590 | 30 | |||
591 | 31 | def get_accounts(self): | ||
592 | 32 | return self.accounts.keys() | ||
593 | 33 | |||
594 | 34 | def delete_account(self, account): | ||
595 | 35 | del self.accounts[account] | ||
596 | 36 | |||
597 | 37 | def get_queues(self, account): | ||
598 | 38 | if account not in self.accounts: | ||
599 | 39 | return [] | ||
600 | 40 | return self.accounts[account].keys() | ||
601 | 41 | |||
602 | 42 | def queue_exists(self, account, queue): | ||
603 | 43 | return account in self.accounts and queue in self.accounts[account] | ||
604 | 44 | |||
605 | 45 | def delete_messages(self, account, queue, limit, marker, match_hidden): | ||
606 | 46 | messages = self._scan_queue(account, queue, limit, marker, | ||
607 | 47 | match_hidden, delete=True) | ||
608 | 48 | if len(self.accounts[account][queue]) == 0: | ||
609 | 49 | del self.accounts[account][queue] | ||
610 | 50 | if len(self.accounts[account]) == 0: | ||
611 | 51 | del self.accounts[account] | ||
612 | 52 | return messages | ||
613 | 53 | |||
614 | 54 | def get_messages(self, account, queue, limit, marker, match_hidden): | ||
615 | 55 | return self._scan_queue(account, queue, limit, marker, match_hidden) | ||
616 | 56 | |||
617 | 57 | def update_messages(self, account, queue, limit, marker, match_hidden, ttl, | ||
618 | 58 | hide): | ||
619 | 59 | return self._scan_queue(account, queue, limit, marker, match_hidden, | ||
620 | 60 | ttl=ttl, hide=hide) | ||
621 | 61 | |||
622 | 62 | def delete_message(self, account, queue, message_id): | ||
623 | 63 | for index in range(0, len(self.accounts[account][queue])): | ||
624 | 64 | message = self.accounts[account][queue][index] | ||
625 | 65 | if message['id'] == message_id: | ||
626 | 66 | del self.accounts[account][queue][index] | ||
627 | 67 | if len(self.accounts[account][queue]) == 0: | ||
628 | 68 | del self.accounts[account][queue] | ||
629 | 69 | if len(self.accounts[account]) == 0: | ||
630 | 70 | del self.accounts[account] | ||
631 | 71 | return message | ||
632 | 72 | return None | ||
633 | 73 | |||
634 | 74 | def get_message(self, account, queue, message_id): | ||
635 | 75 | for index in range(0, len(self.accounts[account][queue])): | ||
636 | 76 | message = self.accounts[account][queue][index] | ||
637 | 77 | if message['id'] == message_id: | ||
638 | 78 | return message | ||
639 | 79 | return None | ||
640 | 80 | |||
641 | 81 | def put_message(self, account, queue, message_id, ttl, hide, body): | ||
642 | 82 | if account not in self.accounts: | ||
643 | 83 | self.accounts[account] = {} | ||
644 | 84 | if queue not in self.accounts[account]: | ||
645 | 85 | self.accounts[account][queue] = [] | ||
646 | 86 | for index in range(0, len(self.accounts[account][queue])): | ||
647 | 87 | message = self.accounts[account][queue][index] | ||
648 | 88 | if message['id'] == message_id: | ||
649 | 89 | message['ttl'] = ttl | ||
650 | 90 | message['hide'] = hide | ||
651 | 91 | message['body'] = body | ||
652 | 92 | if hide == 0: | ||
653 | 93 | self.notify(account, queue) | ||
654 | 94 | return False | ||
655 | 95 | message = dict(id=message_id, ttl=ttl, hide=hide, body=body) | ||
656 | 96 | self.accounts[account][queue].append(message) | ||
657 | 97 | self.notify(account, queue) | ||
658 | 98 | return True | ||
659 | 99 | |||
660 | 100 | def update_message(self, account, queue, message_id, ttl, hide): | ||
661 | 101 | for index in range(0, len(self.accounts[account][queue])): | ||
662 | 102 | message = self.accounts[account][queue][index] | ||
663 | 103 | if message['id'] == message_id: | ||
664 | 104 | if ttl is not None: | ||
665 | 105 | message['ttl'] = ttl | ||
666 | 106 | if hide is not None: | ||
667 | 107 | message['hide'] = hide | ||
668 | 108 | if hide == 0: | ||
669 | 109 | self.notify(account, queue) | ||
670 | 110 | return message | ||
671 | 111 | return None | ||
672 | 112 | |||
673 | 113 | def clean(self): | ||
674 | 114 | now = int(time.time()) | ||
675 | 115 | for account in self.accounts.keys(): | ||
676 | 116 | for queue in self.accounts[account].keys(): | ||
677 | 117 | index = 0 | ||
678 | 118 | notify = False | ||
679 | 119 | total = len(self.accounts[account][queue]) | ||
680 | 120 | while index < total: | ||
681 | 121 | message = self.accounts[account][queue][index] | ||
682 | 122 | if 0 < message['ttl'] <= now: | ||
683 | 123 | del self.accounts[account][queue][index] | ||
684 | 124 | total -= 1 | ||
685 | 125 | else: | ||
686 | 126 | if 0 < message['hide'] <= now: | ||
687 | 127 | message['hide'] = 0 | ||
688 | 128 | notify = True | ||
689 | 129 | index += 1 | ||
690 | 130 | if notify: | ||
691 | 131 | self.notify(account, queue) | ||
692 | 132 | if len(self.accounts[account][queue]) == 0: | ||
693 | 133 | del self.accounts[account][queue] | ||
694 | 134 | if len(self.accounts[account]) == 0: | ||
695 | 135 | del self.accounts[account] | ||
696 | 136 | |||
697 | 137 | def _scan_queue(self, account, queue, limit, marker, match_hidden, | ||
698 | 138 | ttl=None, hide=None, delete=False): | ||
699 | 139 | index = 0 | ||
700 | 140 | notify = False | ||
701 | 141 | if marker is not None: | ||
702 | 142 | found = False | ||
703 | 143 | for index in range(0, len(self.accounts[account][queue])): | ||
704 | 144 | message = self.accounts[account][queue][index] | ||
705 | 145 | if message['id'] == marker: | ||
706 | 146 | index += 1 | ||
707 | 147 | found = True | ||
708 | 148 | break | ||
709 | 149 | if not found: | ||
710 | 150 | index = 0 | ||
711 | 151 | messages = [] | ||
712 | 152 | total = len(self.accounts[account][queue]) | ||
713 | 153 | while index < total: | ||
714 | 154 | message = self.accounts[account][queue][index] | ||
715 | 155 | if not match_hidden and message['hide'] != 0: | ||
716 | 156 | index += 1 | ||
717 | 157 | continue | ||
718 | 158 | if ttl is not None: | ||
719 | 159 | message['ttl'] = ttl | ||
720 | 160 | if hide is not None: | ||
721 | 161 | message['hide'] = hide | ||
722 | 162 | if hide == 0: | ||
723 | 163 | notify = True | ||
724 | 164 | if delete: | ||
725 | 165 | del self.accounts[account][queue][index] | ||
726 | 166 | total -= 1 | ||
727 | 167 | else: | ||
728 | 168 | index += 1 | ||
729 | 169 | messages.append(message) | ||
730 | 170 | if limit: | ||
731 | 171 | limit -= 1 | ||
732 | 172 | if limit == 0: | ||
733 | 173 | break | ||
734 | 174 | if notify: | ||
735 | 175 | self.notify(account, queue) | ||
736 | 176 | return messages | ||
737 | 0 | 177 | ||
738 | === added file 'burrowd/backend/sqlite.py' | |||
739 | --- burrowd/backend/sqlite.py 1970-01-01 00:00:00 +0000 | |||
740 | +++ burrowd/backend/sqlite.py 2011-03-17 23:47:24 +0000 | |||
741 | @@ -0,0 +1,250 @@ | |||
742 | 1 | # Copyright (C) 2011 OpenStack LLC. | ||
743 | 2 | # | ||
744 | 3 | # Licensed under the Apache License, Version 2.0 (the "License"); | ||
745 | 4 | # you may not use this file except in compliance with the License. | ||
746 | 5 | # You may obtain a copy of the License at | ||
747 | 6 | # | ||
748 | 7 | # http://www.apache.org/licenses/LICENSE-2.0 | ||
749 | 8 | # | ||
750 | 9 | # Unless required by applicable law or agreed to in writing, software | ||
751 | 10 | # distributed under the License is distributed on an "AS IS" BASIS, | ||
752 | 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
753 | 12 | # See the License for the specific language governing permissions and | ||
754 | 13 | # limitations under the License. | ||
755 | 14 | |||
756 | 15 | '''Memory backend for the burrow server.''' | ||
757 | 16 | |||
758 | 17 | import sqlite3 | ||
759 | 18 | import time | ||
760 | 19 | |||
761 | 20 | import burrowd.backend | ||
762 | 21 | |||
763 | 22 | # Default configuration values for this module. | ||
764 | 23 | DEFAULT_DATABASE = ':memory:' | ||
765 | 24 | |||
766 | 25 | |||
767 | 26 | class Backend(burrowd.backend.Backend): | ||
768 | 27 | |||
769 | 28 | def __init__(self, config): | ||
770 | 29 | super(Backend, self).__init__(config) | ||
771 | 30 | database = self.config.get('database', DEFAULT_DATABASE) | ||
772 | 31 | self.db = sqlite3.connect(database) | ||
773 | 32 | queries = [ | ||
774 | 33 | 'CREATE TABLE queues (' | ||
775 | 34 | 'account VARCHAR(255) NOT NULL,' | ||
776 | 35 | 'queue VARCHAR(255) NOT NULL,' | ||
777 | 36 | 'PRIMARY KEY (account, queue))', | ||
778 | 37 | 'CREATE TABLE messages (' | ||
779 | 38 | 'queue INT UNSIGNED NOT NULL,' | ||
780 | 39 | 'name VARCHAR(255) NOT NULL,' | ||
781 | 40 | 'ttl INT UNSIGNED NOT NULL,' | ||
782 | 41 | 'hide INT UNSIGNED NOT NULL,' | ||
783 | 42 | 'body BLOB NOT NULL,' | ||
784 | 43 | 'PRIMARY KEY (queue, name))'] | ||
785 | 44 | for query in queries: | ||
786 | 45 | self.db.execute(query) | ||
787 | 46 | |||
788 | 47 | def delete_accounts(self): | ||
789 | 48 | self.db.execute("DELETE FROM queues") | ||
790 | 49 | self.db.execute("DELETE FROM messages") | ||
791 | 50 | |||
792 | 51 | def get_accounts(self): | ||
793 | 52 | result = self.db.execute("SELECT account FROM queues").fetchall() | ||
794 | 53 | return [row[0] for row in result] | ||
795 | 54 | |||
796 | 55 | def delete_account(self, account): | ||
797 | 56 | query = "SELECT rowid FROM queues WHERE account='%s'" % account | ||
798 | 57 | result = self.db.execute(query).fetchall() | ||
799 | 58 | if len(result) == 0: | ||
800 | 59 | return | ||
801 | 60 | queues = [str(queue[0]) for queue in result] | ||
802 | 61 | query = "DELETE FROM messages WHERE queue IN (%s)" % (','.join(queues)) | ||
803 | 62 | self.db.execute(query) | ||
804 | 63 | self.db.execute("DELETE FROM queues WHERE account='%s'" % account) | ||
805 | 64 | |||
806 | 65 | def get_queues(self, account): | ||
807 | 66 | query = "SELECT queue FROM queues WHERE account='%s'" % account | ||
808 | 67 | result = self.db.execute(query).fetchall() | ||
809 | 68 | return [row[0] for row in result] | ||
810 | 69 | |||
811 | 70 | def queue_exists(self, account, queue): | ||
812 | 71 | query = "SELECT COUNT(*) FROM queues " \ | ||
813 | 72 | "WHERE account='%s' AND queue='%s'" % \ | ||
814 | 73 | (account, queue) | ||
815 | 74 | result = self.db.execute(query).fetchall() | ||
816 | 75 | if len(result) == 0: | ||
817 | 76 | return False | ||
818 | 77 | self.rowid = result[0][0] | ||
819 | 78 | return True | ||
820 | 79 | |||
821 | 80 | def delete_messages(self, account, queue, limit, marker, match_hidden): | ||
822 | 81 | messages = self.get_messages(account, queue, limit, marker, | ||
823 | 82 | match_hidden) | ||
824 | 83 | ids = [message['id'] for message in messages] | ||
825 | 84 | query = "DELETE FROM messages WHERE queue=%d AND name IN (%s)" % \ | ||
826 | 85 | (self.rowid, ','.join(ids)) | ||
827 | 86 | self.db.execute(query) | ||
828 | 87 | query = "SELECT rowid FROM messages WHERE queue=%d LIMIT 1" % \ | ||
829 | 88 | self.rowid | ||
830 | 89 | if len(self.db.execute(query).fetchall()) == 0: | ||
831 | 90 | query = "DELETE FROM queues WHERE rowid=%d" % self.rowid | ||
832 | 91 | self.db.execute(query) | ||
833 | 92 | return messages | ||
834 | 93 | |||
835 | 94 | def get_messages(self, account, queue, limit, marker, match_hidden): | ||
836 | 95 | if marker is not None: | ||
837 | 96 | query = "SELECT rowid FROM messages " \ | ||
838 | 97 | "WHERE queue=%d AND name='%s'" % \ | ||
839 | 98 | (self.rowid, marker) | ||
840 | 99 | result = self.db.execute(query).fetchall() | ||
841 | 100 | if len(result) == 0: | ||
842 | 101 | marker = None | ||
843 | 102 | else: | ||
844 | 103 | marker = result[0][0] | ||
845 | 104 | query = "SELECT name,ttl,hide,body FROM messages WHERE queue=%d" % \ | ||
846 | 105 | self.rowid | ||
847 | 106 | if match_hidden is False: | ||
848 | 107 | query += " AND hide == 0" | ||
849 | 108 | if marker is not None: | ||
850 | 109 | query += " AND rowid > %d" % marker | ||
851 | 110 | if limit is not None: | ||
852 | 111 | query += " LIMIT %d" % limit | ||
853 | 112 | result = self.db.execute(query).fetchall() | ||
854 | 113 | messages = [] | ||
855 | 114 | for row in result: | ||
856 | 115 | messages.append(dict(id=row[0], ttl=row[1], hide=row[2], | ||
857 | 116 | body=row[3])) | ||
858 | 117 | return messages | ||
859 | 118 | |||
860 | 119 | def update_messages(self, account, queue, limit, marker, match_hidden, ttl, | ||
861 | 120 | hide): | ||
862 | 121 | messages = self.get_messages(account, queue, limit, marker, | ||
863 | 122 | match_hidden) | ||
864 | 123 | query = "UPDATE messages SET" | ||
865 | 124 | comma = '' | ||
866 | 125 | if ttl is not None: | ||
867 | 126 | query += "%s ttl=%d" % (comma, ttl) | ||
868 | 127 | comma = ',' | ||
869 | 128 | if hide is not None: | ||
870 | 129 | query += "%s hide=%d" % (comma, hide) | ||
871 | 130 | comma = ',' | ||
872 | 131 | if comma == '': | ||
873 | 132 | return (False, message) | ||
874 | 133 | ids = [] | ||
875 | 134 | for message in messages: | ||
876 | 135 | ids.append(message['id']) | ||
877 | 136 | if ttl is not None: | ||
878 | 137 | message['ttl'] = ttl | ||
879 | 138 | if hide is not None: | ||
880 | 139 | message['hide'] = hide | ||
881 | 140 | query += " WHERE queue=%d AND name IN (%s)" % \ | ||
882 | 141 | (self.rowid, ','.join(ids)) | ||
883 | 142 | self.db.execute(query) | ||
884 | 143 | self.notify(account, queue) | ||
885 | 144 | return messages | ||
886 | 145 | |||
887 | 146 | def delete_message(self, account, queue, message_id): | ||
888 | 147 | message = self.get_message(account, queue, message_id) | ||
889 | 148 | if message is None: | ||
890 | 149 | return None | ||
891 | 150 | query = "DELETE FROM messages WHERE queue=%d AND name='%s'" % \ | ||
892 | 151 | (self.rowid, message_id) | ||
893 | 152 | self.db.execute(query) | ||
894 | 153 | query = "SELECT rowid FROM messages WHERE queue=%d LIMIT 1" % \ | ||
895 | 154 | self.rowid | ||
896 | 155 | if len(self.db.execute(query).fetchall()) == 0: | ||
897 | 156 | query = "DELETE FROM queues WHERE rowid=%d" % self.rowid | ||
898 | 157 | self.db.execute(query) | ||
899 | 158 | return message | ||
900 | 159 | |||
901 | 160 | def get_message(self, account, queue, message_id): | ||
902 | 161 | query = "SELECT name,ttl,hide,body FROM messages " \ | ||
903 | 162 | "WHERE queue=%d AND name='%s'" % (self.rowid, message_id) | ||
904 | 163 | result = self.db.execute(query).fetchall() | ||
905 | 164 | if len(result) == 0: | ||
906 | 165 | return None | ||
907 | 166 | row = result[0] | ||
908 | 167 | return dict(id=row[0], ttl=row[1], hide=row[2], body=row[3]) | ||
909 | 168 | |||
910 | 169 | def put_message(self, account, queue, message_id, ttl, hide, body): | ||
911 | 170 | query = "SELECT rowid FROM queues " \ | ||
912 | 171 | "WHERE account='%s' AND queue='%s'" % (account, queue) | ||
913 | 172 | result = self.db.execute(query).fetchall() | ||
914 | 173 | if len(result) == 0: | ||
915 | 174 | query = "INSERT INTO queues VALUES ('%s', '%s')" % (account, queue) | ||
916 | 175 | rowid = self.db.execute(query).lastrowid | ||
917 | 176 | else: | ||
918 | 177 | rowid = result[0][0] | ||
919 | 178 | query = "SELECT rowid FROM messages WHERE queue=%d AND name='%s'" % \ | ||
920 | 179 | (rowid, message_id) | ||
921 | 180 | result = self.db.execute(query).fetchall() | ||
922 | 181 | if len(result) == 0: | ||
923 | 182 | query = "INSERT INTO messages VALUES (%d, '%s', %d, %d, '%s')" % \ | ||
924 | 183 | (rowid, message_id, ttl, hide, body) | ||
925 | 184 | self.db.execute(query) | ||
926 | 185 | self.notify(account, queue) | ||
927 | 186 | return True | ||
928 | 187 | query = "UPDATE messages SET ttl=%d, hide=%d, body='%s'" \ | ||
929 | 188 | "WHERE rowid=%d" % (ttl, hide, body, result[0][0]) | ||
930 | 189 | self.db.execute(query) | ||
931 | 190 | if hide == 0: | ||
932 | 191 | self.notify(account, queue) | ||
933 | 192 | return False | ||
934 | 193 | |||
935 | 194 | def update_message(self, account, queue, message_id, ttl, hide): | ||
936 | 195 | message = self.get_message(account, queue, message_id) | ||
937 | 196 | if message is None: | ||
938 | 197 | return None | ||
939 | 198 | query = "UPDATE messages SET" | ||
940 | 199 | comma = '' | ||
941 | 200 | if ttl is not None: | ||
942 | 201 | query += "%s ttl=%d" % (comma, ttl) | ||
943 | 202 | comma = ',' | ||
944 | 203 | if hide is not None: | ||
945 | 204 | query += "%s hide=%d" % (comma, hide) | ||
946 | 205 | comma = ',' | ||
947 | 206 | if comma == '': | ||
948 | 207 | return message | ||
949 | 208 | query += " WHERE queue=%d AND name='%s'" % (self.rowid, message_id) | ||
950 | 209 | self.db.execute(query) | ||
951 | 210 | if hide == 0: | ||
952 | 211 | self.notify(account, queue) | ||
953 | 212 | return message | ||
954 | 213 | |||
955 | 214 | def clean(self): | ||
956 | 215 | now = int(time.time()) | ||
957 | 216 | query = "SELECT rowid,queue FROM messages " \ | ||
958 | 217 | "WHERE ttl > 0 AND ttl <= %d" % now | ||
959 | 218 | result = self.db.execute(query).fetchall() | ||
960 | 219 | if len(result) > 0: | ||
961 | 220 | messages = [] | ||
962 | 221 | queues = [] | ||
963 | 222 | for row in result: | ||
964 | 223 | messages.append(str(row[0])) | ||
965 | 224 | queues.append(row[1]) | ||
966 | 225 | query = 'DELETE FROM messages WHERE rowid in (%s)' % \ | ||
967 | 226 | ','.join(messages) | ||
968 | 227 | self.db.execute(query) | ||
969 | 228 | for queue in queues: | ||
970 | 229 | query = "SELECT rowid FROM messages WHERE queue=%d LIMIT 1" % \ | ||
971 | 230 | queue | ||
972 | 231 | if len(self.db.execute(query).fetchall()) == 0: | ||
973 | 232 | query = "DELETE FROM queues WHERE rowid=%d" % queue | ||
974 | 233 | self.db.execute(query) | ||
975 | 234 | query = "SELECT rowid,queue FROM messages WHERE " \ | ||
976 | 235 | "hide > 0 AND hide <= %d" % now | ||
977 | 236 | result = self.db.execute(query).fetchall() | ||
978 | 237 | if len(result) > 0: | ||
979 | 238 | messages = [] | ||
980 | 239 | queues = [] | ||
981 | 240 | for row in result: | ||
982 | 241 | messages.append(str(row[0])) | ||
983 | 242 | queues.append(row[1]) | ||
984 | 243 | query = 'UPDATE messages SET hide=0 WHERE rowid in (%s)' % \ | ||
985 | 244 | ','.join(messages) | ||
986 | 245 | self.db.execute(query) | ||
987 | 246 | for queue in queues: | ||
988 | 247 | query = "SELECT account,queue FROM queues WHERE rowid=%d" % \ | ||
989 | 248 | queue | ||
990 | 249 | result = self.db.execute(query).fetchall()[0] | ||
991 | 250 | self.notify(result[0], result[1]) | ||
992 | 0 | 251 | ||
993 | === added file 'burrowd/config.py' | |||
994 | --- burrowd/config.py 1970-01-01 00:00:00 +0000 | |||
995 | +++ burrowd/config.py 2011-03-17 23:47:24 +0000 | |||
996 | @@ -0,0 +1,55 @@ | |||
997 | 1 | # Copyright (C) 2011 OpenStack LLC. | ||
998 | 2 | # | ||
999 | 3 | # Licensed under the Apache License, Version 2.0 (the "License"); | ||
1000 | 4 | # you may not use this file except in compliance with the License. | ||
1001 | 5 | # You may obtain a copy of the License at | ||
1002 | 6 | # | ||
1003 | 7 | # http://www.apache.org/licenses/LICENSE-2.0 | ||
1004 | 8 | # | ||
1005 | 9 | # Unless required by applicable law or agreed to in writing, software | ||
1006 | 10 | # distributed under the License is distributed on an "AS IS" BASIS, | ||
1007 | 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
1008 | 12 | # See the License for the specific language governing permissions and | ||
1009 | 13 | # limitations under the License. | ||
1010 | 14 | |||
1011 | 15 | '''Configuration module for the burrow server.''' | ||
1012 | 16 | |||
1013 | 17 | import ConfigParser | ||
1014 | 18 | |||
1015 | 19 | |||
1016 | 20 | class Config(object): | ||
1017 | 21 | '''Configuration class that wraps the ConfigParser get* | ||
1018 | 22 | methods. These wrappers automatically check for options in | ||
1019 | 23 | a specific instance section first before the regular section | ||
1020 | 24 | (section:instance and then section). They will also return a | ||
1021 | 25 | default value if given instead of throwing an exception.''' | ||
1022 | 26 | |||
1023 | 27 | def __init__(self, config, section, instance=None): | ||
1024 | 28 | self.config = config | ||
1025 | 29 | self.section = section | ||
1026 | 30 | if instance is None: | ||
1027 | 31 | self.instance = None | ||
1028 | 32 | else: | ||
1029 | 33 | self.instance = '%s:%s' % (section, instance) | ||
1030 | 34 | |||
1031 | 35 | def get(self, option, default=None): | ||
1032 | 36 | return self._get(self.config.get, option, default) | ||
1033 | 37 | |||
1034 | 38 | def getboolean(self, option, default=None): | ||
1035 | 39 | return self._get(self.config.getboolean, option, default) | ||
1036 | 40 | |||
1037 | 41 | def getfloat(self, option, default=None): | ||
1038 | 42 | return self._get(self.config.getfloat, option, default) | ||
1039 | 43 | |||
1040 | 44 | def getint(self, option, default=None): | ||
1041 | 45 | return self._get(self.config.getint, option, default) | ||
1042 | 46 | |||
1043 | 47 | def _get(self, method, option, default): | ||
1044 | 48 | if self.instance is not None: | ||
1045 | 49 | if self.config.has_option(self.instance, option): | ||
1046 | 50 | return method(self.instance, option) | ||
1047 | 51 | if self.config.has_option(self.section, option): | ||
1048 | 52 | return method(self.section, option) | ||
1049 | 53 | if self.config.has_option(ConfigParser.DEFAULTSECT, option): | ||
1050 | 54 | return method(ConfigParser.DEFAULTSECT, option) | ||
1051 | 55 | return default | ||
1052 | 0 | 56 | ||
1053 | === added directory 'burrowd/frontend' | |||
1054 | === added file 'burrowd/frontend/__init__.py' | |||
1055 | --- burrowd/frontend/__init__.py 1970-01-01 00:00:00 +0000 | |||
1056 | +++ burrowd/frontend/__init__.py 2011-03-17 23:47:24 +0000 | |||
1057 | @@ -0,0 +1,30 @@ | |||
1058 | 1 | # Copyright (C) 2011 OpenStack LLC. | ||
1059 | 2 | # | ||
1060 | 3 | # Licensed under the Apache License, Version 2.0 (the "License"); | ||
1061 | 4 | # you may not use this file except in compliance with the License. | ||
1062 | 5 | # You may obtain a copy of the License at | ||
1063 | 6 | # | ||
1064 | 7 | # http://www.apache.org/licenses/LICENSE-2.0 | ||
1065 | 8 | # | ||
1066 | 9 | # Unless required by applicable law or agreed to in writing, software | ||
1067 | 10 | # distributed under the License is distributed on an "AS IS" BASIS, | ||
1068 | 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
1069 | 12 | # See the License for the specific language governing permissions and | ||
1070 | 13 | # limitations under the License. | ||
1071 | 14 | |||
1072 | 15 | '''Frontends for the burrow server.''' | ||
1073 | 16 | |||
1074 | 17 | import burrowd | ||
1075 | 18 | |||
1076 | 19 | |||
1077 | 20 | class Frontend(burrowd.Module): | ||
1078 | 21 | '''Interface that frontend implementations must provide.''' | ||
1079 | 22 | |||
1080 | 23 | def __init__(self, config, backend): | ||
1081 | 24 | super(Frontend, self).__init__(config) | ||
1082 | 25 | self.backend = backend | ||
1083 | 26 | |||
1084 | 27 | def run(self, thread_pool): | ||
1085 | 28 | '''Run the frontend instance, adding any threads to the | ||
1086 | 29 | thread_pool if needed.''' | ||
1087 | 30 | pass | ||
1088 | 0 | 31 | ||
1089 | === added file 'burrowd/frontend/wsgi.py' | |||
1090 | --- burrowd/frontend/wsgi.py 1970-01-01 00:00:00 +0000 | |||
1091 | +++ burrowd/frontend/wsgi.py 2011-03-17 23:47:24 +0000 | |||
1092 | @@ -0,0 +1,276 @@ | |||
1093 | 1 | # Copyright (C) 2011 OpenStack LLC. | ||
1094 | 2 | # | ||
1095 | 3 | # Licensed under the Apache License, Version 2.0 (the "License"); | ||
1096 | 4 | # you may not use this file except in compliance with the License. | ||
1097 | 5 | # You may obtain a copy of the License at | ||
1098 | 6 | # | ||
1099 | 7 | # http://www.apache.org/licenses/LICENSE-2.0 | ||
1100 | 8 | # | ||
1101 | 9 | # Unless required by applicable law or agreed to in writing, software | ||
1102 | 10 | # distributed under the License is distributed on an "AS IS" BASIS, | ||
1103 | 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
1104 | 12 | # See the License for the specific language governing permissions and | ||
1105 | 13 | # limitations under the License. | ||
1106 | 14 | |||
1107 | 15 | '''WSGI frontend for the burrow server.''' | ||
1108 | 16 | |||
1109 | 17 | import json | ||
1110 | 18 | import time | ||
1111 | 19 | |||
1112 | 20 | import eventlet | ||
1113 | 21 | import eventlet.wsgi | ||
1114 | 22 | import routes | ||
1115 | 23 | import routes.middleware | ||
1116 | 24 | import webob.dec | ||
1117 | 25 | import webob.exc | ||
1118 | 26 | |||
1119 | 27 | import burrowd.frontend | ||
1120 | 28 | |||
1121 | 29 | # Default configuration values for this module. | ||
1122 | 30 | DEFAULT_HOST = '0.0.0.0' | ||
1123 | 31 | DEFAULT_PORT = 8080 | ||
1124 | 32 | DEFAULT_BACKLOG = 64 | ||
1125 | 33 | DEFAULT_SSL = False | ||
1126 | 34 | DEFAULT_SSL_CERTFILE = 'example.pem' | ||
1127 | 35 | DEFAULT_SSL_KEYFILE = 'example.key' | ||
1128 | 36 | DEFAULT_THREAD_POOL_SIZE = 0 | ||
1129 | 37 | DEFAULT_TTL = 600 | ||
1130 | 38 | DEFAULT_HIDE = 0 | ||
1131 | 39 | |||
1132 | 40 | |||
1133 | 41 | def queue_exists(method): | ||
1134 | 42 | '''Decorator to ensure an account and queue exists. If the wait | ||
1135 | 43 | option is given, this will block until a message in the queue is | ||
1136 | 44 | ready or the timeout expires.''' | ||
1137 | 45 | def wrapper(self, req, account, queue, *args, **kwargs): | ||
1138 | 46 | wait = 0 | ||
1139 | 47 | if 'wait' in req.params: | ||
1140 | 48 | wait = int(req.params['wait']) | ||
1141 | 49 | if wait > 0: | ||
1142 | 50 | wait += time.time() | ||
1143 | 51 | res = webob.exc.HTTPNotFound() | ||
1144 | 52 | while True: | ||
1145 | 53 | if self.backend.queue_exists(account, queue): | ||
1146 | 54 | res = method(self, req, account, queue, *args, **kwargs) | ||
1147 | 55 | if wait == 0 or res.status_int != 404: | ||
1148 | 56 | break | ||
1149 | 57 | now = time.time() | ||
1150 | 58 | if wait - now > 0: | ||
1151 | 59 | self.backend.wait(account, queue, wait - now) | ||
1152 | 60 | if wait < time.time(): | ||
1153 | 61 | break | ||
1154 | 62 | return res | ||
1155 | 63 | return wrapper | ||
1156 | 64 | |||
1157 | 65 | |||
1158 | 66 | class Frontend(burrowd.frontend.Frontend): | ||
1159 | 67 | |||
1160 | 68 | def __init__(self, config, backend): | ||
1161 | 69 | super(Frontend, self).__init__(config, backend) | ||
1162 | 70 | self.default_ttl = self.config.get('default_ttl', DEFAULT_TTL) | ||
1163 | 71 | self.default_hide = self.config.get('default_hide', DEFAULT_HIDE) | ||
1164 | 72 | mapper = routes.Mapper() | ||
1165 | 73 | mapper.connect('/', action='root') | ||
1166 | 74 | mapper.connect('/{account}', action='account') | ||
1167 | 75 | mapper.connect('/{account}/{queue}', action='queue') | ||
1168 | 76 | mapper.connect('/{account}/{queue}/{message_id}', action='message') | ||
1169 | 77 | self._routes = routes.middleware.RoutesMiddleware(self._route, mapper) | ||
1170 | 78 | |||
1171 | 79 | def run(self, thread_pool): | ||
1172 | 80 | '''Create the listening socket and start the thread that runs | ||
1173 | 81 | the WSGI server. This extra thread is needed since the WSGI | ||
1174 | 82 | server function blocks.''' | ||
1175 | 83 | host = self.config.get('host', DEFAULT_HOST) | ||
1176 | 84 | port = self.config.getint('port', DEFAULT_PORT) | ||
1177 | 85 | backlog = self.config.getint('backlog', DEFAULT_BACKLOG) | ||
1178 | 86 | socket = eventlet.listen((host, port), backlog=backlog) | ||
1179 | 87 | self.log.info(_('Listening on %s:%d') % (host, port)) | ||
1180 | 88 | if self.config.getboolean('ssl', DEFAULT_SSL): | ||
1181 | 89 | certfile = self.config.get('ssl_certfile', DEFAULT_SSL_CERTFILE) | ||
1182 | 90 | keyfile = self.config.get('ssl_keyfile', DEFAULT_SSL_KEYFILE) | ||
1183 | 91 | socket = eventlet.green.ssl.wrap_socket(socket, certfile=certfile, | ||
1184 | 92 | keyfile=keyfile) | ||
1185 | 93 | thread_pool.spawn_n(self._run, socket, thread_pool) | ||
1186 | 94 | |||
1187 | 95 | def _run(self, socket, thread_pool): | ||
1188 | 96 | '''Thread to run the WSGI server.''' | ||
1189 | 97 | thread_pool_size = self.config.getint('thread_pool_size', | ||
1190 | 98 | DEFAULT_THREAD_POOL_SIZE) | ||
1191 | 99 | log_format = '%(client_ip)s "%(request_line)s" %(status_code)s ' \ | ||
1192 | 100 | '%(body_length)s %(wall_seconds).6f' | ||
1193 | 101 | if thread_pool_size == 0: | ||
1194 | 102 | eventlet.wsgi.server(socket, self, log=WSGILog(self.log), | ||
1195 | 103 | log_format=log_format, custom_pool=thread_pool) | ||
1196 | 104 | else: | ||
1197 | 105 | eventlet.wsgi.server(socket, self, log=WSGILog(self.log), | ||
1198 | 106 | log_format=log_format, max_size=thread_pool_size) | ||
1199 | 107 | |||
1200 | 108 | def __call__(self, *args, **kwargs): | ||
1201 | 109 | return self._routes(*args, **kwargs) | ||
1202 | 110 | |||
1203 | 111 | @webob.dec.wsgify | ||
1204 | 112 | def _route(self, req): | ||
1205 | 113 | args = req.environ['wsgiorg.routing_args'][1] | ||
1206 | 114 | if not args: | ||
1207 | 115 | return webob.exc.HTTPNotFound() | ||
1208 | 116 | action = args.pop('action') | ||
1209 | 117 | method = getattr(self, '_%s_%s' % (req.method.lower(), action), False) | ||
1210 | 118 | if not method: | ||
1211 | 119 | return webob.exc.HTTPBadRequest() | ||
1212 | 120 | return method(req, **args) | ||
1213 | 121 | |||
1214 | 122 | @webob.dec.wsgify | ||
1215 | 123 | def _delete_root(self, req): | ||
1216 | 124 | self.backend.delete_accounts() | ||
1217 | 125 | return webob.exc.HTTPNoContent() | ||
1218 | 126 | |||
1219 | 127 | @webob.dec.wsgify | ||
1220 | 128 | def _get_root(self, req): | ||
1221 | 129 | accounts = self.backend.get_accounts() | ||
1222 | 130 | if len(accounts) == 0: | ||
1223 | 131 | return webob.exc.HTTPNotFound() | ||
1224 | 132 | return webob.exc.HTTPOk(body=json.dumps(accounts, indent=2)) | ||
1225 | 133 | |||
1226 | 134 | @webob.dec.wsgify | ||
1227 | 135 | def _delete_account(self, req, account): | ||
1228 | 136 | self.backend.delete_account(account) | ||
1229 | 137 | return webob.exc.HTTPNoContent() | ||
1230 | 138 | |||
1231 | 139 | @webob.dec.wsgify | ||
1232 | 140 | def _get_account(self, req, account): | ||
1233 | 141 | queues = self.backend.get_queues(account) | ||
1234 | 142 | if len(queues) == 0: | ||
1235 | 143 | return webob.exc.HTTPNotFound() | ||
1236 | 144 | return webob.exc.HTTPOk(body=json.dumps(queues, indent=2)) | ||
1237 | 145 | |||
1238 | 146 | @webob.dec.wsgify | ||
1239 | 147 | @queue_exists | ||
1240 | 148 | def _delete_queue(self, req, account, queue): | ||
1241 | 149 | limit, marker, match_hidden = self._parse_filters(req) | ||
1242 | 150 | messages = self.backend.delete_messages(account, queue, limit, marker, | ||
1243 | 151 | match_hidden) | ||
1244 | 152 | return self._return_messages(req, account, queue, messages, 'none') | ||
1245 | 153 | |||
1246 | 154 | @webob.dec.wsgify | ||
1247 | 155 | @queue_exists | ||
1248 | 156 | def _get_queue(self, req, account, queue): | ||
1249 | 157 | limit, marker, match_hidden = self._parse_filters(req) | ||
1250 | 158 | messages = self.backend.get_messages(account, queue, limit, marker, | ||
1251 | 159 | match_hidden) | ||
1252 | 160 | return self._return_messages(req, account, queue, messages, 'all') | ||
1253 | 161 | |||
1254 | 162 | @webob.dec.wsgify | ||
1255 | 163 | @queue_exists | ||
1256 | 164 | def _post_queue(self, req, account, queue): | ||
1257 | 165 | limit, marker, match_hidden = self._parse_filters(req) | ||
1258 | 166 | ttl, hide = self._parse_metadata(req) | ||
1259 | 167 | messages = self.backend.update_messages(account, queue, limit, marker, | ||
1260 | 168 | match_hidden, ttl, hide) | ||
1261 | 169 | return self._return_messages(req, account, queue, messages, 'all') | ||
1262 | 170 | |||
1263 | 171 | @webob.dec.wsgify | ||
1264 | 172 | @queue_exists | ||
1265 | 173 | def _delete_message(self, req, account, queue, message_id): | ||
1266 | 174 | message = self.backend.delete_message(account, queue, message_id) | ||
1267 | 175 | if message is None: | ||
1268 | 176 | return webob.exc.HTTPNotFound() | ||
1269 | 177 | return self._return_message(req, account, queue, message, 'none') | ||
1270 | 178 | |||
1271 | 179 | @webob.dec.wsgify | ||
1272 | 180 | @queue_exists | ||
1273 | 181 | def _get_message(self, req, account, queue, message_id): | ||
1274 | 182 | message = self.backend.get_message(account, queue, message_id) | ||
1275 | 183 | if message is None: | ||
1276 | 184 | return webob.exc.HTTPNotFound() | ||
1277 | 185 | return self._return_message(req, account, queue, message, 'all') | ||
1278 | 186 | |||
1279 | 187 | @webob.dec.wsgify | ||
1280 | 188 | @queue_exists | ||
1281 | 189 | def _post_message(self, req, account, queue, message_id): | ||
1282 | 190 | ttl, hide = self._parse_metadata(req) | ||
1283 | 191 | message = self.backend.update_message(account, queue, message_id, ttl, | ||
1284 | 192 | hide) | ||
1285 | 193 | if message is None: | ||
1286 | 194 | return webob.exc.HTTPNotFound() | ||
1287 | 195 | return self._return_message(req, account, queue, message, 'id') | ||
1288 | 196 | |||
1289 | 197 | @webob.dec.wsgify | ||
1290 | 198 | def _put_message(self, req, account, queue, message_id): | ||
1291 | 199 | (ttl, hide) = self._parse_metadata(req, self.default_ttl, | ||
1292 | 200 | self.default_hide) | ||
1293 | 201 | if self.backend.put_message(account, queue, message_id, ttl, hide, \ | ||
1294 | 202 | req.body): | ||
1295 | 203 | return webob.exc.HTTPCreated() | ||
1296 | 204 | return webob.exc.HTTPNoContent() | ||
1297 | 205 | |||
1298 | 206 | def _filter_message(self, detail, message): | ||
1299 | 207 | if detail == 'id': | ||
1300 | 208 | return dict(id=message['id']) | ||
1301 | 209 | elif detail == 'metadata': | ||
1302 | 210 | message = message.copy() | ||
1303 | 211 | del message['body'] | ||
1304 | 212 | return message | ||
1305 | 213 | elif detail == 'all': | ||
1306 | 214 | return message | ||
1307 | 215 | return None | ||
1308 | 216 | |||
1309 | 217 | def _return_message(self, req, account, queue, message, detail): | ||
1310 | 218 | if 'detail' in req.params: | ||
1311 | 219 | detail = req.params['detail'] | ||
1312 | 220 | message = self._filter_message(detail, message) | ||
1313 | 221 | if message is not None: | ||
1314 | 222 | body = {account: {queue: [message]}} | ||
1315 | 223 | return webob.exc.HTTPOk(body=json.dumps(body, indent=2)) | ||
1316 | 224 | return webob.exc.HTTPNoContent() | ||
1317 | 225 | |||
1318 | 226 | def _return_messages(self, req, account, queue, messages, detail): | ||
1319 | 227 | if len(messages) == 0: | ||
1320 | 228 | return webob.exc.HTTPNotFound() | ||
1321 | 229 | if 'detail' in req.params: | ||
1322 | 230 | detail = req.params['detail'] | ||
1323 | 231 | filtered_messages = [] | ||
1324 | 232 | for message in messages: | ||
1325 | 233 | message = self._filter_message(detail, message) | ||
1326 | 234 | if message is not None: | ||
1327 | 235 | filtered_messages.append(message) | ||
1328 | 236 | if len(filtered_messages) == 0: | ||
1329 | 237 | return webob.exc.HTTPNoContent() | ||
1330 | 238 | body = {account: {queue: filtered_messages}} | ||
1331 | 239 | return webob.exc.HTTPOk(body=json.dumps(body, indent=2)) | ||
1332 | 240 | |||
1333 | 241 | def _parse_filters(self, req): | ||
1334 | 242 | limit = None | ||
1335 | 243 | if 'limit' in req.params: | ||
1336 | 244 | limit = int(req.params['limit']) | ||
1337 | 245 | marker = None | ||
1338 | 246 | if 'marker' in req.params: | ||
1339 | 247 | marker = req.params['marker'] | ||
1340 | 248 | match_hidden = False | ||
1341 | 249 | if 'hidden' in req.params and req.params['hidden'].lower() == 'true': | ||
1342 | 250 | match_hidden = True | ||
1343 | 251 | return limit, marker, match_hidden | ||
1344 | 252 | |||
1345 | 253 | def _parse_metadata(self, req, default_ttl=None, default_hide=None): | ||
1346 | 254 | if 'ttl' in req.params: | ||
1347 | 255 | ttl = int(req.params['ttl']) | ||
1348 | 256 | else: | ||
1349 | 257 | ttl = default_ttl | ||
1350 | 258 | if ttl is not None and ttl > 0: | ||
1351 | 259 | ttl += int(time.time()) | ||
1352 | 260 | if 'hide' in req.params: | ||
1353 | 261 | hide = int(req.params['hide']) | ||
1354 | 262 | else: | ||
1355 | 263 | hide = default_hide | ||
1356 | 264 | if hide is not None and hide > 0: | ||
1357 | 265 | hide += int(time.time()) | ||
1358 | 266 | return ttl, hide | ||
1359 | 267 | |||
1360 | 268 | |||
1361 | 269 | class WSGILog(object): | ||
1362 | 270 | '''Class for eventlet.wsgi.server to forward logging messages.''' | ||
1363 | 271 | |||
1364 | 272 | def __init__(self, log): | ||
1365 | 273 | self.log = log | ||
1366 | 274 | |||
1367 | 275 | def write(self, message): | ||
1368 | 276 | self.log.debug(message.rstrip()) | ||
1369 | 0 | 277 | ||
1370 | === added directory 'etc' | |||
1371 | === added file 'etc/burrowd.conf' | |||
1372 | --- etc/burrowd.conf 1970-01-01 00:00:00 +0000 | |||
1373 | +++ etc/burrowd.conf 2011-03-17 23:47:24 +0000 | |||
1374 | @@ -0,0 +1,99 @@ | |||
1375 | 1 | [DEFAULT] | ||
1376 | 2 | |||
1377 | 3 | # Log level to use. All sections below prefixed with 'burrowd' can define | ||
1378 | 4 | # this to override this default. | ||
1379 | 5 | log_level = DEBUG | ||
1380 | 6 | |||
1381 | 7 | # Default expiration time in seconds to set for messages. | ||
1382 | 8 | default_ttl = 600 | ||
1383 | 9 | |||
1384 | 10 | # Default hide time in seconds to set for messages. | ||
1385 | 11 | default_hide = 0 | ||
1386 | 12 | |||
1387 | 13 | |||
1388 | 14 | [burrowd] | ||
1389 | 15 | |||
1390 | 16 | # Backend to use for storing messages. | ||
1391 | 17 | backend = burrowd.backend.sqlite | ||
1392 | 18 | |||
1393 | 19 | # Comma separated list of frontends to run. | ||
1394 | 20 | # frontends = burrowd.frontend.wsgi,burrowd.frontend.wsgi:ssl | ||
1395 | 21 | frontends = burrowd.frontend.wsgi | ||
1396 | 22 | |||
1397 | 23 | # Size of the thread pool to use for the server. | ||
1398 | 24 | thread_pool_size = 1000 | ||
1399 | 25 | |||
1400 | 26 | |||
1401 | 27 | [burrowd.backend.sqlite] | ||
1402 | 28 | |||
1403 | 29 | # Database file to use, passed to sqlite3.connect. | ||
1404 | 30 | database = :memory: | ||
1405 | 31 | |||
1406 | 32 | |||
1407 | 33 | [burrowd.frontend.wsgi] | ||
1408 | 34 | |||
1409 | 35 | # Host to listen on. | ||
1410 | 36 | host = 0.0.0.0 | ||
1411 | 37 | |||
1412 | 38 | # Port to listen on. | ||
1413 | 39 | port = 8080 | ||
1414 | 40 | |||
1415 | 41 | # Size of backlog for listener socket. | ||
1416 | 42 | backlog = 64 | ||
1417 | 43 | |||
1418 | 44 | # Whether to enable SSL. | ||
1419 | 45 | ssl = False | ||
1420 | 46 | |||
1421 | 47 | # If SSL is enabled, which certfile to use. | ||
1422 | 48 | ssl_certfile = example.pem | ||
1423 | 49 | |||
1424 | 50 | # If SSL is enabled, which keyfile to use. | ||
1425 | 51 | ssl_keyfile = example.key | ||
1426 | 52 | |||
1427 | 53 | # Size of thread pool for the WSGI server. If the size is 0, use the main | ||
1428 | 54 | # burrowd thread pool. | ||
1429 | 55 | thread_pool_size = 0 | ||
1430 | 56 | |||
1431 | 57 | # Default expiration time in seconds to set for messages. This overrides | ||
1432 | 58 | # the value in the DEFAULT section. | ||
1433 | 59 | # default_ttl = 600 | ||
1434 | 60 | |||
1435 | 61 | # Default hide time in seconds to set for messages. This overrides the | ||
1436 | 62 | # value in the DEFAULT section. | ||
1437 | 63 | # default_hide = 0 | ||
1438 | 64 | |||
1439 | 65 | |||
1440 | 66 | [burrowd.frontend.wsgi:ssl] | ||
1441 | 67 | |||
1442 | 68 | # Port to listen on. | ||
1443 | 69 | port = 8443 | ||
1444 | 70 | |||
1445 | 71 | # Whether to enable SSL. | ||
1446 | 72 | ssl = True | ||
1447 | 73 | |||
1448 | 74 | |||
1449 | 75 | # Logging configuration following the logging.config format. | ||
1450 | 76 | |||
1451 | 77 | [loggers] | ||
1452 | 78 | keys=root | ||
1453 | 79 | |||
1454 | 80 | [logger_root] | ||
1455 | 81 | qualname=root | ||
1456 | 82 | level=WARNING | ||
1457 | 83 | handlers=console | ||
1458 | 84 | |||
1459 | 85 | [handlers] | ||
1460 | 86 | keys=console | ||
1461 | 87 | |||
1462 | 88 | [handler_console] | ||
1463 | 89 | class=StreamHandler | ||
1464 | 90 | level=DEBUG | ||
1465 | 91 | formatter=simple | ||
1466 | 92 | args=(sys.stdout,) | ||
1467 | 93 | |||
1468 | 94 | [formatters] | ||
1469 | 95 | keys=simple | ||
1470 | 96 | |||
1471 | 97 | [formatter_simple] | ||
1472 | 98 | format=%(asctime)s - %(name)s - %(levelname)s - %(message)s | ||
1473 | 99 | datefmt= | ||
1474 | 0 | 100 | ||
1475 | === added file 'setup.py' | |||
1476 | --- setup.py 1970-01-01 00:00:00 +0000 | |||
1477 | +++ setup.py 2011-03-17 23:47:24 +0000 | |||
1478 | @@ -0,0 +1,74 @@ | |||
1479 | 1 | #!/usr/bin/python | ||
1480 | 2 | # Copyright (C) 2011 OpenStack LLC. | ||
1481 | 3 | # | ||
1482 | 4 | # Licensed under the Apache License, Version 2.0 (the "License"); | ||
1483 | 5 | # you may not use this file except in compliance with the License. | ||
1484 | 6 | # You may obtain a copy of the License at | ||
1485 | 7 | # | ||
1486 | 8 | # http://www.apache.org/licenses/LICENSE-2.0 | ||
1487 | 9 | # | ||
1488 | 10 | # Unless required by applicable law or agreed to in writing, software | ||
1489 | 11 | # distributed under the License is distributed on an "AS IS" BASIS, | ||
1490 | 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
1491 | 13 | # See the License for the specific language governing permissions and | ||
1492 | 14 | # limitations under the License. | ||
1493 | 15 | |||
1494 | 16 | from setuptools import setup, find_packages | ||
1495 | 17 | from setuptools.command.sdist import sdist | ||
1496 | 18 | import os | ||
1497 | 19 | import subprocess | ||
1498 | 20 | try: | ||
1499 | 21 | from babel.messages import frontend | ||
1500 | 22 | except ImportError: | ||
1501 | 23 | frontend = None | ||
1502 | 24 | |||
1503 | 25 | |||
1504 | 26 | class local_sdist(sdist): | ||
1505 | 27 | """Customized sdist hook - builds the ChangeLog file from VC first""" | ||
1506 | 28 | |||
1507 | 29 | def run(self): | ||
1508 | 30 | if os.path.isdir('.bzr'): | ||
1509 | 31 | # We're in a bzr branch | ||
1510 | 32 | |||
1511 | 33 | log_cmd = subprocess.Popen(["bzr", "log", "--gnu"], | ||
1512 | 34 | stdout=subprocess.PIPE) | ||
1513 | 35 | changelog = log_cmd.communicate()[0] | ||
1514 | 36 | with open("ChangeLog", "w") as changelog_file: | ||
1515 | 37 | changelog_file.write(changelog) | ||
1516 | 38 | sdist.run(self) | ||
1517 | 39 | |||
1518 | 40 | |||
1519 | 41 | name = 'burrow' | ||
1520 | 42 | |||
1521 | 43 | |||
1522 | 44 | cmdclass = {'sdist': local_sdist} | ||
1523 | 45 | |||
1524 | 46 | |||
1525 | 47 | if frontend: | ||
1526 | 48 | cmdclass.update({ | ||
1527 | 49 | 'compile_catalog': frontend.compile_catalog, | ||
1528 | 50 | 'extract_messages': frontend.extract_messages, | ||
1529 | 51 | 'init_catalog': frontend.init_catalog, | ||
1530 | 52 | 'update_catalog': frontend.update_catalog}) | ||
1531 | 53 | |||
1532 | 54 | |||
1533 | 55 | setup( | ||
1534 | 56 | name=name, | ||
1535 | 57 | version='0.1', | ||
1536 | 58 | description='Burrow', | ||
1537 | 59 | license='Apache License (2.0)', | ||
1538 | 60 | author='OpenStack, LLC.', | ||
1539 | 61 | author_email='openstack-admins@lists.launchpad.net', | ||
1540 | 62 | url='https://launchpad.net/burrow', | ||
1541 | 63 | packages=find_packages(exclude=['test', 'bin']), | ||
1542 | 64 | test_suite='nose.collector', | ||
1543 | 65 | cmdclass=cmdclass, | ||
1544 | 66 | classifiers=[ | ||
1545 | 67 | 'Development Status :: 3 - Alpha', | ||
1546 | 68 | 'License :: OSI Approved :: Apache Software License', | ||
1547 | 69 | 'Operating System :: POSIX :: Linux', | ||
1548 | 70 | 'Programming Language :: Python :: 2.6', | ||
1549 | 71 | 'Environment :: No Input/Output (Daemon)'], | ||
1550 | 72 | scripts=[ | ||
1551 | 73 | 'bin/burrow', | ||
1552 | 74 | 'bin/burrowd']) | ||
1553 | 0 | 75 | ||
1554 | === added directory 'test' | |||
1555 | === added file 'test/__init__.py' | |||
1556 | === added directory 'test/frontend' | |||
1557 | === added file 'test/frontend/__init__.py' | |||
1558 | === added file 'test/frontend/test_wsgi.py' | |||
1559 | --- test/frontend/test_wsgi.py 1970-01-01 00:00:00 +0000 | |||
1560 | +++ test/frontend/test_wsgi.py 2011-03-17 23:47:24 +0000 | |||
1561 | @@ -0,0 +1,313 @@ | |||
1562 | 1 | # Copyright (C) 2011 OpenStack LLC. | ||
1563 | 2 | # | ||
1564 | 3 | # Licensed under the Apache License, Version 2.0 (the "License"); | ||
1565 | 4 | # you may not use this file except in compliance with the License. | ||
1566 | 5 | # You may obtain a copy of the License at | ||
1567 | 6 | # | ||
1568 | 7 | # http://www.apache.org/licenses/LICENSE-2.0 | ||
1569 | 8 | # | ||
1570 | 9 | # Unless required by applicable law or agreed to in writing, software | ||
1571 | 10 | # distributed under the License is distributed on an "AS IS" BASIS, | ||
1572 | 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
1573 | 12 | # See the License for the specific language governing permissions and | ||
1574 | 13 | # limitations under the License. | ||
1575 | 14 | |||
1576 | 15 | import ConfigParser | ||
1577 | 16 | import json | ||
1578 | 17 | import time | ||
1579 | 18 | import unittest | ||
1580 | 19 | |||
1581 | 20 | import eventlet | ||
1582 | 21 | import webob | ||
1583 | 22 | |||
1584 | 23 | import burrowd.backend.memory | ||
1585 | 24 | import burrowd.backend.sqlite | ||
1586 | 25 | import burrowd.frontend.wsgi | ||
1587 | 26 | |||
1588 | 27 | |||
1589 | 28 | class TestWSGIMemory(unittest.TestCase): | ||
1590 | 29 | '''Unittests for the WSGI frontend to SQLite backend.''' | ||
1591 | 30 | backend_class = burrowd.backend.memory.Backend | ||
1592 | 31 | |||
1593 | 32 | def setUp(self): | ||
1594 | 33 | config = (ConfigParser.ConfigParser(), 'test') | ||
1595 | 34 | self.backend = self.backend_class(config) | ||
1596 | 35 | self.frontend = burrowd.frontend.wsgi.Frontend(config, self.backend) | ||
1597 | 36 | self.frontend.default_ttl = 0 | ||
1598 | 37 | self._get_url('/', status=404) | ||
1599 | 38 | self._get_url('/a', status=404) | ||
1600 | 39 | self._get_url('/a/q', status=404) | ||
1601 | 40 | |||
1602 | 41 | def tearDown(self): | ||
1603 | 42 | self._get_url('/a/q', status=404) | ||
1604 | 43 | self._get_url('/a', status=404) | ||
1605 | 44 | self._get_url('/', status=404) | ||
1606 | 45 | |||
1607 | 46 | def test_account(self): | ||
1608 | 47 | self._put_url('/a/q/1') | ||
1609 | 48 | accounts = self._get_url('/') | ||
1610 | 49 | self.assertEquals(accounts, ['a']) | ||
1611 | 50 | self._delete_url('/a') | ||
1612 | 51 | |||
1613 | 52 | def test_queue(self): | ||
1614 | 53 | self._put_url('/a/q/1') | ||
1615 | 54 | accounts = self._get_url('/a') | ||
1616 | 55 | self.assertEquals(accounts, ['q']) | ||
1617 | 56 | self._delete_url('/a/q') | ||
1618 | 57 | |||
1619 | 58 | def test_message(self): | ||
1620 | 59 | self._put_url('/a/q/1', body='b') | ||
1621 | 60 | accounts = self._get_url('/a/q') | ||
1622 | 61 | self.assertMessages(accounts, 'a', 'q', [self.message('1', body='b')]) | ||
1623 | 62 | self._delete_url('/a/q/1') | ||
1624 | 63 | |||
1625 | 64 | def test_message_post(self): | ||
1626 | 65 | self._put_url('/a/q/1', body='b') | ||
1627 | 66 | for x in range(0, 3): | ||
1628 | 67 | accounts = self._post_url('/a/q/1?ttl=%d&hide=%d' % (x, x)) | ||
1629 | 68 | self.assertEquals(accounts, {'a': {'q': [{'id': '1'}]}}) | ||
1630 | 69 | accounts = self._get_url('/a/q?hidden=true') | ||
1631 | 70 | message = self.message('1', x, x, body='b') | ||
1632 | 71 | self.assertMessages(accounts, 'a', 'q', [message]) | ||
1633 | 72 | self._delete_url('/a/q/1') | ||
1634 | 73 | |||
1635 | 74 | def test_message_put(self): | ||
1636 | 75 | for x in range(0, 3): | ||
1637 | 76 | url = '/a/q/1?ttl=%d&hide=%d' % (x, x) | ||
1638 | 77 | status = 201 if x == 0 else 204 | ||
1639 | 78 | self._put_url(url, body=str(x), status=status) | ||
1640 | 79 | accounts = self._get_url('/a/q?hidden=true') | ||
1641 | 80 | message = self.message('1', x, x, body=str(x)) | ||
1642 | 81 | self.assertMessages(accounts, 'a', 'q', [message]) | ||
1643 | 82 | self._delete_url('/a/q/1') | ||
1644 | 83 | |||
1645 | 84 | def test_message_delete_limit(self): | ||
1646 | 85 | [self._put_url('/a/q/%d' % x) for x in range(1, 5)] | ||
1647 | 86 | accounts = self._delete_url('/a/q?limit=3&detail=all', status=200) | ||
1648 | 87 | messages = [] | ||
1649 | 88 | messages.append(self.message('1')) | ||
1650 | 89 | messages.append(self.message('2')) | ||
1651 | 90 | messages.append(self.message('3')) | ||
1652 | 91 | self.assertMessages(accounts, 'a', 'q', messages) | ||
1653 | 92 | accounts = self._delete_url('/a/q?limit=3&detail=all', status=200) | ||
1654 | 93 | message = self.message('4') | ||
1655 | 94 | self.assertMessages(accounts, 'a', 'q', [message]) | ||
1656 | 95 | |||
1657 | 96 | def test_message_get_limit(self): | ||
1658 | 97 | [self._put_url('/a/q/%d' % x) for x in range(1, 5)] | ||
1659 | 98 | for x in range(0, 4): | ||
1660 | 99 | accounts = self._get_url('/a/q?limit=3') | ||
1661 | 100 | messages = [] | ||
1662 | 101 | for y in range(x, 4)[:3]: | ||
1663 | 102 | messages.append(self.message(str(y + 1))) | ||
1664 | 103 | self.assertMessages(accounts, 'a', 'q', messages) | ||
1665 | 104 | self._delete_url('/a/q/%d' % (x + 1)) | ||
1666 | 105 | |||
1667 | 106 | def test_message_post_limit(self): | ||
1668 | 107 | [self._put_url('/a/q/%d' % x) for x in range(1, 5)] | ||
1669 | 108 | for x in range(0, 4): | ||
1670 | 109 | accounts = self._post_url('/a/q?limit=3&ttl=%d&detail=all' % x) | ||
1671 | 110 | messages = [] | ||
1672 | 111 | for y in range(x, 4)[:3]: | ||
1673 | 112 | messages.append(self.message(str(y + 1), x)) | ||
1674 | 113 | self.assertMessages(accounts, 'a', 'q', messages) | ||
1675 | 114 | self._delete_url('/a/q/%d' % (x + 1)) | ||
1676 | 115 | |||
1677 | 116 | def test_message_delete_marker(self): | ||
1678 | 117 | [self._put_url('/a/q/%d' % x) for x in range(1, 5)] | ||
1679 | 118 | accounts = self._delete_url('/a/q?marker=2&detail=all', status=200) | ||
1680 | 119 | messages = [] | ||
1681 | 120 | messages.append(self.message('3')) | ||
1682 | 121 | messages.append(self.message('4')) | ||
1683 | 122 | self.assertMessages(accounts, 'a', 'q', messages) | ||
1684 | 123 | accounts = self._delete_url('/a/q?marker=5&detail=all', status=200) | ||
1685 | 124 | messages = [] | ||
1686 | 125 | messages.append(self.message('1')) | ||
1687 | 126 | messages.append(self.message('2')) | ||
1688 | 127 | self.assertMessages(accounts, 'a', 'q', messages) | ||
1689 | 128 | |||
1690 | 129 | def test_message_get_marker(self): | ||
1691 | 130 | [self._put_url('/a/q/%d' % x) for x in range(1, 5)] | ||
1692 | 131 | for x in range(0, 4): | ||
1693 | 132 | accounts = self._get_url('/a/q?marker=%d' % x) | ||
1694 | 133 | messages = [] | ||
1695 | 134 | for y in range(x, 4): | ||
1696 | 135 | messages.append(self.message(str(y + 1))) | ||
1697 | 136 | self.assertMessages(accounts, 'a', 'q', messages) | ||
1698 | 137 | self._delete_url('/a/q/%d' % (x + 1)) | ||
1699 | 138 | |||
1700 | 139 | def test_message_post_marker(self): | ||
1701 | 140 | [self._put_url('/a/q/%d' % x) for x in range(1, 5)] | ||
1702 | 141 | for x in range(0, 4): | ||
1703 | 142 | url = '/a/q?marker=%d&ttl=%d&detail=all' % (x, x) | ||
1704 | 143 | accounts = self._post_url(url) | ||
1705 | 144 | messages = [] | ||
1706 | 145 | for y in range(x, 4): | ||
1707 | 146 | messages.append(self.message(str(y + 1), x)) | ||
1708 | 147 | self.assertMessages(accounts, 'a', 'q', messages) | ||
1709 | 148 | self._delete_url('/a/q/%d' % (x + 1)) | ||
1710 | 149 | |||
1711 | 150 | def test_message_delete_limit_marker(self): | ||
1712 | 151 | [self._put_url('/a/q/%d' % x) for x in range(1, 5)] | ||
1713 | 152 | url = '/a/q?limit=2&marker=1&detail=all' | ||
1714 | 153 | accounts = self._delete_url(url, status=200) | ||
1715 | 154 | messages = [] | ||
1716 | 155 | messages.append(self.message('2')) | ||
1717 | 156 | messages.append(self.message('3')) | ||
1718 | 157 | self.assertMessages(accounts, 'a', 'q', messages) | ||
1719 | 158 | url = '/a/q?limit=2&marker=5&detail=all' | ||
1720 | 159 | accounts = self._delete_url(url, status=200) | ||
1721 | 160 | messages = [] | ||
1722 | 161 | messages.append(self.message('1')) | ||
1723 | 162 | messages.append(self.message('4')) | ||
1724 | 163 | self.assertMessages(accounts, 'a', 'q', messages) | ||
1725 | 164 | |||
1726 | 165 | def test_message_get_limit_marker(self): | ||
1727 | 166 | [self._put_url('/a/q/%d' % x) for x in range(1, 5)] | ||
1728 | 167 | for x in range(0, 4): | ||
1729 | 168 | accounts = self._get_url('/a/q?limit=2&marker=%d' % x) | ||
1730 | 169 | messages = [] | ||
1731 | 170 | for y in range(x, 4)[:2]: | ||
1732 | 171 | messages.append(self.message(str(y + 1))) | ||
1733 | 172 | self.assertMessages(accounts, 'a', 'q', messages) | ||
1734 | 173 | self._delete_url('/a/q/%d' % (x + 1)) | ||
1735 | 174 | |||
1736 | 175 | def test_message_post_limit_marker(self): | ||
1737 | 176 | [self._put_url('/a/q/%d' % x) for x in range(1, 5)] | ||
1738 | 177 | for x in range(0, 4): | ||
1739 | 178 | url = '/a/q?limit=2&marker=%d&ttl=%d&detail=all' % (x, x) | ||
1740 | 179 | accounts = self._post_url(url) | ||
1741 | 180 | messages = [] | ||
1742 | 181 | for y in range(x, 4)[:2]: | ||
1743 | 182 | messages.append(self.message(str(y + 1), x)) | ||
1744 | 183 | self.assertMessages(accounts, 'a', 'q', messages) | ||
1745 | 184 | self._delete_url('/a/q/%d' % (x + 1)) | ||
1746 | 185 | |||
1747 | 186 | def test_message_ttl(self): | ||
1748 | 187 | self._put_url('/a/q/1?ttl=1') | ||
1749 | 188 | accounts = self._get_url('/a/q/1') | ||
1750 | 189 | message = self.message('1', 1) | ||
1751 | 190 | self.assertMessages(accounts, 'a', 'q', [self.message('1', 1)]) | ||
1752 | 191 | time.sleep(1) | ||
1753 | 192 | self.backend.clean() | ||
1754 | 193 | self._get_url('/a/q/1', status=404) | ||
1755 | 194 | self._put_url('/a/q/1') | ||
1756 | 195 | accounts = self._get_url('/a/q/1') | ||
1757 | 196 | self.assertMessages(accounts, 'a', 'q', [self.message('1')]) | ||
1758 | 197 | self._post_url('/a/q/1?ttl=1') | ||
1759 | 198 | accounts = self._get_url('/a/q/1') | ||
1760 | 199 | self.assertMessages(accounts, 'a', 'q', [self.message('1', 1)]) | ||
1761 | 200 | time.sleep(1) | ||
1762 | 201 | self.backend.clean() | ||
1763 | 202 | self._get_url('/a/q/1', status=404) | ||
1764 | 203 | |||
1765 | 204 | def test_message_hide(self): | ||
1766 | 205 | self._put_url('/a/q/1?hide=1') | ||
1767 | 206 | accounts = self._get_url('/a/q/1') | ||
1768 | 207 | self.assertMessages(accounts, 'a', 'q', [self.message('1', hide=1)]) | ||
1769 | 208 | time.sleep(1) | ||
1770 | 209 | self.backend.clean() | ||
1771 | 210 | accounts = self._get_url('/a/q/1') | ||
1772 | 211 | self.assertMessages(accounts, 'a', 'q', [self.message('1')]) | ||
1773 | 212 | self._post_url('/a/q/1?hide=1') | ||
1774 | 213 | accounts = self._get_url('/a/q/1') | ||
1775 | 214 | self.assertMessages(accounts, 'a', 'q', [self.message('1', hide=1)]) | ||
1776 | 215 | time.sleep(1) | ||
1777 | 216 | self.backend.clean() | ||
1778 | 217 | accounts = self._get_url('/a/q/1') | ||
1779 | 218 | self.assertMessages(accounts, 'a', 'q', [self.message('1')]) | ||
1780 | 219 | self._delete_url('/a/q/1') | ||
1781 | 220 | |||
1782 | 221 | def _message_wait(self): | ||
1783 | 222 | accounts = self._get_url('/a/q?wait=2') | ||
1784 | 223 | self.assertMessages(accounts, 'a', 'q', [self.message('1')]) | ||
1785 | 224 | self.success = True | ||
1786 | 225 | |||
1787 | 226 | def test_message_put_wait(self): | ||
1788 | 227 | self.success = False | ||
1789 | 228 | thread = eventlet.spawn(self._message_wait) | ||
1790 | 229 | eventlet.spawn_after(0.2, self._put_url, '/a/q/1') | ||
1791 | 230 | thread.wait() | ||
1792 | 231 | self.assertTrue(self.success) | ||
1793 | 232 | self._delete_url('/a/q/1') | ||
1794 | 233 | |||
1795 | 234 | def test_message_put_wait_overwrite(self): | ||
1796 | 235 | self.success = False | ||
1797 | 236 | self._put_url('/a/q/1?hide=10') | ||
1798 | 237 | thread = eventlet.spawn(self._message_wait) | ||
1799 | 238 | eventlet.spawn_after(0.2, self._put_url, '/a/q/1?hide=0', status=204) | ||
1800 | 239 | thread.wait() | ||
1801 | 240 | self.assertTrue(self.success) | ||
1802 | 241 | self._delete_url('/a/q/1') | ||
1803 | 242 | |||
1804 | 243 | def test_message_put_wait_cleanup(self): | ||
1805 | 244 | self.success = False | ||
1806 | 245 | self._put_url('/a/q/1?hide=1') | ||
1807 | 246 | thread = eventlet.spawn(self._message_wait) | ||
1808 | 247 | eventlet.spawn_after(1, self.backend.clean) | ||
1809 | 248 | thread.wait() | ||
1810 | 249 | self.assertTrue(self.success) | ||
1811 | 250 | self._delete_url('/a/q/1') | ||
1812 | 251 | |||
1813 | 252 | def test_message_post_wait(self): | ||
1814 | 253 | self.success = False | ||
1815 | 254 | self._put_url('/a/q/1?hide=10') | ||
1816 | 255 | thread = eventlet.spawn(self._message_wait) | ||
1817 | 256 | eventlet.spawn_after(0.2, self._post_url, '/a/q/1?hide=0') | ||
1818 | 257 | thread.wait() | ||
1819 | 258 | self.assertTrue(self.success) | ||
1820 | 259 | self._delete_url('/a/q/1') | ||
1821 | 260 | |||
1822 | 261 | def test_message_post_wait_queue(self): | ||
1823 | 262 | self.success = False | ||
1824 | 263 | self._put_url('/a/q/1?hide=10') | ||
1825 | 264 | thread = eventlet.spawn(self._message_wait) | ||
1826 | 265 | eventlet.spawn_after(0.2, self._post_url, '/a/q?hide=0&hidden=true') | ||
1827 | 266 | thread.wait() | ||
1828 | 267 | self.assertTrue(self.success) | ||
1829 | 268 | self._delete_url('/a/q/1') | ||
1830 | 269 | |||
1831 | 270 | def message(self, id, ttl=0, hide=0, body=''): | ||
1832 | 271 | return dict(id=id, ttl=ttl, hide=hide, body=body) | ||
1833 | 272 | |||
1834 | 273 | def assertMessages(self, accounts, account, queue, messages): | ||
1835 | 274 | self.assertEquals(len(accounts), 1) | ||
1836 | 275 | self.assertEquals(len(accounts['a']), 1) | ||
1837 | 276 | self.assertEquals(len(accounts['a']['q']), len(messages)) | ||
1838 | 277 | for x in range(0, len(messages)): | ||
1839 | 278 | self.assertEquals(accounts['a']['q'][x]['id'], messages[x]['id']) | ||
1840 | 279 | ttl = messages[x]['ttl'] | ||
1841 | 280 | if ttl > 0: | ||
1842 | 281 | ttl += int(time.time()) | ||
1843 | 282 | self.assertAlmostEquals(accounts['a']['q'][0]['ttl'], ttl) | ||
1844 | 283 | hide = messages[x]['hide'] | ||
1845 | 284 | if hide > 0: | ||
1846 | 285 | hide += int(time.time()) | ||
1847 | 286 | self.assertAlmostEquals(accounts['a']['q'][0]['hide'], hide) | ||
1848 | 287 | body = messages[x]['body'] | ||
1849 | 288 | self.assertEquals(accounts['a']['q'][x]['body'], body) | ||
1850 | 289 | |||
1851 | 290 | def _delete_url(self, url, status=204, **kwargs): | ||
1852 | 291 | return self._url('DELETE', url, status=status, **kwargs) | ||
1853 | 292 | |||
1854 | 293 | def _get_url(self, url, **kwargs): | ||
1855 | 294 | return self._url('GET', url, **kwargs) | ||
1856 | 295 | |||
1857 | 296 | def _post_url(self, url, **kwargs): | ||
1858 | 297 | return self._url('POST', url, **kwargs) | ||
1859 | 298 | |||
1860 | 299 | def _put_url(self, url, status=201, **kwargs): | ||
1861 | 300 | return self._url('PUT', url, status=status, **kwargs) | ||
1862 | 301 | |||
1863 | 302 | def _url(self, method, url, body='', status=200): | ||
1864 | 303 | req = webob.Request.blank(url, method=method, body=body) | ||
1865 | 304 | res = req.get_response(self.frontend) | ||
1866 | 305 | self.assertEquals(res.status_int, status) | ||
1867 | 306 | if status == 200: | ||
1868 | 307 | return json.loads(res.body) | ||
1869 | 308 | return None | ||
1870 | 309 | |||
1871 | 310 | |||
1872 | 311 | class TestWSGISQLite(TestWSGIMemory): | ||
1873 | 312 | '''Unittests for the WSGI frontend to SQLite backend.''' | ||
1874 | 313 | backend_class = burrowd.backend.sqlite.Backend |