Merge lp://qastaging/~cmiller/desktopcouch/config-file-clean-up into lp://qastaging/desktopcouch

Proposed by Chad Miller
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
Reviewer Review Type Date Requested Status
Nicola Larosa (community) Approve
Tim Cole (community) Approve
Review via email: mp+15163@code.qastaging.launchpad.net

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.

To post a comment you must log in.
Revision history for this message
Tim Cole (tcole) wrote :

Hmm. Okay.

review: Approve
Revision history for this message
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_random_string": does it really need to be private anyway? The underscore in _Configuration also seems weird, never seen a private class at module level before. What's the use?

review: Approve
Revision history for this message
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/start_local_couchdb.py

107. By Chad Miller

Merge to trunk.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
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-

Subscribers

People subscribed via source and target branches