Merge lp:~jseutter/landscape-client/record-playback into lp:~landscape/landscape-client/trunk

Proposed by Jerry Seutter
Status: Merged
Approved by: Thomas Herve
Approved revision: 347
Merge reported by: Jerry Seutter
Merged at revision: not available
Proposed branch: lp:~jseutter/landscape-client/record-playback
Merge into: lp:~landscape/landscape-client/trunk
Diff against target: 646 lines (+258/-46)
9 files modified
landscape/broker/config.py (+7/-0)
landscape/broker/service.py (+8/-3)
landscape/broker/tests/test_store.py (+14/-8)
landscape/broker/tests/test_transport.py (+139/-12)
landscape/broker/transport.py (+72/-6)
landscape/hal.py (+1/-1)
landscape/lib/persist.py (+15/-14)
landscape/package/taskhandler.py (+1/-1)
scripts/landscape-dbus-proxy (+1/-1)
To merge this branch: bzr merge lp:~jseutter/landscape-client/record-playback
Reviewer Review Type Date Requested Status
Free Ekanayaka (community) Approve
Alberto Donato (community) Approve
Frank Wierzbicki Pending
Review via email: mp+65839@code.launchpad.net

Description of the change

With this change, landscape-client can now record the information that was sent from the client broker to landscape server. It adds --record and --record-dir command line options to the client broker.

I created a PayloadRecorder class that persists information to disk. I could not figure out how mocker could stub out the os module for unit testing, so some of the methods are not tested. Suggestions on how to improve the unit tests would be appreciated.

There are several lintian-triggered changes in the diff as well.

To post a comment you must log in.
343. By Jerry Seutter

Cleanups.

Revision history for this message
Alberto Donato (ack) wrote :

+0. The branch is ok, but I'd like to see a few changes:

[1]

+ parser.add_option("--record", action="store_true",
+ help="Record data sent to the server on disk (see "
+ "--record-dir).")

Rather than "on disk" I would use something like "on filesystem".

[2]

+ if self.last_payload_time == payload_time:
+ payload_time = payload_time + .001

Can you please use 0.001 here?

[3]

+ self._time = time.time
+ self._time_offset = self._time()

and

+ payload_time = self._time() - self._time_offset

AFAICT saving the reference to the time function is only needed for tests.
self._time should be dropped.

You can use the functionalities provided by Mocker to override the time() function.

Something like this should do:

    def test_get_payload_filename(self):
        """
        L{PayloadRecorder.get_payload_filename} should return a filename that
        is equal to the number of seconds since it was created.
        """
        mock = self.mocker.replace("time.time")
        mock()
        self.mocker.result(0.0)
        mock()
        self.mocker.result(12.345)
        self.mocker.count(1)
        self.mocker.replay()

        recorder = PayloadRecorder(True, None)

        payload_name = recorder.get_payload_filename()
        self.assertEquals("12.345", payload_name)

Similarly for the other related test.

Thanks!

review: Needs Fixing
344. By Jerry Seutter

- Implemented fixes suggested by ack.
- Improved unit test coverage.

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

Thanks ack. 1, 2 and 3 are fixed. Thank you for the pointers on mocker. The unit tests are much more thorough now.

Revision history for this message
Free Ekanayaka (free.ekanayaka) wrote :
Download full text (4.2 KiB)

Hi Jerry, looks good, however please use mocking only for time.time.

[1]

+ parser.add_option("--record-dir", metavar="record_dir",
+ help="Directory used to store server exchanges ("
+ "use with --record).")

Is there some use case where we want to set this to a particular directory? If not, I'd drop the option and go for something like:

class BrokerConfiguration(Configuration):

    @property
    def record_directory(self):
        return os.path.join(self.data_path, "exchanges")

which will return /var/lib/landscape/client/exchanges by default.

[2]

+ if self._payload_recorder:
+ self._payload_recorder.save(spayload)

Please check against None explicitly:

        if self._payload_recorder is not None:

[3]

+ file(self.destination_dir + '/' + payload_name, 'w').write(payload)

You can use landscape.lib.create_file instead, which closes the file description explicitly, also please use os.path.join:

+ create_file(os.path.join(self.destination_dir, payload_name), payload)

[3]

+ if self.recording and self.destination_dir != None:

and

+ if self.recording:

Instead of duplicating the check for self.recording, I think you could drop self.recording entirely and delegate the decision upper in the stack in landscape.broker.service.BrokerService.__init__:

[4]

+ self.persist = Persist(filename=self.persist_filename)

This change doesn't seem to be needed (in landscape/broker/tests/test_store.py)

[5]

- self.assertEquals(r.request.received_headers["x-computer-id"], "34")
+ self.assertEquals(r.request.received_headers["x-computer-id"],
+ "34")

Instead of breaking the line, you could make lint happy by s/assertEquals/assertEqual/, as assertEquals is also deprecated. (there are couple of this)

[6]

+class PayloadRecorderTest(MockerTestCase):
+ def test_get_payload_filename(self):

Please put a blank line between the class and the test.

[7]

+ def __init__(self, url, pubkey=None, payload_recorder=None):

Please add a @param entry of the new parameter in the class docstring.

[8]

+ def __init__(self, recording, destination_dir):

Please add a @param for these two parameters in the class docstring.

[9]

+ self.destination_dir = destination_dir
+ self.last_payload_time = -1
+ self.recording = recording

It seems none of these attributes needs to be exposed by the PayloadRecorder interface, I'd suggest making them private.

[10]

+ for file in os.listdir(self.destination_dir):
+ if os.path.isfile(self.destination_dir + '/' + file):
+ os.unlink(self.destination_dir + '/' + file)

Please use os.path.join, and a temp variable to avoid duplication. Also I'd recommend s/file/filename/ as 'file' overrides Python's builtin type :

+ for filename in os.listdir(self.destination_dir):
+ path = os.path.join(self.destination_dir, filename)
+ if os.path.isfile(path):
+ os.unlink(path)

[11]

+ recorder = PayloadRecorder(True, "./tmp")

You can use self.makeDir() instead, it...

Read more...

review: Needs Fixing
Revision history for this message
Alberto Donato (ack) wrote :

Thanks for taking care of comments.

review: Approve
345. By Jerry Seutter

Changes based on review feedback.

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

Hi Free,

1, 2, 3, 3(2), 4 are fixed.

> [5]
>
> - self.assertEquals(r.request.received_headers["x-computer-id"],
> "34")
> + self.assertEquals(r.request.received_headers["x-computer-id"],
> + "34")
>
> Instead of breaking the line, you could make lint happy by
> s/assertEquals/assertEqual/, as assertEquals is also deprecated. (there are
> couple of this)

assertEquals is used in several places. I will create a separate bug for this.

6, 7 are fixed.

> [8]
>
> + def __init__(self, recording, destination_dir):
>
> Please add a @param for these two parameters in the class docstring.

Are you sure? These are documented in the docstring for the method.

I will create a separate bug to fix this in other classes as well as this one.

9, 10, 11, 12, 13 are fixed.

346. By Jerry Seutter

Making docstring match code.

347. By Jerry Seutter

Fixing unit tests.

Revision history for this message
Free Ekanayaka (free.ekanayaka) wrote :

Nice work, +1!

[14]

+ def record_directory(self):

Please make it a @property for consistency with other similar methods in the same class, like exchange_store_path and message_store_path. Maybe also rename it to record_path, also for consistency.

[15]

+ if config.record is not None:
+ self.payload_recorder = PayloadRecorder(config.record_dir)
+ else:
+ self.payload_recorder = None

Well done, moving this conditional up the stack looks better :)

[16]

+ self.assertEquals("payload data", file(file_path).read())

You can use landscape.lib.fs.read_file, which will also close the file descriptor.

[17]

+ @param payload_recorder: PayloadRecorder used for recording exchanges
+ with the server.

I'd append a "If C{None}, exchanges won't be recorded at all."

[18]

+ if self._payload_recorder is not None:
+ self._payload_recorder.save(spayload)

This isn't unit-tested, afaics. It'd be good add a test to landscape.broker.tests.test_transport.HTTPTransportTest, passing a non-None PayloadRecorder to the HTTPTransport constructor, running an exchange and asserting that the payload gets saved.

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

> Nice work, +1!
>
> [14]
>
> + def record_directory(self):
>
> Please make it a @property for consistency with other similar methods in the
> same class, like exchange_store_path and message_store_path. Maybe also rename
> it to record_path, also for consistency.

Oops. Fixed.

> [16]
>
> + self.assertEquals("payload data", file(file_path).read())
>
> You can use landscape.lib.fs.read_file, which will also close the file
> descriptor.

