Merge lp:~salgado/launchpad/easier-read-only-switch into lp:launchpad

Proposed by Guilherme Salgado
Status: Merged
Merged at revision: not available
Proposed branch: lp:~salgado/launchpad/easier-read-only-switch
Merge into: lp:launchpad
Diff against target: 1294 lines (+510/-155)
28 files modified
configs/development/launchpad-lazr.conf (+6/-4)
configs/replicated-development/launchpad-lazr.conf (+4/-2)
configs/test-playground/launchpad-lazr.conf (+4/-2)
configs/testrunner/launchpad-lazr.conf (+6/-2)
lib/canonical/config/__init__.py (+25/-52)
lib/canonical/config/schema-lazr.conf (+4/-7)
lib/canonical/config/tests/test_database_config.py (+61/-0)
lib/canonical/database/sqlbase.py (+15/-14)
lib/canonical/launchpad/configure.zcml (+0/-1)
lib/canonical/launchpad/doc/canonical-config.txt (+1/-1)
lib/canonical/launchpad/pagetests/standalone/xx-dbpolicy.txt (+1/-1)
lib/canonical/launchpad/pagetests/standalone/xx-opstats.txt (+2/-2)
lib/canonical/launchpad/pagetests/standalone/xx-read-only-mode.txt (+10/-18)
lib/canonical/launchpad/readonly.py (+86/-0)
lib/canonical/launchpad/tests/readonly.py (+34/-0)
lib/canonical/launchpad/tests/test_readonly.py (+78/-0)
lib/canonical/launchpad/webapp/adapter.py (+13/-2)
lib/canonical/launchpad/webapp/authorization.py (+2/-2)
lib/canonical/launchpad/webapp/configure.zcml (+6/-0)
lib/canonical/launchpad/webapp/dbpolicy.py (+3/-6)
lib/canonical/launchpad/webapp/login.py (+2/-1)
lib/canonical/launchpad/webapp/publication.py (+31/-9)
lib/canonical/launchpad/webapp/tests/test_authorization.py (+6/-7)
lib/canonical/launchpad/webapp/tests/test_dbpolicy.py (+6/-9)
lib/canonical/launchpad/webapp/tests/test_publication.py (+90/-1)
lib/canonical/librarian/client.py (+5/-3)
lib/canonical/testing/ftests/test_mockdb.py (+4/-4)
lib/lp/translations/model/pofile.py (+5/-5)
To merge this branch: bzr merge lp:~salgado/launchpad/easier-read-only-switch
Reviewer Review Type Date Requested Status
Tom Haddon Approve
Gary Poster (community) Approve
Review via email: mp+17302@code.launchpad.net
To post a comment you must log in.
Revision history for this message
Guilherme Salgado (salgado) wrote :
Download full text (4.7 KiB)

= Summary =

Today, switching an app server to run in read-only mode means putting
new configs in place and restarting the server using a different
LPCONFIG env var. That's rather painful to LOSAs, who need to do it for
all app servers before a roll out, so we'll make that easier.

== Proposed fix ==

From now on, to turn read-only mode on, one just need to create a
read-only.txt file under the root of our tree. No more config changes and/or
server restarts.

The [database] config section doesn't have main_master/slave variables
anymore. These are now properties on DatabaseConfig, which return
ro_main_master/slave or rw_main_master/slave, depending on the mode we are on
(i.e. what is returned by IIsReadOnly.isReadOnly()).

When .isReadOnly() is called as part of processing a request, we cache its
return value in the request so that it doesn't need to hit the filesystem in
further calls.

At the beginning of a request, we check the databases to which the main_master
and main_slave stores are connected to. If they differ from
dbconfig.main_master/slave, we remove these stores from ZStorm, forcing them
to be created again the next time they are needed. When they're created again
they get a new connection, which uses dbconfig.main_master/slave and thus will
go to the correct db.

After removing the stores from ZStorm, this is what happens when the store is
needed again:

   - ZStorm.get('main-master', 'launchpad:main-master') is called
   - ZStorm finds a Database object for that uri
   - ZStorm instantiates a new Store object, passing the existing Database
   - Store calls Database.connect(), which instantiates and returns a
     Connection
   - The Connection's __init__() calls Database.raw_connect(), which finally
     reads dbconfig.main_master/slave and creates a new psycopg2 connection
     using that.

The above only works because our custom Database class (LaunchpadDatabase)
sets self._dsn inside raw_connect(), instead of inside __init__() (like its
parent class does). That's needed because the Database objects are reused
across threads/stores by ZStorm.

Tests
=====

- pagetests/standalone/xx-read-only-mode.txt works as the integration test
- test_readonly.py
- webapp/tests/test_publication.py

= Launchpad lint =

Checking for conflicts. and issues in doctests and templates.
Running jslint, xmllint, pyflakes, and pylint.
Using normal rules.

Linting changed files:
  lib/canonical/database/sqlbase.py
  configs/test-playground/launchpad-lazr.conf
  lib/canonical/librarian/client.py
  lib/canonical/launchpad/webapp/publication.py
  lib/canonical/config/schema-lazr.conf
  lib/canonical/launchpad/doc/canonical-config.txt
  lib/canonical/launchpad/webapp/authorization.py
  lib/canonical/launchpad/webapp/tests/test_publication.py
  lib/canonical/launchpad/readonly.py
  lib/canonical/launchpad/pagetests/standalone/xx-read-only-mode.txt
  lib/canonical/testing/ftests/test_mockdb.py
  lib/canonical/launchpad/pagetests/standalone/xx-opstats.txt
  lib/canonical/launchpad/webapp/login.py
  lib/lp/translations/model/pofile.py
  BRANCH.TODO
  lib/canonical/launchpad/tests/test_readonly.py
  lib/canonical/launchpad/webapp/configure.zcml
  lib/canon...

Read more...

Revision history for this message
Gary Poster (gary) wrote :

On IRC, I asked for a comment explaining use of _template suffix for readonly dbs in config.

Correcting this would be nice:

lib/canonical/launchpad/webapp/publication.py
    573: [E1002, LaunchpadBrowserPublication.beginErrorHandlingTransaction] Use super on an old style class

DatabaseConfig.main_master: zcml issue we discussed...

I'm eager to see QA on staging from SAs. Have you discussed with LOSAs how we might test this on staging? I wonder if this would warrant a CP.

I asked you about the first two assertions in TestDatabaseConfig.test_overlay, because I didn't really see the point of them. You said that they look up the values in the wrong config section, which doesn't seem all that compelling to me, but you also said that you had simply moved those from the class' docstring to the new location. I won't worry about them, then.

The following looks like it deserves a bug.

397 + # XXX: Instead of all this we could probably just remove the
398 + # store from ZStorm (zstorm.remove(store)).

Twice you assert that we must use a rw db in a given context. Would it be relatively easy and quick to hint as to why, or do you think that it is quite obvious in context of the full code, rather than the diff?

What would you think of _touch_read_only_file and _remove_read_only_file losing their initial underline and moving to a "testing" module somewhere? They seem to be imported a lot for tests, so they really are a testing API AFAICT, and should be encouraged for (only) that purpose.

Shouldn't lib/canonical/launchpad/readonly.py have an __all__?

In this line from lib/canonical/launchpad/readonly.py...

603 +named read-only.txt under the root of the Launchpad tree.

...I suggest s/under/in/.

In lib/canonical/launchpad/readonly.py, I suggest using os.path.absdir to calculate the ``root`` path just for cleanliness. (There's probably a way to get the path from an environmental var that buildout sets so you don't have to do path funny business, but there's precedent for the funny business, so I'm OK with what you have.)

lib/canonical/launchpad/readonly.py: _currently_in_read_only: is that really part of the utility's interface? It seems like it is an implementation detail.

lib/canonical/launchpad/readonly.py: Just for Python 2.5 fun, why don't you use ``from __future__ import with_statement`` and then ``with self._lock`` instead of the whole try/except acquire/release dance?

I'm submitting these notes now since I am breaking for lunch. More when I return.

Revision history for this message
Guilherme Salgado (salgado) wrote :

There was only one test failing after my changes, and the fix for it is below:

=== modified file 'lib/canonical/launchpad/webapp/tests/test_authorization.py'
--- lib/canonical/launchpad/webapp/tests/test_authorization.py 2009-06-25 05:30:52 +0000
+++ lib/canonical/launchpad/webapp/tests/test_authorization.py 2010-01-13 18:30:34 +0000
@@ -18,6 +18,7 @@
 from canonical.lazr.interfaces import IObjectPrivacy

 from canonical.launchpad.interfaces.account import IAccount
