Merge lp:~salgado/launchpad/easier-read-only-switch into lp:launchpad
- easier-read-only-switch
- Merge into devel
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 |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Tom Haddon | Approve | ||
Gary Poster (community) | Approve | ||
Review via email:
|
Commit message
Description of the change
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
Guilherme Salgado (salgado) wrote : | # |
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
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/
573: [E1002, LaunchpadBrowse
DatabaseConfig.
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 TestDatabaseCon
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.
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_
Shouldn't lib/canonical/
In this line from lib/canonical/
603 +named read-only.txt under the root of the Launchpad tree.
...I suggest s/under/in/.
In lib/canonical/
lib/canonical/
lib/canonical/
I'm submitting these notes now since I am breaking for lunch. More when I return.
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
Guilherme Salgado (salgado) wrote : | # |
There was only one test failing after my changes, and the fix for it is below:
=== modified file 'lib/canonical/
--- lib/canonical/
+++ lib/canonical/
@@ -18,6 +18,7 @@
from canonical.
from canonical.
+from canonical.
from canonical.
from canonical.
from canonical.
@@ -129,6 +130,11 @@
pass
+class FakeIsReadOnlyU
+ def isReadOnly(self):
+ return False
+
+
class TestCheckPermis
"""Test the caching done by `LaunchpadSecur
@@ -137,6 +143,9 @@
+ # LaunchpadSecuri
+ # utility, so we provide a fake one to please it.
+ ztapi.provideUt
def makeRequest(self):
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
Guilherme Salgado (salgado) wrote : | # |
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/
> 573: [E1002,
> LaunchpadBrowse
> 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.
>
> 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
> TestDatabaseCon
> 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.
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/
new file, so I don't think a comment change is needed there.
>
> What would you think of _touch_
> _remove_
> "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/
Yes, that and a __metaclass__ one.
>
> In this line from lib/canonical/
>
> 603 +named read-only.txt under the root of the Launchpad tree.
>
> ...I suggest s/under/in/.
Sure thing; changed.
>
> In lib/canonical/
> to calculate the ``root`` path just for cleanliness. (There's
> probably a way to get the path from an environmental var...
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
Gary Poster (gary) wrote : | # |
Continuing the review notes from the previous comment...
You are using ztapi because other code is doing it (``ztapi.
In lib/canonical/
686 + # Only call get_current_
687 + # or else it will choke.
688 + if queryInteraction() is not None:
689 + request = get_current_
...this is a look-before-
I question have IIsReadOnly as a utility. Why don't we just have this as a function? The ``_currently_
Do I need to know why/when we use DatabaseBlocked
OK, saving these comments; back soon. :-)
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
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.
> test_authorizat
> zope.component; it now has all the necessary convenience functions
> (although the order of arguments for zope.component.
> 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/
>
> 686 + # Only call get_current_
> interaction,
> 687 + # or else it will choke.
> 688 + if queryInteraction() is not None:
> 689 + request = get_current_
>
> ...this is a look-before-
> generally prefers try-except, and I do too. :-)
I didn't do that because I think get_current_
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_
> 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 DatabaseBlocked
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>
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
Gary Poster (gary) wrote : | # |
test_no_
Now relying to your replies:
> > Correcting this would be nice:
> >
> > lib/canonical/
> > 573: [E1002,
> > LaunchpadBrowse
> > 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-
> > generally prefers try-except, and I do too. :-)
>
> I didn't do that because I think get_current_
> 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
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
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?).
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
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-
Thanks, Tom
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
Guilherme Salgado (salgado) wrote : | # |
On Wed, 2010-01-13 at 21:30 +0000, Gary Poster wrote:
> Review: Approve
> test_no_
> 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/
> > > 573: [E1002,
> > > LaunchpadBrowse
> > > 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-
> > > generally prefers try-except, and I do too. :-)
> >
> > I didn't do that because I think get_current_
> > 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:/
>
> 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>
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
Guilherme Salgado (salgado) wrote : | # |
http://
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
Tom Haddon (mthaddon) wrote : | # |
I've tested this on staging and can confirm it works as expected. So, fwiw, +1 from me :)
Preview Diff
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: |
= 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 master/ slave or rw_main_ master/ slave, depending on the mode we are on isReadOnly( )).
anymore. These are now properties on DatabaseConfig, which return
ro_main_
(i.e. what is returned by IIsReadOnly.
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 main_master/ slave, we remove these stores from ZStorm, forcing them main_master/ slave and thus will
and main_slave stores are connected to. If they differ from
dbconfig.
to be created again the next time they are needed. When they're created again
they get a new connection, which uses dbconfig.
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 raw_connect( ), which finally main_master/ slave and creates a new psycopg2 connection
- 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.
reads dbconfig.
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 tests/test_ publication. py
- test_readonly.py
- webapp/
= Launchpad lint =
Checking for conflicts. and issues in doctests and templates.
Running jslint, xmllint, pyflakes, and pylint.
Using normal rules.
Linting changed files: /database/ sqlbase. py test-playground /launchpad- lazr.conf /librarian/ client. py /launchpad/ webapp/ publication. py /config/ schema- lazr.conf /launchpad/ doc/canonical- config. txt /launchpad/ webapp/ authorization. py /launchpad/ webapp/ tests/test_ publication. py /launchpad/ readonly. py /launchpad/ pagetests/ standalone/ xx-read- only-mode. txt /testing/ ftests/ test_mockdb. py /launchpad/ pagetests/ standalone/ xx-opstats. txt /launchpad/ webapp/ login.py translations/ model/pofile. py /launchpad/ tests/test_ readonly. py /launchpad/ webapp/ configure. zcml
lib/canonical
configs/
lib/canonical
lib/canonical
lib/canonical
lib/canonical
lib/canonical
lib/canonical
lib/canonical
lib/canonical
lib/canonical
lib/canonical
lib/canonical
lib/lp/
BRANCH.TODO
lib/canonical
lib/canonical
lib/canon...