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
=== modified file 'desktopcouch/local_files.py'
--- desktopcouch/local_files.py 2009-11-24 13:11:54 +0000
+++ desktopcouch/local_files.py 2009-11-27 03:36:11 +0000
@@ -26,6 +26,10 @@
26import xdg.BaseDirectory as xdg_base_dirs26import xdg.BaseDirectory as xdg_base_dirs
27import subprocess27import subprocess
28import logging28import logging
29import tempfile
30import random
31import string
32import gnomekeyring
29try:33try:
30 import ConfigParser as configparser34 import ConfigParser as configparser
31except ImportError:35except ImportError:
@@ -41,6 +45,128 @@
41 raise ImportError("Could not find couchdb")45 raise ImportError("Could not find couchdb")
4246
4347
48class _Configuration(object):
49 def __init__(self, ctx):
50 super(_Configuration, self).__init__()
51 self.file_name_used = ctx.file_ini
52 self._c = configparser.SafeConfigParser()
53 # monkeypatch ConfigParser to stop it lower-casing option names
54 self._c.optionxform = lambda s: s
55
56 try:
57 self._fill_from_file(self.file_name_used)
58 return
59 except (IOError,):
60 pass # Loading failed. Let's fill it with sensible defaults.
61
62 try:
63 data = gnomekeyring.find_items_sync(gnomekeyring.ITEM_GENERIC_SECRET,
64 {'desktopcouch': 'basic'})
65 admin_username, admin_password = data[0].secret.split(":", 1)
66 except gnomekeyring.NoMatchError:
67 admin_username = self._make_random_string(10)
68 admin_password = _self._make_random_string(10)
69
70 try:
71 # save admin account details in keyring
72 item_id = gnomekeyring.item_create_sync(None,
73 gnomekeyring.ITEM_GENERIC_SECRET,
74 'Desktop Couch user authentication',
75 {'desktopcouch': 'basic'},
76 ":".join([admin_username, admin_password]), True)
77 except gnomekeyring.NoKeyringDaemonError:
78 logging.warn("There is no keyring to store our admin credentials.")
79
80 consumer_key = self._make_random_string(10)
81 consumer_secret = self._make_random_string(10)
82 token = self._make_random_string(10)
83 token_secret = self._make_random_string(10)
84
85 # Save the new OAuth creds so that 3rd-party apps can authenticate by
86 # accessing the keyring first. This is one-way. We don't read from keyring.
87 item_id = gnomekeyring.item_create_sync(
88 None, gnomekeyring.ITEM_GENERIC_SECRET,
89 'Desktop Couch user authentication', {'desktopcouch': 'oauth'},
90 ":".join([consumer_key, consumer_secret, token, token_secret]),
91 True)
92
93 local = {
94 'couchdb': {
95 'database_dir': ctx.db_dir,
96 'view_index_dir': ctx.db_dir,
97 },
98 'httpd': {
99 'bind_address': '127.0.0.1',
100 'port': '0',
101 },
102 'log': {
103 'file': ctx.file_log,
104 'level': ctx.couchdb_log_level,
105 },
106 'admins': {
107 admin_username: admin_password
108 },
109 'oauth_consumer_secrets': {
110 consumer_key: consumer_secret
111 },
112 'oauth_token_secrets': {
113 token: token_secret
114 },
115 'oauth_token_users': {
116 token: admin_username
117 },
118 'couch_httpd_auth': {
119 'require_valid_user': 'true'
120 }
121 }
122
123 self._fill_from_structure(local)
124 self.save_to_file(self.file_name_used)
125
126 # randomly generate tokens and usernames
127 @staticmethod
128 def _make_random_string(count):
129 entropy = random.SystemRandom()
130 return ''.join([entropy.choice(string.letters) for x in range(count)])
131
132 def _fill_from_structure(self, structure):
133 for section_name in structure:
134 for key in structure[section_name]:
135 self.set_item(section_name, key, structure[section_name][key])
136
137 def _fill_from_file(self, file_name):
138 with file(file_name) as f:
139 self._c.readfp(f)
140
141 def save_to_file(self, file_name):
142 container = os.path.split(file_name)[0]
143 fd, temp_file_name = tempfile.mkstemp(dir=container)
144 f = os.fdopen(fd, "w")
145 try:
146 self._c.write(f)
147 finally:
148 f.close()
149 os.rename(temp_file_name, file_name)
150
151 def items_in_section(self, section_name):
152 try:
153 return self._c.items(section_name)
154 except configparser.NoSectionError:
155 raise ValueError("Section %r not present." % (section_name,))
156
157 def set_item(self, section_name, key, value):
158 if not self._c.has_section(section_name):
159 self._c.add_section(section_name)
160 self._c.set(section_name, key, value)
161
162 def sync(self):
163 """Write back to the file named when we tried to read in init."""
164 self.save_to_file(self.file_name_used)
165
166 def __str__(self):
167 return self.file_name_used
168
169
44class Context():170class Context():
45 """A mimic of xdg BaseDirectory, with overridable values that do not171 """A mimic of xdg BaseDirectory, with overridable values that do not
46 depend on environment variables."""172 depend on environment variables."""
@@ -71,6 +197,7 @@
71 '-o', self.file_stdout,197 '-o', self.file_stdout,
72 '-e', self.file_stderr]198 '-e', self.file_stderr]
73199
200 self.configuration = _Configuration(self)
74201
75 def ensure_files_not_readable(self):202 def ensure_files_not_readable(self):
76 for descr in ("ini", "pid", "log", "stdout", "stderr",):203 for descr in ("ini", "pid", "log", "stdout", "stderr",):
@@ -113,13 +240,11 @@
113240
114 return chain241 return chain
115242
116
117DEFAULT_CONTEXT = Context(243DEFAULT_CONTEXT = Context(
118 os.path.join(xdg_base_dirs.xdg_cache_home, "desktop-couch"),244 os.path.join(xdg_base_dirs.xdg_cache_home, "desktop-couch"),
119 xdg_base_dirs.save_data_path("desktop-couch"),245 xdg_base_dirs.save_data_path("desktop-couch"),
120 xdg_base_dirs.save_config_path("desktop-couch"))246 xdg_base_dirs.save_config_path("desktop-couch"))
121247
122
123class NoOAuthTokenException(Exception):248class NoOAuthTokenException(Exception):
124 def __init__(self, file_name):249 def __init__(self, file_name):
125 super(Exception, self).__init__()250 super(Exception, self).__init__()
@@ -128,7 +253,10 @@
128 return "OAuth details were not found in the ini file (%s)" % (253 return "OAuth details were not found in the ini file (%s)" % (
129 self.file_name)254 self.file_name)
130255
131def get_oauth_tokens(config_file_name=None):256def get_admin_credentials(ctx=DEFAULT_CONTEXT):
257 return ctx.configuration.items_in_section("admins")[0] # return first tuple
258
259def get_oauth_tokens(ctx=DEFAULT_CONTEXT):
132 """Return the OAuth tokens from the desktop Couch ini file.260 """Return the OAuth tokens from the desktop Couch ini file.
133 CouchDB OAuth is two-legged OAuth (not three-legged like most OAuth).261 CouchDB OAuth is two-legged OAuth (not three-legged like most OAuth).
134 We have one "consumer", defined by a consumer_key and a secret,262 We have one "consumer", defined by a consumer_key and a secret,
@@ -138,55 +266,34 @@
138 (More traditional 3-legged OAuth starts with a "request token" which is266 (More traditional 3-legged OAuth starts with a "request token" which is
139 then used to procure an "access token". We do not require this.)267 then used to procure an "access token". We do not require this.)
140 """268 """
141 if config_file_name is None:
142 config_file_name = DEFAULT_CONTEXT.file_ini
143269
144 c = configparser.ConfigParser()270 cf = ctx.configuration
145 # monkeypatch ConfigParser to stop it lower-casing option names
146 c.optionxform = lambda s: s
147 c.read(config_file_name)
148 try:271 try:
149 oauth_token_secrets = c.items("oauth_token_secrets")272 oauth_token_secrets = cf.items_in_section("oauth_token_secrets")[0]
150 oauth_consumer_secrets = c.items("oauth_consumer_secrets")273 oauth_consumer_secrets = cf.items_in_section("oauth_consumer_secrets")[0]
151 except configparser.NoSectionError:274 except configparser.NoSectionError:
152 raise NoOAuthTokenException(config_file_name)275 raise NoOAuthTokenException(cf)
276 except IndexError:
277 raise NoOAuthTokenException(cf)
153 if not oauth_token_secrets or not oauth_consumer_secrets:278 if not oauth_token_secrets or not oauth_consumer_secrets:
154 raise NoOAuthTokenException(config_file_name)279 raise NoOAuthTokenException(cf)
155 try:280 try:
156 out = {281 out = {
157 "token": oauth_token_secrets[0][0],282 "token": oauth_token_secrets[0],
158 "token_secret": oauth_token_secrets[0][1],283 "token_secret": oauth_token_secrets[1],
159 "consumer_key": oauth_consumer_secrets[0][0],284 "consumer_key": oauth_consumer_secrets[0],
160 "consumer_secret": oauth_consumer_secrets[0][1]285 "consumer_secret": oauth_consumer_secrets[1]
161 }286 }
162 except IndexError:287 except IndexError:
163 raise NoOAuthTokenException(config_file_name)288 raise NoOAuthTokenException(cf)
164 return out289 return out
165290
166def get_bind_address(config_file_name=None):291def get_bind_address(ctx=DEFAULT_CONTEXT):
167 """Retreive a string if it exists, or None if it doesn't."""292 """Retreive a string if it exists, or None if it doesn't."""
168 if config_file_name is None:293 for k, v in ctx.configuration.items_in_section("httpd"):
169 config_file_name = DEFAULT_CONTEXT.file_ini294 if k == "bind_address":
170295 return v
171 c = configparser.ConfigParser()296
172 try:297def set_bind_address(address, ctx=DEFAULT_CONTEXT):
173 c.read(config_file_name)298 ctx.configuration.set_item("httpd", "bind_address", address)
174 return c.get("httpd", "bind_address")299 ctx.configuration.sync()
175 except (configparser.NoOptionError, OSError), e:
176 logging.warn("config file %r error. %s", config_file_name, e)
177 return None
178
179def set_bind_address(address, config_file_name=None):
180 if config_file_name is None:
181 config_file_name = DEFAULT_CONTEXT.file_ini
182
183 c = configparser.SafeConfigParser()
184 # monkeypatch ConfigParser to stop it lower-casing option names
185 c.optionxform = lambda s: s
186 c.read(config_file_name)
187 if not c.has_section("httpd"):
188 c.add_section("httpd")
189 c.set("httpd", "bind_address", address)
190 with open(config_file_name, 'wb') as configfile:
191 c.write(configfile)
192
193300
=== modified file 'desktopcouch/records/server.py'
--- desktopcouch/records/server.py 2009-11-11 16:24:22 +0000
+++ desktopcouch/records/server.py 2009-11-27 03:36:11 +0000
@@ -36,7 +36,7 @@
36 if ctx is None:36 if ctx is None:
37 ctx = desktopcouch.local_files.DEFAULT_CONTEXT37 ctx = desktopcouch.local_files.DEFAULT_CONTEXT
38 if oauth_tokens is None:38 if oauth_tokens is None:
39 oauth_tokens = desktopcouch.local_files.get_oauth_tokens(ctx.file_ini)39 oauth_tokens = desktopcouch.local_files.get_oauth_tokens(ctx)
40 (consumer_key, consumer_secret, token, token_secret) = (40 (consumer_key, consumer_secret, token, token_secret) = (
41 oauth_tokens["consumer_key"], oauth_tokens["consumer_secret"], 41 oauth_tokens["consumer_key"], oauth_tokens["consumer_secret"],
42 oauth_tokens["token"], oauth_tokens["token_secret"])42 oauth_tokens["token"], oauth_tokens["token_secret"])
4343
=== modified file 'desktopcouch/start_local_couchdb.py'
--- desktopcouch/start_local_couchdb.py 2009-11-24 17:27:27 +0000
+++ desktopcouch/start_local_couchdb.py 2009-11-27 03:36:11 +0000
@@ -33,7 +33,7 @@
33"""33"""
3434
35from __future__ import with_statement35from __future__ import with_statement
36import os, subprocess, sys, glob, random, string36import os, subprocess, sys, glob
37import desktopcouch37import desktopcouch
38from desktopcouch import local_files38from desktopcouch import local_files
39import xdg.BaseDirectory39import xdg.BaseDirectory
@@ -43,103 +43,6 @@
43import logging43import logging
44import itertools44import itertools
4545
46ACCEPTABLE_USERNAME_PASSWORD_CHARS = string.lowercase + string.uppercase
47
48def dump_ini(data, filename):
49 """Dump INI data with sorted sections and keywords"""
50 fd = open(filename, 'w')
51 sections = data.keys()
52 sections.sort()
53 for section in sections:
54 fd.write("[%s]\n" % (section))
55 keys = data[section].keys()
56 keys.sort()
57 for key in keys:
58 fd.write("%s=%s\n" % (key, data[section][key]))
59 fd.write("\n")
60 fd.close()
61
62def create_ini_file(port="0", ctx=local_files.DEFAULT_CONTEXT):
63 """Write CouchDB ini file if not already present"""
64
65 if os.path.exists(ctx.file_ini):
66 # load the username and password from the keyring
67 try:
68 data = gnomekeyring.find_items_sync(gnomekeyring.ITEM_GENERIC_SECRET,
69 {'desktopcouch': 'basic'})
70 except gnomekeyring.NoMatchError:
71 data = None
72 if data:
73 username, password = data[0].secret.split(":")
74 return username, password
75 # otherwise fall through; for some reason the access details aren't
76 # in the keyring, so re-create the ini file and do it all again
77
78 # randomly generate tokens and usernames
79 def make_random_string(count):
80 return ''.join([
81 random.SystemRandom().choice(ACCEPTABLE_USERNAME_PASSWORD_CHARS)
82 for x in range(count)])
83
84 admin_account_username = make_random_string(10)
85 admin_account_basic_auth_password = make_random_string(10)
86 consumer_key = make_random_string(10)
87 consumer_secret = make_random_string(10)
88 token = make_random_string(10)
89 token_secret = make_random_string(10)
90 local = {
91 'couchdb': {
92 'database_dir': ctx.db_dir,
93 'view_index_dir': ctx.db_dir,
94 },
95 'httpd': {
96 'bind_address': '127.0.0.1',
97 'port': port,
98 },
99 'log': {
100 'file': ctx.file_log,
101 'level': ctx.couchdb_log_level,
102 },
103 'admins': {
104 admin_account_username: admin_account_basic_auth_password
105 },
106 'oauth_consumer_secrets': {
107 consumer_key: consumer_secret
108 },
109 'oauth_token_secrets': {
110 token: token_secret
111 },
112 'oauth_token_users': {
113 token: admin_account_username
114 },
115 'couch_httpd_auth': {
116 'require_valid_user': 'true'
117 }
118 }
119
120 dump_ini(local, ctx.file_ini)
121 ctx.ensure_files_not_readable()
122
123 # save admin account details in keyring
124 item_id = gnomekeyring.item_create_sync(
125 None,
126 gnomekeyring.ITEM_GENERIC_SECRET,
127 'Desktop Couch user authentication',
128 {'desktopcouch': 'basic'},
129 "%s:%s" % (
130 admin_account_username, admin_account_basic_auth_password),
131 True)
132 # and oauth tokens
133 item_id = gnomekeyring.item_create_sync(
134 None,
135 gnomekeyring.ITEM_GENERIC_SECRET,
136 'Desktop Couch user authentication',
137 {'desktopcouch': 'oauth'},
138 "%s:%s:%s:%s" % (
139 consumer_key, consumer_secret, token, token_secret),
140 True)
141 return (admin_account_username, admin_account_basic_auth_password)
142
14346
144def process_is_couchdb__linux(pid):47def process_is_couchdb__linux(pid):
145 if pid is None:48 if pid is None:
@@ -317,7 +220,7 @@
317220
318def start_couchdb(ctx=local_files.DEFAULT_CONTEXT):221def start_couchdb(ctx=local_files.DEFAULT_CONTEXT):
319 """Execute each step to start a desktop CouchDB."""222 """Execute each step to start a desktop CouchDB."""
320 username, password = create_ini_file(ctx=ctx)223 username, password = local_files.get_admin_credentials(ctx=ctx)
321 pid, port = run_couchdb(ctx=ctx)224 pid, port = run_couchdb(ctx=ctx)
322 # Note that we do not call update_design_documents here. This is because225 # Note that we do not call update_design_documents here. This is because
323 # Couch won't actually have started yet, so when update_design_documents226 # Couch won't actually have started yet, so when update_design_documents
324227
=== modified file 'desktopcouch/tests/test_local_files.py'
--- desktopcouch/tests/test_local_files.py 2009-11-18 18:43:41 +0000
+++ desktopcouch/tests/test_local_files.py 2009-11-27 03:36:11 +0000
@@ -7,6 +7,11 @@
77
8class TestLocalFiles(testtools.TestCase):8class TestLocalFiles(testtools.TestCase):
9 """Testing that local files returns the right things"""9 """Testing that local files returns the right things"""
10
11 def setUp(self):
12 cf = test_environment.test_context.configuration
13 cf._fill_from_file(test_environment.test_context.file_ini) # Test loading from file.
14
10 def test_all_files_returned(self):15 def test_all_files_returned(self):
11 "Does local_files list all the files that it needs to?"16 "Does local_files list all the files that it needs to?"
12 import desktopcouch.local_files17 import desktopcouch.local_files
@@ -29,9 +34,6 @@
29 self.assertTrue(len(ok) > 0)34 self.assertTrue(len(ok) > 0)
3035
31 def test_bind_address(self):36 def test_bind_address(self):
32 desktopcouch.start_local_couchdb.create_ini_file()
33 desktopcouch.start_local_couchdb.create_ini_file() # Make sure the loader works.
34
35 old = desktopcouch.local_files.get_bind_address()37 old = desktopcouch.local_files.get_bind_address()
36 octets = old.split(".")38 octets = old.split(".")
37 octets[2] = str((int(octets[2]) + 128) % 256)39 octets[2] = str((int(octets[2]) + 128) % 256)
@@ -41,4 +43,3 @@
41 self.assertEquals(desktopcouch.local_files.get_bind_address(), new)43 self.assertEquals(desktopcouch.local_files.get_bind_address(), new)
42 desktopcouch.local_files.set_bind_address(old)44 desktopcouch.local_files.set_bind_address(old)
43 self.assertEquals(desktopcouch.local_files.get_bind_address(), old)45 self.assertEquals(desktopcouch.local_files.get_bind_address(), old)
44

Subscribers

People subscribed via source and target branches