+from canonical.launchpad.readonly import IIsReadOnly
 from canonical.launchpad.security import AuthorizationBase
 from canonical.launchpad.webapp.authentication import LaunchpadPrincipal
 from canonical.launchpad.webapp.authorization import (
@@ -129,6 +130,11 @@
         pass

+class FakeIsReadOnlyUtility:
+ def isReadOnly(self):
+ return False
+
+
 class TestCheckPermissionCaching(CleanUp, unittest.TestCase):
     """Test the caching done by `LaunchpadSecurityPolicy.checkPermission`."""

@@ -137,6 +143,9 @@
         super(TestCheckPermissionCaching, self).setUp()
         self.factory = ObjectFactory()
         ztapi.provideUtility(IStoreSelector, FakeStoreSelector)
+ # LaunchpadSecurityPolicy.checkPermission() needs an IIsReadOnly
+ # utility, so we provide a fake one to please it.
+ ztapi.provideUtility(IIsReadOnly, FakeIsReadOnlyUtility())

     def makeRequest(self):
         """Construct an arbitrary `LaunchpadBrowserRequest` object."""

Revision history for this message
Guilherme Salgado (salgado) wrote :
Download full text (3.9 KiB)

On Wed, 2010-01-13 at 18:12 +0000, Gary Poster wrote:
> On IRC, I asked for a comment explaining use of _template suffix for readonly dbs in config.
>
> Correcting this would be nice:
>
> lib/canonical/launchpad/webapp/publication.py
> 573: [E1002,
> LaunchpadBrowserPublication.beginErrorHandlingTransaction] Use super
> on an old style class

It actually is a new-style class, I think thanks to the __metaclass__ =
type line we have there. I don't know why pylint is confused here, but
I could suppress that error.

>
> DatabaseConfig.main_master: zcml issue we discussed...
>
> I'm eager to see QA on staging from SAs. Have you discussed with
> LOSAs how we might test this on staging? I wonder if this would
> warrant a CP.

No, I haven't talked to them about that yet, but I'm guessing we'll need
a read-only database to be able to QA this properly.

We could CP it a few days before the roll out. That way we get some
real-world testing (on edge) of the changes I did to webapp/ code and we
can still take advantage of the easier switching during the roll out
itself.

>
> I asked you about the first two assertions in
> TestDatabaseConfig.test_overlay, because I didn't really see the point
> of them. You said that they look up the values in the wrong config
> section, which doesn't seem all that compelling to me, but you also
> said that you had simply moved those from the class' docstring to the
> new location. I won't worry about them, then.
>
> The following looks like it deserves a bug.
>
> 397 + # XXX: Instead of all this we could probably just
> remove the
> 398 + # store from ZStorm (zstorm.remove(store)).

I'm going to do the change there (in another branch) and run the test
suite again. If it succeeds, great, otherwise I'll revert it and the
bug probably won't be needed. For now I'll just remove the XXX from
here.

>
> Twice you assert that we must use a rw db in a given context. Would
> it be relatively easy and quick to hint as to why, or do you think
> that it is quite obvious in context of the full code, rather than the
> diff?

In the case of database/sqlbase.py it may not be, so I changed the
comment. The other is in librarian/client.py, in the method to upload a
new file, so I don't think a comment change is needed there.

>
> What would you think of _touch_read_only_file and
> _remove_read_only_file losing their initial underline and moving to a
> "testing" module somewhere? They seem to be imported a lot for tests,
> so they really are a testing API AFAICT, and should be encouraged for
> (only) that purpose.

That's correct, they are a testing API and as such I'll move them to a
more appropriate place.

>
> Shouldn't lib/canonical/launchpad/readonly.py have an __all__?

Yes, that and a __metaclass__ one.

>
> In this line from lib/canonical/launchpad/readonly.py...
>
> 603 +named read-only.txt under the root of the Launchpad tree.
>
> ...I suggest s/under/in/.

Sure thing; changed.

>
> In lib/canonical/launchpad/readonly.py, I suggest using os.path.absdir
> to calculate the ``root`` path just for cleanliness. (There's
> probably a way to get the path from an environmental var...

Read more...

Revision history for this message
Gary Poster (gary) wrote :

Continuing the review notes from the previous comment...

You are using ztapi because other code is doing it (``ztapi.provideUtility(IIsReadOnly, FakeIsReadOnlyUtility())`` in test_authorization.py). ztapi is deprecated in favor of zope.component; it now has all the necessary convenience functions (although the order of arguments for zope.component.provideUtility is reversed IIRC). I'd like to see new code use the "one way to do it".

In lib/canonical/launchpad/readonly.py...

686 + # Only call get_current_browser_request() when we have an interaction,
687 + # or else it will choke.
688 + if queryInteraction() is not None:
689 + request = get_current_browser_request()

...this is a look-before-you-leap approach. Python style guides generally prefers try-except, and I do too. :-)

I question have IIsReadOnly as a utility. Why don't we just have this as a function? The ``_currently_in_read_only`` value (which I said I believe is an implementation detail above) would be a module global--and you already have a mutex to handle modifying that well anyway. Then people would import the function rather than the interface, and things would be more direct. The only downside I can see is that it might be a bit harder to change the behavior of the isReadOnly function for tests--but do you really need to? So in sum, making this a utility doesn't buy us anything AFAICS, and adds extra indirection when a simple function would do.

Do I need to know why/when we use DatabaseBlockedPolicy?

OK, saving these comments; back soon. :-)

Revision history for this message
Guilherme Salgado (salgado) wrote :

On Wed, 2010-01-13 at 19:54 +0000, Gary Poster wrote:
> Continuing the review notes from the previous comment...
>
> You are using ztapi because other code is doing it
> (``ztapi.provideUtility(IIsReadOnly, FakeIsReadOnlyUtility())`` in
> test_authorization.py). ztapi is deprecated in favor of
> zope.component; it now has all the necessary convenience functions
> (although the order of arguments for zope.component.provideUtility is
> reversed IIRC). I'd like to see new code use the "one way to do it".

I didn't know about that and indeed had just copied and pasted the
existing code. That test no longer uses ztapi now.

>
> In lib/canonical/launchpad/readonly.py...
>
> 686 + # Only call get_current_browser_request() when we have an
> interaction,
> 687 + # or else it will choke.
> 688 + if queryInteraction() is not None:
> 689 + request = get_current_browser_request()
>
> ...this is a look-before-you-leap approach. Python style guides
> generally prefers try-except, and I do too. :-)

I didn't do that because I think get_current_browser_request() should
return None and not let an AttributeError through when there's no
interaction. I'd fix the function myself, but it's in lazr.restful so I
wouldn't benefit instantly.

I guess the best would be to turn my comment into an XXX and file a bug
against lazr.restful?

>
> I question have IIsReadOnly as a utility. Why don't we just have this
> as a function? The ``_currently_in_read_only`` value (which I said I
> believe is an implementation detail above) would be a module
> global--and you already have a mutex to handle modifying that well
> anyway. Then people would import the function rather than the
> interface, and things would be more direct. The only downside I can
> see is that it might be a bit harder to change the behavior of the
> isReadOnly function for tests--but do you really need to? So in sum,
> making this a utility doesn't buy us anything AFAICS, and adds extra
> indirection when a simple function would do.

I think that'd work, and I don't need to change its behaviour for tests,
so I'll give this a try tomorrow morning.

>
> Do I need to know why/when we use DatabaseBlockedPolicy?

Oh, right, that's the policy used for the +opstas page, to make sure
nothing tries to use the database during a request for that page.

>
> OK, saving these comments; back soon. :-)

--
Guilherme Salgado <email address hidden>

Revision history for this message
Gary Poster (gary) wrote :

test_no_mode_changes: Would it be worthwhile to check that we have the same connection object? It is black box, but it might be nice to verify that we don't trash the connection unnecessarily.

Now relying to your replies:

> > Correcting this would be nice:
> >
> > lib/canonical/launchpad/webapp/publication.py
> > 573: [E1002,
> > LaunchpadBrowserPublication.beginErrorHandlingTransaction] Use super
> > on an old style class
>
> It actually is a new-style class, I think thanks to the __metaclass__ =
> type line we have there. I don't know why pylint is confused here, but
> I could suppress that error.

Yes, let's go ahead and do that, please.

In regards to your comments on QA and a CP, I have a call on Friday with Tom and James. I'll talk to them about that then. I'll invite you on the call if you are available.

> > ...this is a look-before-you-leap approach. Python style guides
> > generally prefers try-except, and I do too. :-)
>
> I didn't do that because I think get_current_browser_request() should
> return None and not let an AttributeError through when there's no
> interaction. I'd fix the function myself, but it's in lazr.restful so I
> wouldn't benefit instantly.

I see.

> I guess the best would be to turn my comment into an XXX and file a bug
> against lazr.restful?

+1.

The rest looks good. Thank you very much for the great work.

I'm going to approve this, but ask Tom Haddon for a review so that we have a discussion of QA before we merge.

Gary

review: Approve
Revision history for this message
Gary Poster (gary) wrote :

Tom, I asked you for a review so that we could discuss QA.

I think we're delivering exactly what we discussed, but I'd also like verification of that as well. As Salgado says, if you add a read-only.txt to the top the tree, or remove it, the application should notice as soon as it processes a new request. It will then acknowledge the change in the main launchpad.log. Because our requests time out in about 15 seconds, webapp machines should be ready to switch easily within a minute. This does not affect long running processes, which would presumably need to be stopped when switching to readonly, and restarted when resuming (as usual?).

Revision history for this message
Tom Haddon (mthaddon) wrote :

This sounds good in terms of delivering what was specified, and we're happy to deal with long running processes by killing them if we need to when switching from read-write to read-only.

In terms of QA, I think we'd want to start by merging this branch as is onto staging, creating the read-only.txt file and then trying to perform some changes. Just let me know when the branch is ready.

We'll also want to prepare the changes to lp-production-configs and canonical-identity-provider at the same time, from what I understand.

Thanks, Tom

Revision history for this message
Guilherme Salgado (salgado) wrote :

On Wed, 2010-01-13 at 21:30 +0000, Gary Poster wrote:
> Review: Approve
> test_no_mode_changes: Would it be worthwhile to check that we have the
> same connection object? It is black box, but it might be nice to
> verify that we don't trash the connection unnecessarily.

It won't hurt to do this extra check there, so, sure.

>
> Now relying to your replies:
>
> > > Correcting this would be nice:
> > >
> > > lib/canonical/launchpad/webapp/publication.py
> > > 573: [E1002,
> > > LaunchpadBrowserPublication.beginErrorHandlingTransaction] Use super
> > > on an old style class
> >
> > It actually is a new-style class, I think thanks to the __metaclass__ =
> > type line we have there. I don't know why pylint is confused here, but
> > I could suppress that error.
>
> Yes, let's go ahead and do that, please.

Done.

