Merge lp:~jseutter/landscape-client/srv-autodiscover into lp:~landscape/landscape-client/trunk

Proposed by Jerry Seutter
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
Reviewer Review Type Date Requested Status
Frank Wierzbicki (community) Approve
Thomas Herve (community) Approve
Review via email: mp+89512@code.launchpad.net

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.

To post a comment you must log in.
Revision history for this message
Thomas Herve (therve) wrote :

[1]
+ if self.server_autodiscover.lower() == "true":
+ self.server_autodiscover = True
+ else:
+ self.server_autodiscover = Fals

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]
+ def lookup_failed(result):
+ logging.info("SRV lookup of %s failed." % service_name)
+ return result

This is untested.

[7]
+ def lookup_failed(result):
+ logging.info("Name lookup of %s failed." % hostname)
+ return None

And this seems to be as well.

[8]
+ def handle_result(result):
+ if result is None:
+ logging.warning("Autodiscovery failed. Reverting to previous "
+ "settings.")
+ return result
+
+ def do_rest(result):
+ if result:
+ self.url = "http://%s/ping" % result

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!

review: Needs Fixing
Revision history for this message
Jerry Seutter (jseutter) wrote :

Hi Thomas,

1,2,3,4,5,6,7 should be fixed.

> [8]
> + def handle_result(result):
> + if result is None:
> + logging.warning("Autodiscovery failed. Reverting to previous
> "
> + "settings.")
> + return result
> +
> + def do_rest(result):
> + if result:
> + self.url = "http://%s/ping" % result
>
> 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.

Revision history for this message
Thomas Herve (therve) wrote :

[10] pep8

landscape/broker/tests/test_dnslookup.py:109:9: E301 expected 1 blank line, found 0
landscape/broker/tests/test_dnslookup.py:158:9: E301 expected 1 blank line, found 0
landscape/tests/helpers.py:616:27: E261 at least two spaces before inline comment
landscape/tests/helpers.py:647:1: E302 expected 2 blank lines, found 1

[11] pyflakes

landscape/broker/service.py:4: 'logging' imported but unused
landscape/broker/service.py:5: 'dns' imported but unused
landscape/broker/service.py:6: 'Resolver' imported but unused
landscape/broker/tests/test_service.py:9: 'defer' imported but unused
landscape/broker/dnslookup.py:13: undefined name '_lookup_server_record'
landscape/broker/dnslookup.py:14: undefined name '_lookup_hostname'
dev/dns_server.py:4: 'twisted' imported but unused
dev/dns_server.py:71: local variable 'resolver' is assigned to but never us

Ooops. Please add tests to discover_server.

[12]
+ error_result = []
+ def check_error(result):
+ error_result.append(result.value)
+
+ d = lookup_hostname(None, bad_resolver)
+ d.addErrback(check_error)
+
+ self.assertIsInstance(error_result[0], ResolverError)

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!

review: Needs Fixing
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.

Revision history for this message
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.

Revision history for this message
Thomas Herve (therve) wrote :

[14]
+ SRV_RESPONSE = args["srv_response"]
+ 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:

review: Approve
Revision history for this message
Frank Wierzbicki (fwierzbicki) wrote :

Looks good +1!

review: Approve
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.

Revision history for this message
Jerry Seutter (jseutter) wrote :

> [14]
> + SRV_RESPONSE = args["srv_response"]
> + 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

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
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+

Subscribers

People subscribed via source and target branches

to all changes: