Merge lp:~bjornt/landscape-client/add-remove-dpkg-holds into lp:~landscape/landscape-client/trunk

Proposed by Björn Tillenius
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
Reviewer Review Type Date Requested Status
Geoff Teale (community) Approve
Alberto Donato (community) Approve
Review via email: mp+89253@code.launchpad.net

Description of the change

Add a message for adding and removing package holds, handling the
message in the package changer.

To post a comment you must log in.
Revision history for this message
Alberto Donato (ack) wrote :

Looks good! +1

#1:
+ """Log that a package holds result is sent and sent the response."""

typo, "and send"

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

+1 Looks good. Please fix:

landscape/package/tests/test_changer.py:1067:1: E303 too many blank lines (3)

review: Approve
448. By Björn Tillenius

Typo.

449. By Björn Tillenius

Lint.

Revision history for this message
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://launchpad.net/~bjornt

Revision history for this message
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/package/tests/test_changer.py:1067:1: E303 too many blank lines (3)

Oops, missed that. Fixed.

--
Björn Tillenius | https://launchpad.net/~bjornt

Preview Diff

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

Subscribers

People subscribed via source and target branches

to all changes: