Merge lp:~therve/landscape-client/restore-hardware-monitor into lp:~landscape/landscape-client/trunk

Proposed by Thomas Herve
Status: Merged
Approved by: Geoff Teale
Approved revision: 471
Merged at revision: 473
Proposed branch: lp:~therve/landscape-client/restore-hardware-monitor
Merge into: lp:~landscape/landscape-client/trunk
Diff against target: 1516 lines (+1060/-27)
32 files modified
README (+10/-1)
dbus/landscape.conf (+62/-0)
debian/landscape-client.install (+2/-0)
debian/landscape-client.postinst (+11/-0)
debian/rules (+9/-6)
landscape/broker/exchange.py (+3/-1)
landscape/hal.py (+52/-0)
landscape/lib/bpickle_dbus.py (+65/-0)
landscape/monitor/config.py (+5/-4)
landscape/monitor/hardwareinventory.py (+114/-0)
landscape/monitor/mountinfo.py (+65/-2)
landscape/monitor/tests/test_hardwareinventory.py (+273/-0)
landscape/monitor/tests/test_mountinfo.py (+76/-0)
landscape/monitor/tests/test_service.py (+9/-3)
landscape/package/releaseupgrader.py (+21/-0)
landscape/package/tests/test_releaseupgrader.py (+54/-0)
landscape/reactor.py (+11/-0)
landscape/service.py (+6/-0)
landscape/tests/test_configuration.py (+1/-1)
landscape/tests/test_hal.py (+87/-0)
landscape/tests/test_service.py (+9/-0)
landscape/tests/test_textmessage.py (+2/-2)
landscape/textmessage.py (+2/-1)
landscape/watchdog.py (+5/-2)
man/landscape-client.1 (+6/-1)
man/landscape-client.txt (+2/-0)
man/landscape-config.1 (+6/-1)
man/landscape-config.txt (+2/-0)
man/landscape-message.1 (+5/-1)
man/landscape-message.txt (+1/-0)
scripts/landscape-dbus-proxy (+82/-0)
setup.py (+2/-1)
To merge this branch: bzr merge lp:~therve/landscape-client/restore-hardware-monitor
Reviewer Review Type Date Requested Status
Geoff Teale (community) Approve
Fernando Correa Neto (community) Approve
Review via email: mp+95906@code.launchpad.net

Description of the change

The branch basically reverts r414 from trunk, except keeping the new hardwareinfo manager plugin.

To post a comment you must log in.
Revision history for this message
Fernando Correa Neto (fcorrea) wrote :

Everything went fine on my system (Oneiric)

PASSED (successes=2298)

+1!

review: Approve
Revision history for this message
Geoff Teale (tealeg) wrote :

+1 Good for me.

One small thing.

+ logging.error("Couldn't to connect to Hal via DBus")

The "to" there is unecessary.

review: Approve
472. By Thomas Herve