Done.

> [17]
>
> + @param payload_recorder: PayloadRecorder used for recording exchanges
> + with the server.
>
> I'd append a "If C{None}, exchanges won't be recorded at all."

Good point. Added.

> [18]
>
> + if self._payload_recorder is not None:
> + self._payload_recorder.save(spayload)
>
> This isn't unit-tested, afaics. It'd be good add a test to
> landscape.broker.tests.test_transport.HTTPTransportTest, passing a non-None
> PayloadRecorder to the HTTPTransport constructor, running an exchange and
> asserting that the payload gets saved.

I added a test when payload_recorder is defined and a second test without payloa
d_recorder defined.

348. By Jerry Seutter

Addressing review comments.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'landscape/broker/config.py'
2--- landscape/broker/config.py 2011-01-12 16:30:14 +0000
3+++ landscape/broker/config.py 2011-06-29 17:53:31 +0000
4@@ -22,6 +22,10 @@
5 def exchange_store_path(self):
6 return os.path.join(self.data_path, "exchange.database")
7
8+ @property
9+ def record_directory(self):
10+ return os.path.join(self.data_path, "exchanges")
11+
12 def make_parser(self):
13 """Parser factory for broker-specific options.
14
15@@ -39,6 +43,7 @@
16 - C{https_proxy}
17 - C{cloud}
18 - C{otp}
19+ - C{record}
20 """
21 parser = super(BrokerConfiguration, self).make_parser()
22
23@@ -76,6 +81,8 @@
24 parser.add_option("--tags",
25 help="Comma separated list of tag names to be sent "
26 "to the server.")
27+ parser.add_option("--record", action="store_true",
28+ help="Record data sent to the server on filesystem ")
29 return parser
30
31 @property
32
33=== modified file 'landscape/broker/service.py'
34--- landscape/broker/service.py 2011-03-25 08:42:48 +0000
35+++ landscape/broker/service.py 2011-06-29 17:53:31 +0000
36@@ -5,7 +5,7 @@
37 from landscape.service import LandscapeService, run_landscape_service
38 from landscape.broker.registration import RegistrationHandler, Identity
39 from landscape.broker.config import BrokerConfiguration
40-from landscape.broker.transport import HTTPTransport
41+from landscape.broker.transport import HTTPTransport, PayloadRecorder
42 from landscape.broker.exchange import MessageExchange
43 from landscape.broker.exchangestore import ExchangeStore
44 from landscape.broker.ping import Pinger
45@@ -48,8 +48,13 @@
46 self.persist_filename = os.path.join(
47 config.data_path, "%s.bpickle" % (self.service_name,))
48 super(BrokerService, self).__init__(config)
49- self.transport = self.transport_factory(config.url,
50- config.ssl_public_key)
51+
52+ if config.record is not None:
53+ self.payload_recorder = PayloadRecorder(config.record_directory)
54+ else:
55+ self.payload_recorder = None
56+ self.transport = self.transport_factory(
57+ config.url, config.ssl_public_key, self.payload_recorder)
58 self.message_store = get_default_message_store(
59 self.persist, config.message_store_path)
60 self.identity = Identity(self.config, self.persist)
61
62=== modified file 'landscape/broker/tests/test_store.py'
63--- landscape/broker/tests/test_store.py 2011-06-16 20:05:54 +0000
64+++ landscape/broker/tests/test_store.py 2011-06-29 17:53:31 +0000
65@@ -4,7 +4,8 @@
66
67 from landscape.lib.persist import Persist
68 from landscape.broker.store import MessageStore
69-from landscape.schema import InvalidError, Message, Int, String, UnicodeOrString
70+from landscape.schema import (
71+ InvalidError, Message, Int, String, UnicodeOrString)
72
73 from landscape.tests.helpers import LandscapeTest
74 from landscape.tests.mocker import ANY
75@@ -22,7 +23,8 @@
76
77 def create_store(self):
78 persist = Persist(filename=self.persist_filename)
79- store = MessageStore(persist, self.temp_dir, 20, get_time=self.get_time)
80+ store = MessageStore(persist, self.temp_dir, 20,
81+ get_time=self.get_time)
82 store.set_accepted_types(["empty", "data"])
83 store.add_schema(Message("empty", {}))
84 store.add_schema(Message("empty2", {}))
85@@ -156,20 +158,23 @@
86
87 def test_unaccepted(self):
88 for i in range(10):
89- self.store.add(dict(type=["data", "unaccepted"][i%2], data=str(i)))
90+ self.store.add(dict(type=["data", "unaccepted"][i % 2],
91+ data=str(i)))
92 il = [m["data"] for m in self.store.get_pending_messages(20)]
93 self.assertEquals(il, map(str, [0, 2, 4, 6, 8]))
94
95 def test_unaccepted_with_offset(self):
96 for i in range(10):
97- self.store.add(dict(type=["data", "unaccepted"][i%2], data=str(i)))
98+ self.store.add(dict(type=["data", "unaccepted"][i % 2],
99+ data=str(i)))
100 self.store.set_pending_offset(2)
101 il = [m["data"] for m in self.store.get_pending_messages(20)]
102 self.assertEquals(il, map(str, [4, 6, 8]))
103
104 def test_unaccepted_reaccepted(self):
105 for i in range(10):
106- self.store.add(dict(type=["data", "unaccepted"][i%2], data=str(i)))
107+ self.store.add(dict(type=["data", "unaccepted"][i % 2],
108+ data=str(i)))
109 self.store.set_pending_offset(2)
110 il = [m["data"] for m in self.store.get_pending_messages(2)]
111 self.store.set_accepted_types(["data", "unaccepted"])
112@@ -178,7 +183,8 @@
113
114 def test_accepted_unaccepted(self):
115 for i in range(10):
116- self.store.add(dict(type=["data", "unaccepted"][i%2], data=str(i)))
117+ self.store.add(dict(type=["data", "unaccepted"][i % 2],
118+ data=str(i)))
119 # Setting pending offset here means that the first two
120 # messages, even though becoming unaccepted now, were already
121 # accepted before, so they shouldn't be marked for hold.
122@@ -192,7 +198,8 @@
123
124 def test_accepted_unaccepted_old(self):
125 for i in range(10):
126- self.store.add(dict(type=["data", "unaccepted"][i%2], data=str(i)))
127+ self.store.add(dict(type=["data", "unaccepted"][i % 2],
128+ data=str(i)))
129 self.store.set_pending_offset(2)
130 self.store.set_accepted_types(["unaccepted"])
131 il = [m["data"] for m in self.store.get_pending_messages(20)]
132@@ -323,7 +330,6 @@
133 self.assertRaises(InvalidError,
134 self.store.add, {"type": "data", "data": 3})
135
136-
137 def test_coercion_ignores_custom_api(self):
138 """
139 If a custom 'api' key is specified in the message, it should
140
141=== modified file 'landscape/broker/tests/test_transport.py'
142--- landscape/broker/tests/test_transport.py 2009-03-16 22:10:37 +0000
143+++ landscape/broker/tests/test_transport.py 2011-06-29 17:53:31 +0000
144@@ -1,11 +1,13 @@
145 import os
146
147 from landscape import VERSION
148-from landscape.broker.transport import HTTPTransport
149+from landscape.broker.transport import HTTPTransport, PayloadRecorder
150 from landscape.lib.fetch import PyCurlError
151+from landscape.lib.fs import create_file, read_file
152 from landscape.lib import bpickle
153
154-from landscape.tests.helpers import LandscapeTest, LogKeeperHelper
155+from landscape.tests.helpers import (
156+ LandscapeTest, LogKeeperHelper, MockerTestCase)
157
158 from twisted.web import server, resource
159 from twisted.internet import reactor
160@@ -25,6 +27,7 @@
161
162 class DataCollectingResource(resource.Resource):
163 request = content = None
164+
165 def getChild(self, request, name):
166 return self
167
168@@ -67,13 +70,14 @@
169 r = DataCollectingResource()
170 port = reactor.listenTCP(0, server.Site(r), interface="127.0.0.1")
171 self.ports.append(port)
172- transport = HTTPTransport("http://localhost:%d/"
173- % (port.getHost().port,))
174+ transport = HTTPTransport(
175+ "http://localhost:%d/" % (port.getHost().port,))
176 result = deferToThread(transport.exchange, "HI", computer_id="34",
177 message_api="X.Y")
178
179 def got_result(ignored):
180- self.assertEquals(r.request.received_headers["x-computer-id"], "34")
181+ self.assertEquals(r.request.received_headers["x-computer-id"],
182+ "34")
183 self.assertEquals(r.request.received_headers["user-agent"],
184 "landscape-client/%s" % (VERSION,))
185 self.assertEquals(r.request.received_headers["x-message-api"],
186@@ -89,19 +93,18 @@
187 public key specified.
188 """
189 r = DataCollectingResource()
190- context_factory = DefaultOpenSSLContextFactory(PRIVKEY,
191- PUBKEY)
192+ context_factory = DefaultOpenSSLContextFactory(PRIVKEY, PUBKEY)
193 port = reactor.listenSSL(0, server.Site(r), context_factory,
194 interface="127.0.0.1")
195 self.ports.append(port)
196- transport = HTTPTransport("https://localhost:%d/"
197- % (port.getHost().port,),
198- pubkey=PUBKEY)
199+ transport = HTTPTransport(
200+ "https://localhost:%d/" % (port.getHost().port,), PUBKEY)
201 result = deferToThread(transport.exchange, "HI", computer_id="34",
202 message_api="X.Y")
203
204 def got_result(ignored):
205- self.assertEquals(r.request.received_headers["x-computer-id"], "34")
206+ self.assertEquals(r.request.received_headers["x-computer-id"],
207+ "34")
208 self.assertEquals(r.request.received_headers["user-agent"],
209 "landscape-client/%s" % (VERSION,))
210 self.assertEquals(r.request.received_headers["x-message-api"],
211@@ -110,7 +113,6 @@
212 result.addCallback(got_result)
213 return result
214
215-
216 def test_ssl_verification_negative(self):
217 """
218 If the SSL server provides a key which is not verified by the
219@@ -130,6 +132,7 @@
220
221 result = deferToThread(transport.exchange, "HI", computer_id="34",
222 message_api="X.Y")
223+
224 def got_result(ignored):
225 self.assertEquals(r.request, None)
226 self.assertEquals(r.content, None)
227@@ -137,3 +140,127 @@
228 in self.logfile.getvalue())
229 result.addCallback(got_result)
230 return result
231+
232+ def test_payload_recording_works(self):
233+ """
234+ When C{HTTPTransport} is configured with a payload recorder, exchanges
235+ with the server should be saved to the filesystem.
236+ """
237+ path = self.makeDir()
238+ recorder = PayloadRecorder(path)
239+
240+ def static_filename():
241+ return "filename"
242+ recorder.get_payload_filename = static_filename
243+
244+ transport = HTTPTransport("http://localhost",
245+ payload_recorder=recorder)
246+
247+ def fake_curl(param1, param2, param3):
248+ """Stub out the curl network call."""
249+ class Curly(object):
250+ def getinfo(self, param1):
251+ return 200
252+ return (Curly(), bpickle.dumps("pay load response"))
253+ transport._curl = fake_curl
254+
255+ transport.exchange("pay load")
256+
257+ file_path = os.path.join(path, static_filename())
258+ self.assertEquals("pay load", bpickle.loads(read_file(file_path)))
259+
260+ def test_exchange_works_without_payload_recording(self):
261+ """
262+ When C{HTTPTransport} is configured without a payload recorder,
263+ exchanges with the server should still complete.
264+ """
265+ transport = HTTPTransport("http://localhost")
266+ self.called = False
267+
268+ def fake_curl(param1, param2, param3):
269+ """Stub out the curl network call."""
270+ self.called = True
271+ class Curly(object):
272+ def getinfo(self, param1):
273+ return 200
274+ return (Curly(), bpickle.dumps("pay load response"))
275+ transport._curl = fake_curl
276+
277+ transport.exchange("pay load")
278+
279+ self.assertTrue(self.called)
280+
281+class PayloadRecorderTest(MockerTestCase):
282+
283+ def test_get_payload_filename(self):
284+ """
285+ L{PayloadRecorder.get_payload_filename} should return a filename that
286+ is equal to the number of seconds since it was created.
287+ """
288+ mock = self.mocker.replace("time.time")
289+ mock()
290+ self.mocker.result(0.0)
291+ mock()
292+ self.mocker.result(12.3456)
293+ self.mocker.replay()
294+ recorder = PayloadRecorder(None)
295+
296+ payload_name = recorder.get_payload_filename()
297+
298+ self.assertEquals("12.346", payload_name)
299+
300+ def test_get_payload_filename_no_duplicates(self):
301+ """
302+ L{PayloadRecorder.get_payload_filename} should not generate duplicate
303+ payload names.
304+ """
305+ mock = self.mocker.replace("time.time")
306+ mock()
307+ self.mocker.result(0.0)
308+ mock()
309+ self.mocker.result(12.345)
310+ mock()
311+ self.mocker.result(12.345)
312+ self.mocker.replay()
313+
314+ recorder = PayloadRecorder(None)
315+
316+ payload_name_1 = recorder.get_payload_filename()
317+ payload_name_2 = recorder.get_payload_filename()
318+
319+ self.assertEquals("12.345", payload_name_1)
320+ self.assertEquals("12.346", payload_name_2)
321+
322+ def test_save(self):
323+ """L{PayloadRecorder.save} should save the payload to the filesystem.
324+ """
325+ path = self.makeDir()
326+ recorder = PayloadRecorder(path)
327+
328+ def static_filename():
329+ return "filename"
330+ recorder.get_payload_filename = static_filename
331+ recorder.save("payload data")
332+ file_path = os.path.join(path, static_filename())
333+ self.assertEquals("payload data", read_file(file_path))
334+
335+ def test_create_destination_dir(self):
336+ """
337+ L{PayloadRecorder} should create the destination directory if it does
338+ not exist.
339+ """
340+ path = self.makeDir()
341+ os.rmdir(path)
342+ PayloadRecorder(path)
343+ self.assertTrue(os.path.isdir(path))
344+
345+ def test_delete_old_payloads(self):
346+ """
347+ L{PayloadRecorder} should remove all files from the destination
348+ directory before writing new files.
349+ """
350+ path = self.makeDir()
351+ create_file(os.path.join(path, "one"), "one")
352+ create_file(os.path.join(path, "two"), "two")
353+ PayloadRecorder(path)
354+ self.assertEquals([], os.listdir(path))
355
356=== modified file 'landscape/broker/transport.py'
357--- landscape/broker/transport.py 2010-09-09 16:10:07 +0000
358+++ landscape/broker/transport.py 2011-06-29 17:53:31 +0000
359@@ -1,4 +1,5 @@
360 """Low-level server communication."""
361+import os
362 import time
363 import logging
364 import pprint
365@@ -6,6 +7,7 @@
366 import pycurl
367
368 from landscape.lib.fetch import fetch
369+from landscape.lib.fs import create_file
370 from landscape.lib import bpickle
371 from landscape.log import format_delta
372 from landscape import SERVER_API, VERSION
373@@ -14,13 +16,16 @@
374 class HTTPTransport(object):
375 """Transport makes a request to exchange message data over HTTP."""
376
377- def __init__(self, url, pubkey=None):
378+ def __init__(self, url, pubkey=None, payload_recorder=None):
379 """
380 @param url: URL of the remote Landscape server message system.
381 @param pubkey: SSH public key used for secure communication.
382+ @param payload_recorder: PayloadRecorder used for recording exchanges
383+ with the server. If C{None}, exchanges will not be recorded.
384 """
385 self._url = url
386 self._pubkey = pubkey
387+ self._payload_recorder = payload_recorder
388
389 def get_url(self):
390 """Get the URL of the remote message system."""
391@@ -31,9 +36,9 @@
392 self._url = url
393
394 def _curl(self, payload, computer_id, message_api):
395- headers= {"X-Message-API": message_api,
396- "User-Agent": "landscape-client/%s" % VERSION,
397- "Content-Type": "application/octet-stream"}
398+ headers = {"X-Message-API": message_api,
399+ "User-Agent": "landscape-client/%s" % VERSION,
400+ "Content-Type": "application/octet-stream"}
401 if computer_id:
402 headers["X-Computer-ID"] = computer_id
403 curl = pycurl.Curl()
404@@ -55,6 +60,8 @@
405
406 """
407 spayload = bpickle.dumps(payload)
408+ if self._payload_recorder is not None:
409+ self._payload_recorder.save(spayload)
410 try:
411 start_time = time.time()
412 if logging.getLogger().getEffectiveLevel() <= logging.DEBUG:
413@@ -85,11 +92,70 @@
414 return response
415
416
417+class PayloadRecorder(object):
418+ """
419+ L{PayloadRecorder} records client exchanges with the server to disk for
420+ later playback.
421+
422+ Exchange payloads will be stored one per file, where the file name is
423+ the elapsed time since the client was started.
424+ """
425+
426+ def __init__(self, destination_dir):
427+ """
428+ @param destination_dir - The directory to record exchanges in.
429+ """
430+ self._time_offset = time.time()
431+ self._destination_dir = destination_dir
432+ self._last_payload_time = -1
433+ if self._destination_dir is not None:
434+ self._create_destination_dir(self._destination_dir)
435+ self._delete_old_payloads()
436+
437+ def _create_destination_dir(self, destination_dir):
438+ """Create the destination directory if it does not exist.
439+
440+ @param destination_dir: The directory to be created.
441+ """
442+ if not os.path.exists(destination_dir):
443+ os.mkdir(destination_dir)
444+
445+ def _delete_old_payloads(self):
446+ """Delete payloads lying around from a previous session."""
447+ for filename in os.listdir(self._destination_dir):
448+ file_path = os.path.join(self._destination_dir, filename)
449+ if os.path.isfile(file_path):
450+ os.unlink(file_path)
451+
452+ def save(self, payload):
453+ """Persist the given payload to disk.
454+
455+ @param payload: The payload to be persisted.
456+ """
457+ payload_name = self.get_payload_filename()
458+ create_file(os.path.join(self._destination_dir, payload_name),
459+ payload)
460+
461+ def get_payload_filename(self):
462+ """
463+ Generate a payload filename. This method ensures that payloads
464+ will have a unique name.
465+ """
466+ payload_time = time.time() - self._time_offset
467+ last_payload_time = '%.3f' % self._last_payload_time
468+ this_payload_time = '%.3f' % payload_time
469+ if last_payload_time == this_payload_time:
470+ payload_time = payload_time + 0.001
471+ self._last_payload_time = payload_time
472+ return '%.3f' % payload_time
473+
474+
475 class FakeTransport(object):
476 """Fake transport for testing purposes."""
477
478- def __init__(self, url=None, pubkey=None):
479+ def __init__(self, url=None, pubkey=None, payload_recorder=None):
480 self._pubkey = pubkey
481+ self._payload_recorder = payload_recorder
482 self.payloads = []
483 self.responses = []
484 self._current_response = 0
485@@ -101,7 +167,7 @@
486
487 def get_url(self):
488 return self._url
489-
490+
491 def set_url(self, url):
492 self._url = url
493
494
495=== modified file 'landscape/hal.py'
496--- landscape/hal.py 2010-06-11 13:23:00 +0000
497+++ landscape/hal.py 2011-06-29 17:53:31 +0000
498@@ -21,7 +21,7 @@
499 """Returns a list of HAL devices.
500
501 @note: If it wasn't possible to connect to HAL over DBus, then an
502- emtpy list will be returned. This can happen if the HAL or DBus
503+ empty list will be returned. This can happen if the HAL or DBus
504 services are not running.
505 """
506 if not self._manager:
507
508=== modified file 'landscape/lib/persist.py'
509--- landscape/lib/persist.py 2009-04-02 10:25:04 +0000
510+++ landscape/lib/persist.py 2011-06-29 17:53:31 +0000
511@@ -18,7 +18,8 @@
512 # along with this Python module; if not, write to the Free Software
513 # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
514 #
515-import sys, os
516+import os
517+import sys
518 import copy
519 import re
520
521@@ -43,7 +44,7 @@
522
523 """Persist a hierarchical database of key=>value pairs.
524
525- There are three different kinds of opition maps, regarding the
526+ There are three different kinds of option maps, regarding the
527 persistence and priority that maps are queried.
528
529 - hard - Options are persistent.
530@@ -113,7 +114,7 @@
531 try:
532 self._hardmap = self._backend.load(filepath)
533 except:
534- filepathold = filepath+".old"
535+ filepathold = filepath + ".old"
536 if (os.path.isfile(filepathold) and
537 os.path.getsize(filepathold) > 0):
538 # warning("Broken configuration file at %s" % filepath)
539@@ -139,7 +140,7 @@
540 filepath = self.filename
541 filepath = os.path.expanduser(filepath)
542 if os.path.isfile(filepath):
543- os.rename(filepath, filepath+".old")
544+ os.rename(filepath, filepath + ".old")
545 dirname = os.path.dirname(filepath)
546 if dirname and not os.path.isdir(dirname):
547 os.makedirs(dirname)
548@@ -263,7 +264,7 @@
549 current = self._traverse(map, path)
550 if type(current) is list and value in current:
551 return
552- path = path+(sys.maxint,)
553+ path = path + (sys.maxint,)
554 self._traverse(map, path, setvalue=value)
555
556 def remove(self, path, value=NOTHING, soft=False, weak=False):
557@@ -360,49 +361,50 @@
558 def has(self, path, value=NOTHING, soft=False, hard=False, weak=False):
559 if type(path) is str:
560 path = path_string_to_tuple(path)
561- return self.parent.has(self.root+path, value, soft, hard, weak)
562+ return self.parent.has(self.root + path, value, soft, hard, weak)
563
564 def keys(self, path, soft=False, hard=False, weak=False):
565 if type(path) is str:
566 path = path_string_to_tuple(path)
567- return self.parent.keys(self.root+path, soft, hard, weak)
568+ return self.parent.keys(self.root + path, soft, hard, weak)
569
570 def get(self, path, default=None, soft=False, hard=False, weak=False):
571 if type(path) is str:
572 path = path_string_to_tuple(path)
573- return self.parent.get(self.root+path, default, soft, hard, weak)
574+ return self.parent.get(self.root + path, default, soft, hard, weak)
575
576 def set(self, path, value, soft=False, weak=False):
577 if type(path) is str:
578 path = path_string_to_tuple(path)
579- return self.parent.set(self.root+path, value, soft, weak)
580+ return self.parent.set(self.root + path, value, soft, weak)
581
582 def add(self, path, value, unique=False, soft=False, weak=False):
583 if type(path) is str:
584 path = path_string_to_tuple(path)
585- return self.parent.add(self.root+path, value, unique, soft, weak)
586+ return self.parent.add(self.root + path, value, unique, soft, weak)
587
588 def remove(self, path, value=NOTHING, soft=False, weak=False):
589 if type(path) is str:
590 path = path_string_to_tuple(path)
591- return self.parent.remove(self.root+path, value, soft, weak)
592+ return self.parent.remove(self.root + path, value, soft, weak)
593
594 def move(self, oldpath, newpath, soft=False, weak=False):
595 if type(oldpath) is str:
596 oldpath = path_string_to_tuple(oldpath)
597 if type(newpath) is str:
598 newpath = path_string_to_tuple(newpath)
599- return self.parent.move(self.root+oldpath, self.root+newpath,
600+ return self.parent.move(self.root + oldpath, self.root + newpath,
601 soft, weak)
602
603 def root_at(self, path):
604 if type(path) is str:
605 path = path_string_to_tuple(path)
606- return self.parent.root_at(self.root+path)
607+ return self.parent.root_at(self.root + path)
608
609
610 _splitpath = re.compile(r"(\[-?\d+\])|(?<!\\)\.").split
611
612+
613 def path_string_to_tuple(path):
614 if "." not in path and "[" not in path:
615 return (path,)
616@@ -610,4 +612,3 @@
617 return Backend.copy(self, value)
618
619 # vim:ts=4:sw=4:et
620-
621
622=== modified file 'landscape/package/taskhandler.py'
623--- landscape/package/taskhandler.py 2010-04-20 08:59:44 +0000
624+++ landscape/package/taskhandler.py 2011-06-29 17:53:31 +0000
625@@ -132,7 +132,7 @@
626 failure.trap(PackageTaskError)
627
628 def handle_task(self, task):
629- """Handle a sigle task.
630+ """Handle a single task.
631
632 Sub-classes must override this method in order to trigger task-specific
633 actions.
634
635=== modified file 'scripts/landscape-dbus-proxy'
636--- scripts/landscape-dbus-proxy 2010-07-09 14:51:01 +0000
637+++ scripts/landscape-dbus-proxy 2011-06-29 17:53:31 +0000
638@@ -33,7 +33,7 @@
639
640
641 class BrokerDBusObject(Object):
642- """A DBus-published object proxing L{RemoteBroker.send_message}.
643+ """A DBus-published object proxying L{RemoteBroker.send_message}.
644
645 It is used when upgrading from a DBus-based version of the Landscape client
646 to the newer AMP-based one, for letting the old package-changer process

Subscribers

People subscribed via source and target branches

to all changes: