Merge lp:~bjornt/landscape-client/add-remove-dpkg-holds into lp:~landscape/landscape-client/trunk
- add-remove-dpkg-holds
- Merge into trunk
Status: | Merged | ||||
---|---|---|---|---|---|
Approved by: | Geoff Teale | ||||
Approved revision: | 447 | ||||
Merged at revision: | 435 | ||||
Proposed branch: | lp:~bjornt/landscape-client/add-remove-dpkg-holds | ||||
Merge into: | lp:~landscape/landscape-client/trunk | ||||
Diff against target: |
438 lines (+314/-3) 6 files modified
landscape/message_schemas.py (+6/-0) landscape/package/changer.py (+55/-1) landscape/package/facade.py (+41/-0) landscape/package/tests/helpers.py (+0/-1) landscape/package/tests/test_changer.py (+123/-1) landscape/package/tests/test_facade.py (+89/-0) |
||||
To merge this branch: | bzr merge lp:~bjornt/landscape-client/add-remove-dpkg-holds | ||||
Related bugs: |
|
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Geoff Teale (community) | Approve | ||
Alberto Donato (community) | Approve | ||
Review via email: mp+89253@code.launchpad.net |
Commit message
Description of the change
Add a message for adding and removing package holds, handling the
message in the package changer.
Geoff Teale (tealeg) wrote : | # |
+1 Looks good. Please fix:
landscape/
- 448. By Björn Tillenius
-
Typo.
- 449. By Björn Tillenius
-
Lint.
Björn Tillenius (bjornt) wrote : | # |
On Thu, Jan 19, 2012 at 04:06:23PM -0000, Alberto Donato wrote:
> Review: Approve
>
> Looks good! +1
>
> #1:
> + """Log that a package holds result is sent and sent the response."""
>
> typo, "and send"
Thanks, fixed.
--
Björn Tillenius | https:/
Björn Tillenius (bjornt) wrote : | # |
On Fri, Jan 20, 2012 at 10:43:24AM -0000, Geoff Teale wrote:
> Review: Approve
>
> +1 Looks good. Please fix:
>
> landscape/
Oops, missed that. Fixed.
--
Björn Tillenius | https:/
Preview Diff
1 | === modified file 'landscape/message_schemas.py' |
2 | --- landscape/message_schemas.py 2012-01-11 13:37:17 +0000 |
3 | +++ landscape/message_schemas.py 2012-01-20 12:58:25 +0000 |
4 | @@ -294,6 +294,12 @@ |
5 | "deleted": package_locks}, |
6 | optional=["created", "deleted"]) |
7 | |
8 | +CHANGE_PACKAGE_HOLDS = Message( |
9 | + "change-package-holds", |
10 | + {"created": List(utf8), |
11 | + "deleted": List(utf8)}, |
12 | + optional=["created", "deleted"]) |
13 | + |
14 | CHANGE_PACKAGES_RESULT = Message( |
15 | "change-packages-result", |
16 | {"operation-id": Int(), |
17 | |
18 | === modified file 'landscape/package/changer.py' |
19 | --- landscape/package/changer.py 2012-01-13 14:01:43 +0000 |
20 | +++ landscape/package/changer.py 2012-01-20 12:58:25 +0000 |
21 | @@ -18,7 +18,7 @@ |
22 | from landscape.package.taskhandler import ( |
23 | PackageTaskHandler, PackageTaskHandlerConfiguration, PackageTaskError, |
24 | run_task_handler) |
25 | -from landscape.manager.manager import SUCCEEDED |
26 | +from landscape.manager.manager import FAILED, SUCCEEDED |
27 | |
28 | |
29 | class UnknownPackageData(Exception): |
30 | @@ -103,6 +103,8 @@ |
31 | return result.addErrback(self.unknown_package_data_error, task) |
32 | if message["type"] == "change-package-locks": |
33 | return self.handle_change_package_locks(message) |
34 | + if message["type"] == "change-package-holds": |
35 | + return self.handle_change_package_holds(message) |
36 | |
37 | def unknown_package_data_error(self, failure, task): |
38 | """Handle L{UnknownPackageData} data errors. |
39 | @@ -296,6 +298,58 @@ |
40 | "exchange urgently.") |
41 | return self._broker.send_message(response, True) |
42 | |
43 | + def _send_change_package_holds_response(self, response): |
44 | + """Log that a package holds result is sent and send the response.""" |
45 | + logging.info("Queuing message with change package holds results to " |
46 | + "exchange urgently.") |
47 | + return self._broker.send_message(response, True) |
48 | + |
49 | + def handle_change_package_holds(self, message): |
50 | + """Handle a C{change-package-holds} message. |
51 | + |
52 | + Create and delete package holds as requested by the given C{message}. |
53 | + """ |
54 | + if not self._facade.supports_package_holds: |
55 | + response = { |
56 | + "type": "operation-result", |
57 | + "operation-id": message.get("operation-id"), |
58 | + "status": FAILED, |
59 | + "result-text": "This client doesn't support package holds.", |
60 | + "result-code": 1} |
61 | + return self._send_change_package_holds_response(response) |
62 | + |
63 | + not_installed = set() |
64 | + holds_to_create = message.get("create", []) |
65 | + for name in holds_to_create: |
66 | + versions = self._facade.get_packages_by_name(name) |
67 | + if not versions or not versions[0].package.installed: |
68 | + not_installed.add(name) |
69 | + if not_installed: |
70 | + response = { |
71 | + "type": "operation-result", |
72 | + "operation-id": message.get("operation-id"), |
73 | + "status": FAILED, |
74 | + "result-text": "Package holds not added, since the following" + |
75 | + " packages are not installed: %s" % ( |
76 | + ", ".join(sorted(not_installed))), |
77 | + "result-code": 1} |
78 | + return self._send_change_package_holds_response(response) |
79 | + |
80 | + for hold in holds_to_create: |
81 | + self._facade.set_package_hold(hold) |
82 | + self._facade.reload_channels() |
83 | + for hold in message.get("delete", []): |
84 | + self._facade.remove_package_hold(hold) |
85 | + self._facade.reload_channels() |
86 | + |
87 | + response = {"type": "operation-result", |
88 | + "operation-id": message.get("operation-id"), |
89 | + "status": SUCCEEDED, |
90 | + "result-text": "Package holds successfully changed.", |
91 | + "result-code": 0} |
92 | + |
93 | + return self._send_change_package_holds_response(response) |
94 | + |
95 | @staticmethod |
96 | def find_command(): |
97 | return find_changer_command() |
98 | |
99 | === modified file 'landscape/package/facade.py' |
100 | --- landscape/package/facade.py 2012-01-16 15:08:15 +0000 |
101 | +++ landscape/package/facade.py 2012-01-20 12:58:25 +0000 |
102 | @@ -1,5 +1,6 @@ |
103 | import hashlib |
104 | import os |
105 | +import subprocess |
106 | import tempfile |
107 | from cStringIO import StringIO |
108 | |
109 | @@ -79,10 +80,14 @@ |
110 | database. |
111 | """ |
112 | |
113 | + supports_package_holds = True |
114 | + |
115 | def __init__(self, root=None): |
116 | self._root = root |
117 | + self._dpkg_args = [] |
118 | if self._root is not None: |
119 | self._ensure_dir_structure() |
120 | + self._dpkg_args.extend(["--root", self._root]) |
121 | # don't use memonly=True here because of a python-apt bug on Natty when |
122 | # sources.list contains invalid lines (LP: #886208) |
123 | self._cache = apt.cache.Cache(rootdir=root) |
124 | @@ -100,6 +105,10 @@ |
125 | self._ensure_sub_dir("var/cache/apt/archives/partial") |
126 | self._ensure_sub_dir("var/lib/apt/lists/partial") |
127 | dpkg_dir = self._ensure_sub_dir("var/lib/dpkg") |
128 | + self._ensure_sub_dir("var/lib/dpkg/info") |
129 | + self._ensure_sub_dir("var/lib/dpkg/updates") |
130 | + self._ensure_sub_dir("var/lib/dpkg/triggers") |
131 | + create_file(os.path.join(dpkg_dir, "available"), "") |
132 | self._dpkg_status = os.path.join(dpkg_dir, "status") |
133 | if not os.path.exists(self._dpkg_status): |
134 | create_file(self._dpkg_status, "") |
135 | @@ -157,6 +166,37 @@ |
136 | """ |
137 | return [] |
138 | |
139 | + def get_package_holds(self): |
140 | + """Return the name of all the packages that are on hold.""" |
141 | + return [version.package.name for version in self.get_locked_packages()] |
142 | + |
143 | + def _set_dpkg_selections(self, selection): |
144 | + """Set the dpkg selection. |
145 | + |
146 | + It basically does "echo $selection | dpkg --set-selections". |
147 | + """ |
148 | + process = subprocess.Popen( |
149 | + ["dpkg", "--set-selections"] + self._dpkg_args, |
150 | + stdin=subprocess.PIPE) |
151 | + process.communicate(selection) |
152 | + |
153 | + def set_package_hold(self, name): |
154 | + """Add a dpkg hold for a package. |
155 | + |
156 | + @param name: The name of the package to hold. |
157 | + """ |
158 | + self._set_dpkg_selections(name + " hold") |
159 | + |
160 | + def remove_package_hold(self, name): |
161 | + """Removes a dpkg hold for a package. |
162 | + |
163 | + @param name: The name of the package to unhold. |
164 | + """ |
165 | + versions = self.get_packages_by_name(name) |
166 | + if not versions or not self._is_package_held(versions[0].package): |
167 | + return |
168 | + self._set_dpkg_selections(name + " install") |
169 | + |
170 | def reload_channels(self): |
171 | """Reload the channels and update the cache.""" |
172 | self._cache.open(None) |
173 | @@ -469,6 +509,7 @@ |
174 | """ |
175 | |
176 | _deb_package_type = None |
177 | + supports_package_holds = False |
178 | |
179 | def __init__(self, smart_init_kwargs={}, sysconf_args=None): |
180 | self._smart_init_kwargs = smart_init_kwargs.copy() |
181 | |
182 | === modified file 'landscape/package/tests/helpers.py' |
183 | --- landscape/package/tests/helpers.py 2011-12-14 10:41:04 +0000 |
184 | +++ landscape/package/tests/helpers.py 2012-01-20 12:58:25 +0000 |
185 | @@ -40,7 +40,6 @@ |
186 | Architecture: %(architecture)s |
187 | Source: source |
188 | Version: %(version)s |
189 | - Config-Version: 1.0 |
190 | Description: description |
191 | """ % {"name": name, "version": version, |
192 | "architecture": architecture}) |
193 | |
194 | === modified file 'landscape/package/tests/test_changer.py' |
195 | --- landscape/package/tests/test_changer.py 2011-12-13 10:29:04 +0000 |
196 | +++ landscape/package/tests/test_changer.py 2012-01-20 12:58:25 +0000 |
197 | @@ -24,7 +24,7 @@ |
198 | from landscape.package.tests.helpers import ( |
199 | SmartFacadeHelper, HASH1, HASH2, HASH3, PKGDEB1, PKGDEB2, PKGNAME2, |
200 | AptFacadeHelper, SimpleRepositoryHelper) |
201 | -from landscape.manager.manager import SUCCEEDED |
202 | +from landscape.manager.manager import FAILED, SUCCEEDED |
203 | |
204 | |
205 | class PackageChangerTestMixin(object): |
206 | @@ -1037,6 +1037,31 @@ |
207 | self.assertIn("(2)", text) |
208 | return result.addCallback(got_result) |
209 | |
210 | + def test_change_package_holds(self): |
211 | + """ |
212 | + If C{SmartFacade} is used, the L{PackageChanger.handle_tasks} |
213 | + method fails the activity, since it can't add or remove dpkg holds. |
214 | + """ |
215 | + self.facade.reload_channels() |
216 | + self.store.add_task("changer", {"type": "change-package-holds", |
217 | + "create": ["name1"], |
218 | + "delete": ["name2"], |
219 | + "operation-id": 123}) |
220 | + |
221 | + def assert_result(result): |
222 | + self.assertIn("Queuing message with change package holds results " |
223 | + "to exchange urgently.", self.logfile.getvalue()) |
224 | + self.assertMessages( |
225 | + self.get_pending_messages(), |
226 | + [{"type": "operation-result", |
227 | + "operation-id": 123, |
228 | + "status": FAILED, |
229 | + "result-text": "This client doesn't support package holds.", |
230 | + "result-code": 1}]) |
231 | + |
232 | + result = self.changer.handle_tasks() |
233 | + return result.addCallback(assert_result) |
234 | + |
235 | |
236 | class AptPackageChangerTest(LandscapeTest, PackageChangerTestMixin): |
237 | |
238 | @@ -1129,3 +1154,100 @@ |
239 | def get_package_name(self, version): |
240 | """Return the name of the package.""" |
241 | return version.package.name |
242 | + |
243 | + def test_change_package_holds(self): |
244 | + """ |
245 | + The L{PackageChanger.handle_tasks} method appropriately creates and |
246 | + deletes package holds as requested by the C{change-package-holds} |
247 | + message. |
248 | + """ |
249 | + self._add_system_package("foo") |
250 | + self._add_system_package("bar") |
251 | + self.facade.set_package_hold("bar") |
252 | + self.facade.reload_channels() |
253 | + self.store.add_task("changer", {"type": "change-package-holds", |
254 | + "create": ["foo"], |
255 | + "delete": ["bar"], |
256 | + "operation-id": 123}) |
257 | + |
258 | + def assert_result(result): |
259 | + self.assertEqual(["foo"], self.facade.get_package_holds()) |
260 | + self.assertIn("Queuing message with change package holds results " |
261 | + "to exchange urgently.", self.logfile.getvalue()) |
262 | + self.assertMessages( |
263 | + self.get_pending_messages(), |
264 | + [{"type": "operation-result", |
265 | + "operation-id": 123, |
266 | + "status": SUCCEEDED, |
267 | + "result-text": "Package holds successfully changed.", |
268 | + "result-code": 0}]) |
269 | + |
270 | + result = self.changer.handle_tasks() |
271 | + return result.addCallback(assert_result) |
272 | + |
273 | + def test_change_package_holds_create_not_installed(self): |
274 | + """ |
275 | + If the C{change-package-holds} message requests to add holds for |
276 | + packages that aren't installed, the whole activity is failed. If |
277 | + multiple holds are specified, those won't be added. There's no |
278 | + difference between a package that is available in some |
279 | + repository and a package that the facade doesn't know about at |
280 | + all. |
281 | + """ |
282 | + self._add_system_package("foo") |
283 | + self._add_package_to_deb_dir(self.repository_dir, "bar") |
284 | + self.facade.reload_channels() |
285 | + self.store.add_task("changer", {"type": "change-package-holds", |
286 | + "create": ["foo", "bar", "baz"], |
287 | + "operation-id": 123}) |
288 | + |
289 | + def assert_result(result): |
290 | + self.facade.reload_channels() |
291 | + self.assertEqual([], self.facade.get_package_holds()) |
292 | + self.assertIn("Queuing message with change package holds results " |
293 | + "to exchange urgently.", self.logfile.getvalue()) |
294 | + self.assertMessages( |
295 | + self.get_pending_messages(), |
296 | + [{"type": "operation-result", |
297 | + "operation-id": 123, |
298 | + "status": FAILED, |
299 | + "result-text": "Package holds not added, since the " |
300 | + "following packages are not installed: " |
301 | + "bar, baz", |
302 | + "result-code": 1}]) |
303 | + |
304 | + result = self.changer.handle_tasks() |
305 | + return result.addCallback(assert_result) |
306 | + |
307 | + def test_change_package_holds_delete_not_held(self): |
308 | + """ |
309 | + If the C{change-package-holds} message requests to remove holds |
310 | + for packages that aren't held, the activity still succeeds. If |
311 | + other valid holds are specified, those will be removed. |
312 | + There's no difference between a package that is installed |
313 | + and a package that isn't installed. In either case, the |
314 | + result is that the package isn't held. |
315 | + """ |
316 | + self._add_system_package("foo") |
317 | + self._add_system_package("bar") |
318 | + self.facade.set_package_hold("bar") |
319 | + self.facade.reload_channels() |
320 | + self.store.add_task("changer", {"type": "change-package-holds", |
321 | + "delete": ["foo", "bar", "baz"], |
322 | + "operation-id": 123}) |
323 | + |
324 | + def assert_result(result): |
325 | + self.facade.reload_channels() |
326 | + self.assertEqual([], self.facade.get_package_holds()) |
327 | + self.assertIn("Queuing message with change package holds results " |
328 | + "to exchange urgently.", self.logfile.getvalue()) |
329 | + self.assertMessages( |
330 | + self.get_pending_messages(), |
331 | + [{"type": "operation-result", |
332 | + "operation-id": 123, |
333 | + "status": SUCCEEDED, |
334 | + "result-text": "Package holds successfully changed.", |
335 | + "result-code": 0}]) |
336 | + |
337 | + result = self.changer.handle_tasks() |
338 | + return result.addCallback(assert_result) |
339 | |
340 | === modified file 'landscape/package/tests/test_facade.py' |
341 | --- landscape/package/tests/test_facade.py 2012-01-16 15:08:15 +0000 |
342 | +++ landscape/package/tests/test_facade.py 2012-01-20 12:58:25 +0000 |
343 | @@ -1385,6 +1385,95 @@ |
344 | sorted(error.packages, key=self.version_sortkey), |
345 | sorted([bar, baz], key=self.version_sortkey)) |
346 | |
347 | + def test_get_package_holds_with_no_hold(self): |
348 | + """ |
349 | + If no package holds are set, C{get_package_holds} returns |
350 | + an empty C{list}. |
351 | + """ |
352 | + self._add_system_package("foo") |
353 | + self.facade.reload_channels() |
354 | + self.assertEqual([], self.facade.get_package_holds()) |
355 | + |
356 | + def test_get_package_holds_with_holds(self): |
357 | + """ |
358 | + If package holds are set, C{get_package_holds} returns |
359 | + the name of the packages that are held. |
360 | + """ |
361 | + self._add_system_package( |
362 | + "foo", control_fields={"Status": "hold ok installed"}) |
363 | + self._add_system_package("bar") |
364 | + self._add_system_package( |
365 | + "baz", control_fields={"Status": "hold ok installed"}) |
366 | + self.facade.reload_channels() |
367 | + |
368 | + self.assertEqual( |
369 | + ["baz", "foo"], sorted(self.facade.get_package_holds())) |
370 | + |
371 | + def test_set_package_hold(self): |
372 | + """ |
373 | + C{set_package_hold} marks a package to be on hold. |
374 | + """ |
375 | + self._add_system_package("foo") |
376 | + self.facade.reload_channels() |
377 | + self.facade.set_package_hold("foo") |
378 | + self.facade.reload_channels() |
379 | + |
380 | + self.assertEqual(["foo"], self.facade.get_package_holds()) |
381 | + |
382 | + def test_set_package_hold_existing_hold(self): |
383 | + """ |
384 | + If a package is already hel, C{set_package_hold} doesn't return |
385 | + an error. |
386 | + """ |
387 | + self._add_system_package( |
388 | + "foo", control_fields={"Status": "hold ok installed"}) |
389 | + self.facade.reload_channels() |
390 | + self.facade.set_package_hold("foo") |
391 | + self.facade.reload_channels() |
392 | + |
393 | + self.assertEqual(["foo"], self.facade.get_package_holds()) |
394 | + |
395 | + def test_remove_package_hold(self): |
396 | + """ |
397 | + C{remove_package_hold} marks a package not to be on hold. |
398 | + """ |
399 | + self._add_system_package( |
400 | + "foo", control_fields={"Status": "hold ok installed"}) |
401 | + self.facade.reload_channels() |
402 | + self.facade.remove_package_hold("foo") |
403 | + self.facade.reload_channels() |
404 | + |
405 | + self.assertEqual([], self.facade.get_package_holds()) |
406 | + |
407 | + def test_remove_package_hold_no_package(self): |
408 | + """ |
409 | + If a package doesn't exist, C{remove_package_hold} doesn't |
410 | + return an error. It's up to the caller to make sure that the |
411 | + package exist, if it's important. |
412 | + """ |
413 | + self._add_system_package("foo") |
414 | + self.facade.reload_channels() |
415 | + self.facade.remove_package_hold("bar") |
416 | + self.facade.reload_channels() |
417 | + |
418 | + self.assertEqual([], self.facade.get_package_holds()) |
419 | + |
420 | + def test_remove_package_hold_no_hold(self): |
421 | + """ |
422 | + If a package isn't held, the existing selection is retained when |
423 | + C{remove_package_hold} is called. |
424 | + """ |
425 | + self._add_system_package( |
426 | + "foo", control_fields={"Status": "deinstall ok installed"}) |
427 | + self.facade.reload_channels() |
428 | + self.facade.remove_package_hold("foo") |
429 | + self.facade.reload_channels() |
430 | + |
431 | + self.assertEqual([], self.facade.get_package_holds()) |
432 | + [foo] = self.facade.get_packages_by_name("foo") |
433 | + self.assertEqual( |
434 | + apt_pkg.SELSTATE_DEINSTALL, foo.package._pkg.selected_state) |
435 | + |
436 | if not hasattr(Package, "shortname"): |
437 | # The 'shortname' attribute was added when multi-arch support |
438 | # was added to python-apt. So if it's not there, it means that |
Looks good! +1
#1:
+ """Log that a package holds result is sent and sent the response."""
typo, "and send"