Merge lp:~cmiller/desktopcouch/pairing-fixups into lp:desktopcouch
- pairing-fixups
- Merge into trunk
Proposed by
Chad Miller
Status: | Merged |
---|---|
Approved by: | Tim Cole |
Approved revision: | 53 |
Merged at revision: | not available |
Proposed branch: | lp:~cmiller/desktopcouch/pairing-fixups |
Merge into: | lp:desktopcouch |
Diff against target: | None lines |
To merge this branch: | bzr merge lp:~cmiller/desktopcouch/pairing-fixups |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Tim Cole (community) | Approve | ||
John O'Brien (community) | Approve | ||
Review via email: mp+10926@code.launchpad.net |
Commit message
In pairing and replication, be smarter about the bind address of the desktopcouch daemon.
Fix a problem with records marked as deleted not excluded from the default view code. Add versioning of view functions to force upgrades of old, bad, function.
Description of the change
To post a comment you must log in.
Revision history for this message
Chad Miller (cmiller) wrote : | # |
Revision history for this message
John O'Brien (jdobrien) wrote : | # |
Looks good, tests run. I'm unsure how to test if this stuff works yet,
review:
Approve
Revision history for this message
Tim Cole (tcole) wrote : | # |
Looks reasonable. I don't really care for the JavaScript one-liner though; could it perhaps be formatted a little more legibly? (If you want the output JavaScript to be a one-liner, you still have the option of breaking the python string so that it at least appears formatted/indented reasonably in the Python source.)
review:
Approve
- 54. By Chad Miller
-
Reformat JavaScript blob of code in server views code.
Preview Diff
[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1 | === modified file 'bin/desktopcouch-pair' | |||
2 | --- bin/desktopcouch-pair 2009-08-26 18:01:29 +0000 | |||
3 | +++ bin/desktopcouch-pair 2009-08-31 15:27:31 +0000 | |||
4 | @@ -64,6 +64,7 @@ | |||
5 | 64 | 64 | ||
6 | 65 | from desktopcouch.pair.couchdb_pairing import network_io | 65 | from desktopcouch.pair.couchdb_pairing import network_io |
7 | 66 | from desktopcouch.pair.couchdb_pairing import dbus_io | 66 | from desktopcouch.pair.couchdb_pairing import dbus_io |
8 | 67 | from desktopcouch.pair import pairing_record_type | ||
9 | 67 | 68 | ||
10 | 68 | discovery_tool_version = "1" | 69 | discovery_tool_version = "1" |
11 | 69 | 70 | ||
12 | @@ -697,7 +698,7 @@ | |||
13 | 697 | # Create a paired server record | 698 | # Create a paired server record |
14 | 698 | service_data = CLOUD_SERVICES[name] | 699 | service_data = CLOUD_SERVICES[name] |
15 | 699 | data = { | 700 | data = { |
17 | 700 | "record_type": "http://www.freedesktop.org/wiki/Specifications/desktopcouch/paired_server", | 701 | "record_type": pairing_record_type, |
18 | 701 | "pairing_identifier": str(uuid.uuid4()), | 702 | "pairing_identifier": str(uuid.uuid4()), |
19 | 702 | "server": "%s:%s" % (hostname, port), | 703 | "server": "%s:%s" % (hostname, port), |
20 | 703 | "oauth": { | 704 | "oauth": { |
21 | @@ -733,6 +734,43 @@ | |||
22 | 733 | success_note.run() | 734 | success_note.run() |
23 | 734 | success_note.destroy() | 735 | success_note.destroy() |
24 | 735 | 736 | ||
25 | 737 | |||
26 | 738 | def set_couchdb_bind_address(): | ||
27 | 739 | from desktopcouch.records.server import CouchDatabase | ||
28 | 740 | from desktopcouch import local_files | ||
29 | 741 | bind_address = local_files.get_bind_address() | ||
30 | 742 | |||
31 | 743 | if bind_address not in ("127.0.0.1", "0.0.0.0", "::1", None): | ||
32 | 744 | logging.info("we're not qualified to change explicit address %s", | ||
33 | 745 | bind_address) | ||
34 | 746 | return False | ||
35 | 747 | |||
36 | 748 | db = CouchDatabase("management", create=True) | ||
37 | 749 | results = db.get_records(create_view=True) | ||
38 | 750 | count = 0 | ||
39 | 751 | for row in results[pairing_record_type]: | ||
40 | 752 | if "server" in row.value and row.value["server"] != "": | ||
41 | 753 | # Is the record of something that probably connects back to us? | ||
42 | 754 | logging.debug("not counting fully-addressed machine %r", row.value["server"]) | ||
43 | 755 | continue | ||
44 | 756 | count += 1 | ||
45 | 757 | logging.debug("paired machine count is %d", count) | ||
46 | 758 | if count > 0: | ||
47 | 759 | if ":" in bind_address: | ||
48 | 760 | want_bind_address = "::0" | ||
49 | 761 | else: | ||
50 | 762 | want_bind_address = "0.0.0.0" | ||
51 | 763 | else: | ||
52 | 764 | if ":" in bind_address: | ||
53 | 765 | want_bind_address = "::0" | ||
54 | 766 | else: | ||
55 | 767 | want_bind_address = "127.0.0.1" | ||
56 | 768 | |||
57 | 769 | if bind_address != want_bind_address: | ||
58 | 770 | local_files.set_bind_address(want_bind_address) | ||
59 | 771 | logging.warning("changing the desktopcouch bind address from %r to %r", | ||
60 | 772 | bind_address, want_bind_address) | ||
61 | 773 | |||
62 | 736 | def main(args): | 774 | def main(args): |
63 | 737 | """Start execution.""" | 775 | """Start execution.""" |
64 | 738 | global pick_or_listen # pylint: disable-msg=W0601 | 776 | global pick_or_listen # pylint: disable-msg=W0601 |
65 | @@ -747,6 +785,7 @@ | |||
66 | 747 | pick_or_listen = PickOrListen() | 785 | pick_or_listen = PickOrListen() |
67 | 748 | return run_program() | 786 | return run_program() |
68 | 749 | finally: | 787 | finally: |
69 | 788 | set_couchdb_bind_address() | ||
70 | 750 | logging.debug("exiting couchdb pairing tool") | 789 | logging.debug("exiting couchdb pairing tool") |
71 | 751 | 790 | ||
72 | 752 | 791 | ||
73 | 753 | 792 | ||
74 | === modified file 'bin/desktopcouch-paired-replication-manager' | |||
75 | --- bin/desktopcouch-paired-replication-manager 2009-08-26 19:04:39 +0000 | |||
76 | +++ bin/desktopcouch-paired-replication-manager 2009-08-30 12:25:52 +0000 | |||
77 | @@ -32,6 +32,7 @@ | |||
78 | 32 | import xdg.BaseDirectory | 32 | import xdg.BaseDirectory |
79 | 33 | 33 | ||
80 | 34 | import desktopcouch | 34 | import desktopcouch |
81 | 35 | from desktopcouch import local_files | ||
82 | 35 | from desktopcouch.pair.couchdb_pairing import couchdb_io | 36 | from desktopcouch.pair.couchdb_pairing import couchdb_io |
83 | 36 | from desktopcouch.pair.couchdb_pairing import dbus_io | 37 | from desktopcouch.pair.couchdb_pairing import dbus_io |
84 | 37 | 38 | ||
85 | @@ -84,12 +85,37 @@ | |||
86 | 84 | rotating_log.setLevel(logging.DEBUG) | 85 | rotating_log.setLevel(logging.DEBUG) |
87 | 85 | formatter = logging.Formatter('%(asctime)s %(levelname)-8s %(message)s') | 86 | formatter = logging.Formatter('%(asctime)s %(levelname)-8s %(message)s') |
88 | 86 | rotating_log.setFormatter(formatter) | 87 | rotating_log.setFormatter(formatter) |
89 | 88 | console = logging.StreamHandler() | ||
90 | 89 | console.setLevel(logging.WARNING) | ||
91 | 87 | logging.getLogger('').addHandler(rotating_log) | 90 | logging.getLogger('').addHandler(rotating_log) |
92 | 91 | logging.getLogger('').addHandler(console) | ||
93 | 88 | logging.getLogger('').setLevel(logging.DEBUG) | 92 | logging.getLogger('').setLevel(logging.DEBUG) |
94 | 89 | 93 | ||
95 | 90 | try: | 94 | try: |
96 | 91 | log.info("Starting.") | 95 | log.info("Starting.") |
97 | 92 | 96 | ||
98 | 97 | bind_addr = local_files.get_bind_address() | ||
99 | 98 | if bind_addr is not None and bind_addr != '': | ||
100 | 99 | from socket import inet_ntop, inet_pton | ||
101 | 100 | from socket import error as socketerror | ||
102 | 101 | try: | ||
103 | 102 | # There are more than dotted decimal quad to | ||
104 | 103 | # contend with. 183468213 == 0xaef80b5 == | ||
105 | 104 | # 0xa.0xef.0x80.0xb5 == 012.0357.0200.0265 == | ||
106 | 105 | # 10.239.128.181. Just ask the system instead. | ||
107 | 106 | addr_standard_form = inet_ntop(inet_pton(bind_addr)) | ||
108 | 107 | except socketerror: | ||
109 | 108 | log.warn("bind address is illegal, %r", bind_addr) | ||
110 | 109 | log.warn("(If couchdb does understand it, please open a bug!)") | ||
111 | 110 | sys.exit(1) | ||
112 | 111 | |||
113 | 112 | if addr_standard_form.startswith("127."): # entire /8 is local. | ||
114 | 113 | log.warn("couchdb bound to addr %r; cannot accept connections", | ||
115 | 114 | bind_addr) | ||
116 | 115 | elif addr_standard_form == "::1": # no addr block on v6. Just one. | ||
117 | 116 | log.warn("couchdb bound to addr %r; cannot accept connections", | ||
118 | 117 | bind_addr) | ||
119 | 118 | |||
120 | 93 | unique_identifiers = couchdb_io.get_my_host_unique_id() | 119 | unique_identifiers = couchdb_io.get_my_host_unique_id() |
121 | 94 | if unique_identifiers is None: | 120 | if unique_identifiers is None: |
122 | 95 | log.warn("No unique hostaccount id is set, so pairing not enabled.") | 121 | log.warn("No unique hostaccount id is set, so pairing not enabled.") |
123 | 96 | 122 | ||
124 | === modified file 'desktopcouch/local_files.py' | |||
125 | --- desktopcouch/local_files.py 2009-08-25 16:01:15 +0000 | |||
126 | +++ desktopcouch/local_files.py 2009-08-31 15:27:31 +0000 | |||
127 | @@ -26,7 +26,11 @@ | |||
128 | 26 | import os | 26 | import os |
129 | 27 | import xdg.BaseDirectory | 27 | import xdg.BaseDirectory |
130 | 28 | import subprocess | 28 | import subprocess |
132 | 29 | 29 | import logging | |
133 | 30 | try: | ||
134 | 31 | import ConfigParser as configparser | ||
135 | 32 | except ImportError: | ||
136 | 33 | import configparser | ||
137 | 30 | 34 | ||
138 | 31 | def mkpath(rootdir, path): | 35 | def mkpath(rootdir, path): |
139 | 32 | "Remove .. from paths" | 36 | "Remove .. from paths" |
140 | @@ -57,7 +61,10 @@ | |||
141 | 57 | stdout=subprocess.PIPE) | 61 | stdout=subprocess.PIPE) |
142 | 58 | line = process.stdout.read().split('\n')[0] | 62 | line = process.stdout.read().split('\n')[0] |
143 | 59 | couchversion = line.split()[-1] | 63 | couchversion = line.split()[-1] |
145 | 60 | if couchversion.startswith('0.1'): | 64 | |
146 | 65 | import distutils.version | ||
147 | 66 | if distutils.version.LooseVersion(couchversion) >= \ | ||
148 | 67 | distutils.version.LooseVersion('0.10'): | ||
149 | 61 | chain = '-a' | 68 | chain = '-a' |
150 | 62 | else: | 69 | else: |
151 | 63 | chain = '-C' | 70 | chain = '-C' |
152 | @@ -65,10 +72,13 @@ | |||
153 | 65 | return chain | 72 | return chain |
154 | 66 | 73 | ||
155 | 67 | class NoOAuthTokenException(Exception): | 74 | class NoOAuthTokenException(Exception): |
156 | 75 | def __init__(self, file_name): | ||
157 | 76 | super(Exception, self).__init__() | ||
158 | 77 | self.file_name = file_name | ||
159 | 68 | def __str__(self): | 78 | def __str__(self): |
161 | 69 | return "OAuth details were not found in the ini file (%s)" % FILE_INI | 79 | return "OAuth details were not found in the ini file (%s)" % self.file_name |
162 | 70 | 80 | ||
164 | 71 | def get_oauth_tokens(): | 81 | def get_oauth_tokens(config_file_name=FILE_INI): |
165 | 72 | """Return the OAuth tokens from the desktop Couch ini file. | 82 | """Return the OAuth tokens from the desktop Couch ini file. |
166 | 73 | CouchDB OAuth is two-legged OAuth (not three-legged like most OAuth). | 83 | CouchDB OAuth is two-legged OAuth (not three-legged like most OAuth). |
167 | 74 | We have one "consumer", defined by a consumer_key and a secret, | 84 | We have one "consumer", defined by a consumer_key and a secret, |
168 | @@ -78,18 +88,17 @@ | |||
169 | 78 | (More traditional 3-legged OAuth starts with a "request token" which is | 88 | (More traditional 3-legged OAuth starts with a "request token" which is |
170 | 79 | then used to procure an "access token". We do not require this.) | 89 | then used to procure an "access token". We do not require this.) |
171 | 80 | """ | 90 | """ |
174 | 81 | import ConfigParser | 91 | c = configparser.ConfigParser() |
173 | 82 | c = ConfigParser.ConfigParser() | ||
175 | 83 | # monkeypatch ConfigParser to stop it lower-casing option names | 92 | # monkeypatch ConfigParser to stop it lower-casing option names |
176 | 84 | c.optionxform = lambda s: s | 93 | c.optionxform = lambda s: s |
178 | 85 | c.read(FILE_INI) | 94 | c.read(config_file_name) |
179 | 86 | try: | 95 | try: |
180 | 87 | oauth_token_secrets = c.items("oauth_token_secrets") | 96 | oauth_token_secrets = c.items("oauth_token_secrets") |
181 | 88 | oauth_consumer_secrets = c.items("oauth_consumer_secrets") | 97 | oauth_consumer_secrets = c.items("oauth_consumer_secrets") |
184 | 89 | except ConfigParser.NoSectionError: | 98 | except configparser.NoSectionError: |
185 | 90 | raise NoOAuthTokenException | 99 | raise NoOAuthTokenException(config_file_name) |
186 | 91 | if not oauth_token_secrets or not oauth_consumer_secrets: | 100 | if not oauth_token_secrets or not oauth_consumer_secrets: |
188 | 92 | raise NoOAuthTokenException | 101 | raise NoOAuthTokenException(config_file_name) |
189 | 93 | try: | 102 | try: |
190 | 94 | out = { | 103 | out = { |
191 | 95 | "token": oauth_token_secrets[0][0], | 104 | "token": oauth_token_secrets[0][0], |
192 | @@ -98,9 +107,30 @@ | |||
193 | 98 | "consumer_secret": oauth_consumer_secrets[0][1] | 107 | "consumer_secret": oauth_consumer_secrets[0][1] |
194 | 99 | } | 108 | } |
195 | 100 | except IndexError: | 109 | except IndexError: |
197 | 101 | raise NoOAuthTokenException | 110 | raise NoOAuthTokenException(config_file_name) |
198 | 102 | return out | 111 | return out |
199 | 103 | 112 | ||
200 | 113 | |||
201 | 114 | def get_bind_address(config_file_name=FILE_INI): | ||
202 | 115 | """Retreive a string if it exists, or None if it doesn't.""" | ||
203 | 116 | c = configparser.ConfigParser() | ||
204 | 117 | try: | ||
205 | 118 | c.read(config_file_name) | ||
206 | 119 | return c.get("httpd", "bind_address") | ||
207 | 120 | except (configparser.NoOptionError, OSError), e: | ||
208 | 121 | logging.warn("config file %r error. %s", config_file_name, e) | ||
209 | 122 | return None | ||
210 | 123 | |||
211 | 124 | def set_bind_address(address, config_file_name=FILE_INI): | ||
212 | 125 | c = configparser.SafeConfigParser() | ||
213 | 126 | c.read(config_file_name) | ||
214 | 127 | if not c.has_section("httpd"): | ||
215 | 128 | c.add_section("httpd") | ||
216 | 129 | c.set("httpd", "bind_address", address) | ||
217 | 130 | with open(config_file_name, 'wb') as configfile: | ||
218 | 131 | c.write(configfile) | ||
219 | 132 | |||
220 | 133 | |||
221 | 104 | # You will need to add -b or -k on the end of this | 134 | # You will need to add -b or -k on the end of this |
222 | 105 | COUCH_EXEC_COMMAND = [COUCH_EXE, couch_chain_flag(), FILE_INI, '-p', FILE_PID, | 135 | COUCH_EXEC_COMMAND = [COUCH_EXE, couch_chain_flag(), FILE_INI, '-p', FILE_PID, |
223 | 106 | '-o', FILE_STDOUT, '-e', FILE_STDERR] | 136 | '-o', FILE_STDOUT, '-e', FILE_STDERR] |
224 | 107 | 137 | ||
225 | === modified file 'desktopcouch/pair/__init__.py' | |||
226 | --- desktopcouch/pair/__init__.py 2009-07-08 17:48:11 +0000 | |||
227 | +++ desktopcouch/pair/__init__.py 2009-08-31 15:27:31 +0000 | |||
228 | @@ -14,3 +14,5 @@ | |||
229 | 14 | # You should have received a copy of the GNU Lesser General Public License | 14 | # You should have received a copy of the GNU Lesser General Public License |
230 | 15 | # along with desktopcouch. If not, see <http://www.gnu.org/licenses/>. | 15 | # along with desktopcouch. If not, see <http://www.gnu.org/licenses/>. |
231 | 16 | """The pair module.""" | 16 | """The pair module.""" |
232 | 17 | |||
233 | 18 | pairing_record_type = "http://www.freedesktop.org/wiki/Specifications/desktopcouch/paired_server" | ||
234 | 17 | 19 | ||
235 | === modified file 'desktopcouch/records/server.py' | |||
236 | --- desktopcouch/records/server.py 2009-08-24 20:35:25 +0000 | |||
237 | +++ desktopcouch/records/server.py 2009-08-31 15:28:46 +0000 | |||
238 | @@ -44,6 +44,17 @@ | |||
239 | 44 | "passing create=True)") % self.database | 44 | "passing create=True)") % self.database |
240 | 45 | 45 | ||
241 | 46 | 46 | ||
242 | 47 | def row_is_deleted(row): | ||
243 | 48 | """Test if a row is marked as deleted. Smart views 'maps' should not | ||
244 | 49 | return rows that are marked as deleted, so this function is not often | ||
245 | 50 | required.""" | ||
246 | 51 | try: | ||
247 | 52 | return row['application_annotations']['Ubuntu One']\ | ||
248 | 53 | ['private_application_annotations']['deleted'] | ||
249 | 54 | except KeyError: | ||
250 | 55 | return False | ||
251 | 56 | |||
252 | 57 | |||
253 | 47 | class CouchDatabase(object): | 58 | class CouchDatabase(object): |
254 | 48 | """An small records specific abstraction over a couch db database.""" | 59 | """An small records specific abstraction over a couch db database.""" |
255 | 49 | 60 | ||
256 | @@ -212,10 +223,12 @@ | |||
257 | 212 | return [] | 223 | return [] |
258 | 213 | 224 | ||
259 | 214 | def get_records(self, record_type=None, create_view=False, | 225 | def get_records(self, record_type=None, create_view=False, |
264 | 215 | design_doc=DEFAULT_DESIGN_DOCUMENT): | 226 | design_doc=DEFAULT_DESIGN_DOCUMENT, version="1"): |
265 | 216 | """A convenience function to get records. We optionally create a view | 227 | """A convenience function to get records from a view named |
266 | 217 | in the design document. C<create_view> may be True or False, and a | 228 | C{get_records_and_type}, suffixed with C{__v} and the supplied version |
267 | 218 | special value, None, is analogous to O_EXCL|O_CREAT . | 229 | string (or default of "1"). We optionally create a view in the design |
268 | 230 | document. C{create_view} may be True or False, and a special value, | ||
269 | 231 | None, is analogous to O_EXCL|O_CREAT . | ||
270 | 219 | 232 | ||
271 | 220 | Set record_type to a string to retrieve records of only that | 233 | Set record_type to a string to retrieve records of only that |
272 | 221 | specified type. Otherwise, usse the view to return *all* records. | 234 | specified type. Otherwise, usse the view to return *all* records. |
273 | @@ -233,11 +246,14 @@ | |||
274 | 233 | =>> people = results[['Person']:['Person','ZZZZ']] | 246 | =>> people = results[['Person']:['Person','ZZZZ']] |
275 | 234 | """ | 247 | """ |
276 | 235 | view_name = "get_records_and_type" | 248 | view_name = "get_records_and_type" |
278 | 236 | view_map_js = """function(doc) { emit(doc.record_type, doc) }""" | 249 | view_map_js = """function(doc) { try {if (! doc['application_annotations']['Ubuntu One']['private_application_annotations']['deleted']) { emit(doc.record_type, doc);} } catch (e) { emit(doc.record_type, doc); } }""" |
279 | 237 | 250 | ||
280 | 238 | if design_doc is None: | 251 | if design_doc is None: |
281 | 239 | design_doc = view_name | 252 | design_doc = view_name |
282 | 240 | 253 | ||
283 | 254 | if not version is None: # versions do not affect design_doc name. | ||
284 | 255 | view_name = view_name + "__v" + version | ||
285 | 256 | |||
286 | 241 | exists = self.view_exists(view_name, design_doc) | 257 | exists = self.view_exists(view_name, design_doc) |
287 | 242 | 258 | ||
288 | 243 | if exists: | 259 | if exists: |
289 | @@ -255,4 +271,3 @@ | |||
290 | 255 | return viewdata | 271 | return viewdata |
291 | 256 | else: | 272 | else: |
292 | 257 | return viewdata[record_type] | 273 | return viewdata[record_type] |
293 | 258 | |||
294 | 259 | 274 | ||
295 | === modified file 'desktopcouch/records/tests/test_server.py' | |||
296 | --- desktopcouch/records/tests/test_server.py 2009-08-24 20:35:25 +0000 | |||
297 | +++ desktopcouch/records/tests/test_server.py 2009-08-31 15:28:46 +0000 | |||
298 | @@ -18,7 +18,7 @@ | |||
299 | 18 | 18 | ||
300 | 19 | """testing database/contact.py module""" | 19 | """testing database/contact.py module""" |
301 | 20 | import testtools | 20 | import testtools |
303 | 21 | from desktopcouch.records.server import CouchDatabase | 21 | from desktopcouch.records.server import CouchDatabase, row_is_deleted |
304 | 22 | from desktopcouch.records.record import Record | 22 | from desktopcouch.records.record import Record |
305 | 23 | from desktopcouch.records.tests import get_uri | 23 | from desktopcouch.records.tests import get_uri |
306 | 24 | 24 | ||
307 | @@ -146,10 +146,17 @@ | |||
308 | 146 | other_record_type = "http://example.com/unittest/bad" | 146 | other_record_type = "http://example.com/unittest/bad" |
309 | 147 | 147 | ||
310 | 148 | for i in range(7): | 148 | for i in range(7): |
311 | 149 | record = Record({'record_number': i}, | ||
312 | 150 | record_type=good_record_type) | ||
313 | 149 | if i % 3 == 1: | 151 | if i % 3 == 1: |
314 | 150 | record = Record({'record_number': i}, | 152 | record = Record({'record_number': i}, |
315 | 151 | record_type=good_record_type) | 153 | record_type=good_record_type) |
316 | 152 | record_ids_we_care_about.add(self.database.put_record(record)) | 154 | record_ids_we_care_about.add(self.database.put_record(record)) |
317 | 155 | elif i % 3 == 2: | ||
318 | 156 | record = Record({'record_number': i}, | ||
319 | 157 | record_type=good_record_type) | ||
320 | 158 | record_id = self.database.put_record(record) # correct type, | ||
321 | 159 | self.database.delete_record(record_id) # but marked deleted! | ||
322 | 153 | else: | 160 | else: |
323 | 154 | record = Record({'record_number': i}, | 161 | record = Record({'record_number': i}, |
324 | 155 | record_type=other_record_type) | 162 | record_type=other_record_type) |
325 | @@ -160,6 +167,7 @@ | |||
326 | 160 | for row in results[good_record_type]: # index notation | 167 | for row in results[good_record_type]: # index notation |
327 | 161 | self.assertTrue(row.id in record_ids_we_care_about) | 168 | self.assertTrue(row.id in record_ids_we_care_about) |
328 | 162 | record_ids_we_care_about.remove(row.id) | 169 | record_ids_we_care_about.remove(row.id) |
329 | 170 | self.assertFalse(row_is_deleted(row)) | ||
330 | 163 | 171 | ||
331 | 164 | self.assertTrue(len(record_ids_we_care_about) == 0, "expected zero") | 172 | self.assertTrue(len(record_ids_we_care_about) == 0, "expected zero") |
332 | 165 | 173 |
Fix Bug#419969: at pairing time, change couchdb pairing address to public
Fix Bug#419973: in replication daemon, be sure local couchdb bind address is not 127/8 .