CLeanups

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'README'
2--- README 2011-12-01 14:01:29 +0000
3+++ README 2012-03-06 09:20:24 +0000
4@@ -23,6 +23,14 @@
5
6 == Developing ==
7
8+To run the full test suite, you must have a dbus session bus
9+running. If you don't have one (for example, if you're running the
10+tests in an ssh session), run the following command:
11+
12+export DBUS_SESSION_BUS_ADDRESS=`dbus-daemon --print-address=1 --session --fork`
13+
14+Then your tests should pass.
15+
16 When you want to test the landscape client manually without management
17 features, you can simply run:
18
19@@ -31,7 +39,8 @@
20 This defaults to the 'landscape-client.conf' configuration file.
21
22 When you want to test management features manually, you'll need to run as root.
23-There's a configuration file 'root-client.conf'.
24+There's a configuration file 'root-client.conf' which specifies use of the
25+system bus.
26
27 $ sudo ./scripts/landscape-client -c root-client.conf
28
29
30=== added directory 'dbus'
31=== added file 'dbus/landscape.conf'
32--- dbus/landscape.conf 1970-01-01 00:00:00 +0000
33+++ dbus/landscape.conf 2012-03-06 09:20:24 +0000
34@@ -0,0 +1,62 @@
35+<!DOCTYPE busconfig PUBLIC
36+ "-//freedesktop//DTD D-BUS Bus Configuration 1.0//EN"
37+ "http://www.freedesktop.org/standards/dbus/1.0/busconfig.dtd">
38+<busconfig>
39+
40+ <policy user="landscape">
41+ <allow own="com.canonical.landscape.Broker" />
42+ <allow own="com.canonical.landscape.Monitor" />
43+
44+ <allow send_destination="com.canonical.landscape.Broker" />
45+ <allow receive_sender="com.canonical.landscape.Broker" />
46+
47+ <allow send_destination="com.canonical.landscape.Monitor" />
48+ <allow receive_sender="com.canonical.landscape.Monitor" />
49+
50+ <allow send_destination="com.canonical.landscape.Manager" />
51+ <allow receive_sender="com.canonical.landscape.Manager" />
52+
53+ <allow send_interface="org.freedesktop.Hal.Manager" />
54+ <allow send_interface="org.freedesktop.Hal.Device" />
55+
56+ </policy>
57+
58+ <!-- this is a horrible hack -->
59+ <policy user="haldaemon">
60+
61+ <allow receive_sender="com.canonical.landscape.Manager" />
62+ <allow receive_sender="com.canonical.landscape.Monitor" />
63+ <allow receive_sender="com.canonical.landscape.Broker" />
64+
65+ </policy>
66+
67+ <policy user="root">
68+ <allow own="com.canonical.landscape.Manager" />
69+
70+ <allow send_destination="com.canonical.landscape.Broker" />
71+ <allow receive_sender="com.canonical.landscape.Broker" />
72+
73+ <allow send_destination="com.canonical.landscape.Monitor" />
74+ <allow receive_sender="com.canonical.landscape.Monitor" />
75+
76+ <allow send_destination="com.canonical.landscape.Manager" />
77+ <allow receive_sender="com.canonical.landscape.Manager" />
78+ </policy>
79+
80+ <policy context="default">
81+ <deny own="com.canonical.landscape.Broker" />
82+ <deny own="com.canonical.landscape.Monitor" />
83+ <deny own="com.canonical.landscape.Manager" />
84+
85+ <deny send_destination="com.canonical.landscape.Broker" />
86+ <deny receive_sender="com.canonical.landscape.Broker" />
87+
88+ <deny send_destination="com.canonical.landscape.Monitor" />
89+ <deny receive_sender="com.canonical.landscape.Monitor" />
90+
91+ <deny send_destination="com.canonical.landscape.Manager" />
92+ <deny receive_sender="com.canonical.landscape.Manager" />
93+
94+ </policy>
95+
96+</busconfig>
97
98=== modified file 'debian/landscape-client.install'
99--- debian/landscape-client.install 2011-12-01 14:01:29 +0000
100+++ debian/landscape-client.install 2012-03-06 09:20:24 +0000
101@@ -8,5 +8,7 @@
102 usr/bin/landscape-package-reporter
103 usr/bin/landscape-release-upgrader
104 usr/bin/landscape-is-cloud-managed
105+usr/bin/landscape-dbus-proxy
106 usr/share/landscape/cloud-default.conf
107+etc/dbus-1/system.d/landscape.conf
108 usr/lib/landscape
109
110=== modified file 'debian/landscape-client.postinst'
111--- debian/landscape-client.postinst 2012-02-28 17:47:57 +0000
112+++ debian/landscape-client.postinst 2012-03-06 09:20:24 +0000
113@@ -129,6 +129,17 @@
114 if [ -e $very_old_cron_job ]; then
115 rm $very_old_cron_job
116 fi
117+
118+ # Check if we're upgrading from a D-Bus version
119+ if ! [ -z $2 ]; then
120+ if dpkg --compare-versions $2 lt 1.5.1; then
121+ # Launch a proxy service that will forward requests over DBus
122+ # from the old package-changer to the new AMP-based broker. This
123+ # is a one-off only needed for the DBus->AMP upgrade
124+ start-stop-daemon -x /usr/bin/landscape-dbus-proxy -b -c landscape -u landscape -S
125+ fi
126+ fi
127+
128 ;;
129
130 abort-upgrade|abort-remove|abort-deconfigure)
131
132=== modified file 'debian/rules'
133--- debian/rules 2012-02-10 17:59:13 +0000
134+++ debian/rules 2012-03-06 09:20:24 +0000
135@@ -60,6 +60,7 @@
136 install -D -o root -g root -m 644 debian/cloud-default.conf $(root_dir)/usr/share/landscape/cloud-default.conf
137 install -D -o root -g root -m 755 smart-update/smart-update $(root_dir)/usr/lib/landscape/smart-update
138 install -D -o root -g root -m 755 apt-update/apt-update $(root_dir)/usr/lib/landscape/apt-update
139+ install -D -o root -g root -m 644 dbus/landscape.conf $(root_dir)/etc/dbus-1/system.d/landscape.conf
140
141 binary-indep:
142 # do nothing
143@@ -83,22 +84,24 @@
144
145 ifneq (,$(findstring $(dist_release),"dapper"))
146 # We need python2.4-pysqlite2 and a non-buggy libcurl3-gnutls on dapper
147- echo "extra:Depends=python2.4-pysqlite2, libcurl3-gnutls (>= 7.15.1-1ubuntu3), python-smartpm (>= 1.1.1~bzr20081010-0ubuntu1.6.06.0)" >> $(landscape_common_substvars)
148- echo "extra:Depends=python2.4-pycurl" >> $(landscape_client_substvars)
149+ echo "extra:Depends=python2.4-pysqlite2, libcurl3-gnutls (>= 7.15.1-1ubuntu3), python-smartpm (>= 1.1.1~bzr20081010-0ubuntu1.6.06.0), python2.4-dbus" >> $(landscape_common_substvars)
150+ echo "extra:Depends=python2.4-pycurl, hal" >> $(landscape_client_substvars)
151 endif
152 ifneq (,$(findstring $(dist_release),"hardy"))
153 # We want the smart 1.1.1 from the Landscape repository on hardy
154- echo "extra:Depends=python-smartpm (>= 1.1.1~bzr20081010-0ubuntu1.8.04.1)" >> $(landscape_common_substvars)
155- echo "extra:Depends=python-pycurl" >> $(landscape_client_substvars)
156+ echo "extra:Depends=python-smartpm (>= 1.1.1~bzr20081010-0ubuntu1.8.04.1), python-dbus" >> $(landscape_common_substvars)
157+ echo "extra:Depends=python-pycurl, hal" >> $(landscape_client_substvars)
158 endif
159 ifneq (,$(filter $(dist_release),karmic lucid maverick))
160 # We want libpam-modules in karmic, and smart 1.2
161- echo "extra:Depends=libpam-modules (>= 1.0.1-9ubuntu3), python-smartpm (>= 1.2-4)" >> $(landscape_common_substvars)
162- echo "extra:Depends=python-pycurl" >> $(landscape_client_substvars)
163+ echo "extra:Depends=libpam-modules (>= 1.0.1-9ubuntu3), python-smartpm (>= 1.2-4), python-dbus" >> $(landscape_common_substvars)
164+ echo "extra:Depends=python-pycurl, hal" >> $(landscape_client_substvars)
165 endif
166 ifeq (,$(filter $(dist_release),dapper hardy karmic lucid maverick))
167+ # Starting natty, no more hal or dbus
168 echo "extra:Depends=libpam-modules (>= 1.0.1-9ubuntu3), python-smartpm (>= 1.2-4)" >> $(landscape_common_substvars)
169 echo "extra:Depends=python-pycurl, gir1.2-gudev-1.0 (>= 165-0ubuntu2)" >> $(landscape_client_substvars)
170+ echo "extra:Suggests=python-dbus, hal" >> $(landscape_client_substvars)
171 endif
172 ifeq (,$(filter $(dist_release),dapper hardy karmic))
173 # The python-image-store-proxy package is needed for the eucalyptus plugin
174
175=== modified file 'landscape/broker/exchange.py'
176--- landscape/broker/exchange.py 2011-12-01 14:01:29 +0000
177+++ landscape/broker/exchange.py 2012-03-06 09:20:24 +0000
178@@ -422,7 +422,9 @@
179 handler(message)
180
181 def register_client_accepted_message_type(self, type):
182- self._client_accepted_types.add(type)
183+ # stringify the type because it's a dbus.String. It should work
184+ # anyway, but this is just for sanity and less confusing logs.
185+ self._client_accepted_types.add(str(type))
186
187 def get_client_accepted_message_types(self):
188 return sorted(self._client_accepted_types)
189
190=== added file 'landscape/hal.py'
191--- landscape/hal.py 1970-01-01 00:00:00 +0000
192+++ landscape/hal.py 2012-03-06 09:20:24 +0000
193@@ -0,0 +1,52 @@
194+import logging
195+
196+from dbus import Interface, SystemBus
197+from dbus.exceptions import DBusException
198+
199+
200+class HALManager(object):
201+
202+ def __init__(self, bus=None):
203+ try:
204+ self._bus = bus or SystemBus()
205+ manager = self._bus.get_object("org.freedesktop.Hal",
206+ "/org/freedesktop/Hal/Manager")
207+ except DBusException:
208+ logging.error("Couldn't connect to Hal via DBus")
209+ self._manager = None
210+ else:
211+ self._manager = Interface(manager, "org.freedesktop.Hal.Manager")
212+
213+ def get_devices(self):
214+ """Returns a list of HAL devices.
215+
216+ @note: If it wasn't possible to connect to HAL over DBus, then an
217+ empty list will be returned. This can happen if the HAL or DBus
218+ services are not running.
219+ """
220+ if not self._manager:
221+ return []
222+ devices = []
223+ for udi in self._manager.GetAllDevices():
224+ device = self._bus.get_object("org.freedesktop.Hal", udi)
225+ device = Interface(device, "org.freedesktop.Hal.Device")
226+ device = HALDevice(device)
227+ devices.append(device)
228+ return devices
229+
230+
231+class HALDevice(object):
232+
233+ def __init__(self, device):
234+ self._children = []
235+ self._device = device
236+ self.properties = device.GetAllProperties()
237+ self.udi = self.properties["info.udi"]
238+ self.parent = None
239+
240+ def add_child(self, device):
241+ self._children.append(device)
242+ device.parent = self
243+
244+ def get_children(self):
245+ return self._children
246
247=== added file 'landscape/lib/bpickle_dbus.py'
248--- landscape/lib/bpickle_dbus.py 1970-01-01 00:00:00 +0000
249+++ landscape/lib/bpickle_dbus.py 2012-03-06 09:20:24 +0000
250@@ -0,0 +1,65 @@
251+"""
252+Different versions of the Python DBus bindings return different types
253+to represent integers, strings, lists, etc. Older versions return
254+builtin Python types: C{int}, C{str}, C{list}, etc. Newer versions
255+return DBus-specific wrappers: C{Int16}, C{String}, C{Array}, etc.
256+Failures occur when DBus types are used because bpickle doesn't know
257+that an C{Int16} is really an C{int} and that an C{Array} is really a
258+C{list}.
259+
260+L{install} and L{uninstall} can install and remove extensions that
261+make bpickle work with DBus types.
262+"""
263+
264+import dbus
265+
266+from landscape.lib import bpickle
267+
268+
269+def install():
270+ """Install bpickle extensions for DBus types."""
271+ for type, function in get_dbus_types():
272+ bpickle.dumps_table[type] = function
273+
274+
275+def uninstall():
276+ """Uninstall bpickle extensions for DBus types."""
277+ for type, function in get_dbus_types():
278+ del bpickle.dumps_table[type]
279+
280+
281+def dumps_utf8string(obj):
282+ """
283+ Convert the specified L{dbus.types.UTF8String} to bpickle's
284+ representation for C{unicode} data.
285+ """
286+ return "u%s:%s" % (len(obj), obj)
287+
288+
289+def dumps_double(obj):
290+ """
291+ Convert a dbus.types.Double into a floating point representation.
292+ """
293+ return "f%r;" % float(obj)
294+
295+
296+def get_dbus_types():
297+ """
298+ Generator yields C{(type, bpickle_function)} for available DBus
299+ types.
300+ """
301+ for (type_name, function) in [("Boolean", bpickle.dumps_bool),
302+ ("Int16", bpickle.dumps_int),
303+ ("UInt16", bpickle.dumps_int),
304+ ("Int32", bpickle.dumps_int),
305+ ("UInt32", bpickle.dumps_int),
306+ ("Int64", bpickle.dumps_int),
307+ ("UInt64", bpickle.dumps_int),
308+ ("Double", dumps_double),
309+ ("Array", bpickle.dumps_list),
310+ ("Dictionary", bpickle.dumps_dict),
311+ ("String", bpickle.dumps_unicode),
312+ ("UTF8String", dumps_utf8string)]:
313+ type = getattr(dbus.types, type_name, None)
314+ if type is not None:
315+ yield type, function
316
317=== modified file 'landscape/monitor/config.py'
318--- landscape/monitor/config.py 2011-11-30 09:28:10 +0000
319+++ landscape/monitor/config.py 2012-03-06 09:20:24 +0000
320@@ -1,10 +1,11 @@
321 from landscape.deployment import Configuration
322
323
324-ALL_PLUGINS = ["ActiveProcessInfo", "ComputerInfo", "LoadAverage",
325- "MemoryInfo", "MountInfo", "ProcessorInfo", "Temperature",
326- "PackageMonitor", "UserMonitor", "RebootRequired",
327- "AptPreferences", "NetworkActivity", "NetworkDevice"]
328+ALL_PLUGINS = ["ActiveProcessInfo", "ComputerInfo", "HardwareInventory",
329+ "LoadAverage", "MemoryInfo", "MountInfo", "ProcessorInfo",
330+ "Temperature", "PackageMonitor", "UserMonitor",
331+ "RebootRequired", "AptPreferences", "NetworkActivity",
332+ "NetworkDevice"]
333
334
335 class MonitorConfiguration(Configuration):
336
337=== added file 'landscape/monitor/hardwareinventory.py'
338--- landscape/monitor/hardwareinventory.py 1970-01-01 00:00:00 +0000
339+++ landscape/monitor/hardwareinventory.py 2012-03-06 09:20:24 +0000
340@@ -0,0 +1,114 @@
341+import logging
342+
343+from twisted.internet.defer import succeed
344+
345+from landscape.lib.log import log_failure
346+
347+from landscape.diff import diff
348+from landscape.monitor.plugin import MonitorPlugin
349+
350+
351+class HardwareInventory(MonitorPlugin):
352+
353+ persist_name = "hardware-inventory"
354+
355+ def __init__(self, hal_manager=None):
356+ super(HardwareInventory, self).__init__()
357+ self._persist_sets = []
358+ self._persist_removes = []
359+ self.enabled = True
360+ try:
361+ from landscape.hal import HALManager
362+ except ImportError:
363+ self.enabled = False
364+ else:
365+ self._hal_manager = hal_manager or HALManager()
366+
367+ def register(self, manager):
368+ if not self.enabled:
369+ return
370+ super(HardwareInventory, self).register(manager)
371+ self.call_on_accepted("hardware-inventory", self.exchange, True)
372+
373+ def send_message(self, urgent):
374+ devices = self.create_message()
375+ if devices:
376+ message = {"type": "hardware-inventory", "devices": devices}
377+ result = self.registry.broker.send_message(message, urgent=urgent)
378+ result.addCallback(self.persist_data)
379+ result.addErrback(log_failure)
380+ logging.info("Queueing a message with hardware-inventory "
381+ "information.")
382+ else:
383+ result = succeed(None)
384+ return result
385+
386+ def exchange(self, urgent=False):
387+ if not self.enabled:
388+ return
389+ return self.registry.broker.call_if_accepted("hardware-inventory",
390+ self.send_message, urgent)
391+
392+ def persist_data(self, message_id):
393+ for key, udi, value in self._persist_sets:
394+ self._persist.set((key, udi), value)
395+ for key in self._persist_removes:
396+ self._persist.remove(key)
397+ del self._persist_sets[:]
398+ del self._persist_removes[:]
399+ # This forces the registry to write the persistent store to disk
400+ # This means that the persistent data reflects the state of the
401+ # messages sent.
402+ self.registry.flush()
403+
404+ def create_message(self):
405+ # FIXME Using persist to keep track of changes here uses a
406+ # fair amount of memory. On my machine a rough test seemed to
407+ # indicate that memory usage grew by 1.3mb, about 12% of the
408+ # overall process size. Look here to save memory.
409+ del self._persist_sets[:]
410+ del self._persist_removes[:]
411+ devices = []
412+ previous_devices = self._persist.get("devices", {})
413+ current_devices = set()
414+
415+ for device in self._hal_manager.get_devices():
416+ previous_properties = previous_devices.get(device.udi)
417+ if not previous_properties:
418+ devices.append(("create", device.properties))
419+ elif previous_properties != device.properties:
420+ creates, updates, deletes = diff(previous_properties,
421+ device.properties)
422+ devices.append(("update", device.udi,
423+ creates, updates, deletes))
424+ current_devices.add(device.udi)
425+ self._persist_sets.append(
426+ ("devices", device.udi, device.properties))
427+
428+ items_with_parents = {}
429+ deleted_devices = set()
430+ for udi, value in previous_devices.iteritems():
431+ if udi not in current_devices:
432+ if "info.parent" in value:
433+ items_with_parents[udi] = value["info.parent"]
434+ deleted_devices.add(udi)
435+
436+ # We remove the deleted devices from our persistent store it's
437+ # only the information we're sending to the server that we're
438+ # compressing.
439+ for udi in deleted_devices:
440+ self._persist_removes.append(("devices", udi))
441+
442+ # We can now flatten the list of devices we send to the server
443+ # For each of the items_with_parents, if both the item and it's parent
444+ # are in the deleted_devices set, then we can remove this item from the
445+ # set.
446+ minimal_deleted_devices = deleted_devices.copy()
447+ for child, parent in items_with_parents.iteritems():
448+ if child in deleted_devices and parent in deleted_devices:
449+ minimal_deleted_devices.remove(child)
450+ # We now build the deleted devices message
451+ for udi in minimal_deleted_devices:
452+ devices.append(("delete", udi))
453+
454+ return devices
455
456=== modified file 'landscape/monitor/mountinfo.py'
457--- landscape/monitor/mountinfo.py 2011-12-01 13:38:58 +0000
458+++ landscape/monitor/mountinfo.py 2012-03-06 09:20:24 +0000
459@@ -15,7 +15,7 @@
460
461 def __init__(self, interval=300, monitor_interval=60 * 60,
462 mounts_file="/proc/mounts", create_time=time.time,
463- statvfs=None, mtab_file="/etc/mtab"):
464+ statvfs=None, hal_manager=None, mtab_file="/etc/mtab"):
465 self.run_interval = interval
466 self._monitor_interval = monitor_interval
467 self._create_time = create_time
468@@ -29,6 +29,12 @@
469 self._mount_info = []
470 self._mount_info_to_persist = None
471 try:
472+ from landscape.hal import HALManager
473+ except ImportError:
474+ self._hal_manager = hal_manager
475+ else:
476+ self._hal_manager = hal_manager or HALManager()
477+ try:
478 from gi.repository import GUdev
479 except ImportError:
480 self._gudev_client = None
481@@ -116,7 +122,9 @@
482 current_mount_points.add(mount_point)
483
484 def _get_removable_devices(self):
485- if self._gudev_client is not None:
486+ if self._hal_manager is not None:
487+ return self._get_hal_removable_devices()
488+ elif self._gudev_client is not None:
489 return self._get_udev_removable_devices()
490 else:
491 return set()
492@@ -130,6 +138,61 @@
493 return False
494 return is_removable()
495
496+ def _get_hal_removable_devices(self):
497+ block_devices = {} # {udi: [device, ...]}
498+ children = {} # {parent_udi: [child_udi, ...]}
499+ removable = set()
500+
501+ # We walk the list of devices building up a dictionary of all removable
502+ # devices, and a mapping of {UDI => [block devices]}
503+ # We differentiate between devices that we definitely know are
504+ # removable and devices that _may_ be removable, depending on their
505+ # parent device, e.g. /dev/sdb1 isn't flagged as removable, but
506+ # /dev/sdb may well be removable.
507+
508+ # Unfortunately, HAL doesn't guarantee the order of the devices
509+ # returned from get_devices(), so we may not know that a parent device
510+ # is removable when we find it's first child.
511+ devices = self._hal_manager.get_devices()
512+ for device in devices:
513+ block_device = device.properties.get("block.device")
514+ if block_device:
515+ if device.properties.get("storage.removable"):
516+ removable.add(device.udi)
517+
518+ try:
519+ block_devices[device.udi].append(block_device)
520+ except KeyError:
521+ block_devices[device.udi] = [block_device]
522+
523+ parent_udi = device.properties.get("info.parent")
524+ if parent_udi is not None:
525+ try:
526+ children[parent_udi].append(device.udi)
527+ except KeyError:
528+ children[parent_udi] = [device.udi]
529+
530+ # Propagate the removable flag from each node all the way to
531+ # its leaf children.
532+ updated = True
533+ while updated:
534+ updated = False
535+ for parent_udi in children:
536+ if parent_udi in removable:
537+ for child_udi in children[parent_udi]:
538+ if child_udi not in removable:
539+ removable.add(child_udi)
540+ updated = True
541+
542+ # We've now seen _all_ devices, and have the definitive list of
543+ # removable UDIs, so we can now find all the removable devices in the
544+ # system.
545+ removable_devices = set()
546+ for udi in removable:
547+ removable_devices.update(block_devices[udi])
548+
549+ return removable_devices
550+
551 def _get_mount_info(self):
552 """Generator yields local mount points worth recording data for."""
553 removable_devices = self._get_removable_devices()
554
555=== added file 'landscape/monitor/tests/test_hardwareinventory.py'
556--- landscape/monitor/tests/test_hardwareinventory.py 1970-01-01 00:00:00 +0000
557+++ landscape/monitor/tests/test_hardwareinventory.py 2012-03-06 09:20:24 +0000
558@@ -0,0 +1,273 @@
559+from twisted.internet.defer import fail, succeed
560+
561+from landscape.monitor.hardwareinventory import HardwareInventory
562+from landscape.tests.test_hal import MockHALManager, MockRealHALDevice
563+from landscape.tests.helpers import LandscapeTest, MonitorHelper
564+from landscape.tests.mocker import ANY
565+from landscape.message_schemas import HARDWARE_INVENTORY
566+
567+
568+class HardwareInventoryTest(LandscapeTest):
569+
570+ helpers = [MonitorHelper]
571+
572+ def setUp(self):
573+ super(HardwareInventoryTest, self).setUp()
574+ self.mstore.set_accepted_types(["hardware-inventory"])
575+ devices = [MockRealHALDevice({u"info.udi": u"wubble",
576+ u"info.product": u"Wubble"}),
577+ MockRealHALDevice({u"info.udi": u"ooga",
578+ u"info.product": u"Ooga"})]
579+ self.hal_manager = MockHALManager(devices)
580+ self.plugin = HardwareInventory(hal_manager=self.hal_manager)
581+ self.monitor.add(self.plugin)
582+
583+ def assertSchema(self, devices):
584+ full_message = {"type": "hardware-inventory", "devices": devices}
585+ self.assertEqual(HARDWARE_INVENTORY.coerce(full_message),
586+ full_message)
587+
588+ def test_hal_devices(self):
589+ """
590+ The first time the plugin runs it should report information
591+ about all HAL devices found on the system. Every UDI provided
592+ by HAL should be present in the devices list as is from HAL.
593+ """
594+ message = self.plugin.create_message()
595+ actual_udis = [part[1][u"info.udi"] for part in message]
596+ expected_udis = [device.udi for device
597+ in self.hal_manager.get_devices()]
598+ self.assertEqual(set(actual_udis), set(expected_udis))
599+
600+ def test_first_message(self):
601+ """
602+ The first time the plugin runs it should report information
603+ about all HAL devices found on the system. All new devices
604+ will be reported with 'create' actions.
605+ """
606+ message = self.plugin.create_message()
607+ actions = [part[0] for part in message]
608+ self.assertEqual(set(actions), set(["create"]))
609+ self.assertSchema(message)
610+
611+ def test_no_changes(self):
612+ """
613+ Messages should not be created if hardware information is
614+ unchanged since the last server exchange.
615+ """
616+ self.plugin.exchange()
617+ self.assertNotEquals(len(self.mstore.get_pending_messages()), 0)
618+
619+ messages = self.mstore.get_pending_messages()
620+ self.plugin.exchange()
621+ self.assertEqual(self.mstore.get_pending_messages(), messages)
622+
623+ def test_update(self):
624+ """
625+ If a change is detected for a device that was previously
626+ reported to the server, the changed device should be reported
627+ with an 'update' action. Property changes are reported at a
628+ key/value pair level.
629+ """
630+ self.hal_manager.devices = [
631+ MockRealHALDevice({u"info.udi": u"wubble",
632+ u"info.product": u"Wubble"})]
633+ registry_mocker = self.mocker.replace(self.plugin.registry)
634+ registry_mocker.flush()
635+ self.mocker.count(2)
636+ self.mocker.result(None)
637+ self.mocker.replay()
638+ message = self.plugin.create_message()
639+ self.plugin.persist_data(None)
640+ self.assertEqual(message, [("create", {u"info.udi": u"wubble",
641+ u"info.product": u"Wubble"})])
642+
643+ self.hal_manager.devices[0] = MockRealHALDevice(
644+ {u"info.udi": u"wubble", u"info.product": u"Ooga"})
645+ message = self.plugin.create_message()
646+ self.plugin.persist_data(None)
647+ self.assertEqual(message, [("update", u"wubble",
648+ {}, {u"info.product": u"Ooga"}, {})])
649+ self.assertSchema(message)
650+ self.assertEqual(self.plugin.create_message(), [])
651+
652+ def test_update_list(self):
653+ """
654+ An update should be sent to the server when a strlist device
655+ property changes. No updates should be sent if a device is
656+ unchanged.
657+ """
658+ self.hal_manager.devices = [
659+ MockRealHALDevice({u"info.udi": u"wubble",
660+ u"info.product": u"Wubble",
661+ u"info.capabilities": [u"foo", u"bar"]})]
662+
663+ message = self.plugin.create_message()
664+ self.plugin.persist_data(None)
665+ self.assertEqual(message, [("create",
666+ {u"info.udi": u"wubble",
667+ u"info.product": u"Wubble",
668+ u"info.capabilities": [u"foo", u"bar"]}),
669+ ])
670+
671+ self.assertSchema(message)
672+
673+ self.hal_manager.devices[0] = MockRealHALDevice(
674+ {u"info.udi": u"wubble", u"info.product": u"Wubble",
675+ u"info.capabilities": [u"foo"]})
676+ message = self.plugin.create_message()
677+ self.plugin.persist_data(None)
678+ self.assertEqual(message, [("update", u"wubble",
679+ {}, {u"info.capabilities": [u"foo"]}, {}),
680+ ])
681+ self.assertSchema(message)
682+
683+ self.assertEqual(self.plugin.create_message(), [])
684+
685+ def test_update_complex(self):
686+ """
687+ The 'update' action reports property create, update and
688+ delete changes.
689+ """
690+ self.hal_manager.devices = [
691+ MockRealHALDevice({u"info.udi": u"wubble",
692+ u"info.product": u"Wubble",
693+ u"linux.acpi_type": 11})]
694+
695+ message = self.plugin.create_message()
696+ self.plugin.persist_data(None)
697+ self.assertEqual(message, [("create", {u"info.udi": u"wubble",
698+ u"info.product": u"Wubble",
699+ u"linux.acpi_type": 11})])
700+
701+ self.hal_manager.devices[0] = MockRealHALDevice(
702+ {u"info.udi": u"wubble", u"info.product": u"Ooga",
703+ u"info.category": u"unittest"})
704+ message = self.plugin.create_message()
705+ self.plugin.persist_data(None)
706+ self.assertEqual(message, [("update", u"wubble",
707+ {u"info.category": u"unittest"},
708+ {u"info.product": u"Ooga"},
709+ {u"linux.acpi_type": 11})])
710+ self.assertSchema(message)
711+
712+ self.assertEqual(self.plugin.create_message(), [])
713+
714+ def test_delete(self):
715+ """
716+ If a device that was previously reported is no longer present
717+ in a system a device entry should be created with a 'delete'
718+ action.
719+ """
720+ self.hal_manager.devices = [
721+ MockRealHALDevice({u"info.udi": u"wubble",
722+ u"info.product": u"Wubble"}),
723+ MockRealHALDevice({u"info.udi": u"ooga",
724+ u"info.product": u"Ooga"})]
725+
726+ message = self.plugin.create_message()
727+ self.plugin.persist_data(None)
728+ self.assertEqual(message, [("create", {u"info.udi": u"wubble",
729+ u"info.product": u"Wubble"}),
730+ ("create", {u"info.udi": u"ooga",
731+ u"info.product": u"Ooga"})])
732+ self.assertSchema(message)
733+
734+ self.hal_manager.devices.pop(1)
735+ message = self.plugin.create_message()
736+ self.plugin.persist_data(None)
737+ self.assertEqual(message, [("delete", u"ooga")])
738+ self.assertSchema(message)
739+ self.assertEqual(self.plugin.create_message(), [])
740+
741+ def test_minimal_delete(self):
742+ self.hal_manager.devices = [
743+ MockRealHALDevice({u"info.udi": u"wubble",
744+ u"block.device": u"/dev/scd",
745+ u"storage.removable": True}),
746+ MockRealHALDevice({u"info.udi": u"wubble0",
747+ u"block.device": u"/dev/scd0",
748+ u"info.parent": u"wubble"}),
749+ MockRealHALDevice({u"info.udi": u"wubble1",
750+ u"block.device": u"/dev/scd1",
751+ u"info.parent": u"wubble"}),
752+ MockRealHALDevice({u"info.udi": u"wubble2",
753+ u"block.device": u"/dev/scd1",
754+ u"info.parent": u"wubble0"}),
755+ MockRealHALDevice({u"info.udi": u"wubble3",
756+ u"block.device": u"/dev/scd1",
757+ u"info.parent": u"wubble2"})]
758+
759+ message = self.plugin.create_message()
760+ self.plugin.persist_data(None)
761+
762+ del self.hal_manager.devices[:]
763+
764+ message = self.plugin.create_message()
765+ self.plugin.persist_data(None)
766+
767+ self.assertEqual(message, [("delete", u"wubble")])
768+ self.assertEqual(self.plugin.create_message(), [])
769+
770+ def test_resynchronize(self):
771+ """
772+ If a 'resynchronize' reactor event is fired, the plugin should
773+ send a message that contains all data as if the server has
774+ none.
775+ """
776+ self.plugin.exchange()
777+ self.reactor.fire("resynchronize")
778+ self.plugin.exchange()
779+
780+ messages = self.mstore.get_pending_messages()
781+ self.assertEqual(len(messages), 2)
782+ self.assertEqual(messages[0]["devices"], messages[1]["devices"])
783+
784+ def test_call_on_accepted(self):
785+ remote_broker_mock = self.mocker.replace(self.remote)
786+ remote_broker_mock.send_message(ANY, urgent=True)
787+ self.mocker.result(succeed(None))
788+ self.mocker.replay()
789+
790+ self.reactor.fire(("message-type-acceptance-changed",
791+ "hardware-inventory"),
792+ True)
793+
794+ def test_no_message_if_not_accepted(self):
795+ """
796+ Don't add any messages at all if the broker isn't currently
797+ accepting their type.
798+ """
799+ self.mstore.set_accepted_types([])
800+ self.reactor.advance(self.monitor.step_size * 2)
801+ self.monitor.exchange()
802+
803+ self.mstore.set_accepted_types(["hardware-inventory"])
804+ self.assertMessages(list(self.mstore.get_pending_messages()), [])
805+
806+ def test_do_not_persist_changes_when_send_message_fails(self):
807+ """
808+ When the plugin is run it persists data that it uses on
809+ subsequent checks to calculate the delta to send. It should
810+ only persist data when the broker confirms that the message
811+ sent by the plugin has been sent.
812+ """
813+
814+ class MyException(Exception):
815+ pass
816+
817+ self.log_helper.ignore_errors(MyException)
818+
819+ broker_mock = self.mocker.replace(self.monitor.broker)
820+ broker_mock.send_message(ANY, urgent=ANY)
821+ self.mocker.result(fail(MyException()))
822+ self.mocker.replay()
823+
824+ message = self.plugin.create_message()
825+
826+ def assert_message(message_id):
827+ self.assertEqual(message, self.plugin.create_message())
828+
829+ result = self.plugin.exchange()
830+ result.addCallback(assert_message)
831+ return result
832
833=== modified file 'landscape/monitor/tests/test_mountinfo.py'
834--- landscape/monitor/tests/test_mountinfo.py 2011-12-01 13:38:58 +0000
835+++ landscape/monitor/tests/test_mountinfo.py 2012-03-06 09:20:24 +0000
836@@ -3,6 +3,7 @@
837 from twisted.internet.defer import succeed
838
839 from landscape.monitor.mountinfo import MountInfo
840+from landscape.tests.test_hal import MockHALManager, MockRealHALDevice
841 from landscape.tests.helpers import LandscapeTest, mock_counter, MonitorHelper
842 from landscape.tests.mocker import ANY
843
844@@ -21,6 +22,8 @@
845 self.log_helper.ignore_errors("Typelib file for namespace")
846
847 def get_mount_info(self, *args, **kwargs):
848+ hal_devices = kwargs.pop("hal_devices", [])
849+ kwargs["hal_manager"] = MockHALManager(hal_devices)
850 if "statvfs" not in kwargs:
851 kwargs["statvfs"] = lambda path: (0,) * 10
852 return MountInfo(*args, **kwargs)
853@@ -320,8 +323,51 @@
854 message = plugin.create_mount_info_message()
855 self.assertEqual(message, None)
856
857+ def test_ignore_removable_partitions(self):
858+ """
859+ Partitions on removable devices don't directly report
860+ storage.removable : True, but they do point to their parent and the
861+ parent will be marked removable if appropriate.
862+ """
863+ devices = [MockRealHALDevice({"info.udi": "wubble",
864+ "block.device": "/dev/scd",
865+ "storage.removable": True}),
866+ MockRealHALDevice({"info.udi": "wubble0",
867+ "block.device": "/dev/scd0",
868+ "info.parent": "wubble"})]
869+
870+ filename = self.makeFile("""\
871+/dev/scd0 /media/Xerox_M750 iso9660 ro,nosuid,nodev,uid=1000,utf8 0 0
872+""")
873+ plugin = self.get_mount_info(mounts_file=filename, hal_devices=devices,
874+ mtab_file=filename)
875+ self.monitor.add(plugin)
876+ plugin.run()
877+
878+ message = plugin.create_mount_info_message()
879+ self.assertEqual(message, None)
880+
881 def test_ignore_removable_devices(self):
882 """
883+ The mount info plugin should only report data about
884+ non-removable devices.
885+ """
886+ devices = [MockRealHALDevice({"info.udi": "wubble",
887+ "block.device": "/dev/scd0",
888+ "storage.removable": True})]
889+ filename = self.makeFile("""\
890+/dev/scd0 /media/Xerox_M750 iso9660 ro,nosuid,nodev,uid=1000,utf8 0 0
891+""")
892+ plugin = self.get_mount_info(mounts_file=filename, hal_devices=devices,
893+ mtab_file=filename)
894+ self.monitor.add(plugin)
895+ plugin.run()
896+
897+ message = plugin.create_mount_info_message()
898+ self.assertEqual(message, None)
899+
900+ def test_ignore_removable_devices_gudev(self):
901+ """
902 The mount info plugin uses gudev to retrieve removable information
903 about devices.
904 """
905@@ -330,6 +376,7 @@
906 """)
907 plugin = self.get_mount_info(mounts_file=filename,
908 mtab_file=filename)
909+ plugin._hal_manager = None
910
911 class MockDevice(object):
912 def get_sysfs_attr_as_boolean(self, attr):
913@@ -348,6 +395,35 @@
914 message = plugin.create_mount_info_message()
915 self.assertEqual(message, None)
916
917+ def test_ignore_multiparented_removable_devices(self):
918+ """
919+ Some removable devices might be the grand-children of a device that is
920+ marked as "storage.removable".
921+ """
922+ devices = [MockRealHALDevice({"info.udi": "wubble",
923+ "block.device": "/dev/scd",
924+ "storage.removable": True}),
925+ MockRealHALDevice({"info.udi": "wubble0",
926+ "block.device": "/dev/scd0",
927+ "info.parent": "wubble"}),
928+ MockRealHALDevice({"info.udi": "wubble0a",
929+ "block.device": "/dev/scd0a",
930+ "info.parent": "wubble0"}),
931+ MockRealHALDevice({"info.udi": "wubble0b",
932+ "block.device": "/dev/scd0b",
933+ "info.parent": "wubble0"})]
934+
935+ filename = self.makeFile("""\
936+/dev/scd0a /media/Xerox_M750 iso9660 ro,nosuid,nodev,uid=1000,utf8 0 0
937+""")
938+ plugin = self.get_mount_info(mounts_file=filename, hal_devices=devices,
939+ mtab_file=filename)
940+ self.monitor.add(plugin)
941+ plugin.run()
942+
943+ message = plugin.create_mount_info_message()
944+ self.assertEqual(message, None)
945+
946 def test_sample_free_space(self):
947 """Test collecting information about free space."""
948 def statvfs(path, multiplier=mock_counter(1).next):
949
950=== modified file 'landscape/monitor/tests/test_service.py'
951--- landscape/monitor/tests/test_service.py 2011-12-02 08:35:25 +0000
952+++ landscape/monitor/tests/test_service.py 2012-03-06 09:20:24 +0000
953@@ -1,3 +1,4 @@
954+from landscape.tests.mocker import ANY
955 from landscape.tests.helpers import LandscapeTest, FakeBrokerServiceHelper
956 from landscape.reactor import FakeReactor
957 from landscape.monitor.config import MonitorConfiguration, ALL_PLUGINS
958@@ -45,10 +46,15 @@
959 starts the plugins and register the monitor as broker client. It also
960 start listening on its own socket for incoming connections.
961 """
962+ # FIXME: don't actually run the real register method, because at the
963+ # moment the UserMonitor plugin still depends on DBus. We can probably
964+ # drop this mocking once the AMP migration is completed.
965+ for plugin in self.service.plugins:
966+ plugin.register = self.mocker.mock()
967+ plugin.register(ANY)
968+ self.mocker.replay()
969+
970 def stop_service(ignored):
971- for plugin in self.service.plugins:
972- if getattr(plugin, "stop", None) is not None:
973- plugin.stop()
974 [connector] = self.broker_service.broker.get_connectors()
975 connector.disconnect()
976 self.service.stopService()
977
978=== modified file 'landscape/package/releaseupgrader.py'
979--- landscape/package/releaseupgrader.py 2011-12-01 14:01:29 +0000
980+++ landscape/package/releaseupgrader.py 2012-03-06 09:20:24 +0000
981@@ -188,11 +188,32 @@
982 config.add_section("NonInteractive")
983 config.set("NonInteractive", "ForceOverwrite", "no")
984
985+ # Workaround for Bug #174148, which prevents dbus from restarting
986+ # after a dapper->hardy upgrade
987+ if not config.has_section("Distro"):
988+ config.add_section("Distro")
989+ if not config.has_option("Distro", "PostInstallScripts"):
990+ config.set("Distro", "PostInstallScripts", "./dbus.sh")
991+ else:
992+ scripts = config.get("Distro", "PostInstallScripts")
993+ scripts += ", ./dbus.sh"
994+ config.set("Distro", "PostInstallScripts", scripts)
995+
996 # Write config changes to disk
997 fd = open(config_filename, "w")
998 config.write(fd)
999 fd.close()
1000
1001+ # Generate the post-install script that starts DBus
1002+ dbus_sh_filename = os.path.join(upgrade_tool_directory,
1003+ "dbus.sh")
1004+ fd = open(dbus_sh_filename, "w")
1005+ fd.write("#!/bin/sh\n"
1006+ "/etc/init.d/dbus start\n"
1007+ "sleep 10\n")
1008+ fd.close()
1009+ os.chmod(dbus_sh_filename, 0755)
1010+
1011 # On some releases the upgrade-tool doesn't support the allow third
1012 # party environment variable, so this trick is needed to make it
1013 # possible to upgrade against testing client packages from the
1014
1015=== modified file 'landscape/package/tests/test_releaseupgrader.py'
1016--- landscape/package/tests/test_releaseupgrader.py 2011-12-01 14:01:29 +0000
1017+++ landscape/package/tests/test_releaseupgrader.py 2012-03-06 09:20:24 +0000
1018@@ -245,6 +245,60 @@
1019 result.addCallback(check_result)
1020 return result
1021
1022+ def test_tweak_sets_dbus_start_script(self):
1023+ """
1024+ The L{ReleaseUpgrader.tweak} method adds to the upgrade-tool
1025+ configuration a little script that starts dbus after the upgrade.
1026+ """
1027+ config_filename = os.path.join(self.config.upgrade_tool_directory,
1028+ "DistUpgrade.cfg.dapper")
1029+ self.makeFile(path=config_filename,
1030+ content="[Distro]\n"
1031+ "PostInstallScripts=/foo.sh\n")
1032+
1033+ def check_result(ignored):
1034+ config = ConfigParser.ConfigParser()
1035+ config.read(config_filename)
1036+ self.assertEqual(config.get("Distro", "PostInstallScripts"),
1037+ "/foo.sh, ./dbus.sh")
1038+ dbus_sh = os.path.join(self.config.upgrade_tool_directory,
1039+ "dbus.sh")
1040+ self.assertFileContent(dbus_sh,
1041+ "#!/bin/sh\n"
1042+ "/etc/init.d/dbus start\n"
1043+ "sleep 10\n")
1044+
1045+ result = self.upgrader.tweak("dapper")
1046+ result.addCallback(check_result)
1047+ return result
1048+
1049+ def test_tweak_sets_dbus_start_script_with_no_post_install_scripts(self):
1050+ """
1051+ The L{ReleaseUpgrader.tweak} method adds to the upgrade-tool
1052+ configuration a little script that starts dbus after the upgrade. This
1053+ works even when the config file doesn't have a PostInstallScripts entry
1054+ yet.
1055+ """
1056+ config_filename = os.path.join(self.config.upgrade_tool_directory,
1057+ "DistUpgrade.cfg.dapper")
1058+ self.makeFile(path=config_filename, content="")
1059+
1060+ def check_result(ignored):
1061+ config = ConfigParser.ConfigParser()
1062+ config.read(config_filename)
1063+ self.assertEqual(config.get("Distro", "PostInstallScripts"),
1064+ "./dbus.sh")
1065+ dbus_sh = os.path.join(self.config.upgrade_tool_directory,
1066+ "dbus.sh")
1067+ self.assertFileContent(dbus_sh,
1068+ "#!/bin/sh\n"
1069+ "/etc/init.d/dbus start\n"
1070+ "sleep 10\n")
1071+
1072+ result = self.upgrader.tweak("dapper")
1073+ result.addCallback(check_result)
1074+ return result
1075+
1076 def test_default_logs_directory(self):
1077 """
1078 The default directory for the upgrade-tool logs is the system one.
1079
1080=== modified file 'landscape/reactor.py'
1081--- landscape/reactor.py 2012-02-03 18:10:57 +0000
1082+++ landscape/reactor.py 2012-03-06 09:20:24 +0000
1083@@ -16,6 +16,10 @@
1084 """Raised when an invalid ID is used with reactor.cancel_call()."""
1085
1086
1087+class CallHookError(Exception):
1088+ """Raised when hooking on a reactor incorrectly."""
1089+
1090+
1091 class EventID(object):
1092 """Unique identifier for an event handler.
1093
1094@@ -155,6 +159,13 @@
1095 except Exception, e:
1096 logging.exception(e)
1097
1098+ def _hook_threaded_callbacks(self):
1099+ id = self.call_every(0.5, self._run_threaded_callbacks)
1100+ self._run_threaded_callbacks_id = id
1101+
1102+ def _unhook_threaded_callbacks(self):
1103+ self.cancel_call(self._run_threaded_callbacks_id)
1104+
1105
1106 class UnixReactorMixin(object):
1107
1108
1109=== modified file 'landscape/service.py'
1110--- landscape/service.py 2012-02-03 10:06:28 +0000
1111+++ landscape/service.py 2012-03-06 09:20:24 +0000
1112@@ -29,6 +29,12 @@
1113
1114 def __init__(self, config):
1115 self.config = config
1116+ try:
1117+ from landscape.lib import bpickle_dbus
1118+ except ImportError:
1119+ pass
1120+ else:
1121+ bpickle_dbus.install()
1122 self.reactor = self.reactor_factory()
1123 if self.persist_filename:
1124 self.persist = get_versioned_persist(self)
1125
1126=== modified file 'landscape/tests/test_configuration.py'
1127--- landscape/tests/test_configuration.py 2012-02-29 22:13:01 +0000
1128+++ landscape/tests/test_configuration.py 2012-03-06 09:20:24 +0000
1129@@ -1883,7 +1883,7 @@
1130
1131 def test_register_bus_connection_failure_ok_no_register(self):
1132 """
1133- Exit code 0 will be returned if we can't contact Landscape and
1134+ Exit code 0 will be returned if we can't contact Landscape via DBus and
1135 --ok-no-register was passed.
1136 """
1137 print_text_mock = self.mocker.replace(print_text)
1138
1139=== added file 'landscape/tests/test_hal.py'
1140--- landscape/tests/test_hal.py 1970-01-01 00:00:00 +0000
1141+++ landscape/tests/test_hal.py 2012-03-06 09:20:24 +0000
1142@@ -0,0 +1,87 @@
1143+from dbus import SystemBus, Interface
1144+from dbus.exceptions import DBusException
1145+
1146+from landscape.hal import HALDevice, HALManager
1147+from landscape.tests.helpers import LandscapeTest
1148+
1149+
1150+class HALManagerTest(LandscapeTest):
1151+
1152+ def setUp(self):
1153+ super(HALManagerTest, self).setUp()
1154+ self.bus = SystemBus()
1155+
1156+ def test_get_devices(self):
1157+ """
1158+ A HALManager can return a flat list of devices. All available
1159+ devices should be included in the returned list.
1160+ """
1161+ devices = HALManager().get_devices()
1162+ manager = self.bus.get_object("org.freedesktop.Hal",
1163+ "/org/freedesktop/Hal/Manager")
1164+ manager = Interface(manager, "org.freedesktop.Hal.Manager")
1165+ expected_devices = manager.GetAllDevices()
1166+ actual_devices = [device.udi for device in devices]
1167+ self.assertEqual(set(expected_devices), set(actual_devices))
1168+
1169+ def test_get_devices_with_dbus_error(self):
1170+ """
1171+ If the L{HALManager} fails connecting to HAL over D-Bus, then the
1172+ L{HALManager.get_devices} method returns an empty list.
1173+ """
1174+ self.log_helper.ignore_errors("Couldn't connect to Hal via DBus")
1175+ bus = self.mocker.mock()
1176+ bus.get_object("org.freedesktop.Hal", "/org/freedesktop/Hal/Manager")
1177+ self.mocker.throw(DBusException())
1178+ self.mocker.replay()
1179+ devices = HALManager(bus=bus).get_devices()
1180+ self.assertEqual(devices, [])
1181+
1182+ def test_get_devices_with_no_server(self):
1183+ """
1184+ If the L{HALManager} fails connecting to HAL over D-Bus, for example
1185+ because the DBus server is not running at all, then the
1186+ L{HALManager.get_devices} method returns an empty list.
1187+ """
1188+ self.log_helper.ignore_errors("Couldn't connect to Hal via DBus")
1189+ bus_mock = self.mocker.replace("dbus.SystemBus")
1190+ bus_mock()
1191+ self.mocker.throw(DBusException())
1192+ self.mocker.replay()
1193+ devices = HALManager().get_devices()
1194+ self.assertEqual(devices, [])
1195+
1196+
1197+class MockHALManager(object):
1198+
1199+ def __init__(self, devices):
1200+ self.devices = devices
1201+
1202+ def get_devices(self):
1203+ return [HALDevice(device) for device in self.devices]
1204+
1205+
1206+class MockRealHALDevice(object):
1207+
1208+ def __init__(self, properties):
1209+ self._properties = properties
1210+ self.udi = properties.get("info.udi", "fake_udi")
1211+
1212+ def GetAllProperties(self):
1213+ return self._properties
1214+
1215+
1216+class HALDeviceTest(LandscapeTest):
1217+
1218+ def test_init(self):
1219+ device = HALDevice(MockRealHALDevice({"info.udi": "wubble"}))
1220+ self.assertEqual(device.properties, {"info.udi": "wubble"})
1221+ self.assertEqual(device.udi, "wubble")
1222+ self.assertEqual(device.parent, None)
1223+
1224+ def test_add_child(self):
1225+ parent = HALDevice(MockRealHALDevice({"info.udi": "wubble"}))
1226+ child = HALDevice(MockRealHALDevice({"info.udi": "ooga"}))
1227+ parent.add_child(child)
1228+ self.assertEqual(parent.get_children(), [child])
1229+ self.assertEqual(child.parent, parent)
1230
1231=== modified file 'landscape/tests/test_service.py'
1232--- landscape/tests/test_service.py 2011-12-01 13:38:58 +0000
1233+++ landscape/tests/test_service.py 2012-03-06 09:20:24 +0000
1234@@ -56,6 +56,15 @@
1235 service = TestService(self.config)
1236 self.assertFalse(hasattr(service, "persist"))
1237
1238+ def test_install_bpickle_dbus(self):
1239+ """
1240+ A L{LandscapeService} installs the DBus extensions of bpickle.
1241+ """
1242+ dbus_mock = self.mocker.replace("landscape.lib.bpickle_dbus.install")
1243+ dbus_mock()
1244+ self.mocker.replay()
1245+ TestService(self.config)
1246+
1247 def test_usr1_rotates_logs(self):
1248 """
1249 SIGUSR1 should cause logs to be reopened.
1250
1251=== modified file 'landscape/tests/test_textmessage.py'
1252--- landscape/tests/test_textmessage.py 2011-12-01 14:01:29 +0000
1253+++ landscape/tests/test_textmessage.py 2012-03-06 09:20:24 +0000
1254@@ -13,8 +13,8 @@
1255
1256 def test_send_message(self):
1257 """
1258- L{send_message} should send a message of type C{text-message} to the
1259- landscape messaging service.
1260+ L{send_message} should send a message of type
1261+ C{text-message} to the landscape dbus messaging service.
1262 """
1263 service = self.broker_service
1264 service.message_store.set_accepted_types(["text-message"])
1265
1266=== modified file 'landscape/textmessage.py'
1267--- landscape/textmessage.py 2011-12-01 14:01:29 +0000
1268+++ landscape/textmessage.py 2012-03-06 09:20:24 +0000
1269@@ -1,6 +1,7 @@
1270 """
1271 Support code for the C{landscape-message} utility, which sends a text
1272-message to the Landscape web UI via the landscape-client's messaging service.
1273+message to the Landscape web UI via the landscape-client's dbus
1274+messaging service (see L{landscape.plugins.dbus_message}).
1275 """
1276
1277 import sys
1278
1279=== modified file 'landscape/watchdog.py'
1280--- landscape/watchdog.py 2012-02-27 20:49:50 +0000
1281+++ landscape/watchdog.py 2012-03-06 09:20:24 +0000
1282@@ -62,6 +62,8 @@
1283 @cvar program: The name of the executable program that will start this
1284 daemon.
1285 @cvar username: The name of the user to switch to, by default.
1286+ @cvar service: The DBus service name that the program will be expected to
1287+ listen on.
1288 @cvar max_retries: The maximum number of retries before giving up when
1289 trying to connect to the watched daemon.
1290 @cvar factor: The factor by which the delay between subsequent connection
1291@@ -176,7 +178,8 @@
1292
1293 def is_running(self):
1294 # FIXME Error cases may not be handled in the best possible way
1295- # here. We're basically return False if any error happens.
1296+ # here. We're basically return False if any error happens from the
1297+ # dbus ping.
1298 return self._connect_and_call("ping")
1299
1300 def wait(self):
1301@@ -364,7 +367,7 @@
1302 def start(self):
1303 """
1304 Start all daemons. The broker will be started first, and no other
1305- daemons will be started before it is running and responding to
1306+ daemons will be started before it is running and responding to DBUS
1307 messages.
1308
1309 @return: A deferred which fires when all services have successfully
1310
1311=== modified file 'man/landscape-client.1'
1312--- man/landscape-client.1 2011-12-02 09:52:48 +0000
1313+++ man/landscape-client.1 2012-03-06 09:20:24 +0000
1314@@ -1,5 +1,5 @@
1315 .\"Text automatically generated by txt2man
1316-.TH landscape-client 1 "02 December 2011" "" ""
1317+.TH landscape-client "18 January 2010" "" ""
1318 .SH NAME
1319 \fBlandscape-client \fP- Landscape system client
1320 \fB
1321@@ -35,6 +35,11 @@
1322 \fIoptions\fP override settings from the file).
1323 .TP
1324 .B
1325+\fB--bus\fP=BUS
1326+Which DBUS bus to use. One of 'session' or
1327+'system'.
1328+.TP
1329+.B
1330 \fB-d\fP PATH, \fB--data-path\fP=PATH
1331 The directory to store data files in.
1332 .TP
1333
1334=== modified file 'man/landscape-client.txt'
1335--- man/landscape-client.txt 2011-12-01 14:01:29 +0000
1336+++ man/landscape-client.txt 2012-03-06 09:20:24 +0000
1337@@ -17,6 +17,8 @@
1338 -h, --help Show this help message and exit.
1339 -c FILE, --config=FILE Use config from this file (any command line
1340 options override settings from the file).
1341+ --bus=BUS Which DBUS bus to use. One of 'session' or
1342+ 'system'.
1343 -d PATH, --data-path=PATH The directory to store data files in.
1344 -q, --quiet Do not log to the standard output.
1345 -l FILE, --log-dir=FILE The directory to write log files to.
1346
1347=== modified file 'man/landscape-config.1'
1348--- man/landscape-config.1 2011-12-02 09:52:48 +0000
1349+++ man/landscape-config.1 2012-03-06 09:20:24 +0000
1350@@ -1,5 +1,5 @@
1351 .\"Text automatically generated by txt2man
1352-.TH landscape-config 1 "02 December 2011" "" ""
1353+.TH landscape-config "18 January 2010" "" ""
1354 .SH NAME
1355 \fBlandscape-config \fP- configure the Landscape management client
1356 \fB
1357@@ -46,6 +46,11 @@
1358 '/etc/landscape/client.conf').
1359 .TP
1360 .B
1361+\fB--bus\fP=BUS
1362+Which DBUS bus to use. One of 'session' or 'system'
1363+(default: 'system').
1364+.TP
1365+.B
1366 \fB-d\fP PATH, \fB--data-path\fP=PATH
1367 The directory to store data files in (default:
1368 '/var/lib/landscape/client').
1369
1370=== modified file 'man/landscape-config.txt'
1371--- man/landscape-config.txt 2011-12-01 14:01:29 +0000
1372+++ man/landscape-config.txt 2012-03-06 09:20:24 +0000
1373@@ -28,6 +28,8 @@
1374 -c FILE, --config=FILE Use config from this file (any command line options
1375 override settings from the file) (default:
1376 '/etc/landscape/client.conf').
1377+ --bus=BUS Which DBUS bus to use. One of 'session' or 'system'
1378+ (default: 'system').
1379 -d PATH, --data-path=PATH The directory to store data files in (default:
1380 '/var/lib/landscape/client').
1381 -q, --quiet Do not log to the standard output.
1382
1383=== modified file 'man/landscape-message.1'
1384--- man/landscape-message.1 2011-12-02 09:52:48 +0000
1385+++ man/landscape-message.1 2012-03-06 09:20:24 +0000
1386@@ -1,5 +1,5 @@
1387 .\"Text automatically generated by txt2man
1388-.TH landscape-message 1 "02 December 2011" "" ""
1389+.TH landscape-message "18 January 2010" "" ""
1390 .SH NAME
1391 \fBlandscape-message \fP- Send a message to the landscape web interface
1392 \fB
1393@@ -31,6 +31,10 @@
1394 .B
1395 \fB-h\fP, \fB--help\fP
1396 Show this help message and exit.
1397+.TP
1398+.B
1399+\fB-b\fP BUS, \fB--bus\fP=BUS
1400+The DBUS bus to use to send the message.
1401 .SH EXAMPLES
1402
1403 \fBlandscape-message\fP Hello administrator
1404
1405=== modified file 'man/landscape-message.txt'
1406--- man/landscape-message.txt 2011-12-01 14:01:29 +0000
1407+++ man/landscape-message.txt 2012-03-06 09:20:24 +0000
1408@@ -16,6 +16,7 @@
1409 OPTIONS
1410 --version Show program's version number and exit.
1411 -h, --help Show this help message and exit.
1412+ -b BUS, --bus=BUS The DBUS bus to use to send the message.
1413
1414 EXAMPLES
1415
1416
1417=== added file 'scripts/landscape-dbus-proxy'
1418--- scripts/landscape-dbus-proxy 1970-01-01 00:00:00 +0000
1419+++ scripts/landscape-dbus-proxy 2012-03-06 09:20:24 +0000
1420@@ -0,0 +1,82 @@
1421+#!/usr/bin/env python
1422+
1423+import os
1424+import dbus
1425+import dbus.service
1426+import dbus.glib # This as side effects, don't remove it!
1427+
1428+from dbus.service import Object, BusName, method
1429+
1430+from twisted.internet import glib2reactor
1431+glib2reactor.install()
1432+from twisted.internet import reactor
1433+
1434+from landscape.lib.bpickle import loads
1435+from landscape.lib.lock import lock_path, LockError
1436+from landscape.reactor import TwistedReactor
1437+from landscape.deployment import Configuration
1438+from landscape.broker.amp import RemoteBrokerConnector
1439+
1440+
1441+BUS_NAME = "com.canonical.landscape.Broker"
1442+OBJECT_PATH = "/com/canonical/landscape/Broker"
1443+
1444+
1445+def array_to_string(array):
1446+ """Convert an L{Array} of L{Byte}s (or integers) to a Python str."""
1447+ result = []
1448+ for item in array:
1449+ if item < 0:
1450+ item = item + 256
1451+ result.append(chr(item))
1452+ return "".join(result)
1453+
1454+
1455+class BrokerDBusObject(Object):
1456+ """A DBus-published object proxying L{RemoteBroker.send_message}.
1457+
1458+ It is used when upgrading from a DBus-based version of the Landscape client
1459+ to the newer AMP-based one, for letting the old package-changer process
1460+ performing the upgrade communicate with the new version of the client.
1461+ """
1462+
1463+ bus_name = BUS_NAME
1464+ object_path = OBJECT_PATH
1465+
1466+ def __init__(self, config):
1467+ super(BrokerDBusObject, self).__init__(BusName(
1468+ self.bus_name, dbus.SystemBus()), object_path=self.object_path)
1469+ self.config = config
1470+
1471+ @method(BUS_NAME)
1472+ def send_message(self, message, urgent=True):
1473+ """Queue the given message in the message exchange."""
1474+ message = loads(array_to_string(message))
1475+
1476+ def cb_connected(broker):
1477+ result = broker.send_message(message, urgent=True)
1478+ return result.addCallback(cb_done)
1479+
1480+ def cb_done(ignored):
1481+ return reactor.stop()
1482+
1483+ twisted_reactor = TwistedReactor()
1484+ connector = RemoteBrokerConnector(twisted_reactor, self.config)
1485+ connected = connector.connect()
1486+ connected.addCallback(cb_connected)
1487+
1488+
1489+if __name__ == "__main__":
1490+ config = Configuration()
1491+ lock_dir = os.path.join(config.data_path, "package")
1492+ if os.path.isdir(lock_dir):
1493+ lock_filename = os.path.join(lock_dir, "changer.lock")
1494+ try:
1495+ lock_path(lock_filename)
1496+ except LockError:
1497+ # The package-changer is running, this means that we're upgrading from
1498+ # a non-AMP version and that the upgrade is Landscape driven, so let's
1499+ # expose the DBus broker proxy to give a chance to the package-changer
1500+ # to send its result message.
1501+ remote = BrokerDBusObject(config)
1502+ reactor.run()
1503
1504=== modified file 'setup.py'
1505--- setup.py 2012-03-05 15:22:36 +0000
1506+++ setup.py 2012-03-06 09:20:24 +0000
1507@@ -30,7 +30,8 @@
1508 "scripts/landscape-package-reporter",
1509 "scripts/landscape-release-upgrader",
1510 "scripts/landscape-sysinfo",
1511- "scripts/landscape-is-cloud-managed"],
1512+ "scripts/landscape-is-cloud-managed",
1513+ "scripts/landscape-dbus-proxy"],
1514 ext_modules=[Extension("landscape.lib.initgroups",
1515 ["landscape/lib/initgroups.c"])]
1516 )

Subscribers

People subscribed via source and target branches

to all changes: