Merge lp://qastaging/~cmiller/desktopcouch/config-file-clean-up into lp://qastaging/desktopcouch
- config-file-clean-up
- Merge into trunk
Status: | Merged |
---|---|
Approved by: | Chad Miller |
Approved revision: | not available |
Merged at revision: | not available |
Proposed branch: | lp://qastaging/~cmiller/desktopcouch/config-file-clean-up |
Merge into: | lp://qastaging/desktopcouch |
Diff against target: |
423 lines (+159/-148) 4 files modified
desktopcouch/local_files.py (+151/-44) desktopcouch/records/server.py (+1/-1) desktopcouch/start_local_couchdb.py (+2/-99) desktopcouch/tests/test_local_files.py (+5/-4) |
To merge this branch: | bzr merge lp://qastaging/~cmiller/desktopcouch/config-file-clean-up |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Nicola Larosa (community) | Approve | ||
Tim Cole (community) | Approve | ||
Review via email:
|
Commit message
Do not read from the keyring if we have a configuration file that has the information we need.
Move config file manipulation into execution-context code. Make all config access go though explicit context of execution.
Simplify config-value getting and setting.
Hard-code port for new configurations, since we are very unlikely to use a value other than zero.
Description of the change
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
Nicola Larosa (teknico) wrote : | # |
Tests pass, nice code. Thanks for taking code out of exec scripts and into lib files: even more code can be moved, though. ;-)
I don't like static methods, why not just using functions outside classes? Especially for something like "_make_
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
Chad Miller (cmiller) wrote : | # |
Attempt to merge lp:~cmiller/desktopcouch/config-file-clean-up into lp:desktopcouch failed due to merge conflicts:
text conflict in desktopcouch/
- 107. By Chad Miller
-
Merge to trunk.
Preview Diff
1 | === modified file 'desktopcouch/local_files.py' |
2 | --- desktopcouch/local_files.py 2009-11-24 13:11:54 +0000 |
3 | +++ desktopcouch/local_files.py 2009-11-27 03:36:11 +0000 |
4 | @@ -26,6 +26,10 @@ |
5 | import xdg.BaseDirectory as xdg_base_dirs |
6 | import subprocess |
7 | import logging |
8 | +import tempfile |
9 | +import random |
10 | +import string |
11 | +import gnomekeyring |
12 | try: |
13 | import ConfigParser as configparser |
14 | except ImportError: |
15 | @@ -41,6 +45,128 @@ |
16 | raise ImportError("Could not find couchdb") |
17 | |
18 | |
19 | +class _Configuration(object): |
20 | + def __init__(self, ctx): |
21 | + super(_Configuration, self).__init__() |
22 | + self.file_name_used = ctx.file_ini |
23 | + self._c = configparser.SafeConfigParser() |
24 | + # monkeypatch ConfigParser to stop it lower-casing option names |
25 | + self._c.optionxform = lambda s: s |
26 | + |
27 | + try: |
28 | + self._fill_from_file(self.file_name_used) |
29 | + return |
30 | + except (IOError,): |
31 | + pass # Loading failed. Let's fill it with sensible defaults. |
32 | + |
33 | + try: |
34 | + data = gnomekeyring.find_items_sync(gnomekeyring.ITEM_GENERIC_SECRET, |
35 | + {'desktopcouch': 'basic'}) |
36 | + admin_username, admin_password = data[0].secret.split(":", 1) |
37 | + except gnomekeyring.NoMatchError: |
38 | + admin_username = self._make_random_string(10) |
39 | + admin_password = _self._make_random_string(10) |
40 | + |
41 | + try: |
42 | + # save admin account details in keyring |
43 | + item_id = gnomekeyring.item_create_sync(None, |
44 | + gnomekeyring.ITEM_GENERIC_SECRET, |
45 | + 'Desktop Couch user authentication', |
46 | + {'desktopcouch': 'basic'}, |
47 | + ":".join([admin_username, admin_password]), True) |
48 | + except gnomekeyring.NoKeyringDaemonError: |
49 | + logging.warn("There is no keyring to store our admin credentials.") |
50 | + |
51 | + consumer_key = self._make_random_string(10) |
52 | + consumer_secret = self._make_random_string(10) |
53 | + token = self._make_random_string(10) |
54 | + token_secret = self._make_random_string(10) |
55 | + |
56 | + # Save the new OAuth creds so that 3rd-party apps can authenticate by |
57 | + # accessing the keyring first. This is one-way. We don't read from keyring. |
58 | + item_id = gnomekeyring.item_create_sync( |
59 | + None, gnomekeyring.ITEM_GENERIC_SECRET, |
60 | + 'Desktop Couch user authentication', {'desktopcouch': 'oauth'}, |
61 | + ":".join([consumer_key, consumer_secret, token, token_secret]), |
62 | + True) |
63 | + |
64 | + local = { |
65 | + 'couchdb': { |
66 | + 'database_dir': ctx.db_dir, |
67 | + 'view_index_dir': ctx.db_dir, |
68 | + }, |
69 | + 'httpd': { |
70 | + 'bind_address': '127.0.0.1', |
71 | + 'port': '0', |
72 | + }, |
73 | + 'log': { |
74 | + 'file': ctx.file_log, |
75 | + 'level': ctx.couchdb_log_level, |
76 | + }, |
77 | + 'admins': { |
78 | + admin_username: admin_password |
79 | + }, |
80 | + 'oauth_consumer_secrets': { |
81 | + consumer_key: consumer_secret |
82 | + }, |
83 | + 'oauth_token_secrets': { |
84 | + token: token_secret |
85 | + }, |
86 | + 'oauth_token_users': { |
87 | + token: admin_username |
88 | + }, |
89 | + 'couch_httpd_auth': { |
90 | + 'require_valid_user': 'true' |
91 | + } |
92 | + } |
93 | + |
94 | + self._fill_from_structure(local) |
95 | + self.save_to_file(self.file_name_used) |
96 | + |
97 | + # randomly generate tokens and usernames |
98 | + @staticmethod |
99 | + def _make_random_string(count): |
100 | + entropy = random.SystemRandom() |
101 | + return ''.join([entropy.choice(string.letters) for x in range(count)]) |
102 | + |
103 | + def _fill_from_structure(self, structure): |
104 | + for section_name in structure: |
105 | + for key in structure[section_name]: |
106 | + self.set_item(section_name, key, structure[section_name][key]) |
107 | + |
108 | + def _fill_from_file(self, file_name): |
109 | + with file(file_name) as f: |
110 | + self._c.readfp(f) |
111 | + |
112 | + def save_to_file(self, file_name): |
113 | + container = os.path.split(file_name)[0] |
114 | + fd, temp_file_name = tempfile.mkstemp(dir=container) |
115 | + f = os.fdopen(fd, "w") |
116 | + try: |
117 | + self._c.write(f) |
118 | + finally: |
119 | + f.close() |
120 | + os.rename(temp_file_name, file_name) |
121 | + |
122 | + def items_in_section(self, section_name): |
123 | + try: |
124 | + return self._c.items(section_name) |
125 | + except configparser.NoSectionError: |
126 | + raise ValueError("Section %r not present." % (section_name,)) |
127 | + |
128 | + def set_item(self, section_name, key, value): |
129 | + if not self._c.has_section(section_name): |
130 | + self._c.add_section(section_name) |
131 | + self._c.set(section_name, key, value) |
132 | + |
133 | + def sync(self): |
134 | + """Write back to the file named when we tried to read in init.""" |
135 | + self.save_to_file(self.file_name_used) |
136 | + |
137 | + def __str__(self): |
138 | + return self.file_name_used |
139 | + |
140 | + |
141 | class Context(): |
142 | """A mimic of xdg BaseDirectory, with overridable values that do not |
143 | depend on environment variables.""" |
144 | @@ -71,6 +197,7 @@ |
145 | '-o', self.file_stdout, |
146 | '-e', self.file_stderr] |
147 | |
148 | + self.configuration = _Configuration(self) |
149 | |
150 | def ensure_files_not_readable(self): |
151 | for descr in ("ini", "pid", "log", "stdout", "stderr",): |
152 | @@ -113,13 +240,11 @@ |
153 | |
154 | return chain |
155 | |
156 | - |
157 | DEFAULT_CONTEXT = Context( |
158 | os.path.join(xdg_base_dirs.xdg_cache_home, "desktop-couch"), |
159 | xdg_base_dirs.save_data_path("desktop-couch"), |
160 | xdg_base_dirs.save_config_path("desktop-couch")) |
161 | |
162 | - |
163 | class NoOAuthTokenException(Exception): |
164 | def __init__(self, file_name): |
165 | super(Exception, self).__init__() |
166 | @@ -128,7 +253,10 @@ |
167 | return "OAuth details were not found in the ini file (%s)" % ( |
168 | self.file_name) |
169 | |
170 | -def get_oauth_tokens(config_file_name=None): |
171 | +def get_admin_credentials(ctx=DEFAULT_CONTEXT): |
172 | + return ctx.configuration.items_in_section("admins")[0] # return first tuple |
173 | + |
174 | +def get_oauth_tokens(ctx=DEFAULT_CONTEXT): |
175 | """Return the OAuth tokens from the desktop Couch ini file. |
176 | CouchDB OAuth is two-legged OAuth (not three-legged like most OAuth). |
177 | We have one "consumer", defined by a consumer_key and a secret, |
178 | @@ -138,55 +266,34 @@ |
179 | (More traditional 3-legged OAuth starts with a "request token" which is |
180 | then used to procure an "access token". We do not require this.) |
181 | """ |
182 | - if config_file_name is None: |
183 | - config_file_name = DEFAULT_CONTEXT.file_ini |
184 | |
185 | - c = configparser.ConfigParser() |
186 | - # monkeypatch ConfigParser to stop it lower-casing option names |
187 | - c.optionxform = lambda s: s |
188 | - c.read(config_file_name) |
189 | + cf = ctx.configuration |
190 | try: |
191 | - oauth_token_secrets = c.items("oauth_token_secrets") |
192 | - oauth_consumer_secrets = c.items("oauth_consumer_secrets") |
193 | + oauth_token_secrets = cf.items_in_section("oauth_token_secrets")[0] |
194 | + oauth_consumer_secrets = cf.items_in_section("oauth_consumer_secrets")[0] |
195 | except configparser.NoSectionError: |
196 | - raise NoOAuthTokenException(config_file_name) |
197 | + raise NoOAuthTokenException(cf) |
198 | + except IndexError: |
199 | + raise NoOAuthTokenException(cf) |
200 | if not oauth_token_secrets or not oauth_consumer_secrets: |
201 | - raise NoOAuthTokenException(config_file_name) |
202 | + raise NoOAuthTokenException(cf) |
203 | try: |
204 | out = { |
205 | - "token": oauth_token_secrets[0][0], |
206 | - "token_secret": oauth_token_secrets[0][1], |
207 | - "consumer_key": oauth_consumer_secrets[0][0], |
208 | - "consumer_secret": oauth_consumer_secrets[0][1] |
209 | + "token": oauth_token_secrets[0], |
210 | + "token_secret": oauth_token_secrets[1], |
211 | + "consumer_key": oauth_consumer_secrets[0], |
212 | + "consumer_secret": oauth_consumer_secrets[1] |
213 | } |
214 | except IndexError: |
215 | - raise NoOAuthTokenException(config_file_name) |
216 | + raise NoOAuthTokenException(cf) |
217 | return out |
218 | |
219 | -def get_bind_address(config_file_name=None): |
220 | +def get_bind_address(ctx=DEFAULT_CONTEXT): |
221 | """Retreive a string if it exists, or None if it doesn't.""" |
222 | - if config_file_name is None: |
223 | - config_file_name = DEFAULT_CONTEXT.file_ini |
224 | - |
225 | - c = configparser.ConfigParser() |
226 | - try: |
227 | - c.read(config_file_name) |
228 | - return c.get("httpd", "bind_address") |
229 | - except (configparser.NoOptionError, OSError), e: |
230 | - logging.warn("config file %r error. %s", config_file_name, e) |
231 | - return None |
232 | - |
233 | -def set_bind_address(address, config_file_name=None): |
234 | - if config_file_name is None: |
235 | - config_file_name = DEFAULT_CONTEXT.file_ini |
236 | - |
237 | - c = configparser.SafeConfigParser() |
238 | - # monkeypatch ConfigParser to stop it lower-casing option names |
239 | - c.optionxform = lambda s: s |
240 | - c.read(config_file_name) |
241 | - if not c.has_section("httpd"): |
242 | - c.add_section("httpd") |
243 | - c.set("httpd", "bind_address", address) |
244 | - with open(config_file_name, 'wb') as configfile: |
245 | - c.write(configfile) |
246 | - |
247 | + for k, v in ctx.configuration.items_in_section("httpd"): |
248 | + if k == "bind_address": |
249 | + return v |
250 | + |
251 | +def set_bind_address(address, ctx=DEFAULT_CONTEXT): |
252 | + ctx.configuration.set_item("httpd", "bind_address", address) |
253 | + ctx.configuration.sync() |
254 | |
255 | === modified file 'desktopcouch/records/server.py' |
256 | --- desktopcouch/records/server.py 2009-11-11 16:24:22 +0000 |
257 | +++ desktopcouch/records/server.py 2009-11-27 03:36:11 +0000 |
258 | @@ -36,7 +36,7 @@ |
259 | if ctx is None: |
260 | ctx = desktopcouch.local_files.DEFAULT_CONTEXT |
261 | if oauth_tokens is None: |
262 | - oauth_tokens = desktopcouch.local_files.get_oauth_tokens(ctx.file_ini) |
263 | + oauth_tokens = desktopcouch.local_files.get_oauth_tokens(ctx) |
264 | (consumer_key, consumer_secret, token, token_secret) = ( |
265 | oauth_tokens["consumer_key"], oauth_tokens["consumer_secret"], |
266 | oauth_tokens["token"], oauth_tokens["token_secret"]) |
267 | |
268 | === modified file 'desktopcouch/start_local_couchdb.py' |
269 | --- desktopcouch/start_local_couchdb.py 2009-11-24 17:27:27 +0000 |
270 | +++ desktopcouch/start_local_couchdb.py 2009-11-27 03:36:11 +0000 |
271 | @@ -33,7 +33,7 @@ |
272 | """ |
273 | |
274 | from __future__ import with_statement |
275 | -import os, subprocess, sys, glob, random, string |
276 | +import os, subprocess, sys, glob |
277 | import desktopcouch |
278 | from desktopcouch import local_files |
279 | import xdg.BaseDirectory |
280 | @@ -43,103 +43,6 @@ |
281 | import logging |
282 | import itertools |
283 | |
284 | -ACCEPTABLE_USERNAME_PASSWORD_CHARS = string.lowercase + string.uppercase |
285 | - |
286 | -def dump_ini(data, filename): |
287 | - """Dump INI data with sorted sections and keywords""" |
288 | - fd = open(filename, 'w') |
289 | - sections = data.keys() |
290 | - sections.sort() |
291 | - for section in sections: |
292 | - fd.write("[%s]\n" % (section)) |
293 | - keys = data[section].keys() |
294 | - keys.sort() |
295 | - for key in keys: |
296 | - fd.write("%s=%s\n" % (key, data[section][key])) |
297 | - fd.write("\n") |
298 | - fd.close() |
299 | - |
300 | -def create_ini_file(port="0", ctx=local_files.DEFAULT_CONTEXT): |
301 | - """Write CouchDB ini file if not already present""" |
302 | - |
303 | - if os.path.exists(ctx.file_ini): |
304 | - # load the username and password from the keyring |
305 | - try: |
306 | - data = gnomekeyring.find_items_sync(gnomekeyring.ITEM_GENERIC_SECRET, |
307 | - {'desktopcouch': 'basic'}) |
308 | - except gnomekeyring.NoMatchError: |
309 | - data = None |
310 | - if data: |
311 | - username, password = data[0].secret.split(":") |
312 | - return username, password |
313 | - # otherwise fall through; for some reason the access details aren't |
314 | - # in the keyring, so re-create the ini file and do it all again |
315 | - |
316 | - # randomly generate tokens and usernames |
317 | - def make_random_string(count): |
318 | - return ''.join([ |
319 | - random.SystemRandom().choice(ACCEPTABLE_USERNAME_PASSWORD_CHARS) |
320 | - for x in range(count)]) |
321 | - |
322 | - admin_account_username = make_random_string(10) |
323 | - admin_account_basic_auth_password = make_random_string(10) |
324 | - consumer_key = make_random_string(10) |
325 | - consumer_secret = make_random_string(10) |
326 | - token = make_random_string(10) |
327 | - token_secret = make_random_string(10) |
328 | - local = { |
329 | - 'couchdb': { |
330 | - 'database_dir': ctx.db_dir, |
331 | - 'view_index_dir': ctx.db_dir, |
332 | - }, |
333 | - 'httpd': { |
334 | - 'bind_address': '127.0.0.1', |
335 | - 'port': port, |
336 | - }, |
337 | - 'log': { |
338 | - 'file': ctx.file_log, |
339 | - 'level': ctx.couchdb_log_level, |
340 | - }, |
341 | - 'admins': { |
342 | - admin_account_username: admin_account_basic_auth_password |
343 | - }, |
344 | - 'oauth_consumer_secrets': { |
345 | - consumer_key: consumer_secret |
346 | - }, |
347 | - 'oauth_token_secrets': { |
348 | - token: token_secret |
349 | - }, |
350 | - 'oauth_token_users': { |
351 | - token: admin_account_username |
352 | - }, |
353 | - 'couch_httpd_auth': { |
354 | - 'require_valid_user': 'true' |
355 | - } |
356 | - } |
357 | - |
358 | - dump_ini(local, ctx.file_ini) |
359 | - ctx.ensure_files_not_readable() |
360 | - |
361 | - # save admin account details in keyring |
362 | - item_id = gnomekeyring.item_create_sync( |
363 | - None, |
364 | - gnomekeyring.ITEM_GENERIC_SECRET, |
365 | - 'Desktop Couch user authentication', |
366 | - {'desktopcouch': 'basic'}, |
367 | - "%s:%s" % ( |
368 | - admin_account_username, admin_account_basic_auth_password), |
369 | - True) |
370 | - # and oauth tokens |
371 | - item_id = gnomekeyring.item_create_sync( |
372 | - None, |
373 | - gnomekeyring.ITEM_GENERIC_SECRET, |
374 | - 'Desktop Couch user authentication', |
375 | - {'desktopcouch': 'oauth'}, |
376 | - "%s:%s:%s:%s" % ( |
377 | - consumer_key, consumer_secret, token, token_secret), |
378 | - True) |
379 | - return (admin_account_username, admin_account_basic_auth_password) |
380 | - |
381 | |
382 | def process_is_couchdb__linux(pid): |
383 | if pid is None: |
384 | @@ -317,7 +220,7 @@ |
385 | |
386 | def start_couchdb(ctx=local_files.DEFAULT_CONTEXT): |
387 | """Execute each step to start a desktop CouchDB.""" |
388 | - username, password = create_ini_file(ctx=ctx) |
389 | + username, password = local_files.get_admin_credentials(ctx=ctx) |
390 | pid, port = run_couchdb(ctx=ctx) |
391 | # Note that we do not call update_design_documents here. This is because |
392 | # Couch won't actually have started yet, so when update_design_documents |
393 | |
394 | === modified file 'desktopcouch/tests/test_local_files.py' |
395 | --- desktopcouch/tests/test_local_files.py 2009-11-18 18:43:41 +0000 |
396 | +++ desktopcouch/tests/test_local_files.py 2009-11-27 03:36:11 +0000 |
397 | @@ -7,6 +7,11 @@ |
398 | |
399 | class TestLocalFiles(testtools.TestCase): |
400 | """Testing that local files returns the right things""" |
401 | + |
402 | + def setUp(self): |
403 | + cf = test_environment.test_context.configuration |
404 | + cf._fill_from_file(test_environment.test_context.file_ini) # Test loading from file. |
405 | + |
406 | def test_all_files_returned(self): |
407 | "Does local_files list all the files that it needs to?" |
408 | import desktopcouch.local_files |
409 | @@ -29,9 +34,6 @@ |
410 | self.assertTrue(len(ok) > 0) |
411 | |
412 | def test_bind_address(self): |
413 | - desktopcouch.start_local_couchdb.create_ini_file() |
414 | - desktopcouch.start_local_couchdb.create_ini_file() # Make sure the loader works. |
415 | - |
416 | old = desktopcouch.local_files.get_bind_address() |
417 | octets = old.split(".") |
418 | octets[2] = str((int(octets[2]) + 128) % 256) |
419 | @@ -41,4 +43,3 @@ |
420 | self.assertEquals(desktopcouch.local_files.get_bind_address(), new) |
421 | desktopcouch.local_files.set_bind_address(old) |
422 | self.assertEquals(desktopcouch.local_files.get_bind_address(), old) |
423 | - |
Hmm. Okay.