Merge lp://qastaging/~sil/desktopcouch/allow-unpairing into lp://qastaging/desktopcouch
- allow-unpairing
- Merge into trunk
Status: | Merged |
---|---|
Approved by: | Eric Casteleijn |
Approved revision: | 57 |
Merge reported by: | Chad Miller |
Merged at revision: | not available |
Proposed branch: | lp://qastaging/~sil/desktopcouch/allow-unpairing |
Merge into: | lp://qastaging/desktopcouch |
Diff against target: | None lines |
To merge this branch: | bzr merge lp://qastaging/~sil/desktopcouch/allow-unpairing |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Eric Casteleijn (community) | Approve | ||
Nicola Larosa (community) | Approve | ||
Tim Cole (community) | Abstain | ||
Review via email:
|
Commit message
Pair and unpair servers. Allows local pairing.
Description of the change
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
Stuart Langridge (sil) wrote : | # |
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
Tim Cole (tcole) wrote : | # |
The FIXME and "does not work" in the commit messages make me a little charry. What behavior are we supposed to expect from this branch, and why are we merging it to trunk if we don't want to ship it?
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
Tim Cole (tcole) : | # |
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
Stuart Langridge (sil) wrote : | # |
> The FIXME and "does not work" in the commit messages make me a little charry.
> What behavior are we supposed to expect from this branch, and why are we
> merging it to trunk if we don't want to ship it?
It's being merged to trunk because we need to do some work based on it being done. Keeping this out of trunk and doing the further work based on this branch and then merging the lot makes for a mammoth merge in the future, and doing it in bite-size chunks makes review easier. The "does not work" stuff is because the code now knows how to receive oauth data from a remote desktopcouch, but remote desktopcouches don't send that data (this is the future work to be done). So the code itself works if handed correct data, but the overall approach (actually making local desktopcouches pair) does not (because it is not handed corrrect data).
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
Nicola Larosa (teknico) wrote : | # |
This is apparently part of a work in progress, tests pass, code looks good.
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
Eric Casteleijn (thisfred) wrote : | # |
Looks good, tests pass, but I'm getting a few pyflakes messages:
desktopcouch/
desktopcouch/
desktopcouch/
desktopcouch/
desktopcouch/
desktopcouch/
desktopcouch/
(There are more, but both the pairing and the couchgrid do some awful things intentionally that I don't know how to fix cleanly.)
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
Chad Miller (cmiller) wrote : | # |
text conflict in bin/desktopcouc
Preview Diff
1 | === modified file 'bin/desktopcouch-pair' |
2 | --- bin/desktopcouch-pair 2009-08-26 18:01:29 +0000 |
3 | +++ bin/desktopcouch-pair 2009-09-08 12:45:17 +0000 |
4 | @@ -65,10 +65,11 @@ |
5 | from desktopcouch.pair.couchdb_pairing import network_io |
6 | from desktopcouch.pair.couchdb_pairing import dbus_io |
7 | |
8 | +from desktopcouch.records.server import CouchDatabase |
9 | +from desktopcouch.records.record import Record |
10 | + |
11 | discovery_tool_version = "1" |
12 | |
13 | -CLOUD_SERVICES = {} |
14 | - |
15 | def generate_secret(length=7): |
16 | """Create a secret that is easy to write and read. We hate ambiguity and |
17 | errors.""" |
18 | @@ -445,30 +446,24 @@ |
19 | already-listening tool instance. This sets up a "Bob" in the |
20 | module's story.""" |
21 | |
22 | - # positions: host id, description, pair host, pair port, is_cloud |
23 | - listening_hosts = gtk.TreeStore(str, str, str, int, bool) |
24 | - |
25 | - # If there is an Ubuntu One key in the keyring, add Ubuntu One |
26 | - # as an item to be paired with |
27 | - import gnomekeyring |
28 | - try: |
29 | - matches = gnomekeyring.find_items_sync( |
30 | - gnomekeyring.ITEM_GENERIC_SECRET, |
31 | - {'ubuntuone-realm': "https://ubuntuone.com", |
32 | - 'oauth-consumer-key': "ubuntuone"}) |
33 | - except gnomekeyring.NoMatchError: |
34 | - matches = None |
35 | - if matches: |
36 | - # parse "a=b&c=d" to {"a":"b","c":"d"} |
37 | - oauth_data = dict([x.split("=", 1) for x in |
38 | - matches[0].secret.split("&")]) |
39 | - oauth_data.update({ |
40 | - "consumer_key": "ubuntuone", |
41 | - "consumer_secret": "", |
42 | - }) |
43 | - listening_hosts.append(None, ["Ubuntu One", "The Ubuntu One cloud service", |
44 | - "ubuntuone.com", 5984, True]) |
45 | - CLOUD_SERVICES["Ubuntu One"] = oauth_data |
46 | + # positions: host id, descr, host, port, cloud_name |
47 | + self.listening_hosts = gtk.TreeStore(str, str, str, int, str) |
48 | + |
49 | + import desktopcouch.replication_services as services |
50 | + |
51 | + for srv_name in dir(services): |
52 | + if srv_name.startswith("__"): |
53 | + continue |
54 | + srv = getattr(services, srv_name) |
55 | + try: |
56 | + if srv.is_active(): |
57 | + all_paired_cloud_servers = [x.key for x in |
58 | + self.db.execute_view("byservicename", "pairedservers")] |
59 | + if not srv_name in all_paired_cloud_servers: |
60 | + self.listening_hosts.append(None, [srv.name, srv.description, |
61 | + "", 0, srv_name]) |
62 | + except Exception, e: |
63 | + self.logging.exception("service %r has errors", srv_name) |
64 | |
65 | self.inviting = None # pylint: disable-msg=W0201 |
66 | |
67 | @@ -482,7 +477,7 @@ |
68 | pick_box.pack_start(l, False, False, 0) |
69 | l.show() |
70 | |
71 | - tv = gtk.TreeView(listening_hosts) |
72 | + tv = gtk.TreeView(self.listening_hosts) |
73 | tv.set_headers_visible(False) |
74 | tv.set_rules_hint(True) |
75 | tv.show() |
76 | @@ -495,16 +490,22 @@ |
77 | if not iter: |
78 | return |
79 | service = model.get_value(iter, 0) |
80 | + description = model.get_value(iter, 1) |
81 | hostname = model.get_value(iter, 2) |
82 | port = model.get_value(iter, 3) |
83 | - is_cloud = model.get_value(iter, 4) |
84 | + service_name = model.get_value(iter, 4) |
85 | |
86 | - if is_cloud: |
87 | + if service_name: |
88 | # Pairing with a cloud service, which doesn't do key exchange |
89 | - pair_with_cloud_service(service, hostname, port) |
90 | + pair_with_cloud_service(service_name) |
91 | + # remove from listening list |
92 | + self.listening_hosts.remove(iter) |
93 | + # add to already-paired list |
94 | + srv = getattr(services, service_name) |
95 | + self.already_paired_hosts.append(None, [service, description, |
96 | + hostname, port, service_name]) |
97 | return |
98 | |
99 | - |
100 | self.logging.error("Local pairing is not yet supported. Aborting.") |
101 | fail_note = gtk.MessageDialog( |
102 | parent=pick_or_listen.window, |
103 | @@ -514,7 +515,7 @@ |
104 | message_format =_("Sorry, couchdb authentication is not yet enabled, so local pairing is not supported")) |
105 | fail_note.run() |
106 | fail_note.destroy() |
107 | - return False |
108 | + #FIXME return False |
109 | |
110 | |
111 | self.logging.info("connecting to %s:%s tcp to invite", |
112 | @@ -539,38 +540,155 @@ |
113 | def add_service_to_list(name, description, host, port, version): |
114 | """When a zeroconf service appears, this adds it to the |
115 | listing of choices.""" |
116 | - |
117 | - listening_hosts.append(None, [name, description, host, port, False, None]) |
118 | + self.listening_hosts.append(None, [name, description, host, port, None]) |
119 | |
120 | def remove_service_from_list(name): |
121 | """When a zeroconf service disappears, this finds it in the |
122 | listing and removes it as an option for picking.""" |
123 | |
124 | - it = listening_hosts.get_iter_first() |
125 | + it = self.listening_hosts.get_iter_first() |
126 | while it is not None: |
127 | - if listening_hosts.get_value(it, 0) == name: |
128 | - listening_hosts.remove(it) |
129 | + if self.listening_hosts.get_value(it, 0) == name: |
130 | + self.listening_hosts.remove(it) |
131 | return True |
132 | - it = listening_hosts.iter_next(it) |
133 | - |
134 | - dbus_io.discover_services(add_service_to_list, remove_service_from_list) |
135 | - |
136 | - cell = gtk.CellRendererText() |
137 | - cell.set_property("weight", pango.WEIGHT_BOLD) |
138 | - cell.set_property("weight-set", True) |
139 | - tv.append_column(hostid_col) |
140 | - hostid_col.pack_start(cell, True) |
141 | - hostid_col.add_attribute(cell, 'text', 0) |
142 | - |
143 | - cell = gtk.CellRendererText() |
144 | - cell.set_property("ellipsize", pango.ELLIPSIZE_END) |
145 | - cell.set_property("ellipsize-set", True) |
146 | - tv.append_column(description_col) |
147 | - description_col.pack_start(cell, True) |
148 | - description_col.add_attribute(cell, 'text', 1) |
149 | - |
150 | - return pick_box |
151 | - |
152 | + it = self.listening_hosts.iter_next(it) |
153 | + |
154 | + dbus_io.discover_services(add_service_to_list, remove_service_from_list, show_local=True) |
155 | + |
156 | + cell = gtk.CellRendererText() |
157 | + cell.set_property("weight", pango.WEIGHT_BOLD) |
158 | + cell.set_property("weight-set", True) |
159 | + tv.append_column(hostid_col) |
160 | + hostid_col.pack_start(cell, True) |
161 | + hostid_col.add_attribute(cell, 'text', 0) |
162 | + |
163 | + cell = gtk.CellRendererText() |
164 | + cell.set_property("ellipsize", pango.ELLIPSIZE_END) |
165 | + cell.set_property("ellipsize-set", True) |
166 | + tv.append_column(description_col) |
167 | + description_col.pack_start(cell, True) |
168 | + description_col.add_attribute(cell, 'text', 1) |
169 | + |
170 | + return pick_box |
171 | + |
172 | + def create_already_paired_pane(self, container): |
173 | + """Set up the pane that shows servers which are already paired.""" |
174 | + |
175 | + # positions: host id, descr, host, port, cloud_name |
176 | + self.already_paired_hosts = gtk.TreeStore(str, str, str, int, str) |
177 | + |
178 | + import desktopcouch.replication_services as services |
179 | + for already_paired_record in self.db.execute_view("byserver", |
180 | + "pairedservers"): |
181 | + host, port = already_paired_record.value["server"].split(":") |
182 | + try: |
183 | + port = int(port) |
184 | + except: |
185 | + port = 0 |
186 | + self.already_paired_hosts.append(None, [host, None, host, port, None]) |
187 | + |
188 | + for already_paired_record in self.db.execute_view("byservicename", |
189 | + "pairedservers"): |
190 | + srv_name = already_paired_record.value["service_name"] |
191 | + if srv_name.startswith("__"): |
192 | + continue |
193 | + srv = getattr(services, srv_name) |
194 | + if srv.is_active(): |
195 | + self.already_paired_hosts.append(None, [srv.name, srv.description, |
196 | + "", 0, srv_name]) |
197 | + |
198 | + hostid_col = gtk.TreeViewColumn(_("service name")) |
199 | + description_col = gtk.TreeViewColumn(_("service name")) |
200 | + |
201 | + pick_box = gtk.VBox() |
202 | + container.pack_start(pick_box, False, False, 10) |
203 | + |
204 | + l = gtk.Label(_("Desktop Couches that you are already paired with")) |
205 | + pick_box.pack_start(l, False, False, 0) |
206 | + l.show() |
207 | + |
208 | + tv = gtk.TreeView(self.already_paired_hosts) |
209 | + tv.set_headers_visible(False) |
210 | + tv.set_rules_hint(True) |
211 | + tv.show() |
212 | + |
213 | + def clicked(selection): |
214 | + """An item in the list of services was clicked, so now we go |
215 | + about inviting it to pair with us.""" |
216 | + |
217 | + model, iter = selection.get_selected() |
218 | + if not iter: |
219 | + return |
220 | + service = model.get_value(iter, 0) |
221 | + hostname = model.get_value(iter, 2) |
222 | + port = model.get_value(iter, 3) |
223 | + service_name = model.get_value(iter, 4) |
224 | + |
225 | + if service_name: |
226 | + # Pairing with a cloud service, which doesn't do key exchange |
227 | + # delete record |
228 | + for record in self.db.execute_view("byservicename", |
229 | + "pairedservers")[service_name]: |
230 | + self.db.delete_record(record.value["_id"]) |
231 | + |
232 | + # remove from already-paired list |
233 | + self.already_paired_hosts.remove(iter) |
234 | + # add to listening list |
235 | + srv = getattr(services, service_name) |
236 | + self.listening_hosts.append(None, [service, srv.description, |
237 | + hostname, port, service_name]) |
238 | + return |
239 | + |
240 | + self.logging.error("Local pairing is not yet supported. Aborting.") |
241 | + fail_note = gtk.MessageDialog( |
242 | + parent=pick_or_listen.window, |
243 | + flags=gtk.DIALOG_DESTROY_WITH_PARENT, |
244 | + buttons=gtk.BUTTONS_OK, |
245 | + type=gtk.MESSAGE_ERROR, |
246 | + message_format =_("Sorry, couchdb authentication is not yet enabled, so local pairing is not supported")) |
247 | + fail_note.run() |
248 | + fail_note.destroy() |
249 | + #FIXME return False |
250 | + |
251 | + # delete record |
252 | + print list(self.db.execute_view("byserver", |
253 | + "pairedservers")), hostname, port |
254 | + for record in self.db.execute_view("byserver", |
255 | + "pairedservers")["%s:%s" % (hostname, port)]: |
256 | + self.db.delete_record(record.value["_id"]) |
257 | + |
258 | + # remove from already-paired list |
259 | + self.already_paired_hosts.remove(iter) |
260 | + # do not add to listening list -- if it's listening then zeroconf |
261 | + # will pick it up |
262 | + return |
263 | + |
264 | + tv_selection = tv.get_selection() |
265 | + tv_selection.connect("changed", clicked) |
266 | + |
267 | + scrolled_window = gtk.ScrolledWindow(hadjustment=None, vadjustment=None) |
268 | + scrolled_window.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC) |
269 | + scrolled_window.set_border_width(10) |
270 | + scrolled_window.add_with_viewport(tv) |
271 | + scrolled_window.show() |
272 | + |
273 | + pick_box.pack_start(scrolled_window, True, False, 0) |
274 | + |
275 | + cell = gtk.CellRendererText() |
276 | + cell.set_property("weight", pango.WEIGHT_BOLD) |
277 | + cell.set_property("weight-set", True) |
278 | + tv.append_column(hostid_col) |
279 | + hostid_col.pack_start(cell, True) |
280 | + hostid_col.add_attribute(cell, 'text', 0) |
281 | + |
282 | + cell = gtk.CellRendererText() |
283 | + cell.set_property("ellipsize", pango.ELLIPSIZE_END) |
284 | + cell.set_property("ellipsize-set", True) |
285 | + tv.append_column(description_col) |
286 | + description_col.pack_start(cell, True) |
287 | + description_col.add_attribute(cell, 'text', 1) |
288 | + |
289 | + return pick_box |
290 | |
291 | def create_single_listen_pane(self, container): |
292 | """This sets up an "Alice" from the module's story. |
293 | @@ -600,7 +718,7 @@ |
294 | message_format =_("Sorry, couchdb authentication is not yet enabled, so local pairing is not supported")) |
295 | fail_note.run() |
296 | fail_note.destroy() |
297 | - return False |
298 | + #FIXME return False |
299 | |
300 | |
301 | btn.set_sensitive(False) |
302 | @@ -637,6 +755,36 @@ |
303 | #some_row_in_list.connect("clicked", self.listen, target_db_info) |
304 | |
305 | def __init__(self): |
306 | + |
307 | + self.db = CouchDatabase("management", create=True) |
308 | + design_doc = "pairedservers" |
309 | + if not self.db.view_exists("byserver", design_doc): |
310 | + map_js = """function(doc) { |
311 | + if (doc.record_type == 'http://www.freedesktop.org/wiki/Specifications/desktopcouch/paired_server') |
312 | + if (doc.application_annotations && |
313 | + doc.application_annotations["Ubuntu One"] && |
314 | + doc.application_annotations["Ubuntu One"].private_application_annotations && |
315 | + doc.application_annotations["Ubuntu One"].private_application_annotations.deleted) { |
316 | + // don't emit deleted items |
317 | + } else { |
318 | + emit(doc.server, doc); |
319 | + } |
320 | + }""" |
321 | + self.db.add_view("byserver", map_js, None, design_doc) |
322 | + if not self.db.view_exists("byservicename", design_doc): |
323 | + map_js = """function(doc) { |
324 | + if (doc.record_type == 'http://www.freedesktop.org/wiki/Specifications/desktopcouch/paired_server') |
325 | + if (doc.application_annotations && |
326 | + doc.application_annotations["Ubuntu One"] && |
327 | + doc.application_annotations["Ubuntu One"].private_application_annotations && |
328 | + doc.application_annotations["Ubuntu One"].private_application_annotations.deleted) { |
329 | + // don't emit deleted items |
330 | + } else { |
331 | + emit(doc.service_name, doc); |
332 | + } |
333 | + }""" |
334 | + self.db.add_view("byservicename", map_js, None, design_doc) |
335 | + |
336 | self.logging = logging.getLogger(self.__class__.__name__) |
337 | |
338 | self.window = gtk.Window() |
339 | @@ -651,6 +799,7 @@ |
340 | self.window.add(top_vbox) |
341 | |
342 | self.pick_pane = self.create_pick_pane(top_vbox) |
343 | + self.already_paired_pane = self.create_already_paired_pane(top_vbox) |
344 | self.listen_pane = self.create_single_listen_pane(top_vbox) |
345 | |
346 | copyright = gtk.Label(_("Copyright 2009 Canonical")) |
347 | @@ -658,6 +807,7 @@ |
348 | copyright.show() |
349 | |
350 | self.pick_pane.show() |
351 | + self.already_paired_pane.show() |
352 | self.listen_pane.show() |
353 | |
354 | top_vbox.show() |
355 | @@ -673,6 +823,41 @@ |
356 | # successful. |
357 | #couchdb_io.replicate_to(...) |
358 | |
359 | + import uuid |
360 | + |
361 | + # FIXME THIS IS WRONG. We need to get the oauth tokens for the remote |
362 | + # server somehow! |
363 | + oauth_data = {"consumer_key": "XXX", "consumer_secret": "XXX", |
364 | + "oauth_token": "XXX", "oauth_token_secret": "XXX"} |
365 | + try: |
366 | + data = { |
367 | + "record_type": "http://www.freedesktop.org/wiki/Specifications/desktopcouch/paired_server", |
368 | + "pairing_identifier": str(uuid.uuid4()), |
369 | + "server": "%s:%s" % (host, port), |
370 | + "oauth": { |
371 | + "consumer_key": str(oauth_data["consumer_key"]), |
372 | + "consumer_secret": str(oauth_data["consumer_secret"]), |
373 | + "token": str(oauth_data["oauth_token"]), |
374 | + "token_secret": str(oauth_data["oauth_token_secret"]), |
375 | + }, |
376 | + "pull_from_server": True |
377 | + } |
378 | + |
379 | + d = CouchDatabase("management", create=True) |
380 | + r = Record(data) |
381 | + record_id = d.put_record(r) |
382 | + except Exception, e: |
383 | + logging.exception("failure writing record for %s:%s", host, port) |
384 | + fail_note = gtk.MessageDialog( |
385 | + parent=pick_or_listen.window, |
386 | + flags=gtk.DIALOG_DESTROY_WITH_PARENT, |
387 | + buttons=gtk.BUTTONS_OK, |
388 | + type=gtk.MESSAGE_ERROR, |
389 | + message_format =_("Couldn't save pairing details for %s") % host) |
390 | + fail_note.run() |
391 | + fail_note.destroy() |
392 | + return |
393 | + |
394 | success_note = gtk.Dialog(title=_("Paired with %(host)s") % locals(), |
395 | parent=pick_or_listen.window, |
396 | flags=gtk.DIALOG_DESTROY_WITH_PARENT, |
397 | @@ -688,39 +873,40 @@ |
398 | lambda *args: pick_or_listen.window.destroy()) |
399 | success_note.show() |
400 | |
401 | -def pair_with_cloud_service(name, hostname, port): |
402 | +def pair_with_cloud_service(service_name): |
403 | """Write a paired server record for the selected cloud service.""" |
404 | - from desktopcouch.records.server import CouchDatabase |
405 | - from desktopcouch.records.record import Record |
406 | import uuid |
407 | - |
408 | - # Create a paired server record |
409 | - service_data = CLOUD_SERVICES[name] |
410 | - data = { |
411 | - "record_type": "http://www.freedesktop.org/wiki/Specifications/desktopcouch/paired_server", |
412 | - "pairing_identifier": str(uuid.uuid4()), |
413 | - "server": "%s:%s" % (hostname, port), |
414 | - "oauth": { |
415 | - "consumer_key": service_data["consumer_key"], |
416 | - "consumer_secret": service_data["consumer_secret"], |
417 | - "token": service_data["oauth_token"], |
418 | - "token_secret": service_data["oauth_token_secret"], |
419 | - }, |
420 | - "pull_from_server": True |
421 | - } |
422 | + import desktopcouch.replication_services as services |
423 | + srv = getattr(services, service_name) |
424 | + |
425 | try: |
426 | + oauth_data = srv.oauth_data() |
427 | + data = { |
428 | + "record_type": "http://www.freedesktop.org/wiki/Specifications/desktopcouch/paired_server", |
429 | + "pairing_identifier": str(uuid.uuid4()), |
430 | + "service_name": service_name, |
431 | + "oauth": { |
432 | + "consumer_key": str(oauth_data["consumer_key"]), |
433 | + "consumer_secret": str(oauth_data["consumer_secret"]), |
434 | + "token": str(oauth_data["oauth_token"]), |
435 | + "token_secret": str(oauth_data["oauth_token_secret"]), |
436 | + }, |
437 | + "pull_from_server": True |
438 | + } |
439 | + |
440 | d = CouchDatabase("management", create=True) |
441 | r = Record(data) |
442 | record_id = d.put_record(r) |
443 | + |
444 | except Exception, e: |
445 | + logging.exception("failure in module for service %s", service_name) |
446 | fail_note = gtk.MessageDialog( |
447 | parent=pick_or_listen.window, |
448 | flags=gtk.DIALOG_DESTROY_WITH_PARENT, |
449 | buttons=gtk.BUTTONS_OK, |
450 | type=gtk.MESSAGE_ERROR, |
451 | - message_format =_("Couldn't save pairing details for %s") % name) |
452 | + message_format =_("Couldn't save pairing details for %s") % service_name) |
453 | fail_note.run() |
454 | - logging.exception(e) |
455 | fail_note.destroy() |
456 | return |
457 | |
458 | @@ -729,7 +915,7 @@ |
459 | flags=gtk.DIALOG_DESTROY_WITH_PARENT, |
460 | buttons=gtk.BUTTONS_OK, |
461 | type=gtk.MESSAGE_INFO, |
462 | - message_format =_("Successfully paired with %s") % name) |
463 | + message_format =_("Successfully paired with %s") % service_name) |
464 | success_note.run() |
465 | success_note.destroy() |
466 | |
467 | |
468 | === modified file 'bin/desktopcouch-paired-replication-manager' |
469 | --- bin/desktopcouch-paired-replication-manager 2009-08-26 19:04:39 +0000 |
470 | +++ bin/desktopcouch-paired-replication-manager 2009-09-04 17:40:19 +0000 |
471 | @@ -37,7 +37,27 @@ |
472 | |
473 | already_replicating = False |
474 | |
475 | + |
476 | +def db_prefix_for_statically_addressed_replicators(addr, port): |
477 | + """Use the hostname and port to look up what the prefix should be on the |
478 | + databases. This gives an egalitarian way for non-UbuntuOne servers to have |
479 | + their own remote-db-name scheme.""" |
480 | + addrport = addr + "_" + str(port) |
481 | + module_name = addrport.replace(".", "_") |
482 | + try: |
483 | + logging.debug("Looking up prefix %r in mod %s", addrport, module_name) |
484 | + mod = __import__("desktopcouch.replication_hosts", fromlist=[module_name]) |
485 | + return getattr(mod, module_name).db_name_prefix |
486 | + except ImportError, e: |
487 | + logging.info("Not changing remote db name. %s", e) |
488 | + return "" |
489 | + except Exception, e: |
490 | + logging.exception("Not changing remote db name.") |
491 | + return "" |
492 | + |
493 | class ReplicatorThread(threading.Thread): |
494 | + """Offload all the replication stuff to a thread so that the main thread |
495 | + can continue to talk to the zeroconf daemon in a non-blocked way.""" |
496 | def __init__(self, local_port): |
497 | log.debug("starting up replication thread") |
498 | super(ReplicatorThread, self).__init__() |
499 | @@ -48,15 +68,38 @@ |
500 | already_replicating = True # just trying to be polite. |
501 | try: |
502 | for uuid, addr, port in dbus_io.get_seen_paired_hosts(): |
503 | - log.debug("host %s is seen; want to replicate to it", uuid) |
504 | + log.debug("want to replipush to discovered host %r @ %s", |
505 | + uuid, addr) |
506 | for db_name in couchdb_io.get_database_names_replicatable(): |
507 | couchdb_io.replicate(db_name, db_name, |
508 | target_host=addr, target_port=port, |
509 | source_port=self.local_port) |
510 | - |
511 | - |
512 | - # TODO: get static addressed paired hosts and replicate to |
513 | - # those too. |
514 | + |
515 | + for uuid, addr, port, to_pull, to_push in \ |
516 | + couchdb_io.get_static_paired_hosts(): |
517 | + |
518 | + if to_pull: |
519 | + remote_db_prefix = db_prefix_for_statically_addressed_replicators(addr, port) |
520 | + for db_name in couchdb_io.get_database_names_replicatable(): |
521 | + remote_db_name = str(remote_db_prefix)+db_name |
522 | + log.debug("want to replipush %r to static host %r @ %s", |
523 | + remote_db_name, uuid, addr) |
524 | + couchdb_io.replicate(db_name, remote_db_name, |
525 | + target_host=addr, target_port=port, |
526 | + source_port=self.local_port, target_ssl=True) |
527 | + if to_push: |
528 | + for remote_db_name in couchdb_io.get_database_names_replicatable(addr, |
529 | + port): |
530 | + if not remote_db_name.startswith(str(remote_db_prefix)): |
531 | + continue |
532 | + db_name = remote_db_name[len(str(remote_db_prefix)):] |
533 | + if db_name.strip("/") == "management": |
534 | + continue # be paranoid about what we accept. |
535 | + log.debug("want to replipull %r from static host %r @ %s", |
536 | + db_name, uuid, addr) |
537 | + couchdb_io.replicate(remote_db_name, db_name, |
538 | + source_host=addr, source_port=port, |
539 | + target_port=self.local_port, source_ssl=True) |
540 | |
541 | finally: |
542 | already_replicating = False |
543 | @@ -70,6 +113,7 @@ |
544 | |
545 | r = ReplicatorThread(local_port) |
546 | r.start() |
547 | + return r |
548 | |
549 | def main(args): |
550 | log_directory = os.path.join(xdg.BaseDirectory.xdg_cache_home, |
551 | @@ -87,6 +131,8 @@ |
552 | logging.getLogger('').addHandler(rotating_log) |
553 | logging.getLogger('').setLevel(logging.DEBUG) |
554 | |
555 | + print "Logging to", os.path.join(log_directory, "desktop-couch-replication.log") |
556 | + |
557 | try: |
558 | log.info("Starting.") |
559 | |
560 | @@ -117,6 +163,10 @@ |
561 | b.unpublish() |
562 | |
563 | finally: |
564 | + try: |
565 | + t.stop() |
566 | + except: |
567 | + pass |
568 | log.info("Quitting.") |
569 | |
570 | if __name__ == "__main__": |
571 | |
572 | === modified file 'desktopcouch/pair/couchdb_pairing/couchdb_io.py' |
573 | --- desktopcouch/pair/couchdb_pairing/couchdb_io.py 2009-08-26 19:04:39 +0000 |
574 | +++ desktopcouch/pair/couchdb_pairing/couchdb_io.py 2009-09-03 21:26:51 +0000 |
575 | @@ -21,6 +21,7 @@ |
576 | |
577 | from desktopcouch import find_port as desktopcouch_find_port |
578 | from desktopcouch.records import server |
579 | +import socket |
580 | |
581 | RECTYPE_BASE = "http://www.freedesktop.org/wiki/Specifications/desktopcouch/" |
582 | PAIRED_SERVER_RECORD_TYPE = RECTYPE_BASE + "paired_server" |
583 | @@ -30,13 +31,40 @@ |
584 | port = desktopcouch_find_port() # make sure d-c is running. |
585 | return server.CouchDatabase(name, create=create) |
586 | |
587 | -def get_database_names_replicatable(): |
588 | +def get_static_paired_hosts(): |
589 | + db = _get_db("management") |
590 | + results = db.get_records(create_view=True) |
591 | + found = dict() |
592 | + for row in results[PAIRED_SERVER_RECORD_TYPE]: |
593 | + try: |
594 | + if row.value["server"] != "": |
595 | + uuid = row.value["pairing_identifier"] |
596 | + to_push = row.value.get("push_to_server", True) |
597 | + to_pull = row.value.get("pull_from_server", False) |
598 | + addr, port = row.value["server"].split(":") # What about IPv6? |
599 | + found[(addr, int(port))] = uuid, to_pull, to_push |
600 | + except KeyError: |
601 | + pass |
602 | + unique_hosts = [(v1, h1, h2, v2, v3) for |
603 | + (h1, h2), (v1, v2, v3) in found.items()] |
604 | + logging.debug("static pairings are %s", unique_hosts) |
605 | + return unique_hosts |
606 | + |
607 | +def get_database_names_replicatable(host=None, port=None): |
608 | """Find a list of local databases, minus dbs that we do not want to |
609 | replicate (explicitly or implicitly).""" |
610 | |
611 | - port = int(desktopcouch_find_port()) |
612 | - couchdb_server = server.Server("http://localhost:%(port)d/" % locals()) |
613 | - all = set([db_name for db_name in couchdb_server]) |
614 | + if host is None: |
615 | + host = "localhost" |
616 | + if port is None: |
617 | + port = int(desktopcouch_find_port()) |
618 | + |
619 | + try: |
620 | + couchdb_server = server.Server("http://%(host)s:%(port)d/" % locals()) |
621 | + all = set([db_name for db_name in couchdb_server]) |
622 | + except socket.error, e: |
623 | + logging.error("Can't get list of databases from %s", couchdb_server) |
624 | + return set() |
625 | |
626 | excluded = set() |
627 | excluded.add("management") |
628 | @@ -82,51 +110,52 @@ |
629 | logging.debug("found %d %s records", len(values), key) |
630 | return values |
631 | |
632 | -def create_remote_database(dst_host, dst_port, dst_name): |
633 | +def create_database(dst_host, dst_port, dst_name): |
634 | dst_url = u"http://%(dst_host)s:%(dst_port)d/" % locals() |
635 | return server.CouchDatabase(dst_name, dst_url, create=True) |
636 | |
637 | def replicate(source_database, target_database, target_host=None, |
638 | - target_port=None, source_host=None, source_port=None): |
639 | + target_port=None, source_host=None, source_port=None, |
640 | + source_ssl=False, target_ssl=False): |
641 | """This replication is instant and blocking, and does not persist. """ |
642 | |
643 | - data = {} |
644 | - |
645 | + source_protocol = "https" if source_ssl else "http" |
646 | + target_protocol = "https" if target_ssl else "http" |
647 | if source_host: |
648 | if source_port is None: |
649 | - source = "http://%(source_host)s/%(source_database)s" % locals() |
650 | + source = "%(source_protocol)s://%(source_host)s/%(source_database)s" % locals() |
651 | else: |
652 | - source = "http://%(source_host)s:%(source_port)d/%(source_database)s" % locals() |
653 | + source = "%(source_protocol)s://%(source_host)s:%(source_port)d/%(source_database)s" % locals() |
654 | else: |
655 | source = source_database |
656 | |
657 | if target_host: |
658 | if target_port is None: |
659 | - target = "http://%(target_host)s/%(target_database)s" % locals() |
660 | + target = "%(target_protocol)s://%(target_host)s/%(target_database)s" % locals() |
661 | else: |
662 | - target = "http://%(target_host)s:%(target_port)d/%(target_database)s" % locals() |
663 | + target = "%(target_protocol)s://%(target_host)s:%(target_port)d/%(target_database)s" % locals() |
664 | else: |
665 | target = target_database |
666 | |
667 | record = dict(source=source, target=target) |
668 | try: |
669 | - if target_host: |
670 | - # Remote databases must exist before replicating to them. |
671 | - create_remote_database(target_host, target_port, target_database) |
672 | - |
673 | + url = None # so logging works in exception handler |
674 | + port = int(desktopcouch_find_port()) |
675 | # TODO: Get admin username and password from keyring. Populate URL. |
676 | - |
677 | - url = None # so logging works in exception handler |
678 | - port = int(desktopcouch_find_port()) |
679 | url = "http://localhost:%d/" % (port,) |
680 | |
681 | + if target_host: |
682 | + # Target databases must exist before replicating to them. |
683 | + logging.debug("creating %r %s:%d", target_database, target_host, target_port) |
684 | + create_database(target_host, target_port, target_database) |
685 | + |
686 | + |
687 | ### All until python-couchdb gets a Server.replicate() function |
688 | import couchdb |
689 | + logging.debug("asking %r to send %s to %s", url, source, target) |
690 | server = couchdb.client.Server(url) |
691 | resp, data = server.resource.post(path='/_replicate', content=record) |
692 | logging.debug("replicate result: %r %r", resp, data) |
693 | ### |
694 | - logging.debug("#############################") |
695 | except: |
696 | logging.exception("can't talk to couchdb. %r <== %r", url, record) |
697 | - raise |
698 | |
699 | === added directory 'desktopcouch/replication_services' |
700 | === added file 'desktopcouch/replication_services/__init__.py' |
701 | --- desktopcouch/replication_services/__init__.py 1970-01-01 00:00:00 +0000 |
702 | +++ desktopcouch/replication_services/__init__.py 2009-09-04 22:25:44 +0000 |
703 | @@ -0,0 +1,4 @@ |
704 | +"""Modules imported here are available as services.""" |
705 | + |
706 | +import ubuntuone |
707 | +import example |
708 | |
709 | === added file 'desktopcouch/replication_services/example.py' |
710 | --- desktopcouch/replication_services/example.py 1970-01-01 00:00:00 +0000 |
711 | +++ desktopcouch/replication_services/example.py 2009-09-04 22:25:44 +0000 |
712 | @@ -0,0 +1,32 @@ |
713 | +# Note that the __init__.py of this package must import this module for it to |
714 | +# be found. Plugin logic is not pretty, and not implemented yet. |
715 | + |
716 | +# Required |
717 | +name = "Example" |
718 | +# Required; should include the words "cloud service" on the end. |
719 | +description = "Example cloud service" |
720 | + |
721 | +# Required |
722 | +def is_active(): |
723 | + """Can we deliver information?""" |
724 | + return False |
725 | + |
726 | +# Required |
727 | +def oauth_data(): |
728 | + """OAuth information needed to replicate to a server.""" |
729 | + return dict(consumer_key="", consumer_secret="", oauth_token="", |
730 | + oauth_token_secret="") |
731 | + # or to symbolize failure |
732 | + return None |
733 | + |
734 | +# Required |
735 | +def couchdb_location(): |
736 | + """Give a tuple of hostname and port number.""" |
737 | + |
738 | + return "couchdb.example.com", 5984 |
739 | + |
740 | +# Access to this as a string fires off functions. |
741 | +# Required |
742 | +db_name_prefix = "foo" |
743 | +# You can be sure that access to this will always, always be through its |
744 | +# __str__ method. |
745 | |
746 | === added file 'desktopcouch/replication_services/ubuntuone.py' |
747 | --- desktopcouch/replication_services/ubuntuone.py 1970-01-01 00:00:00 +0000 |
748 | +++ desktopcouch/replication_services/ubuntuone.py 2009-09-04 22:25:44 +0000 |
749 | @@ -0,0 +1,128 @@ |
750 | +import hashlib |
751 | +from oauth import oauth |
752 | +import logging |
753 | +import httplib2 |
754 | +import simplejson |
755 | +import gnomekeyring |
756 | + |
757 | +name = "Ubuntu One" |
758 | +description = "The Ubuntu One cloud service" |
759 | + |
760 | +def is_active(): |
761 | + """Can we deliver information?""" |
762 | + return oauth_data() is not None |
763 | + |
764 | +def oauth_data(): |
765 | + """Information needed to replicate to a server.""" |
766 | + try: |
767 | + import gnomekeyring |
768 | + matches = gnomekeyring.find_items_sync( |
769 | + gnomekeyring.ITEM_GENERIC_SECRET, |
770 | + {'ubuntuone-realm': "https://ubuntuone.com", |
771 | + 'oauth-consumer-key': "ubuntuone"}) |
772 | + if matches: |
773 | + # parse "a=b&c=d" to {"a":"b","c":"d"} |
774 | + oauth_data = dict([x.split("=", 1) for x in |
775 | + matches[0].secret.split("&")]) |
776 | + oauth_data.update({ |
777 | + "consumer_key": "ubuntuone", |
778 | + "consumer_secret": "", |
779 | + }) |
780 | + return oauth_data |
781 | + except ImportError, e: |
782 | + logging.info("Can't replicate to Ubuntu One cloud without credentials." |
783 | + " %s", e) |
784 | + except gnomekeyring.NoMatchError: |
785 | + logging.info("This machine hasn't authorized itself to Ubuntu One; " |
786 | + "replication to the cloud isn't possible until it has. See " |
787 | + "'ubuntuone-client-applet'.") |
788 | + except gnomekeyring.NoKeyringDaemonError: |
789 | + logging.error("No keyring daemon found in this session, so we have " |
790 | + "no access to Ubuntu One data.") |
791 | + return None |
792 | + |
793 | +def couchdb_location(): |
794 | + """This can vary more often than the OAuth information. Support SRV |
795 | + records and whatnot.""" |
796 | + |
797 | + # ...eventually. For now, hard-coded. Maybe YAGNI. |
798 | + return "couchdb.one.ubuntu.com", 5984 |
799 | + |
800 | +def whoami_user_id(): |
801 | + def get_oauth_token(consumer): |
802 | + """Get the token from the keyring""" |
803 | + import gobject |
804 | + gobject.set_application_name("desktopcouch replication to Ubuntu One") |
805 | + items = gnomekeyring.find_items_sync( |
806 | + gnomekeyring.ITEM_GENERIC_SECRET, |
807 | + {'ubuntuone-realm': "https://one.ubuntu.com", |
808 | + 'oauth-consumer-key': consumer.key}) |
809 | + if len(items): |
810 | + return oauth.OAuthToken.from_string(items[0].secret) |
811 | + |
812 | + def get_oauth_request_header(consumer, access_token, http_url): |
813 | + """Get an oauth request header given the token and the url""" |
814 | + signature_method = oauth.OAuthSignatureMethod_HMAC_SHA1() |
815 | + oauth_request = oauth.OAuthRequest.from_consumer_and_token( |
816 | + http_url=http_url, |
817 | + http_method="GET", |
818 | + oauth_consumer=consumer, |
819 | + token=access_token) |
820 | + oauth_request.sign_request(signature_method, consumer, access_token) |
821 | + return oauth_request.to_header() |
822 | + |
823 | + url = "https://one.ubuntu.com/api/account/" |
824 | + consumer = oauth.OAuthConsumer("ubuntuone", "hammertime") |
825 | + try: |
826 | + access_token = get_oauth_token(consumer) |
827 | + except gnomekeyring.NoKeyringDaemonError: |
828 | + logging.info("No keyring daemon is running for this session.") |
829 | + return None |
830 | + if not access_token: |
831 | + logging.info("Could not get access token from keyring") |
832 | + return None |
833 | + oauth_header = get_oauth_request_header(consumer, access_token, url) |
834 | + client = httplib2.Http() |
835 | + resp, content = client.request(url, "GET", headers=oauth_header) |
836 | + if resp['status'] == "200": |
837 | + try: |
838 | + userinfo = simplejson.loads(content) |
839 | + return userinfo["id"] |
840 | + except: |
841 | + logging.error("Couldn't find id in response %r", content) |
842 | + return None |
843 | + else: |
844 | + logging.error("Couldn't talk to %r. Got HTTP %s", url, resp['status']) |
845 | + return None |
846 | + |
847 | + |
848 | +class PrefixGetter(): |
849 | + def __init__(self): |
850 | + self.str = None |
851 | + |
852 | + def __str__(self): |
853 | + if self.str is not None: |
854 | + return self.str |
855 | + |
856 | + user_id = whoami_user_id() |
857 | + if user_id is None: |
858 | + raise ValueError |
859 | + |
860 | + hasher = hashlib.md5() |
861 | + hasher.update(str(user_id)) |
862 | + hashed_id_as_hex = hasher.hexdigest() |
863 | + |
864 | + prefix = "u/%s/%s/%d/" % ( |
865 | + hashed_id_as_hex[0:3], |
866 | + hashed_id_as_hex[3:6], |
867 | + user_id) |
868 | + self.str = prefix |
869 | + return prefix |
870 | + |
871 | + |
872 | +# Access to this as a string fires off functions. |
873 | +db_name_prefix = PrefixGetter() |
874 | + |
875 | +if __name__ == "__main__": |
876 | + logging.basicConfig(level=logging.DEBUG, format="%(message)s") |
877 | + print str(db_name_prefix) |
Pair and unpair servers. Allows local pairing -- do not package a desktopcouch with this in. Suggested for merge so cardinalfang can then work with passing oauth details back to newly paired DC servers.