Merge lp:~therve/landscape-client/restore-hardware-monitor into lp:~landscape/landscape-client/trunk
- restore-hardware-monitor
- Merge into 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 | ||||
Related bugs: |
|
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Geoff Teale (community) | Approve | ||
Fernando Correa Neto (community) | Approve | ||
Review via email:
|
Commit message
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
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
Geoff Teale (tealeg) wrote : | # |
+1 Good for me.
One small thing.
+ logging.
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 | ) |
Everything went fine on my system (Oneiric)
PASSED (successes=2298)
+1!