~ubuntu-branches/ubuntu/karmic/desktopcouch/karmic

« back to all changes in this revision

Viewing changes to desktopcouch/start_local_couchdb.py

  • Committer: Bazaar Package Importer
  • Author(s): Ken VanDine, Ken VanDine, Elliot Murphy, Martin Pitt
  • Date: 2009-08-27 15:32:11 UTC
  • mfrom: (1.1.2 upstream)
  • Revision ID: james.westby@ubuntu.com-20090827153211-oi7m8c1yj918u082
Tags: 0.3-0ubuntu1
[ Ken VanDine ]
* New upstream release (LP: #416591)
  - added unit tests for couchwidget, and then fixed bug #412266
  - Change to freedesktop URL for record type spec.
  - First version of the contacts picker, based on CouchWidget
  - Adding the desktopcouch.contacts module.
  - Use subprocess.Popen and ourselves to the wait()ing, 
    since subprocess.call() is buggy.  There's still an EINTR bug 
    in subprocess, though.
  - Occasionally stop couchdb in tests, so we exercise the automatic 
    starting code.  This will lead to spurious errors because of the 
    aforementioned subprocess bug, but it's the right thing to do.
  - Abstract away some of the linuxisms and complain if we're run on 
    an unsupported OS.
  - Fix a race condition in the process-testing code.
  - Replace the TestCase module with one that doesn't complain of dirty 
    twisted reactors.
  - Add a means of stopping the desktop couchdb daemon.
  - Add an additional check that a found PID and process named correctly 
    is indeed a process that this user started, so we don't try to talk 
    to other local users' desktop couchdbs.
  - Get the port at function-call time, instead of storing a port at
    start-time and giving that back. The info can be stale, the old
    way.
  - Don't create a view per record-type; instead, call the standard 
    return-all-records-keyed-by-record-type and use slice notation on 
    the viewresults to only get back the records with that type, 
    which does the same thing but more elegantly.
  - Remove the unused/invalid "utils" import from test_server
  - Change the name of a function tested to be what actually exists in 
    the code.
  - Refactored server.py by renaming server.get_records_and_type to 
    server.get_records. Also modified the function to take a record 
    type param if desired to only create records of that type and 
    optionally create a view name "get_"+ record_type. (LP: #411475)
* debian/control
  - Make python-desktopcouch depend on python-gtk2 and python-gnomekeyring
  - Make python-desktopcouch-records depend on python-gtk2, 
    python-gnomekeyring, and python-oauth.
  - Remove depends for python-distutils-extra
  - Fixed Vcs-Browser tag

[Elliot Murphy]
* debian/control: added build-dep on python-setuptools


[ Martin Pitt ]
* debian/control: Fix Vcs-* links.

Show diffs side-by-side

added added

removed removed

Lines of Context:
16
16
# along with desktopcouch.  If not, see <http://www.gnu.org/licenses/>.
17
17
#
18
18
# Author: Stuart Langridge <stuart.langridge@canonical.com>
 
19
#         Eric Casteleijn <eric.casteleijn@canonical.com>
 
20
 
19
21
"""
20
22
Start local CouchDB server.
21
23
Steps:
32
34
"""
33
35
 
34
36
from __future__ import with_statement
35
 
import os, subprocess, sys
 
37
import os, subprocess, sys, glob, random, string
36
38
import desktopcouch
37
39
from desktopcouch import local_files
38
40
import xdg.BaseDirectory
39
 
import time
 
41
import errno
 
42
import time, gtk, gnomekeyring
 
43
from desktopcouch.records.server import CouchDatabase
 
44
 
 
45
ACCEPTABLE_USERNAME_PASSWORD_CHARS = string.lowercase + string.uppercase
40
46
 
41
47
def dump_ini(data, filename):
42
48
    """Dump INI data with sorted sections and keywords"""
52
58
        fd.write("\n")
53
59
    fd.close()
54
60
 
55
 
 
56
 
 
57
 
def create_ini_file():
 
61
def create_ini_file(port="0"):
58
62
    """Write CouchDB ini file if not already present"""
59
 
    # FIXME add update trigger folder
60
 
    #update_trigger_dir = [
61
 
    #    'lib', 'canonical', 'ubuntuone', 'cloud_server', 'update_triggers']
62
 
    #
63
 
    #timestamp_trigger = os.path.join(
64
 
    #    *update_trigger_dir + ['timestamp_trigger.py'])
65
 
    #update_trigger =  os.path.join(
66
 
    #    *update_trigger_dir + ['update_trigger.py'])
67
 
 
68
63
    if os.path.exists(local_files.FILE_INI):
69
 
        return
 
64
        # load the username and password from the keyring
 
65
        try:
 
66
            data = gnomekeyring.find_items_sync(gnomekeyring.ITEM_GENERIC_SECRET,
 
67
                                            {'desktopcouch': 'basic'})
 
68
        except gnomekeyring.NoMatchError:
 
69
            data = None
 
70
        if data:
 
71
            username, password = data[0].secret.split(":")
 
72
            return username, password
 
73
        # otherwise fall through; for some reason the access details aren't
 
74
        # in the keyring, so re-create the ini file and do it all again
 
75
 
 
76
    # randomly generate tokens and usernames
 
77
    def make_random_string(count):
 
78
        return ''.join([
 
79
             random.SystemRandom().choice(ACCEPTABLE_USERNAME_PASSWORD_CHARS)
 
80
                        for x in range(count)])
 
81
 
 
82
    admin_account_username = make_random_string(10)
 
83
    admin_account_basic_auth_password = make_random_string(10)
 
84
    consumer_key = make_random_string(10)
 
85
    consumer_secret = make_random_string(10)
 
86
    token = make_random_string(10)
 
87
    token_secret = make_random_string(10)
 
88
    db_dir = local_files.DIR_DB
70
89
 
71
90
    local = {
72
91
        'couchdb': {
73
 
            'database_dir': local_files.DIR_DB,
74
 
            'view_index_dir': local_files.DIR_DB,
 
92
            'database_dir': db_dir,
 
93
            'view_index_dir': db_dir,
75
94
        },
76
95
        'httpd': {
77
96
            'bind_address': '127.0.0.1',
78
 
            'port': "0",
 
97
            'port': port,
79
98
        },
80
99
        'log': {
81
100
            'file': local_files.FILE_LOG,
82
101
            'level': 'info',
83
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
        ## },
84
118
    }
85
119
 
86
120
    dump_ini(local, local_files.FILE_INI)
 
121
    # save admin account details in keyring
 
122
    item_id = gnomekeyring.item_create_sync(
 
123
            None,
 
124
            gnomekeyring.ITEM_GENERIC_SECRET,
 
125
            'Desktop Couch user authentication',
 
126
            {'desktopcouch': 'basic'},
 
127
            "%s:%s" % (
 
128
            admin_account_username, admin_account_basic_auth_password),
 
129
            True)
 
130
    # and oauth tokens
 
131
    item_id = gnomekeyring.item_create_sync(
 
132
            None,
 
133
            gnomekeyring.ITEM_GENERIC_SECRET,
 
134
            'Desktop Couch user authentication',
 
135
            {'desktopcouch': 'oauth'},
 
136
            "%s:%s:%s:%s" % (
 
137
            consumer_key, consumer_secret, token, token_secret),
 
138
            True)
 
139
    return (admin_account_username, admin_account_basic_auth_password)
87
140
 
88
141
def run_couchdb():
89
142
    """Actually start the CouchDB process"""
90
143
    local_exec = local_files.COUCH_EXEC_COMMAND + ['-b']
91
144
    try:
92
 
        retcode = subprocess.call(local_exec, shell=False)
 
145
        # subprocess is buggy.  Chad patched, but that takes time to propagate.
 
146
        proc = subprocess.Popen(local_exec)
 
147
        while True:
 
148
            try:
 
149
                retcode = proc.wait()
 
150
                break
 
151
            except OSError, e:
 
152
                if e.errno == errno.EINTR:
 
153
                    continue
 
154
                raise
93
155
        if retcode < 0:
94
156
            print >> sys.stderr, "Child was terminated by signal", -retcode
95
157
        elif retcode > 0:
99
161
        exit(1)
100
162
 
101
163
def update_design_documents():
102
 
    """Check system design documents and update any that need updating"""
103
 
    pass
104
 
 
105
 
def write_bookmark_file():
 
164
    """Check system design documents and update any that need updating
 
165
 
 
166
    A database should be created if
 
167
      $XDG_DATA_DIRs/desktop-couch/databases/dbname/database.cfg exists
 
168
    Design docs are defined by the existence of
 
169
      $XDG_DATA_DIRs/desktop-couch/databases/dbname/_design/designdocname/views/viewname/map.js
 
170
      reduce.js may also exist in the same folder.
 
171
    """
 
172
    for base in xdg.BaseDirectory.xdg_data_dirs:
 
173
        db_spec = os.path.join(
 
174
            base, "desktop-couch", "databases", "*", "database.cfg")
 
175
        for database_path in glob.glob(db_spec):
 
176
            database_root = os.path.split(database_path)[0]
 
177
            database_name = os.path.split(database_root)[1]
 
178
            # Just the presence of database.cfg is enough to create the database
 
179
            db = CouchDatabase(database_name, create=True)
 
180
            # look for design documents
 
181
            dd_spec = os.path.join(
 
182
                database_root, "_design", "*", "views", "*", "map.js")
 
183
            for dd_path in glob.glob(dd_spec):
 
184
                view_root = os.path.split(dd_path)[0]
 
185
                view_name = os.path.split(view_root)[1]
 
186
                dd_root = os.path.split(os.path.split(view_root)[0])[0]
 
187
                dd_name = os.path.split(dd_root)[1]
 
188
 
 
189
                def load_js_file(filename_no_extension):
 
190
                    fn = os.path.join(
 
191
                        view_root, "%s.js" % (filename_no_extension))
 
192
                    if not os.path.isfile(fn): return None
 
193
                    fp = open(fn)
 
194
                    data = fp.read()
 
195
                    fp.close()
 
196
                    return data
 
197
 
 
198
                mapjs = load_js_file("map")
 
199
                reducejs = load_js_file("reduce")
 
200
 
 
201
                # XXX check whether this already exists or not, rather
 
202
                # than inefficiently just overwriting it regardless
 
203
                db.add_view(view_name, mapjs, reducejs, dd_name)
 
204
 
 
205
def write_bookmark_file(username, password):
106
206
    """Write out an HTML document that the user can bookmark to find their DB"""
107
207
    bookmark_file = os.path.join(local_files.DIR_DB, "couchdb.html")
108
208
 
109
 
    if os.path.exists(os.path.join(os.path.split(__file__)[0], "../data/couchdb.tmpl")):
110
 
        bookmark_template = os.path.join(os.path.split(__file__)[0], "../data/couchdb.tmpl")
 
209
    if os.path.exists(
 
210
            os.path.join(os.path.split(__file__)[0], "../data/couchdb.tmpl")):
 
211
        bookmark_template = os.path.join(
 
212
            os.path.split(__file__)[0], "../data/couchdb.tmpl")
111
213
    else:
112
214
        for base in xdg.BaseDirectory.xdg_data_dirs:
113
215
            template_path = os.path.join(base, "desktopcouch", "couchdb.tmpl")
114
216
            if os.path.exists(template_path):
115
 
                bookmark_template = os.path.join(os.path.split(__file__)[0], template_path)
 
217
                bookmark_template = os.path.join(
 
218
                    os.path.split(__file__)[0], template_path)
116
219
 
117
220
    fp = open(bookmark_template)
118
221
    html = fp.read()
119
222
    fp.close()
120
223
 
121
 
    time.sleep(1)
122
 
    pid = desktopcouch.find_pid()
123
 
    port = desktopcouch.find_port(pid)
 
224
    port = None
 
225
    for retry in xrange(10000, 0, -1):
 
226
        pid = desktopcouch.find_pid(start_if_not_running=False)
 
227
        try:
 
228
            port = desktopcouch.find_port()
 
229
            if not port is None:
 
230
                break
 
231
        except IOError:
 
232
            if retry == 1:
 
233
                raise
 
234
            time.sleep(0.1)
 
235
            continue
124
236
 
125
 
    fp = open(bookmark_file, "w")
126
 
    fp.write(html.replace("[[COUCHDB_PORT]]", port))
127
 
    fp.close()
128
 
    print "Browse your desktop CouchDB at file://%s" % \
129
 
      os.path.realpath(bookmark_file)
 
237
    if port is None:
 
238
        print ("We couldn't find desktop-CouchDB's network port.  Bookmark "
 
239
               "file not written.")
 
240
        try:
 
241
            os.remove(bookmark_file)
 
242
        except OSError:
 
243
            pass
 
244
    else:
 
245
        fp = open(bookmark_file, "w")
 
246
        out = html.replace("[[COUCHDB_PORT]]", str(port))
 
247
        out = out.replace("[[COUCHDB_USERNAME]]", username)
 
248
        out = out.replace("[[COUCHDB_PASSWORD]]", password)
 
249
        fp.write(out)
 
250
        fp.close()
 
251
        print "Browse your desktop CouchDB at file://%s" % \
 
252
          os.path.realpath(bookmark_file)
130
253
 
131
254
def start_couchdb():
132
255
    """Execute each step to start a desktop CouchDB"""
133
 
    create_ini_file()
 
256
    username, password = create_ini_file()
134
257
    run_couchdb()
135
 
    update_design_documents()
136
 
    write_bookmark_file()
 
258
    # Note that we do not call update_design_documents here. This is because
 
259
    # Couch won't actually have started yet, so when update_design_documents
 
260
    # calls the Records API, that will call back into get_port and we end up
 
261
    # starting Couch again. Instead, get_port calls update_design_documents
 
262
    # *after* Couch startup has occurred.
 
263
    write_bookmark_file(username, password)
 
264
 
137
265
 
138
266
if __name__ == "__main__":
139
267
    start_couchdb()