Merge lp:~jseutter/landscape-client/srv-autodiscover into lp:~landscape/landscape-client/trunk
- srv-autodiscover
- Merge into trunk
Status: | Merged |
---|---|
Approved by: | Frank Wierzbicki |
Approved revision: | 449 |
Merged at revision: | 449 |
Proposed branch: | lp:~jseutter/landscape-client/srv-autodiscover |
Merge into: | lp:~landscape/landscape-client/trunk |
Diff against target: |
945 lines (+502/-46) 17 files modified
dev/dns-server (+99/-0) landscape-client.conf (+2/-0) landscape/broker/config.py (+5/-0) landscape/broker/dnslookup.py (+73/-0) landscape/broker/ping.py (+48/-19) landscape/broker/service.py (+7/-4) landscape/broker/tests/helpers.py (+2/-1) landscape/broker/tests/test_config.py (+17/-0) landscape/broker/tests/test_dnslookup.py (+199/-0) landscape/broker/tests/test_ping.py (+2/-2) landscape/broker/tests/test_service.py (+1/-2) landscape/broker/tests/test_transport.py (+7/-7) landscape/broker/transport.py (+18/-2) landscape/reactor.py (+1/-1) landscape/tests/helpers.py (+3/-1) landscape/tests/test_configuration.py (+17/-7) root-client.conf (+1/-0) |
To merge this branch: | bzr merge lp:~jseutter/landscape-client/srv-autodiscover |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Frank Wierzbicki (community) | Approve | ||
Thomas Herve (community) | Approve | ||
Review via email: mp+89512@code.launchpad.net |
Commit message
Description of the change
This branch adds server autodiscovery to landscape client.
Currently the strings used during SRV and name lookup are hardcoded. A future branch will add proper parsing and saving of these settings in the config file.
Jerry Seutter (jseutter) wrote : | # |
Hi Thomas,
1,2,3,4,5,6,7 should be fixed.
> [8]
> + def handle_
> + if result is None:
> + logging.
> "
> + "settings.")
> + return result
> +
> + def do_rest(result):
> + if result:
> + self.url = "http://
>
> This is slightly strange, because you encode the logic of autodiscovery
> returning None in both places. You should return the URL in handle_result no
> matter what.
Yeah, the code is a bit tortured here. I tried cleaning it up but without much effect. The problem is that a deferred is created only when autodiscovery is enabled. When autodiscovery is disabled the code calls do_rest(None), so checking for None is done in both places. Were you thinking of a better way to do the same thing?
> [9] It'd be nice to cook an DNS server script to answer those kind of
> requests, so that we can test locally easily.
I added a small script to the dev/ directory that developers can run on their local system. Using it will also require editing /etc/resolv.conf manually to point the dns client to this server.
Thomas Herve (therve) wrote : | # |
[10] pep8
landscape/
landscape/
landscape/
landscape/
[11] pyflakes
landscape/
landscape/
landscape/
landscape/
landscape/
landscape/
dev/dns_
dev/dns_
Ooops. Please add tests to discover_server.
[12]
+ error_result = []
+ def check_error(
+ error_result.
+
+ d = lookup_
+ d.addErrback(
+
+ self.assertIsIn
To properly check for errors, you should use assertFailure, and return the Deferred in your test.
[13] Can you add a docstring to dev/dns_server.py, explaining what to put in /etc/resolv.conf to use it?
Thanks, almost there!
- 446. By Jerry Seutter
-
lintian cleanups.
- 447. By Jerry Seutter
-
- Adding missing unit tests
- Adding missing docstrings
- Renaming dns_server.py to dns-server - 448. By Jerry Seutter
-
Better documentation.
Jerry Seutter (jseutter) wrote : | # |
10,11,12,13 are fixed. I added a specification on the wiki for how to configure bind 9 to answer requests and included the link in the dns-server docstring.
- 449. By Jerry Seutter
-
Resolver creation code was broken.
Thomas Herve (therve) wrote : | # |
[14]
+ SRV_RESPONSE = args["srv_
+ A_RESPONSE = args["a_response"]
This means you always need to pass the options, the defaults defined above aren't used.
[15]
+ name = row.payload.target
The type of target is a dns.Name instance, not a simple string (it works because they have a correct __str__). Can you return name.name instead, and fix the tests?
I guess what remains is the customization of the service name and the hostname? You plan to do that in the next branch?
Thanks:
Frank Wierzbicki (fwierzbicki) wrote : | # |
Looks good +1!
- 450. By Jerry Seutter
-
- Now extract the srv name from the Name result correctly.
- Defaults on dns-server script now work correctly. - 451. By Jerry Seutter
-
Fixed parsing of server_
autodiscover= false. - 452. By Jerry Seutter
-
Removing an unnecessary change.
Jerry Seutter (jseutter) wrote : | # |
> [14]
> + SRV_RESPONSE = args["srv_
> + A_RESPONSE = args["a_response"]
>
> This means you always need to pass the options, the defaults defined above
> aren't used.
You're right. This is fixed now.
> [15]
> + name = row.payload.target
>
> The type of target is a dns.Name instance, not a simple string (it works
> because they have a correct __str__). Can you return name.name instead, and
> fix the tests?
Fixed as well.
> I guess what remains is the customization of the service name and the
> hostname? You plan to do that in the next branch?
Yes, I plan to do this next in a new branch.
Since you looked at this last I found a bug with parsing the config file value. I fixed it and made the existing test more thorough.
I also removed the server_autodiscover @property from the ConfigController as I couldn't see where it was used. The code still works without it.
I don't know if it is customary to look over the changes again or not.
Thanks for your reviews!
- 453. By Jerry Seutter
-
Merge from trunk.
Preview Diff
1 | === added file 'dev/dns-server' |
2 | --- dev/dns-server 1970-01-01 00:00:00 +0000 |
3 | +++ dev/dns-server 2012-02-03 19:35:22 +0000 |
4 | @@ -0,0 +1,99 @@ |
5 | +#!/usr/bin/env python |
6 | +""" |
7 | +This script creates a DNS server that has enough functionality to test |
8 | +the server autodiscovery feature of landscape client. |
9 | + |
10 | +Landscape client uses /etc/resolv.conf to find the location of name servers. |
11 | +To have landscape client use this server, make sure that: |
12 | + |
13 | +nameserver 127.0.0.1 |
14 | + |
15 | +is the first nameserver in /etc/resolv.conf. Linux name lookups only support |
16 | +port 53, so this program must run on port 53 in order to work. Be aware that |
17 | +NetworkManager overwrites this file any time your network configuration |
18 | +changes. |
19 | + |
20 | +This program does not return enough information for a tool like dig to complete |
21 | +successfully. If this is needed, use Bind 9, detailed at |
22 | +https://wiki.canonical.com/Landscape/SpecRegistry/0082 |
23 | +""" |
24 | + |
25 | +import argparse |
26 | +import sys |
27 | +from twisted.internet import reactor, defer |
28 | +from twisted.names import dns, common |
29 | +from twisted.names.server import DNSServerFactory |
30 | + |
31 | + |
32 | +PORT = 5553 |
33 | +SRV_RESPONSE = 'lds1.mylandscapehost.com' |
34 | +A_RESPONSE = '127.0.0.1' |
35 | + |
36 | + |
37 | +class SimpleResolver(common.ResolverBase): |
38 | + def _lookup(self, name, cls, typ, timeout): |
39 | + """ |
40 | + Respond to DNS requests. See documentation for |
41 | + L{twisted.names.common.ResolverBase}. |
42 | + """ |
43 | + # This nameserver returns the same result all the time, regardless |
44 | + # of what name the client asks for. |
45 | + results = [] |
46 | + ttl = 60 |
47 | + if typ == dns.SRV: |
48 | + record = dns.Record_SRV(0, 1, 80, SRV_RESPONSE, ttl) |
49 | + owner = '_landscape._tcp.mylandscapehost.com' |
50 | + results.append(dns.RRHeader(owner, record.TYPE, dns.IN, ttl, |
51 | + record, auth=True)) |
52 | + elif typ == dns.A: |
53 | + record = dns.Record_A(A_RESPONSE) |
54 | + owner = 'landscape.localdomain' |
55 | + results.append(dns.RRHeader(owner, record.TYPE, dns.IN, ttl, |
56 | + record, auth=True)) |
57 | + |
58 | + authority = [] |
59 | + return defer.succeed((results, authority, [])) |
60 | + |
61 | + |
62 | +def parse_command_line(args): |
63 | + global SRV_RESPONSE, A_RESPONSE, PORT |
64 | + description = """ |
65 | + This test tool responds to DNS queries for SRV and A records. It always |
66 | + responds with the same result regardless of the query string sent by the |
67 | + client. |
68 | + |
69 | + To test this tool, try the following commands: |
70 | + dig -p 5553 @127.0.0.1 SRV _landscape._tcp.mylandscapehost.com |
71 | + |
72 | + dig -p 5553 @127.0.0.1 localhost.localdomain |
73 | + """ |
74 | + parser = argparse.ArgumentParser(description=description) |
75 | + parser.add_argument("--srv-response", type=str, default=SRV_RESPONSE, |
76 | + help="Give this reply to SRV queries (eg: localhost)") |
77 | + parser.add_argument("--a-response", type=str, default=A_RESPONSE, |
78 | + help="Give this reply to A queries (eg: 127.0.0.1)") |
79 | + parser.add_argument("--port", type=int, default=PORT, |
80 | + help="Listen on this port (default 5553). DNS " |
81 | + "normally runs on port 53") |
82 | + |
83 | + args = vars(parser.parse_args()) |
84 | + SRV_RESPONSE = args["srv_response"] |
85 | + A_RESPONSE = args["a_response"] |
86 | + PORT = args["port"] |
87 | + |
88 | + |
89 | +def main(): |
90 | + parse_command_line(sys.argv) |
91 | + |
92 | + simple_resolver = SimpleResolver() |
93 | + factory = DNSServerFactory(authorities=[simple_resolver], verbose=1) |
94 | + protocol = dns.DNSDatagramProtocol(factory) |
95 | + print "starting reactor on port %s.." % PORT |
96 | + reactor.listenTCP(PORT, factory) |
97 | + reactor.listenUDP(PORT, protocol) |
98 | + reactor.run() |
99 | + print "reactor stopped..." |
100 | + |
101 | + |
102 | +if __name__ == "__main__": |
103 | + main() |
104 | |
105 | === modified file 'landscape-client.conf' |
106 | --- landscape-client.conf 2012-01-11 16:33:15 +0000 |
107 | +++ landscape-client.conf 2012-02-03 19:35:22 +0000 |
108 | @@ -9,3 +9,5 @@ |
109 | log_level = debug |
110 | pid_file = /tmp/landscape/landscape-client.pid |
111 | ping_url = http://localhost:8081/ping |
112 | +server_autodiscover = false |
113 | + |
114 | |
115 | === modified file 'landscape/broker/config.py' |
116 | --- landscape/broker/config.py 2012-01-09 09:54:49 +0000 |
117 | +++ landscape/broker/config.py 2012-02-03 19:35:22 +0000 |
118 | @@ -89,6 +89,8 @@ |
119 | help="Record data sent to the server on filesystem.") |
120 | parser.add_option("--provisioning-otp", default="", |
121 | help="The OTP to use for a provisioned machine.") |
122 | + parser.add_option("--server-autodiscover", type="string", |
123 | + default="false", help="Enable server autodiscovery.") |
124 | return parser |
125 | |
126 | @property |
127 | @@ -118,3 +120,6 @@ |
128 | |
129 | if self.url is None: |
130 | self.url = self.DEFAULT_URL |
131 | + |
132 | + autodiscover = str(self.server_autodiscover).lower() |
133 | + self.server_autodiscover = (autodiscover == "true") |
134 | |
135 | === added file 'landscape/broker/dnslookup.py' |
136 | --- landscape/broker/dnslookup.py 1970-01-01 00:00:00 +0000 |
137 | +++ landscape/broker/dnslookup.py 2012-02-03 19:35:22 +0000 |
138 | @@ -0,0 +1,73 @@ |
139 | +"""DNS lookups for server autodiscovery.""" |
140 | + |
141 | +import logging |
142 | +from twisted.names import dns |
143 | +from twisted.names.client import Resolver |
144 | + |
145 | + |
146 | +def discover_server(resolver=None): |
147 | + """ |
148 | + Look up the dns location of the landscape server. |
149 | + |
150 | + @type resolver: The resolver to use. If none is specified a resolver that |
151 | + uses settings from /etc/resolv.conf will be created. |
152 | + """ |
153 | + if not resolver: |
154 | + resolver = Resolver("/etc/resolv.conf") |
155 | + d = lookup_server_record(resolver) |
156 | + d.addErrback(lookup_hostname, resolver) |
157 | + return d |
158 | + |
159 | + |
160 | +def lookup_server_record(resolver): |
161 | + """ |
162 | + Do a DNS SRV record lookup for the location of the landscape server. |
163 | + |
164 | + @type resolver: A resolver to use for DNS lookups |
165 | + L{twisted.names.client.Resolver}. |
166 | + @return: A deferred containing either the hostname of the landscape server |
167 | + if found or an empty string if not found. |
168 | + """ |
169 | + service_name = "_landscape._tcp.mylandscapehost.com" |
170 | + |
171 | + def lookup_done(result): |
172 | + name = "" |
173 | + for item in result: |
174 | + for row in item: |
175 | + if row.type == dns.SRV: |
176 | + name = row.payload.target.name |
177 | + break |
178 | + return name |
179 | + |
180 | + def lookup_failed(result): |
181 | + logging.info("SRV lookup of %s failed." % service_name) |
182 | + return result |
183 | + |
184 | + d = resolver.lookupService(service_name) |
185 | + d.addCallback(lookup_done) |
186 | + d.addErrback(lookup_failed) |
187 | + return d |
188 | + |
189 | + |
190 | +def lookup_hostname(result, resolver): |
191 | + """ |
192 | + Do a DNS name lookup for the location of the landscape server. |
193 | + |
194 | + @param result: The result from a call to lookup_server_record. |
195 | + @param resolver: The resolver to use for DNS lookups. |
196 | + @param return: A deferred containing the ip address of the landscape |
197 | + server if found or None if not found. |
198 | + """ |
199 | + hostname = "landscape.localdomain" |
200 | + |
201 | + def lookup_done(result): |
202 | + return result |
203 | + |
204 | + def lookup_failed(result): |
205 | + logging.info("Name lookup of %s failed." % hostname) |
206 | + return result |
207 | + |
208 | + d = resolver.getHostByName(hostname) |
209 | + d.addCallback(lookup_done) |
210 | + d.addErrback(lookup_failed) |
211 | + return d |
212 | |
213 | === modified file 'landscape/broker/ping.py' |
214 | --- landscape/broker/ping.py 2011-07-21 23:55:47 +0000 |
215 | +++ landscape/broker/ping.py 2012-02-03 19:35:22 +0000 |
216 | @@ -4,6 +4,7 @@ |
217 | """ |
218 | |
219 | import urllib |
220 | +import logging |
221 | from logging import info |
222 | |
223 | from twisted.python.failure import Failure |
224 | @@ -12,6 +13,7 @@ |
225 | from landscape.lib.bpickle import loads |
226 | from landscape.lib.fetch import fetch |
227 | from landscape.lib.log import log_failure |
228 | +from landscape.broker.dnslookup import discover_server |
229 | |
230 | |
231 | class PingClient(object): |
232 | @@ -22,13 +24,15 @@ |
233 | @param identity: This client's identity. |
234 | """ |
235 | |
236 | - def __init__(self, reactor, url, identity, get_page=None): |
237 | + def __init__(self, reactor, url, identity, get_page=None, |
238 | + server_autodiscover=False): |
239 | if get_page is None: |
240 | get_page = fetch |
241 | self._reactor = reactor |
242 | self._identity = identity |
243 | self.get_page = get_page |
244 | self.url = url |
245 | + self._server_autodiscover = server_autodiscover |
246 | |
247 | def ping(self): |
248 | """Ask the question. |
249 | @@ -39,20 +43,41 @@ |
250 | @return: A deferred resulting in True if there are messages |
251 | and False otherwise. |
252 | """ |
253 | - insecure_id = self._identity.insecure_id |
254 | - if insecure_id is not None: |
255 | - headers = {"Content-Type": "application/x-www-form-urlencoded"} |
256 | - data = urllib.urlencode({"insecure_id": insecure_id}) |
257 | - page_deferred = defer.Deferred() |
258 | - |
259 | - def errback(type, value, tb): |
260 | - page_deferred.errback(Failure(value, type, tb)) |
261 | - self._reactor.call_in_thread(page_deferred.callback, errback, |
262 | - self.get_page, self.url, post=True, |
263 | - data=data, headers=headers) |
264 | - page_deferred.addCallback(self._got_result) |
265 | - return page_deferred |
266 | - return defer.succeed(False) |
267 | + def handle_result(result): |
268 | + if result is None: |
269 | + logging.warning("Autodiscovery failed. Reverting to previous " |
270 | + "settings.") |
271 | + else: |
272 | + result = "http://%s/ping" % result |
273 | + return result |
274 | + |
275 | + def do_rest(result): |
276 | + if result is not None: |
277 | + self.url = result |
278 | + |
279 | + insecure_id = self._identity.insecure_id |
280 | + if insecure_id is not None: |
281 | + headers = {"Content-Type": "application/x-www-form-urlencoded"} |
282 | + data = urllib.urlencode({"insecure_id": insecure_id}) |
283 | + page_deferred = defer.Deferred() |
284 | + |
285 | + def errback(type, value, tb): |
286 | + page_deferred.errback(Failure(value, type, tb)) |
287 | + self._reactor.call_in_thread(page_deferred.callback, errback, |
288 | + self.get_page, self.url, |
289 | + post=True, data=data, |
290 | + headers=headers) |
291 | + page_deferred.addCallback(self._got_result) |
292 | + return page_deferred |
293 | + return defer.succeed(False) |
294 | + |
295 | + if self._server_autodiscover: |
296 | + lookup_deferred = discover_server() |
297 | + lookup_deferred.addCallback(handle_result) |
298 | + lookup_deferred.addCallback(do_rest) |
299 | + return lookup_deferred |
300 | + else: |
301 | + return do_rest(None) |
302 | |
303 | def _got_result(self, webtext): |
304 | """ |
305 | @@ -77,7 +102,8 @@ |
306 | """ |
307 | |
308 | def __init__(self, reactor, url, identity, exchanger, |
309 | - interval=30, ping_client_factory=PingClient): |
310 | + interval=30, ping_client_factory=PingClient, |
311 | + server_autodiscover=False): |
312 | self._url = url |
313 | self._interval = interval |
314 | self._identity = identity |
315 | @@ -86,6 +112,7 @@ |
316 | self._call_id = None |
317 | self._ping_client = None |
318 | self.ping_client_factory = ping_client_factory |
319 | + self._server_autodiscover = server_autodiscover |
320 | reactor.call_on("message", self._handle_set_intervals) |
321 | |
322 | def get_url(self): |
323 | @@ -101,8 +128,9 @@ |
324 | |
325 | def start(self): |
326 | """Start pinging.""" |
327 | - self._ping_client = self.ping_client_factory(self._reactor, self._url, |
328 | - self._identity) |
329 | + self._ping_client = self.ping_client_factory( |
330 | + self._reactor, self._url, self._identity, |
331 | + server_autodiscover=self._server_autodiscover) |
332 | self._call_id = self._reactor.call_every(self._interval, self.ping) |
333 | |
334 | def ping(self): |
335 | @@ -119,7 +147,8 @@ |
336 | |
337 | def _got_error(self, failure): |
338 | log_failure(failure, |
339 | - "Error contacting ping server at %s" % (self._url,)) |
340 | + "Error contacting ping server at %s" % |
341 | + (self._ping_client.url,)) |
342 | |
343 | def _handle_set_intervals(self, message): |
344 | if message["type"] == "set-intervals" and "ping" in message: |
345 | |
346 | === modified file 'landscape/broker/service.py' |
347 | --- landscape/broker/service.py 2011-08-19 15:47:27 +0000 |
348 | +++ landscape/broker/service.py 2012-02-03 19:35:22 +0000 |
349 | @@ -52,8 +52,10 @@ |
350 | self.payload_recorder = PayloadRecorder(config.record_directory) |
351 | else: |
352 | self.payload_recorder = None |
353 | + |
354 | self.transport = self.transport_factory( |
355 | - config.url, config.ssl_public_key, self.payload_recorder) |
356 | + self.reactor, config.url, config.ssl_public_key, |
357 | + self.payload_recorder, config.server_autodiscover) |
358 | self.message_store = get_default_message_store( |
359 | self.persist, config.message_store_path) |
360 | self.identity = Identity(self.config, self.persist) |
361 | @@ -61,9 +63,10 @@ |
362 | self.exchanger = MessageExchange( |
363 | self.reactor, self.message_store, self.transport, self.identity, |
364 | exchange_store, config) |
365 | - self.pinger = self.pinger_factory(self.reactor, config.ping_url, |
366 | - self.identity, self.exchanger, |
367 | - interval=config.ping_interval) |
368 | + self.pinger = self.pinger_factory( |
369 | + self.reactor, config.ping_url, self.identity, self.exchanger, |
370 | + interval=config.ping_interval, |
371 | + server_autodiscover=config.server_autodiscover) |
372 | self.registration = RegistrationHandler( |
373 | config, self.identity, self.reactor, self.exchanger, self.pinger, |
374 | self.message_store, fetch_async) |
375 | |
376 | === modified file 'landscape/broker/tests/helpers.py' |
377 | --- landscape/broker/tests/helpers.py 2011-03-25 08:42:48 +0000 |
378 | +++ landscape/broker/tests/helpers.py 2012-02-03 19:35:22 +0000 |
379 | @@ -37,6 +37,7 @@ |
380 | "computer_title = Some Computer\n" |
381 | "account_name = some_account\n" |
382 | "ping_url = http://localhost:91910\n" |
383 | + "server_autodiscover = false\n" |
384 | "data_path = %s\n" |
385 | "log_dir = %s\n" % (data_path, log_dir)) |
386 | |
387 | @@ -74,7 +75,7 @@ |
388 | test_case.mstore = get_default_message_store( |
389 | test_case.persist, test_case.config.message_store_path) |
390 | test_case.identity = Identity(test_case.config, test_case.persist) |
391 | - test_case.transport = FakeTransport(test_case.config.url, |
392 | + test_case.transport = FakeTransport(None, test_case.config.url, |
393 | test_case.config.ssl_public_key) |
394 | test_case.reactor = FakeReactor() |
395 | test_case.exchange_store = ExchangeStore( |
396 | |
397 | === modified file 'landscape/broker/tests/test_config.py' |
398 | --- landscape/broker/tests/test_config.py 2012-01-09 15:14:12 +0000 |
399 | +++ landscape/broker/tests/test_config.py 2012-02-03 19:35:22 +0000 |
400 | @@ -91,3 +91,20 @@ |
401 | |
402 | self.assertEqual(configuration.url, |
403 | "https://landscape.canonical.com/message-system") |
404 | + |
405 | + def test_server_autodiscovery_handling(self): |
406 | + """ |
407 | + server_autodiscover is parsed and converted to a boolean value by |
408 | + load(). |
409 | + """ |
410 | + configuration = BrokerConfiguration() |
411 | + configuration.load([]) |
412 | + self.assertEquals(configuration.server_autodiscover, False) |
413 | + |
414 | + configuration = BrokerConfiguration() |
415 | + configuration.load(["--server-autodiscover=true"]) |
416 | + self.assertEquals(configuration.server_autodiscover, True) |
417 | + |
418 | + configuration = BrokerConfiguration() |
419 | + configuration.load(["--server-autodiscover=false"]) |
420 | + self.assertEquals(configuration.server_autodiscover, False) |
421 | |
422 | === added file 'landscape/broker/tests/test_dnslookup.py' |
423 | --- landscape/broker/tests/test_dnslookup.py 1970-01-01 00:00:00 +0000 |
424 | +++ landscape/broker/tests/test_dnslookup.py 2012-02-03 19:35:22 +0000 |
425 | @@ -0,0 +1,199 @@ |
426 | +from landscape.tests.helpers import LandscapeTest |
427 | +from landscape.broker.dnslookup import (lookup_server_record, lookup_hostname, |
428 | + discover_server) |
429 | + |
430 | +from twisted.internet import defer |
431 | +from twisted.names import dns |
432 | +from twisted.names.error import ResolverError |
433 | + |
434 | + |
435 | +class FakeResolverResult(object): |
436 | + """ |
437 | + A fake resolver result returned by L{FakeResolver}. |
438 | + |
439 | + @param type: The result type L{twisted.names.dns.SRV} |
440 | + @param payload: The result contents |
441 | + """ |
442 | + def __init__(self): |
443 | + self.type = None |
444 | + |
445 | + class Payload(object): |
446 | + """ |
447 | + A payload result returned by fake resolver. |
448 | + |
449 | + @param target: The result of the lookup |
450 | + """ |
451 | + def __init__(self): |
452 | + self.target = "" |
453 | + |
454 | + class Target(object): |
455 | + """ |
456 | + A payload target returned by fake resolver. |
457 | + |
458 | + @param name: The name contained by the target. |
459 | + """ |
460 | + def __init__(self): |
461 | + self.name = "" |
462 | + |
463 | + self.payload = Payload() |
464 | + self.payload.target = Target() |
465 | + |
466 | + |
467 | +class FakeResolver(object): |
468 | + """ |
469 | + A fake resolver that mimics L{twisted.names.client.Resolver} |
470 | + """ |
471 | + def __init__(self): |
472 | + self.results = None |
473 | + self.name = None |
474 | + |
475 | + def lookupService(self, arg1): |
476 | + deferred = defer.Deferred() |
477 | + deferred.callback(self.results) |
478 | + return deferred |
479 | + |
480 | + def getHostByName(self, arg1): |
481 | + deferred = defer.Deferred() |
482 | + deferred.callback(self.name) |
483 | + return deferred |
484 | + |
485 | + |
486 | +class BadResolver(object): |
487 | + """ |
488 | + A resolver that mimics L{twisted.names.client.Resolver} and always returns |
489 | + an error. |
490 | + """ |
491 | + def lookupService(self, arg1): |
492 | + deferred = defer.Deferred() |
493 | + deferred.errback(ResolverError("Couldn't connect")) |
494 | + return deferred |
495 | + |
496 | + def getHostByName(self, arg1): |
497 | + deferred = defer.Deferred() |
498 | + deferred.errback(ResolverError("Couldn't connect")) |
499 | + return deferred |
500 | + |
501 | + |
502 | +class DnsSrvLookupTest(LandscapeTest): |
503 | + def test_with_server_found(self): |
504 | + """ |
505 | + Looking up a DNS SRV record should return the result of the lookup. |
506 | + """ |
507 | + fake_result = FakeResolverResult() |
508 | + fake_result.type = dns.SRV |
509 | + fake_result.payload.target.name = "a.b.com" |
510 | + fake_resolver = FakeResolver() |
511 | + fake_resolver.results = [[fake_result]] |
512 | + |
513 | + def check(result): |
514 | + self.assertEquals("a.b.com", result) |
515 | + |
516 | + d = lookup_server_record(fake_resolver) |
517 | + d.addCallback(check) |
518 | + return d |
519 | + |
520 | + def test_with_server_not_found(self): |
521 | + """ |
522 | + Looking up a DNS SRV record and finding nothing exists should return |
523 | + an empty string. |
524 | + """ |
525 | + fake_resolver = FakeResolver() |
526 | + fake_resolver.results = [[]] |
527 | + |
528 | + def check(result): |
529 | + self.assertEquals("", result) |
530 | + |
531 | + d = lookup_server_record(fake_resolver) |
532 | + d.addCallback(check) |
533 | + return d |
534 | + |
535 | + def test_with_resolver_error(self): |
536 | + """A resolver error triggers error handling code.""" |
537 | + # The failure should be properly logged |
538 | + logging_mock = self.mocker.replace("logging.info") |
539 | + logging_mock("SRV lookup of _landscape._tcp.mylandscapehost.com " |
540 | + "failed.") |
541 | + self.mocker.replay() |
542 | + |
543 | + d = lookup_server_record(BadResolver()) |
544 | + self.assertFailure(d, ResolverError) |
545 | + return d |
546 | + |
547 | + |
548 | +class DnsNameLookupTest(LandscapeTest): |
549 | + def test_with_name_found(self): |
550 | + """ |
551 | + Looking up a DNS name record should return the result of the lookup. |
552 | + """ |
553 | + fake_resolver = FakeResolver() |
554 | + fake_resolver.name = "a.b.com" |
555 | + |
556 | + def check(result): |
557 | + self.assertEquals("a.b.com", result) |
558 | + |
559 | + d = lookup_hostname(None, fake_resolver) |
560 | + d.addCallback(check) |
561 | + return d |
562 | + |
563 | + def test_with_name_not_found(self): |
564 | + """ |
565 | + Looking up a DNS NAME record and not finding a result should return |
566 | + None. |
567 | + """ |
568 | + fake_resolver = FakeResolver() |
569 | + fake_resolver.name = None |
570 | + |
571 | + def check(result): |
572 | + self.assertEquals(None, result) |
573 | + |
574 | + d = lookup_hostname(None, fake_resolver) |
575 | + d.addCallback(check) |
576 | + return d |
577 | + |
578 | + def test_with_resolver_error(self): |
579 | + """A resolver error triggers error handling code.""" |
580 | + # The failure should be properly logged |
581 | + logging_mock = self.mocker.replace("logging.info") |
582 | + logging_mock("Name lookup of landscape.localdomain failed.") |
583 | + self.mocker.replay() |
584 | + |
585 | + d = lookup_hostname(None, BadResolver()) |
586 | + self.assertFailure(d, ResolverError) |
587 | + return d |
588 | + |
589 | + |
590 | +class DiscoverServerTest(LandscapeTest): |
591 | + def test_srv_lookup(self): |
592 | + """The DNS name of the server is found using a SRV lookup.""" |
593 | + fake_result = FakeResolverResult() |
594 | + fake_result.type = dns.SRV |
595 | + fake_result.payload.target.name = "a.b.com" |
596 | + fake_resolver = FakeResolver() |
597 | + fake_resolver.results = [[fake_result]] |
598 | + |
599 | + d = discover_server(fake_resolver) |
600 | + |
601 | + def check(result): |
602 | + self.assertEquals("a.b.com", result) |
603 | + |
604 | + d.addCallback(check) |
605 | + return d |
606 | + |
607 | + def test_a_name_lookup(self): |
608 | + """The DNS name of the server is found using an A name lookup.""" |
609 | + fake_resolver = FakeResolver() |
610 | + fake_resolver.name = "x.y.com" |
611 | + |
612 | + d = discover_server(fake_resolver) |
613 | + |
614 | + def check(result): |
615 | + self.assertEquals("x.y.com", result) |
616 | + |
617 | + d.addCallback(check) |
618 | + return d |
619 | + |
620 | + def test_failed_lookup(self): |
621 | + """A resolver error is returned when server autodiscovery fails.""" |
622 | + d = lookup_server_record(BadResolver()) |
623 | + self.assertFailure(d, ResolverError) |
624 | + return d |
625 | |
626 | === modified file 'landscape/broker/tests/test_ping.py' |
627 | --- landscape/broker/tests/test_ping.py 2011-07-05 05:09:11 +0000 |
628 | +++ landscape/broker/tests/test_ping.py 2012-02-03 19:35:22 +0000 |
629 | @@ -128,7 +128,7 @@ |
630 | self.url = "http://localhost:8081/whatever" |
631 | self.page_getter = FakePageGetter(None) |
632 | |
633 | - def factory(reactor, url, insecure_id): |
634 | + def factory(reactor, url, insecure_id, server_autodiscover): |
635 | return PingClient(reactor, url, insecure_id, |
636 | get_page=self.page_getter.get_page) |
637 | self.pinger = Pinger(self.broker_service.reactor, |
638 | @@ -201,7 +201,7 @@ |
639 | |
640 | class BadPingClient(object): |
641 | def __init__(self, *args, **kwargs): |
642 | - pass |
643 | + self.url = args[1] |
644 | |
645 | def ping(self): |
646 | return fail(ZeroDivisionError("Couldn't fetch page")) |
647 | |
648 | === modified file 'landscape/broker/tests/test_service.py' |
649 | --- landscape/broker/tests/test_service.py 2011-08-19 15:47:27 +0000 |
650 | +++ landscape/broker/tests/test_service.py 2012-02-03 19:35:22 +0000 |
651 | @@ -1,13 +1,12 @@ |
652 | import os |
653 | |
654 | -from twisted.internet import reactor |
655 | - |
656 | from landscape.tests.helpers import LandscapeTest |
657 | from landscape.broker.tests.helpers import BrokerConfigurationHelper |
658 | from landscape.broker.service import BrokerService |
659 | from landscape.broker.transport import HTTPTransport |
660 | from landscape.broker.amp import RemoteBrokerConnector |
661 | from landscape.reactor import FakeReactor |
662 | +from twisted.internet import reactor |
663 | |
664 | |
665 | class BrokerServiceTest(LandscapeTest): |
666 | |
667 | === modified file 'landscape/broker/tests/test_transport.py' |
668 | --- landscape/broker/tests/test_transport.py 2011-07-05 05:09:11 +0000 |
669 | +++ landscape/broker/tests/test_transport.py 2012-02-03 19:35:22 +0000 |
670 | @@ -52,11 +52,11 @@ |
671 | |
672 | def test_get_url(self): |
673 | url = "http://example/ooga" |
674 | - transport = HTTPTransport(url) |
675 | + transport = HTTPTransport(None, url) |
676 | self.assertEqual(transport.get_url(), url) |
677 | |
678 | def test_set_url(self): |
679 | - transport = HTTPTransport("http://example/ooga") |
680 | + transport = HTTPTransport(None, "http://example/ooga") |
681 | transport.set_url("http://example/message-system") |
682 | self.assertEqual(transport.get_url(), "http://example/message-system") |
683 | |
684 | @@ -71,7 +71,7 @@ |
685 | port = reactor.listenTCP(0, server.Site(r), interface="127.0.0.1") |
686 | self.ports.append(port) |
687 | transport = HTTPTransport( |
688 | - "http://localhost:%d/" % (port.getHost().port,)) |
689 | + None, "http://localhost:%d/" % (port.getHost().port,)) |
690 | result = deferToThread(transport.exchange, "HI", computer_id="34", |
691 | message_api="X.Y") |
692 | |
693 | @@ -98,7 +98,7 @@ |
694 | interface="127.0.0.1") |
695 | self.ports.append(port) |
696 | transport = HTTPTransport( |
697 | - "https://localhost:%d/" % (port.getHost().port,), PUBKEY) |
698 | + None, "https://localhost:%d/" % (port.getHost().port,), PUBKEY) |
699 | result = deferToThread(transport.exchange, "HI", computer_id="34", |
700 | message_api="X.Y") |
701 | |
702 | @@ -126,7 +126,7 @@ |
703 | port = reactor.listenSSL(0, server.Site(r), context_factory, |
704 | interface="127.0.0.1") |
705 | self.ports.append(port) |
706 | - transport = HTTPTransport("https://localhost:%d/" |
707 | + transport = HTTPTransport(None, "https://localhost:%d/" |
708 | % (port.getHost().port,), |
709 | pubkey=PUBKEY) |
710 | |
711 | @@ -153,7 +153,7 @@ |
712 | return "filename" |
713 | recorder.get_payload_filename = static_filename |
714 | |
715 | - transport = HTTPTransport("http://localhost", |
716 | + transport = HTTPTransport(None, "http://localhost", |
717 | payload_recorder=recorder) |
718 | |
719 | def fake_curl(param1, param2, param3): |
720 | @@ -174,7 +174,7 @@ |
721 | When C{HTTPTransport} is configured without a payload recorder, |
722 | exchanges with the server should still complete. |
723 | """ |
724 | - transport = HTTPTransport("http://localhost") |
725 | + transport = HTTPTransport(None, "http://localhost") |
726 | self.called = False |
727 | |
728 | def fake_curl(param1, param2, param3): |
729 | |
730 | === modified file 'landscape/broker/transport.py' |
731 | --- landscape/broker/transport.py 2011-07-21 23:55:47 +0000 |
732 | +++ landscape/broker/transport.py 2012-02-03 19:35:22 +0000 |
733 | @@ -5,12 +5,14 @@ |
734 | import pprint |
735 | |
736 | import pycurl |
737 | +from twisted.internet.threads import blockingCallFromThread |
738 | |
739 | from landscape.lib.fetch import fetch |
740 | from landscape.lib.fs import create_file |
741 | from landscape.lib import bpickle |
742 | from landscape.log import format_delta |
743 | from landscape import SERVER_API, VERSION |
744 | +from landscape.broker.dnslookup import discover_server |
745 | |
746 | |
747 | class HTTPTransport(object): |
748 | @@ -22,10 +24,13 @@ |
749 | with the server. If C{None}, exchanges will not be recorded. |
750 | """ |
751 | |
752 | - def __init__(self, url, pubkey=None, payload_recorder=None): |
753 | + def __init__(self, reactor, url, pubkey=None, payload_recorder=None, |
754 | + server_autodiscover=False): |
755 | + self._reactor = reactor |
756 | self._url = url |
757 | self._pubkey = pubkey |
758 | self._payload_recorder = payload_recorder |
759 | + self._server_autodiscover = server_autodiscover |
760 | |
761 | def get_url(self): |
762 | """Get the URL of the remote message system.""" |
763 | @@ -36,6 +41,14 @@ |
764 | self._url = url |
765 | |
766 | def _curl(self, payload, computer_id, message_api): |
767 | + if self._server_autodiscover: |
768 | + result = blockingCallFromThread(self._reactor, discover_server) |
769 | + if result is not None: |
770 | + self._url = "https://%s/message-system" % result |
771 | + else: |
772 | + logging.warn("Autodiscovery failed. Falling back to previous " |
773 | + "settings.") |
774 | + |
775 | headers = {"X-Message-API": message_api, |
776 | "User-Agent": "landscape-client/%s" % VERSION, |
777 | "Content-Type": "application/octet-stream"} |
778 | @@ -152,7 +165,8 @@ |
779 | class FakeTransport(object): |
780 | """Fake transport for testing purposes.""" |
781 | |
782 | - def __init__(self, url=None, pubkey=None, payload_recorder=None): |
783 | + def __init__(self, reactor=None, url=None, pubkey=None, |
784 | + payload_recorder=None, server_autodiscover=False): |
785 | self._pubkey = pubkey |
786 | self._payload_recorder = payload_recorder |
787 | self.payloads = [] |
788 | @@ -163,6 +177,8 @@ |
789 | self.message_api = None |
790 | self.extra = {} |
791 | self._url = url |
792 | + self._reactor = reactor |
793 | + self._server_autodiscover = server_autodiscover |
794 | |
795 | def get_url(self): |
796 | return self._url |
797 | |
798 | === modified file 'landscape/reactor.py' |
799 | --- landscape/reactor.py 2012-02-03 10:06:28 +0000 |
800 | +++ landscape/reactor.py 2012-02-03 19:35:22 +0000 |
801 | @@ -309,7 +309,7 @@ |
802 | self._LoopingCall = LoopingCall |
803 | self._reactor = reactor |
804 | self._cleanup() |
805 | - |
806 | + self.callFromThread = reactor.callFromThread |
807 | super(TwistedReactor, self).__init__() |
808 | |
809 | def _cleanup(self): |
810 | |
811 | === modified file 'landscape/tests/helpers.py' |
812 | --- landscape/tests/helpers.py 2011-07-28 12:54:34 +0000 |
813 | +++ landscape/tests/helpers.py 2012-02-03 19:35:22 +0000 |
814 | @@ -271,6 +271,7 @@ |
815 | "computer_title = Some Computer\n" |
816 | "account_name = some_account\n" |
817 | "ping_url = http://localhost:91910\n" |
818 | + "server_autodiscover = false\n" |
819 | "data_path = %s\n" |
820 | "log_dir = %s\n" % (test_case.data_path, log_dir)) |
821 | |
822 | @@ -612,7 +613,7 @@ |
823 | if self._shared and self not in self.__class__._instances: |
824 | self.__class__._instances.add(self) |
825 | result.startTest(self) |
826 | - if self.getSkip(): # don't run test methods that are marked as .skip |
827 | + if self.getSkip(): # don't run test methods that are marked as .skip |
828 | result.addSkip(self, self.getSkip()) |
829 | result.stopTest(self) |
830 | return |
831 | @@ -643,6 +644,7 @@ |
832 | |
833 | ### Copied from Twisted, to fix a bug in trial in Twisted 2.2! ### |
834 | |
835 | + |
836 | class UnsupportedTrialFeature(Exception): |
837 | """A feature of twisted.trial was used that pyunit cannot support.""" |
838 | |
839 | |
840 | === modified file 'landscape/tests/test_configuration.py' |
841 | --- landscape/tests/test_configuration.py 2012-01-11 16:33:15 +0000 |
842 | +++ landscape/tests/test_configuration.py 2012-02-03 19:35:22 +0000 |
843 | @@ -739,6 +739,7 @@ |
844 | computer_title = rex |
845 | data_path = %s |
846 | account_name = account |
847 | +server_autodiscover = False |
848 | """ % config.data_path) |
849 | |
850 | def test_silent_setup_no_register(self): |
851 | @@ -755,6 +756,7 @@ |
852 | self.assertConfigEqual(self.get_content(config), """\ |
853 | [client] |
854 | url = https://landscape.canonical.com/message-system |
855 | +server_autodiscover = False |
856 | data_path = %s |
857 | """ % config.data_path) |
858 | |
859 | @@ -794,6 +796,7 @@ |
860 | "computer_title = \n" |
861 | "https_proxy = \n" |
862 | "ping_url = http://landscape.canonical.com/ping\n" |
863 | + "server_autodiscover = False\n" |
864 | % config.data_path) |
865 | |
866 | def test_silent_setup_without_computer_title(self): |
867 | @@ -875,7 +878,8 @@ |
868 | "computer_title": "rex", |
869 | "include_manager_plugins": "ScriptExecution", |
870 | "script_users": "root, nobody", |
871 | - "account_name": "account"}, |
872 | + "account_name": "account", |
873 | + "server_autodiscover": "False"}, |
874 | dict(parser.items("client"))) |
875 | |
876 | def test_silent_script_users_with_all_user(self): |
877 | @@ -923,7 +927,8 @@ |
878 | "ping_url": "http://localhost/ping", |
879 | "random_key": "random_value", |
880 | "computer_title": "rex", |
881 | - "account_name": "account"}, |
882 | + "account_name": "account", |
883 | + "server_autodiscover": "False"}, |
884 | dict(parser.items("client"))) |
885 | |
886 | def test_setup_with_proxies_from_environment(self): |
887 | @@ -975,7 +980,8 @@ |
888 | "http_proxy": "http://environ", |
889 | "https_proxy": "https://environ", |
890 | "computer_title": "rex", |
891 | - "account_name": "account"}, |
892 | + "account_name": "account", |
893 | + "server_autodiscover": "False"}, |
894 | dict(parser.items("client"))) |
895 | |
896 | def test_setup_prefers_proxies_from_config_over_environment(self): |
897 | @@ -1241,7 +1247,8 @@ |
898 | "registration_password": "New Password", |
899 | "http_proxy": "http://new.proxy", |
900 | "https_proxy": "https://new.proxy", |
901 | - "url": "http://new.url"}) |
902 | + "url": "http://new.url", |
903 | + "server_autodiscover": "False"}) |
904 | |
905 | def test_import_from_empty_file(self): |
906 | self.mocker.replay() |
907 | @@ -1352,7 +1359,8 @@ |
908 | "registration_password": "Command Line Password", |
909 | "http_proxy": "http://old.proxy", |
910 | "https_proxy": "https://old.proxy", |
911 | - "url": "http://new.url"}) |
912 | + "url": "http://new.url", |
913 | + "server_autodiscover": "False"}) |
914 | |
915 | def test_import_from_file_may_reset_old_options(self): |
916 | """ |
917 | @@ -1393,7 +1401,8 @@ |
918 | {"computer_title": "Old Title", |
919 | "account_name": "Old Name", |
920 | "registration_password": "", # <== |
921 | - "url": "http://old.url"}) |
922 | + "url": "http://old.url", |
923 | + "server_autodiscover": "False"}) |
924 | |
925 | def test_import_from_url(self): |
926 | sysvconfig_mock = self.mocker.patch(SysVConfig) |
927 | @@ -1434,7 +1443,8 @@ |
928 | "registration_password": "New Password", |
929 | "http_proxy": "http://new.proxy", |
930 | "https_proxy": "https://new.proxy", |
931 | - "url": "http://new.url"}) |
932 | + "url": "http://new.url", |
933 | + "server_autodiscover": "False"}) |
934 | |
935 | def test_import_from_url_with_http_code_fetch_error(self): |
936 | fetch_mock = self.mocker.replace("landscape.lib.fetch.fetch") |
937 | |
938 | === modified file 'root-client.conf' |
939 | --- root-client.conf 2009-03-16 10:30:07 +0000 |
940 | +++ root-client.conf 2012-02-03 19:35:22 +0000 |
941 | @@ -11,3 +11,4 @@ |
942 | ping_url = http://localhost:8081/ping |
943 | include_manager_plugins = ScriptExecution |
944 | script_users = www-data, nobody, root |
945 | + |
[1] autodiscover. lower() == "true": autodiscover = True autodiscover = Fals
+ if self.server_
+ self.server_
+ else:
+ self.server_
This is untested, and doesn't work because you didn't declare the config value.
[2] There are several test failures mostly in test_config.
[3] Having the methods being class methods on BrokerService isn't much help. What about having a free function?
[4] All functions and tests must have docstrings.
[5]
+ elif row.type == dns.A:
+ pass
+ else:
+ pass
Are those clauses necessary?
[6] failed( result) :
+ def lookup_
+ logging.info("SRV lookup of %s failed." % service_name)
+ return result
This is untested.
[7] failed( result) :
+ def lookup_
+ logging.info("Name lookup of %s failed." % hostname)
+ return None
And this seems to be as well.
[8] result( result) : warning( "Autodiscovery failed. Reverting to previous " %s/ping" % result
+ def handle_
+ if result is None:
+ logging.
+ "settings.")
+ return result
+
+ def do_rest(result):
+ if result:
+ self.url = "http://
This is slightly strange, because you encode the logic of autodiscovery returning None in both places. You should return the URL in handle_result no matter what.
[9] It'd be nice to cook an DNS server script to answer those kind of requests, so that we can test locally easily.
Thanks!