>
> In regards to your comments on QA and a CP, I have a call on Friday
> with Tom and James. I'll talk to them about that then. I'll invite
> you on the call if you are available.

Ok

>
> > > ...this is a look-before-you-leap approach. Python style guides
> > > generally prefers try-except, and I do too. :-)
> >
> > I didn't do that because I think get_current_browser_request() should
> > return None and not let an AttributeError through when there's no
> > interaction. I'd fix the function myself, but it's in lazr.restful so I
> > wouldn't benefit instantly.
>
> I see.
>
> > I guess the best would be to turn my comment into an XXX and file a bug
> > against lazr.restful?
>
> +1.

https://bugs.edge.launchpad.net/lazr.restful/+bug/507447

>
> The rest looks good. Thank you very much for the great work.
>
> I'm going to approve this, but ask Tom Haddon for a review so that we
> have a discussion of QA before we merge.
>
> Gary

--
Guilherme Salgado <email address hidden>

Revision history for this message
Guilherme Salgado (salgado) wrote :

http://paste.ubuntu.com/356558/ is how I got rid of the IIsReadOnly utility by using a function and module globals.

Revision history for this message
Tom Haddon (mthaddon) wrote :

I've tested this on staging and can confirm it works as expected. So, fwiw, +1 from me :)

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'configs/development/launchpad-lazr.conf'
2--- configs/development/launchpad-lazr.conf 2009-12-21 03:18:24 +0000
3+++ configs/development/launchpad-lazr.conf 2010-01-18 11:28:17 +0000
4@@ -91,8 +91,12 @@
5
6
7 [database]
8-main_master: dbname=launchpad_dev
9-main_slave: dbname=launchpad_dev
10+rw_main_master: dbname=launchpad_dev
11+rw_main_slave: dbname=launchpad_dev
12+# Use our _template databases here just so that we have different values from
13+# the rw_* configs.
14+ro_main_master: dbname=launchpad_dev_template
15+ro_main_slave: dbname=launchpad_dev_template
16 auth_master: dbname=launchpad_dev
17 auth_slave: dbname=launchpad_dev
18
19@@ -132,8 +136,6 @@
20 bzr_imports_root_url: file:///tmp/bazaar-branches
21 geoip_database: /usr/share/GeoIP/GeoLiteCity.dat
22 geonames_identity: lpdev
23-# Set to True to test read-only mode.
24-read_only: False
25 storm_cache: generational
26 storm_cache_size: 100
27
28
29=== modified file 'configs/replicated-development/launchpad-lazr.conf'
30--- configs/replicated-development/launchpad-lazr.conf 2008-10-14 11:10:35 +0000
31+++ configs/replicated-development/launchpad-lazr.conf 2010-01-18 11:28:17 +0000
32@@ -6,8 +6,10 @@
33 extends: ../development/launchpad-lazr.conf
34
35 [database]
36-main_master: dbname=launchpad_dev
37-main_slave: dbname=launchpad_dev_slave
38+rw_main_master: dbname=launchpad_dev
39+rw_main_slave: dbname=launchpad_dev_slave
40+ro_main_master: dbname=launchpad_dev
41+ro_main_slave: dbname=launchpad_dev_slave
42 auth_master: dbname=launchpad_dev
43 auth_slave: dbname=launchpad_dev_slave
44
45
46=== modified file 'configs/test-playground/launchpad-lazr.conf'
47--- configs/test-playground/launchpad-lazr.conf 2008-11-10 16:12:10 +0000
48+++ configs/test-playground/launchpad-lazr.conf 2010-01-18 11:28:17 +0000
49@@ -6,7 +6,9 @@
50 extends: ../development/launchpad-lazr.conf
51
52 [database]
53-main_master: dbname=launchpad_ftest_playground
54-main_slave: dbname=launchpad_ftest_playground
55+rw_main_master: dbname=launchpad_ftest_playground
56+rw_main_slave: dbname=launchpad_ftest_playground
57+ro_main_master: dbname=launchpad_ftest_playground
58+ro_main_slave: dbname=launchpad_ftest_playground
59 auth_master: dbname=launchpad_ftest_playground
60 auth_slave: dbname=launchpad_ftest_playground
61
62=== modified file 'configs/testrunner/launchpad-lazr.conf'
63--- configs/testrunner/launchpad-lazr.conf 2009-12-21 03:18:24 +0000
64+++ configs/testrunner/launchpad-lazr.conf 2010-01-18 11:28:17 +0000
65@@ -45,8 +45,12 @@
66 error_dir: /var/tmp/codehosting.test
67
68 [database]
69-main_master: dbname=launchpad_ftest
70-main_slave: dbname=launchpad_ftest
71+rw_main_master: dbname=launchpad_ftest
72+rw_main_slave: dbname=launchpad_ftest
73+# Use our _template databases here just so that we have different values from
74+# the rw_* configs.
75+ro_main_master: dbname=launchpad_ftest_template
76+ro_main_slave: dbname=launchpad_ftest_template
77 auth_master: dbname=launchpad_ftest
78 auth_slave: dbname=launchpad_ftest
79 randomise_select_results: true
80
81=== modified file 'lib/canonical/config/__init__.py'
82--- lib/canonical/config/__init__.py 2010-01-14 02:02:34 +0000
83+++ lib/canonical/config/__init__.py 2010-01-18 11:28:17 +0000
84@@ -22,6 +22,8 @@
85 from lazr.config import ImplicitTypeSchema
86 from lazr.config.interfaces import ConfigErrors
87
88+from canonical.launchpad.readonly import is_read_only
89+
90
91 __all__ = [
92 'DatabaseConfig',
93@@ -164,7 +166,7 @@
94 def setProcess(self, process_name):
95 """Set the name of the process to select a conf file.
96
97- This method is used to set the process_name is if should be
98+ This method is used to set the process_name if it should be
99 different from the name obtained from sys.argv[0]. CanonicalConfig
100 will try to load <process_name>-lazr.conf if it exists. Otherwise,
101 it will load launchpad-lazr.conf.
102@@ -221,14 +223,15 @@
103 self.threads = root_options.threads
104
105 def generate_overrides(self):
106- """Ensure correct config .zcml overrides will be called.
107+ """Ensure correct config.zcml overrides will be called.
108
109 Call this method before letting any ZCML processing occur.
110 """
111 loader_file = os.path.join(self.root, '+config-overrides.zcml')
112 loader = open(loader_file, 'w')
113
114- print >> loader, """<configure xmlns="http://namespaces.zope.org/zope">
115+ print >> loader, """
116+ <configure xmlns="http://namespaces.zope.org/zope">
117 <!-- This file automatically generated using
118 canonical.config.CanonicalConfig.generate_overrides.
119 DO NOT EDIT. -->
120@@ -366,62 +369,32 @@
121
122
123 class DatabaseConfig:
124- """A class to provide the Launchpad database configuration.
125-
126- The dbconfig option overlays the database configurations of a
127- chosen config section over the base section:
128-
129- >>> from canonical.config import config, dbconfig
130- >>> print config.database.main_master
131- dbname=...
132- >>> print config.database.dbuser
133- Traceback (most recent call last):
134- ...
135- AttributeError: ...
136- >>> print config.launchpad.main_master
137- Traceback (most recent call last):
138- ...
139- AttributeError: ...
140- >>> print config.launchpad.dbuser
141- launchpad_main
142- >>> print config.librarian.dbuser
143- librarian
144-
145- >>> dbconfig.setConfigSection('librarian')
146- >>> print dbconfig.main_master
147- dbname=...
148- >>> print dbconfig.dbuser
149- librarian
150-
151- >>> dbconfig.setConfigSection('launchpad')
152- >>> print dbconfig.main_master
153- dbname=...
154- >>> print dbconfig.dbuser
155- launchpad_main
156-
157- Some values are required to have a value, such as dbuser. So we
158- get an exception if they are not set:
159-
160- >>> config.codehosting.dbuser
161- Traceback (most recent call last):
162- ...
163- AttributeError: No section key named dbuser.
164- >>> dbconfig.setConfigSection('codehosting')
165- >>> print dbconfig.dbuser
166- Traceback (most recent call last):
167- ...
168- ValueError: dbuser must be set
169- >>> dbconfig.setConfigSection('launchpad')
170- """
171+ """A class to provide the Launchpad database configuration."""
172 _config_section = None
173 _db_config_attrs = frozenset([
174 'dbuser', 'auth_dbuser',
175- 'main_master', 'main_slave', 'auth_master', 'auth_slave',
176+ 'rw_main_master', 'rw_main_slave',
177+ 'ro_main_master', 'ro_main_slave', 'auth_master', 'auth_slave',
178 'db_statement_timeout', 'db_statement_timeout_precision',
179 'isolation_level', 'randomise_select_results',
180 'soft_request_timeout', 'storm_cache', 'storm_cache_size'])
181 _db_config_required_attrs = frozenset([
182- 'dbuser', 'main_master', 'main_slave', 'auth_master', 'auth_slave'])
183+ 'dbuser', 'rw_main_master', 'rw_main_slave', 'ro_main_master',
184+ 'ro_main_slave', 'auth_master', 'auth_slave'])
185+
186+ @property
187+ def main_master(self):
188+ if is_read_only():
189+ return self.ro_main_master
190+ else:
191+ return self.rw_main_master
192+
193+ @property
194+ def main_slave(self):
195+ if is_read_only():
196+ return self.ro_main_slave
197+ else:
198+ return self.rw_main_slave
199
200 def setConfigSection(self, section_name):
201 self._config_section = section_name
202
203=== modified file 'lib/canonical/config/schema-lazr.conf'
204--- lib/canonical/config/schema-lazr.conf 2009-11-17 11:27:51 +0000
205+++ lib/canonical/config/schema-lazr.conf 2010-01-18 11:28:17 +0000
206@@ -529,8 +529,10 @@
207 # Note that user is not included, as it is specified by the dbuser
208 # option. This allows config sections to easily override just the dbuser.
209 # datatype: pgconnection
210-main_master: dbname=launchpad_prod_3 host=wildcherry.canonical.com
211-main_slave: dbname=launchpad_prod_2 host=chokecherry.canonical.com
212+rw_main_master: dbname=launchpad_prod_3 host=wildcherry.canonical.com
213+rw_main_slave: dbname=launchpad_prod_2 host=chokecherry.canonical.com
214+ro_main_master: dbname=launchpad_standalone_1 host=chokecherry.canonical.com
215+ro_main_slave: dbname=launchpad_standalone_1 host=chokecherry.canonical.com
216 auth_master: dbname=launchpad_prod_3 host=wildcherry.canonical.com
217 auth_slave: dbname=launchpad_prod_2 host=chokecherry.canonical.com
218
219@@ -872,11 +874,6 @@
220 # datatype: boolean
221 launch: True
222
223-# If true, Launchpad is running in read-only mode. Attempts to
224-# write to the Launchpad database will be denied, and an explanatory
225-# error message displayed to the user.
226-read_only: False
227-
228 # If true, the main template will be styled so that it is
229 # obvious to the end user that they are using a demo system
230 # and that any changes they make will be lost at some point.
231
232=== added file 'lib/canonical/config/tests/test_database_config.py'
233--- lib/canonical/config/tests/test_database_config.py 1970-01-01 00:00:00 +0000
234+++ lib/canonical/config/tests/test_database_config.py 2010-01-18 11:28:17 +0000
235@@ -0,0 +1,61 @@
236+# Copyright 2010 Canonical Ltd. This software is licensed under the
237+# GNU Affero General Public License version 3 (see the file LICENSE).
238+
239+__metaclass__ = type
240+
241+from lp.testing import TestCase
242+
243+from canonical.config import config, dbconfig
244+
245+from canonical.launchpad.readonly import read_only_file_exists
246+from canonical.launchpad.tests.readonly import (
247+ remove_read_only_file, touch_read_only_file)
248+
249+
250+class TestDatabaseConfig(TestCase):
251+
252+ def test_overlay(self):
253+ # The dbconfig option overlays the database configurations of a
254+ # chosen config section over the base section.
255+ self.assertRaises(
256+ AttributeError, getattr, config.database, 'dbuser')
257+ self.assertRaises(
258+ AttributeError, getattr, config.launchpad, 'main_master')
259+ self.assertEquals('launchpad_main', config.launchpad.dbuser)
260+ self.assertEquals('librarian', config.librarian.dbuser)
261+
262+ dbconfig.setConfigSection('librarian')
263+ self.assertEquals('dbname=launchpad_ftest', dbconfig.rw_main_master)
264+ self.assertEquals('librarian', dbconfig.dbuser)
265+
266+ dbconfig.setConfigSection('launchpad')
267+ self.assertEquals('dbname=launchpad_ftest', dbconfig.rw_main_master)
268+ self.assertEquals('launchpad_main', dbconfig.dbuser)
269+
270+ def test_required_values(self):
271+ # Some variables are required to have a value, such as dbuser. So we
272+ # get a ValueError if they are not set.
273+ self.assertRaises(
274+ AttributeError, getattr, config.codehosting, 'dbuser')
275+ dbconfig.setConfigSection('codehosting')
276+ self.assertRaises(ValueError, getattr, dbconfig, 'dbuser')
277+ dbconfig.setConfigSection('launchpad')
278+
279+ def test_main_master_and_main_slave(self):
280+ # DatabaseConfig provides two extra properties: main_master and
281+ # main_slave, which return the value of either
282+ # rw_main_master/rw_main_slave or ro_main_master/ro_main_slave,
283+ # depending on whether or not we're in read-only mode.
284+ self.assertFalse(read_only_file_exists())
285+ self.assertEquals(dbconfig.rw_main_master, dbconfig.main_master)
286+ self.assertEquals(dbconfig.rw_main_slave, dbconfig.main_slave)
287+
288+ touch_read_only_file()
289+ try:
290+ self.assertTrue(read_only_file_exists())
291+ self.assertEquals(
292+ dbconfig.ro_main_master, dbconfig.main_master)
293+ self.assertEquals(
294+ dbconfig.ro_main_slave, dbconfig.main_slave)
295+ finally:
296+ remove_read_only_file()
297
298=== modified file 'lib/canonical/database/sqlbase.py'
299--- lib/canonical/database/sqlbase.py 2009-12-14 12:52:19 +0000
300+++ lib/canonical/database/sqlbase.py 2010-01-18 11:28:17 +0000
301@@ -29,7 +29,7 @@
302 from zope.interface import implements
303 from zope.security.proxy import removeSecurityProxy
304
305-from canonical.config import config
306+from canonical.config import config, dbconfig
307 from canonical.database.interfaces import ISQLBase
308
309
310@@ -268,8 +268,11 @@
311 @classmethod
312 def _get_zopeless_connection_config(self, dbname, dbhost):
313 # This method exists for testability.
314- main_connection_string = config.database.main_master
315- auth_connection_string = config.database.auth_master
316+
317+ # This is only used by scripts, so we must connect to the read-write
318+ # DB here -- that's why we use rw_main_master directly.
319+ main_connection_string = dbconfig.rw_main_master
320+ auth_connection_string = dbconfig.auth_master
321
322 # Override dbname and dbhost in the connection string if they
323 # have been passed in.
324@@ -312,7 +315,7 @@
325 # Construct a config fragment:
326 overlay = dedent("""\
327 [database]
328- main_master: %(main_connection_string)s
329+ rw_main_master: %(main_connection_string)s
330 auth_master: %(auth_connection_string)s
331 isolation_level: %(isolation_level)s
332 """ % vars())
333@@ -755,20 +758,14 @@
334
335 Default database name is the one specified in the main configuration file.
336 """
337- con_str = connect_string(user, dbname)
338- con = psycopg2.connect(con_str)
339- con.set_isolation_level(isolation)
340- return con
341-
342-
343-def connect_string(user, dbname=None):
344- """Return a PostgreSQL connection string."""
345 from canonical import lp
346 # We start with the config string from the config file, and overwrite
347 # with the passed in dbname or modifications made by db_options()
348 # command line arguments. This will do until db_options gets an overhaul.
349- con_str = config.database.main_master
350 con_str_overrides = []
351+ # We must connect to the read-write DB here, so we use rw_main_master
352+ # directly.
353+ con_str = dbconfig.rw_main_master
354 assert 'user=' not in con_str, (
355 'Connection string already contains username')
356 if user is not None:
357@@ -782,7 +779,11 @@
358 con_str = re.sub(r'dbname=\S*', '', con_str) # Remove if exists.
359 con_str_overrides.append('dbname=%s' % dbname)
360
361- return ' '.join([con_str] + con_str_overrides)
362+ con_str = ' '.join([con_str] + con_str_overrides)
363+
364+ con = psycopg2.connect(con_str)
365+ con.set_isolation_level(isolation)
366+ return con
367
368
369 class cursor:
370
371=== modified file 'lib/canonical/launchpad/configure.zcml'
372--- lib/canonical/launchpad/configure.zcml 2009-12-03 19:12:02 +0000
373+++ lib/canonical/launchpad/configure.zcml 2010-01-18 11:28:17 +0000
374@@ -93,5 +93,4 @@
375 name="PlotKit"
376 directory="../../contrib/PlotKit"
377 />
378-
379 </configure>
380
381=== modified file 'lib/canonical/launchpad/doc/canonical-config.txt'
382--- lib/canonical/launchpad/doc/canonical-config.txt 2009-06-15 21:53:14 +0000
383+++ lib/canonical/launchpad/doc/canonical-config.txt 2010-01-18 11:28:17 +0000
384@@ -14,7 +14,7 @@
385 simple configuration).
386
387 >>> from canonical.config import config
388- >>> print config.database.main_master
389+ >>> print config.database.rw_main_master
390 dbname=launchpad_ftest
391 >>> config.database.db_statement_timeout is None
392 True
393
394=== modified file 'lib/canonical/launchpad/pagetests/standalone/xx-dbpolicy.txt'
395--- lib/canonical/launchpad/pagetests/standalone/xx-dbpolicy.txt 2009-08-13 19:03:36 +0000
396+++ lib/canonical/launchpad/pagetests/standalone/xx-dbpolicy.txt 2010-01-18 11:28:17 +0000
397@@ -13,7 +13,7 @@
398 >>> from textwrap import dedent
399 >>> config_overlay = dedent("""
400 ... [database]
401- ... main_slave: dbname=launchpad_empty
402+ ... rw_main_slave: dbname=launchpad_empty
403 ... """)
404 >>> config.push('empty_slave', config_overlay)
405
406
407=== modified file 'lib/canonical/launchpad/pagetests/standalone/xx-opstats.txt'
408--- lib/canonical/launchpad/pagetests/standalone/xx-opstats.txt 2009-12-02 11:28:49 +0000
409+++ lib/canonical/launchpad/pagetests/standalone/xx-opstats.txt 2010-01-18 11:28:17 +0000
410@@ -234,8 +234,8 @@
411
412 >>> no_db_overrides = """
413 ... [database]
414- ... main_master: dbname=nonexistant
415- ... main_slave: dbname=nonexistant
416+ ... rw_main_master: dbname=nonexistant
417+ ... rw_main_slave: dbname=nonexistant
418 ... auth_master: dbname=nonexistant
419 ... auth_slave: dbname=nonexistant
420 ...
421
422=== modified file 'lib/canonical/launchpad/pagetests/standalone/xx-read-only-mode.txt'
423--- lib/canonical/launchpad/pagetests/standalone/xx-read-only-mode.txt 2009-07-08 21:47:23 +0000
424+++ lib/canonical/launchpad/pagetests/standalone/xx-read-only-mode.txt 2010-01-18 11:28:17 +0000
425@@ -5,14 +5,9 @@
426 database but attempts to access the master database or make database
427 changes fail, returning an error page to the user.
428
429- >>> from canonical.config import config
430- >>> def push_read_only_mode(flag):
431- ... config.push('read only mode', """
432- ... [launchpad]
433- ... read_only: %s""" % str(flag))
434- >>> def pop_read_only_mode():
435- ... config.pop('read only mode')
436- >>> push_read_only_mode(True)
437+ >>> from canonical.launchpad.tests.readonly import (
438+ ... touch_read_only_file, remove_read_only_file)
439+ >>> touch_read_only_file()
440
441
442 == Notification of read-only mode. ==
443@@ -35,18 +30,18 @@
444
445 There is no warning when Launchpad is running normally.
446
447- >>> push_read_only_mode(False)
448+ >>> remove_read_only_file()
449 >>> user_browser.open('http://launchpad.dev')
450 >>> print first_tag_by_class(
451 ... user_browser.contents, 'warning message')
452 None
453- >>> pop_read_only_mode()
454
455 == Operations requiring write permissions fail ==
456
457 In read-only mode, all requests for non-read permissions are denied.
458 This causes edit buttons and similar to not be displayed.
459
460+ >>> touch_read_only_file()
461 >>> user_browser.open('http://launchpad.dev/people/+me')
462 >>> user_browser.getLink('Change details')
463 Traceback (most recent call last):
464@@ -58,16 +53,16 @@
465 forgetting to properly protect the edit buttons, the form is replaced
466 with a nice 503 error page informing the user what is going on.
467
468- >>> push_read_only_mode(False)
469+ >>> remove_read_only_file()
470 >>> user_browser.open('http://launchpad.dev/people/+me')
471 >>> edit_link = user_browser.getLink('Change details')
472 >>> edit_link is None
473 False
474- >>> pop_read_only_mode()
475 >>> # XXX StuartBishop 20090423 bug=365378: raiseHttpErrors is broken,
476 >>> # requiring the try/except dance.
477 >>> user_browser.handleErrors = True
478 >>> user_browser.raiseHttpErrors = False
479+ >>> touch_read_only_file()
480 >>> try:
481 ... edit_link.click()
482 ... except:
483@@ -81,15 +76,15 @@
484 And even if a user manages to get a form and submit it, they get the
485 same 503 error page.
486
487- >>> push_read_only_mode(False)
488+ >>> remove_read_only_file()
489 >>> user_browser.handleErrors = True
490 >>> user_browser.open('http://launchpad.dev/people/+me')
491 >>> user_browser.getLink('Change details').click()
492- >>> pop_read_only_mode()
493 >>> user_browser.getControl(name='field.displayname').value = 'Different'
494 >>> user_browser.raiseHttpErrors = False
495 >>> # XXX StuartBishop 20090423 bug=365378: raiseHttpErrors is broken,
496 >>> # requiring the try/except dance.
497+ >>> touch_read_only_file()
498 >>> try:
499 ... user_browser.getControl('Save Changes').click()
500 ... except:
501@@ -143,16 +138,13 @@
502
503 === Bug page ===
504
505- >>> push_read_only_mode(True)
506 >>> user_browser.open('http://launchpad.dev/bugs/5')
507 >>> print user_browser.title
508 Bug #5...
509 >>> print user_browser.headers['status']
510 200 Ok
511- >>> pop_read_only_mode()
512
513
514 == Cleanup ==
515
516- >>> pop_read_only_mode()
517-
518+ >>> remove_read_only_file()
519
520=== added file 'lib/canonical/launchpad/readonly.py'
521--- lib/canonical/launchpad/readonly.py 1970-01-01 00:00:00 +0000
522+++ lib/canonical/launchpad/readonly.py 2010-01-18 11:28:17 +0000
523@@ -0,0 +1,86 @@
524+# Copyright 2010 Canonical Ltd. This software is licensed under the
525+# GNU Affero General Public License version 3 (see the file LICENSE).
526+
527+"""Helpers for running Launchpad in read-only mode.
528+
529+To switch an app server to read-only mode, all you need to do is create a file
530+named read-only.txt in the root of the Launchpad tree.
531+"""
532+
533+from __future__ import with_statement
534+
535+__metaclass__ = type
536+__all__ = [
537+ 'is_read_only',
538+ 'read_only_file_exists',
539+ 'read_only_file_path',
540+ ]
541+
542+import logging
543+import os
544+import threading
545+
546+from zope.security.management import queryInteraction
547+
548+from lazr.restful.utils import get_current_browser_request
549+
550+
551+root = os.path.abspath(os.path.join(
552+ os.path.dirname(__file__), os.pardir, os.pardir, os.pardir))
553+read_only_file_path = os.path.join(root, 'read-only.txt')
554+READ_ONLY_MODE_ANNOTATIONS_KEY = 'launchpad.read_only_mode'
555+
556+
557+def read_only_file_exists():
558+ """Does a read-only.txt file exists in the root of our tree?"""
559+ return os.path.isfile(read_only_file_path)
560+
561+
562+_lock = threading.Lock()
563+_currently_in_read_only = False
564+
565+
566+def is_read_only():
567+ """Are we in read-only mode?
568+
569+ If called as part of the processing of a request, we'll look in the
570+ request's annotations for a read-only key
571+ (READ_ONLY_MODE_ANNOTATIONS_KEY), and if it exists we'll just return its
572+ value.
573+
574+ If there's no request or the key doesn't exist, we check for the presence
575+ of a read-only.txt file in the root of our tree, set the read-only key in
576+ the request's annotations (when there is a request), update
577+ _currently_in_read_only (in case it changed, also logging the change)
578+ and return it.
579+ """
580+ # pylint: disable-msg=W0603
581+ global _currently_in_read_only
582+ request = None
583+ # XXX: salgado, 2010-01-14, bug=507447: Only call
584+ # get_current_browser_request() when we have an interaction, or else
585+ # it will raise an AttributeError.
586+ if queryInteraction() is not None:
587+ request = get_current_browser_request()
588+ if request is not None:
589+ if READ_ONLY_MODE_ANNOTATIONS_KEY in request.annotations:
590+ return request.annotations[READ_ONLY_MODE_ANNOTATIONS_KEY]
591+
592+ read_only = read_only_file_exists()
593+ if request is not None:
594+ request.annotations[READ_ONLY_MODE_ANNOTATIONS_KEY] = read_only
595+
596+ log_change = False
597+ with _lock:
598+ if _currently_in_read_only != read_only:
599+ _currently_in_read_only = read_only
600+ log_change = True
601+
602+ if log_change:
603+ logging.warning(
604+ 'Read-only mode change detected; now read-only is %s' % read_only)
605+
606+ return read_only
607+
608+
609+_currently_in_read_only = is_read_only()
610
611=== added file 'lib/canonical/launchpad/tests/readonly.py'
612--- lib/canonical/launchpad/tests/readonly.py 1970-01-01 00:00:00 +0000
613+++ lib/canonical/launchpad/tests/readonly.py 2010-01-18 11:28:17 +0000
614@@ -0,0 +1,34 @@
615+# Copyright 2010 Canonical Ltd. This software is licensed under the
616+# GNU Affero General Public License version 3 (see the file LICENSE).
617+
618+"""Helpers for creating and removing a read-only.txt in the root of our tree.
619+"""
620+
621+__metaclass__ = type
622+__all__ = [
623+ 'touch_read_only_file',
624+ 'remove_read_only_file',
625+ ]
626+
627+import os
628+
629+from canonical.launchpad.readonly import (
630+ read_only_file_exists, read_only_file_path)
631+
632+
633+def touch_read_only_file():
634+ """Create an empty file named read-only.txt under the root of the tree.
635+
636+ This function must not be called if a file with that name already exists.
637+ """
638+ assert not read_only_file_exists(), (
639+ "This function must not be called when a read-only.txt file "
640+ "already exists.")
641+ f = open(read_only_file_path, 'w')
642+ f.close()
643+
644+
645+def remove_read_only_file():
646+ """Remove the file named read-only.txt from the root of the tree."""
647+ os.remove(read_only_file_path)
648+
649
650=== added file 'lib/canonical/launchpad/tests/test_readonly.py'
651--- lib/canonical/launchpad/tests/test_readonly.py 1970-01-01 00:00:00 +0000
652+++ lib/canonical/launchpad/tests/test_readonly.py 2010-01-18 11:28:17 +0000
653@@ -0,0 +1,78 @@
654+# Copyright 2010 Canonical Ltd. This software is licensed under the
655+# GNU Affero General Public License version 3 (see the file LICENSE).
656+
657+__metaclass__ = type
658+
659+from lazr.restful.utils import get_current_browser_request
660+
661+from lp.testing import TestCase
662+
663+from canonical.launchpad.ftests import ANONYMOUS, login, logout
664+from canonical.launchpad.readonly import (
665+ is_read_only, read_only_file_exists, READ_ONLY_MODE_ANNOTATIONS_KEY)
666+from canonical.launchpad.tests.readonly import (
667+ remove_read_only_file, touch_read_only_file)
668+from canonical.testing.layers import FunctionalLayer
669+
670+
671+class TestReadOnlyModeDetection(TestCase):
672+
673+ def test_read_only_file_exists(self):
674+ # By default we run in read-write mode.
675+ self.assertFalse(read_only_file_exists())
676+
677+ # When a file named 'read-only.txt' exists under the root of the tree,
678+ # we run in read-only mode.
679+ touch_read_only_file()
680+ try:
681+ self.assertTrue(read_only_file_exists())
682+ finally:
683+ remove_read_only_file()
684+
685+ # Once the file is removed, we're back into read-write mode.
686+ self.assertFalse(read_only_file_exists())
687+
688+
689+class Test_is_read_only(TestCase):
690+ layer = FunctionalLayer
691+
692+ def tearDown(self):
693+ # Safety net just in case a test leaves the read-only.txt file behind.
694+ if read_only_file_exists():
695+ remove_read_only_file()
696+ super(Test_is_read_only, self).tearDown()
697+
698+ def test_is_read_only(self):
699+ # By default we run in read-write mode.
700+ logout()
701+ self.assertFalse(is_read_only())
702+
703+ # When a file named 'read-only.txt' exists under the root of the tree,
704+ # we run in read-only mode.
705+ touch_read_only_file()
706+ try:
707+ self.assertTrue(is_read_only())
708+ finally:
709+ remove_read_only_file()
710+
711+ def test_caching_in_request(self):
712+ # When called as part of a request processing, is_read_only() will
713+ # stash the read-only flag in the request's annotations.
714+ login(ANONYMOUS)
715+ request = get_current_browser_request()
716+ self.assertIs(
717+ None,
718+ request.annotations.get(READ_ONLY_MODE_ANNOTATIONS_KEY))
719+ self.assertFalse(is_read_only())
720+ self.assertFalse(
721+ request.annotations.get(READ_ONLY_MODE_ANNOTATIONS_KEY))
722+
723+ def test_cached_value_takes_precedence(self):
724+ # Once the request has the read-only flag, we don't check for the
725+ # presence of the read-only.txt file anymore, so it could be removed
726+ # and the request would still be in read-only mode.
727+ login(ANONYMOUS)
728+ request = get_current_browser_request()
729+ request.annotations[READ_ONLY_MODE_ANNOTATIONS_KEY] = True
730+ self.assertTrue(is_read_only())
731+ self.assertFalse(read_only_file_exists())
732
733=== modified file 'lib/canonical/launchpad/webapp/adapter.py'
734--- lib/canonical/launchpad/webapp/adapter.py 2009-12-08 20:14:06 +0000
735+++ lib/canonical/launchpad/webapp/adapter.py 2010-01-18 11:28:17 +0000
736@@ -7,6 +7,7 @@
737 __metaclass__ = type
738
739 import os
740+import re
741 import sys
742 import thread
743 import threading
744@@ -36,6 +37,7 @@
745 from canonical.config import config, dbconfig, DatabaseConfig
746 from canonical.database.interfaces import IRequestExpired
747 from canonical.launchpad.interfaces import IMasterObject, IMasterStore
748+from canonical.launchpad.readonly import is_read_only
749 from canonical.launchpad.webapp.dbpolicy import MasterDatabasePolicy
750 from canonical.launchpad.webapp.interfaces import (
751 AUTH_STORE, DEFAULT_FLAVOR, IStoreSelector,
752@@ -277,12 +279,14 @@
753 # is raised when an attempt is made to make changes when
754 # the connection has been put in read-only mode.
755 if exception.pgcode == '25006':
756- raise ReadOnlyModeViolation, None, sys.exc_info()[2]
757+ raise ReadOnlyModeViolation(None, sys.exc_info()[2])
758 raise
759
760
761 class LaunchpadDatabase(Postgres):
762
763+ _dsn_user_re = re.compile('user=[^ ]*')
764+
765 def __init__(self, uri):
766 # The uri is just a property name in the config, such as main_master
767 # or auth_slave.
768@@ -293,6 +297,13 @@
769 # A unique name for this database connection.
770 self.name = uri.database
771
772+ @property
773+ def dsn_without_user(self):
774+ """This database's dsn without the 'user=...' bit."""
775+ assert self._dsn is not None, (
776+ 'Must not be called before self._dsn has been set.')
777+ return self._dsn_user_re.sub('', self._dsn)
778+
779 def raw_connect(self):
780 # Prevent database connections from the main thread if
781 # break_main_thread_db_access() has been run.
782@@ -359,7 +370,7 @@
783 If we are running in read-only mode, returns a
784 ReadOnlyModeConnection. Otherwise it returns the Storm default.
785 """
786- if config.launchpad.read_only:
787+ if is_read_only():
788 return ReadOnlyModeConnection
789 return super(LaunchpadDatabase, self).connection_factory
790
791
792=== modified file 'lib/canonical/launchpad/webapp/authorization.py'
793--- lib/canonical/launchpad/webapp/authorization.py 2009-10-20 22:13:21 +0000
794+++ lib/canonical/launchpad/webapp/authorization.py 2010-01-18 11:28:17 +0000
795@@ -21,11 +21,11 @@
796 checkPermission as check_permission_is_registered)
797 from zope.app.security.principalregistry import UnauthenticatedPrincipal
798
799-from canonical.config import config
800 from canonical.lazr.canonicalurl import nearest_adapter
801 from canonical.lazr.interfaces import IObjectPrivacy
802
803 from canonical.database.sqlbase import block_implicit_flushes
804+from canonical.launchpad.readonly import is_read_only
805 from canonical.launchpad.webapp.interfaces import (
806 AccessLevel, IAuthorization, ILaunchpadContainer, ILaunchpadPrincipal)
807 from canonical.launchpad.webapp.metazcml import ILaunchpadPermission
808@@ -123,7 +123,7 @@
809 # accidentally using cached results. This will be important when
810 # Launchpad automatically fails over to read-only mode when the
811 # master database is unavailable.
812- if config.launchpad.read_only:
813+ if is_read_only():
814 lp_permission = getUtility(ILaunchpadPermission, permission)
815 if lp_permission.access_level != "read":
816 return False
817
818=== modified file 'lib/canonical/launchpad/webapp/configure.zcml'
819--- lib/canonical/launchpad/webapp/configure.zcml 2009-11-18 00:22:41 +0000
820+++ lib/canonical/launchpad/webapp/configure.zcml 2010-01-18 11:28:17 +0000
821@@ -678,6 +678,12 @@
822 />
823
824 <adapter
825+ for="canonical.launchpad.webapp.servers.LaunchpadTestRequest"
826+ provides="canonical.launchpad.webapp.interfaces.INotificationResponse"
827+ factory="canonical.launchpad.webapp.servers.adaptRequestToResponse"
828+ />
829+
830+ <adapter
831 factory="canonical.launchpad.webapp.snapshot.snapshot_sql_result" />
832 <!-- It also works for the legacy SQLObject interface. -->
833 <adapter
834
835=== modified file 'lib/canonical/launchpad/webapp/dbpolicy.py'
836--- lib/canonical/launchpad/webapp/dbpolicy.py 2009-12-02 19:33:08 +0000
837+++ lib/canonical/launchpad/webapp/dbpolicy.py 2010-01-18 11:28:17 +0000
838@@ -24,6 +24,7 @@
839 from canonical.config import config, dbconfig
840 from canonical.database.sqlbase import StupidCache
841 from canonical.launchpad.interfaces import IMasterStore, ISlaveStore
842+from canonical.launchpad.readonly import is_read_only
843 from canonical.launchpad.webapp import LaunchpadView
844 from canonical.launchpad.webapp.interfaces import (
845 DEFAULT_FLAVOR, DisallowedStore, IDatabasePolicy, IStoreSelector,
846@@ -76,10 +77,6 @@
847 config_section = self.config_section or dbconfig.getSectionName()
848
849 store_name = '%s-%s-%s' % (config_section, name, flavor)
850- # XXX stub 2009-06-25 bug=392011: zstorm.get seems to be broken
851- # and does not return None if no Store is available. We have
852- # no way of knowing if the returned Store existed previously,
853- # which makes it difficult to do any post-instantiation setup.
854 store = getUtility(IZStorm).get(
855 store_name, 'launchpad:%s' % store_name)
856 if not getattr(store, '_lp_store_initialized', False):
857@@ -162,7 +159,7 @@
858 # of test requests in our automated tests.
859 if request.get('PATH_INFO') == u'/+opstats':
860 return DatabaseBlockedPolicy(request)
861- elif config.launchpad.read_only:
862+ elif is_read_only():
863 return ReadOnlyLaunchpadDatabasePolicy(request)
864 else:
865 return LaunchpadDatabasePolicy(request)
866@@ -298,7 +295,7 @@
867 def WebServiceDatabasePolicyFactory(request):
868 """Return the Launchpad IDatabasePolicy for the current appserver state.
869 """
870- if config.launchpad.read_only:
871+ if is_read_only():
872 return ReadOnlyLaunchpadDatabasePolicy(request)
873 else:
874 # If a session cookie was sent with the request, use the
875
876=== modified file 'lib/canonical/launchpad/webapp/login.py'
877--- lib/canonical/launchpad/webapp/login.py 2009-11-23 03:10:04 +0000
878+++ lib/canonical/launchpad/webapp/login.py 2010-01-18 11:28:17 +0000
879@@ -31,6 +31,7 @@
880 from lp.registry.interfaces.person import (
881 IPerson, IPersonSet, PersonCreationRationale)
882 from canonical.launchpad.interfaces.validation import valid_password
883+from canonical.launchpad.readonly import is_read_only
884 from canonical.launchpad.validators.email import valid_email
885 from canonical.launchpad.webapp.error import SystemErrorView
886 from canonical.launchpad.webapp.interfaces import (
887@@ -58,7 +59,7 @@
888 # to render the read-only failure screen so the user knows their
889 # request failed for operational reasons rather than a genuine
890 # permission problem.
891- if config.launchpad.read_only:
892+ if is_read_only():
893 # Our context is an Unauthorized exception, which acts like
894 # a tuple containing (object, attribute_requested, permission).
895 lp_permission = getUtility(ILaunchpadPermission, self.context[2])
896
897=== modified file 'lib/canonical/launchpad/webapp/publication.py'
898--- lib/canonical/launchpad/webapp/publication.py 2009-10-07 18:41:52 +0000
899+++ lib/canonical/launchpad/webapp/publication.py 2010-01-18 11:28:17 +0000
900@@ -4,10 +4,9 @@
901 __metaclass__ = type
902 __all__ = [
903 'LoginRoot',
904- 'LaunchpadBrowserPublication'
905+ 'LaunchpadBrowserPublication',
906 ]
907
908-
909 import gc
910 import os
911 import re
912@@ -46,18 +45,20 @@
913 import canonical.launchpad.layers as layers
914 import canonical.launchpad.webapp.adapter as da
915
916-from canonical.config import config
917+from canonical.config import config, dbconfig
918 from canonical.mem import (
919 countsByType, deltaCounts, memory, mostRefs, printCounts, readCounts,
920 resident)
921 from canonical.launchpad.interfaces.launchpad import ILaunchpadCelebrities
922+from canonical.launchpad.readonly import is_read_only
923 from lp.registry.interfaces.person import (
924 IPerson, IPersonSet, ITeam)
925 from canonical.launchpad.webapp.interfaces import (
926- IDatabasePolicy, IPlacelessAuthUtility, IPrimaryContext,
927- ILaunchpadRoot, INotificationResponse, IOpenLaunchBag,
928- OffsiteFormPostError, IStoreSelector, MASTER_FLAVOR)
929-from canonical.launchpad.webapp.dbpolicy import LaunchpadDatabasePolicy
930+ IDatabasePolicy, ILaunchpadRoot, INotificationResponse, IOpenLaunchBag,
931+ IPlacelessAuthUtility, IPrimaryContext, IStoreSelector, MAIN_STORE,
932+ MASTER_FLAVOR, OffsiteFormPostError, SLAVE_FLAVOR)
933+from canonical.launchpad.webapp.dbpolicy import (
934+ DatabaseBlockedPolicy, LaunchpadDatabasePolicy)
935 from canonical.launchpad.webapp.menu import structured
936 from canonical.launchpad.webapp.opstats import OpStats
937 from lazr.uri import URI, InvalidURIError
938@@ -166,9 +167,29 @@
939
940 transaction.begin()
941
942+ db_policy = IDatabasePolicy(request)
943+ if not isinstance(db_policy, DatabaseBlockedPolicy):
944+ # Database access is not blocked, so make sure our stores point to
945+ # the appropriate databases, according to the mode we're on.
946+ main_master_store = getUtility(IStoreSelector).get(
947+ MAIN_STORE, MASTER_FLAVOR)
948+ # XXX: 2009-01-12, salgado, bug=506536: We shouldn't need to go
949+ # through private attributes to get to the store's database.
950+ dsn = main_master_store._connection._database.dsn_without_user
951+ if dsn.strip() != dbconfig.main_master.strip():
952+ # Remove the stores from zstorm to force them to be
953+ # re-created, thus using the correct databases for the mode
954+ # we're on right now.
955+ main_slave_store = getUtility(IStoreSelector).get(
956+ MAIN_STORE, SLAVE_FLAVOR)
957+ zstorm = getUtility(IZStorm)
958+ for store in [main_master_store, main_slave_store]:
959+ zstorm.remove(store)
960+ store.close()
961+
962 # Now we are logged in, install the correct IDatabasePolicy for
963 # this request.
964- getUtility(IStoreSelector).push(IDatabasePolicy(request))
965+ getUtility(IStoreSelector).push(db_policy)
966
967 getUtility(IOpenLaunchBag).clear()
968
969@@ -186,7 +207,7 @@
970
971 def maybeNotifyReadOnlyMode(self, request):
972 """Hook to notify about read-only mode."""
973- if config.launchpad.read_only:
974+ if is_read_only():
975 try:
976 INotificationResponse(request).addWarningNotification(
977 structured("""
978@@ -555,6 +576,7 @@
979 We must restart the request timer. Otherwise we can get OOPS errors
980 from our exception views inappropriately.
981 """
982+ # pylint: disable-msg=E1002
983 super(LaunchpadBrowserPublication,
984 self).beginErrorHandlingTransaction(request, ob, note)
985 # XXX: gary 2008-11-04 bug=293614: As the bug describes, we want to
986
987=== modified file 'lib/canonical/launchpad/webapp/tests/test_authorization.py'
988--- lib/canonical/launchpad/webapp/tests/test_authorization.py 2009-06-25 05:30:52 +0000
989+++ lib/canonical/launchpad/webapp/tests/test_authorization.py 2010-01-18 11:28:17 +0000
990@@ -10,8 +10,7 @@
991
992 import transaction
993
994-from zope.app.testing import ztapi
995-from zope.component import provideAdapter
996+from zope.component import provideAdapter, provideUtility
997 from zope.interface import classProvides, implements, Interface
998 from zope.testing.cleanup import CleanUp
999
1000@@ -136,7 +135,7 @@
1001 """Register a new permission and a fake store selector."""
1002 super(TestCheckPermissionCaching, self).setUp()
1003 self.factory = ObjectFactory()
1004- ztapi.provideUtility(IStoreSelector, FakeStoreSelector)
1005+ provideUtility(FakeStoreSelector, IStoreSelector)
1006
1007 def makeRequest(self):
1008 """Construct an arbitrary `LaunchpadBrowserRequest` object."""
1009@@ -152,11 +151,11 @@
1010 `Checker` created by ``checker_factory``.
1011 """
1012 permission = self.factory.getUniqueString()
1013- ztapi.provideUtility(
1014- ILaunchpadPermission, PermissionAccessLevel(), permission)
1015+ provideUtility(
1016+ PermissionAccessLevel(), ILaunchpadPermission, permission)
1017 checker_factory = CheckerFactory()
1018- ztapi.provideAdapter(
1019- Object, IAuthorization, checker_factory, name=permission)
1020+ provideAdapter(
1021+ checker_factory, [Object], IAuthorization, name=permission)
1022 return Object(), permission, checker_factory
1023
1024 def test_checkPermission_cache_unauthenticated(self):
1025
1026=== modified file 'lib/canonical/launchpad/webapp/tests/test_dbpolicy.py'
1027--- lib/canonical/launchpad/webapp/tests/test_dbpolicy.py 2010-01-07 06:16:04 +0000
1028+++ lib/canonical/launchpad/webapp/tests/test_dbpolicy.py 2010-01-18 11:28:17 +0000
1029@@ -14,10 +14,11 @@
1030 from zope.session.interfaces import ISession, IClientIdManager
1031
1032 from lazr.restful.interfaces import IWebServiceConfiguration
1033-from canonical.config import config
1034 from canonical.launchpad.interfaces import IMasterStore, ISlaveStore
1035 from canonical.launchpad.layers import (
1036 FeedsLayer, setFirstLayer, WebServiceLayer)
1037+from canonical.launchpad.tests.readonly import (
1038+ remove_read_only_file, touch_read_only_file)
1039 from lp.testing import TestCase
1040 from canonical.launchpad.webapp.dbpolicy import (
1041 BaseDatabasePolicy, LaunchpadDatabasePolicy, MasterDatabasePolicy,
1042@@ -207,9 +208,7 @@
1043 """WebService requests should use the read only database
1044 policy in read only mode.
1045 """
1046- config.push('read_only', """
1047- [launchpad]
1048- read_only: True""")
1049+ touch_read_only_file()
1050 try:
1051 api_prefix = getUtility(
1052 IWebServiceConfiguration).service_version_uri_prefix
1053@@ -219,19 +218,17 @@
1054 policy = IDatabasePolicy(request)
1055 self.assertIsInstance(policy, ReadOnlyLaunchpadDatabasePolicy)
1056 finally:
1057- config.pop('read_only')
1058+ remove_read_only_file()
1059
1060 def test_read_only_mode_uses_ReadOnlyLaunchpadDatabasePolicy(self):
1061- config.push('read_only', """
1062- [launchpad]
1063- read_only: True""")
1064+ touch_read_only_file()
1065 try:
1066 request = LaunchpadTestRequest(
1067 SERVER_URL='http://launchpad.dev')
1068 policy = IDatabasePolicy(request)
1069 self.assertIsInstance(policy, ReadOnlyLaunchpadDatabasePolicy)
1070 finally:
1071- config.pop('read_only')
1072+ remove_read_only_file()
1073
1074 def test_other_request_uses_LaunchpadDatabasePolicy(self):
1075 """By default, requests should use the LaunchpadDatabasePolicy."""
1076
1077=== modified file 'lib/canonical/launchpad/webapp/tests/test_publication.py'
1078--- lib/canonical/launchpad/webapp/tests/test_publication.py 2009-10-07 18:41:52 +0000
1079+++ lib/canonical/launchpad/webapp/tests/test_publication.py 2010-01-18 11:28:17 +0000
1080@@ -11,16 +11,22 @@
1081 from contrib.oauth import OAuthRequest, OAuthSignatureMethod_PLAINTEXT
1082
1083 from storm.exceptions import DisconnectionError
1084+from storm.zope.interfaces import IZStorm
1085+
1086 from zope.component import getUtility
1087 from zope.error.interfaces import IErrorReportingUtility
1088 from zope.publisher.interfaces import Retry
1089
1090+from canonical.config import dbconfig
1091 from canonical.testing import DatabaseFunctionalLayer
1092 from canonical.launchpad.interfaces.oauth import IOAuthConsumerSet
1093 from canonical.launchpad.ftests import ANONYMOUS, login
1094+from canonical.launchpad.tests.readonly import (
1095+ remove_read_only_file, touch_read_only_file)
1096 from lp.testing import TestCase, TestCaseWithFactory
1097 import canonical.launchpad.webapp.adapter as da
1098-from canonical.launchpad.webapp.interfaces import OAuthPermission
1099+from canonical.launchpad.webapp.interfaces import (
1100+ IStoreSelector, MAIN_STORE, MASTER_FLAVOR, OAuthPermission, SLAVE_FLAVOR)
1101 from canonical.launchpad.webapp.publication import (
1102 is_browser, LaunchpadBrowserPublication)
1103 from canonical.launchpad.webapp.servers import (
1104@@ -51,6 +57,89 @@
1105 publication.callTraversalHooks(request, obj2)
1106 self.assertEquals(request.traversed_objects, [obj1])
1107
1108+class TestReadOnlyModeSwitches(TestCase):
1109+ # At the beginning of every request (in publication.beforeTraversal()), we
1110+ # check to see if we've changed from/to read-only/read-write and if there
1111+ # was a change we remove the main_master/slave stores from ZStorm, forcing
1112+ # them to be recreated the next time they're needed, thus causing them to
1113+ # point to the correct databases.
1114+ layer = DatabaseFunctionalLayer
1115+
1116+ def tearDown(self):
1117+ TestCase.tearDown(self)
1118+ # If a DB policy was installed (e.g. by publication.beforeTraversal),
1119+ # uninstall it.
1120+ try:
1121+ getUtility(IStoreSelector).pop()
1122+ except IndexError:
1123+ pass
1124+ # Cleanup needed so that further tests can start processing other
1125+ # requests (e.g. calling beforeTraversal).
1126+ self.publication.endRequest(self.request, None)
1127+
1128+ def setUp(self):
1129+ TestCase.setUp(self)
1130+ # Get the main_master/slave stores just to make sure they're added to
1131+ # ZStorm.
1132+ master = getUtility(IStoreSelector).get(MAIN_STORE, MASTER_FLAVOR)
1133+ slave = getUtility(IStoreSelector).get(MAIN_STORE, SLAVE_FLAVOR)
1134+ self.master_connection = master._connection
1135+ self.slave_connection = slave._connection
1136+ self.zstorm = getUtility(IZStorm)
1137+ self.publication = LaunchpadBrowserPublication(None)
1138+ self.request = LaunchpadTestRequest()
1139+
1140+ @property
1141+ def zstorm_stores(self):
1142+ return [name for (name, store) in self.zstorm.iterstores()]
1143+
1144+ def test_no_mode_changes(self):
1145+ # Make sure the master/slave stores are present in zstorm.
1146+ self.assertIn('launchpad-main-master', self.zstorm_stores)
1147+ self.assertIn('launchpad-main-slave', self.zstorm_stores)
1148+
1149+ self.publication.beforeTraversal(self.request)
1150+
1151+ # Since the mode didn't change, the stores were left in zstorm.
1152+ self.assertIn('launchpad-main-master', self.zstorm_stores)
1153+ self.assertIn('launchpad-main-slave', self.zstorm_stores)
1154+
1155+ # With the store's connection being the same as before.
1156+ master = getUtility(IStoreSelector).get(MAIN_STORE, MASTER_FLAVOR)
1157+ self.assertIs(self.master_connection, master._connection)
1158+
1159+ # And they still point to the read-write databases.
1160+ self.assertEquals(
1161+ dbconfig.rw_main_master.strip(),
1162+ # XXX: 2009-01-12, salgado, bug=506536: We shouldn't need to go
1163+ # through private attributes to get to the store's database.
1164+ master._connection._database.dsn_without_user.strip())
1165+
1166+ def test_changing_modes(self):
1167+ # Make sure the master/slave stores are present in zstorm.
1168+ self.assertIn('launchpad-main-master', self.zstorm_stores)
1169+ self.assertIn('launchpad-main-slave', self.zstorm_stores)
1170+
1171+ try:
1172+ touch_read_only_file()
1173+ self.publication.beforeTraversal(self.request)
1174+ finally:
1175+ remove_read_only_file()
1176+
1177+ # Here the mode has changed to read-only, so the stores were removed
1178+ # from zstorm.
1179+ self.assertNotIn('launchpad-main-master', self.zstorm_stores)
1180+ self.assertNotIn('launchpad-main-slave', self.zstorm_stores)
1181+
1182+ # If they're needed again, they'll be re-created by ZStorm, and when
1183+ # that happens they will point to the read-only databases.
1184+ master = getUtility(IStoreSelector).get(MAIN_STORE, SLAVE_FLAVOR)
1185+ self.assertEquals(
1186+ dbconfig.ro_main_master.strip(),
1187+ # XXX: 2009-01-12, salgado, bug=506536: We shouldn't need to go
1188+ # through private attributes to get to the store's database.
1189+ master._connection._database.dsn_without_user.strip())
1190+
1191
1192 class TestWebServicePublication(TestCaseWithFactory):
1193 layer = DatabaseFunctionalLayer
1194
1195=== modified file 'lib/canonical/librarian/client.py'
1196--- lib/canonical/librarian/client.py 2009-11-24 15:36:44 +0000
1197+++ lib/canonical/librarian/client.py 2010-01-18 11:28:17 +0000
1198@@ -25,7 +25,7 @@
1199 from storm.store import Store
1200 from zope.interface import implements
1201
1202-from canonical.config import config
1203+from canonical.config import config, dbconfig
1204 from canonical.database.sqlbase import cursor
1205 from canonical.librarian.interfaces import (
1206 DownloadFailed, ILibrarianClient, IRestrictedLibrarianClient,
1207@@ -186,9 +186,11 @@
1208 name = name.encode('utf-8')
1209 self._connect()
1210 try:
1211- # Send command
1212+ # Use dbconfig.rw_main_master directly here because it doesn't
1213+ # make sense to try and use ro_main_master (which might be
1214+ # returned if we use dbconfig.main_master).
1215 database_name = re.search(
1216- r"dbname=(\S*)", config.database.main_master).group(1)
1217+ r"dbname=(\S*)", dbconfig.rw_main_master).group(1)
1218 self._sendLine('STORE %d %s' % (size, name))
1219 self._sendHeader('Database-Name', database_name)
1220 self._sendHeader('Content-Type', str(contentType))
1221
1222=== modified file 'lib/canonical/testing/ftests/test_mockdb.py'
1223--- lib/canonical/testing/ftests/test_mockdb.py 2009-08-21 17:43:28 +0000
1224+++ lib/canonical/testing/ftests/test_mockdb.py 2010-01-18 11:28:17 +0000
1225@@ -13,7 +13,7 @@
1226 import psycopg2
1227 # from zope.testing.testrunner import dont_retry, RetryTest
1228
1229-from canonical.config import config
1230+from canonical.config import config, dbconfig
1231 from canonical.testing import mockdb, DatabaseLayer
1232 from canonical.testing.mockdb import ScriptPlayer, ScriptRecorder
1233
1234@@ -92,7 +92,7 @@
1235 """Open a connection to the (possibly fake) database."""
1236 if connection_string is None:
1237 connection_string = "%s user=%s" % (
1238- config.database.main_master, config.launchpad.dbuser)
1239+ dbconfig.rw_main_master, config.launchpad.dbuser)
1240 if self.mode == 'direct':
1241 con = psycopg2.connect(connection_string)
1242 #con = canonical.ftests.pgsql._org_connect(connection_string)
1243@@ -159,9 +159,9 @@
1244
1245 # @dont_retry
1246 def testFailedConnection(self):
1247- # Ensure failed database connections are reproducable.
1248+ # Ensure failed database connections are reproducible.
1249 for mode in self.modes():
1250- connection_string = config.database.main_master
1251+ connection_string = dbconfig.rw_main_master
1252 connection_string = re.sub(
1253 r"dbname=\S*", r"dbname=not_a_sausage", connection_string)
1254 self.assertRaises(
1255
1256=== modified file 'lib/lp/translations/model/pofile.py'
1257--- lib/lp/translations/model/pofile.py 2010-01-12 21:29:03 +0000
1258+++ lib/lp/translations/model/pofile.py 2010-01-18 11:28:17 +0000
1259@@ -24,12 +24,12 @@
1260 from zope.security.proxy import removeSecurityProxy
1261
1262 from canonical.cachedproperty import cachedproperty
1263-from canonical.config import config
1264 from canonical.database.constants import UTC_NOW
1265 from canonical.database.datetimecol import UtcDateTimeCol
1266 from canonical.database.sqlbase import (
1267 SQLBase, flush_database_updates, quote, quote_like, sqlvalues)
1268 from canonical.launchpad import helpers
1269+from canonical.launchpad.readonly import is_read_only
1270 from canonical.launchpad.interfaces.launchpad import ILaunchpadCelebrities
1271 from canonical.launchpad.webapp.interfaces import (
1272 DEFAULT_FLAVOR, IStoreSelector, MAIN_STORE, MASTER_FLAVOR)
1273@@ -146,8 +146,8 @@
1274 translation team for the given `IPOFile`.translationpermission and the
1275 language associated with this `IPOFile`.
1276 """
1277- # Nothing can be edited in read-only mode.
1278- if config.launchpad.read_only:
1279+ if is_read_only():
1280+ # Nothing can be edited in read-only mode.
1281 return False
1282
1283 # If the person is None, then they cannot edit
1284@@ -191,8 +191,8 @@
1285 any logged-in user for translations in RESTRICTED mode that have a
1286 translation team assigned.
1287 """
1288- # No suggestions can be added in read-only mode.
1289- if config.launchpad.read_only:
1290+ if is_read_only():
1291+ # No suggestions can be added in read-only mode.
1292 return False
1293
1294 if person is None: