Merge lp:~ack/landscape-client/package-reporter-update into lp:~landscape/landscape-client/trunk

Proposed by Alberto Donato
Status: Merged
Approved by: Alberto Donato
Approved revision: 398
Merged at revision: 395
Proposed branch: lp:~ack/landscape-client/package-reporter-update
Merge into: lp:~landscape/landscape-client/trunk
Diff against target: 579 lines (+439/-6)
10 files modified
.bzrignore (+9/-0)
apt-update/Makefile (+7/-0)
apt-update/apt-update.c (+81/-0)
debian/landscape-client.postinst (+7/-2)
debian/landscape-client.prerm (+5/-1)
debian/rules (+3/-0)
landscape/package/reporter.py (+56/-3)
landscape/package/taskhandler.py (+5/-0)
landscape/package/tests/test_reporter.py (+256/-0)
landscape/package/tests/test_taskhandler.py (+10/-0)
To merge this branch: bzr merge lp:~ack/landscape-client/package-reporter-update
Reviewer Review Type Date Requested Status
Free Ekanayaka (community) Approve
Thomas Herve (community) Approve
Review via email: mp+81308@code.launchpad.net

Description of the change

This branch makes the package reporter use APT to update the package cache instead of smart, when USE_APT_FACADE is set.

At the moment, force_smart_update and smart_update_interval are used with the same effect also when updating via APT.

To post a comment you must log in.
Revision history for this message
Thomas Herve (therve) wrote :

It looks good, but I suspect we need to get this reviewed by someone from the platform team. It shouldn't block the branch, but I suggest getting in touch with them sooner than later.

+1!

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

Great! +1

[1]

+ else:
+ err = ("'%s' didn't run, update interval has not passed" %
+ self.apt_update_filename)
+ result = succeed(("", err, 1))

I think we should not reproduce the behavior of smart-update in this case, we should just return 0 as fake exit code, and this will simplify the callback.

[2]

Please add a copyright notice to apt-update.c, like in smart-update.c

review: Approve
398. By Alberto Donato

Addressed Free's comments.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file '.bzrignore'
2--- .bzrignore 2011-10-04 12:13:35 +0000
3+++ .bzrignore 2011-11-08 11:04:00 +0000
4@@ -8,8 +8,17 @@
5 debian/landscape-client.postrm.debhelper
6 debian/landscape-client.prerm.debhelper
7 debian/landscape-client.substvars
8+debian/landscape-client.debhelper.log
9+debian/landscape-common/
10+debian/landscape-common.debhelper.log
11+debian/landscape-common.postinst.debhelper
12+debian/landscape-common.postrm.debhelper
13+debian/landscape-common.prerm.debhelper
14+debian/landscape-common.substvars
15 man/landscape-client.1
16 man/landscape-config.1
17 man/landscape-message.1
18+smart-update/smart-update
19+apt-update/apt-update
20 docs
21 tags
22
23=== added directory 'apt-update'
24=== added file 'apt-update/Makefile'
25--- apt-update/Makefile 1970-01-01 00:00:00 +0000
26+++ apt-update/Makefile 2011-11-08 11:04:00 +0000
27@@ -0,0 +1,7 @@
28+NAME = apt-update
29+
30+$(NAME): $(NAME).c
31+ $(CC) $(CFLAGS) -Wall $< -o $@
32+
33+clean:
34+ rm -f $(NAME)
35
36=== added file 'apt-update/apt-update.c'
37--- apt-update/apt-update.c 1970-01-01 00:00:00 +0000
38+++ apt-update/apt-update.c 2011-11-08 11:04:00 +0000
39@@ -0,0 +1,81 @@
40+/*
41+
42+ Copyright (c) 2011 Canonical, Ltd.
43+
44+*/
45+
46+#define _GNU_SOURCE
47+#include <sys/resource.h>
48+#include <sys/types.h>
49+#include <sys/stat.h>
50+#include <grp.h>
51+#include <unistd.h>
52+#include <stdlib.h>
53+#include <string.h>
54+#include <errno.h>
55+#include <stdio.h>
56+#include <pwd.h>
57+
58+int main(int argc, char *argv[], char *envp[])
59+{
60+ char *apt_argv[] = {"/usr/bin/apt-get", "-q", "update", NULL};
61+ char *apt_envp[] = {"PATH=/bin:/usr/bin", NULL, NULL};
62+
63+ // Set the HOME environment variable
64+ struct passwd *pwd = getpwuid(geteuid());
65+ if (!pwd) {
66+ fprintf(stderr, "error: Unable to find passwd entry for uid %d (%s)\n",
67+ geteuid(), strerror(errno));
68+ exit(1);
69+ }
70+ if (asprintf(&apt_envp[1], "HOME=%s", pwd->pw_dir) == -1) {
71+ perror("error: Unable to create HOME environment variable");
72+ exit(1);
73+ }
74+
75+ // Drop any supplementary group
76+ if (setgroups(0, NULL) == -1) {
77+ perror("error: Unable to set supplementary groups IDs");
78+ exit(1);
79+ }
80+
81+ // Set real/effective gid and uid
82+ if (setregid(pwd->pw_gid, pwd->pw_gid) == -1) {
83+ fprintf(stderr, "error: Unable to set real and effective gid (%s)\n",
84+ strerror(errno));
85+ exit(1);
86+ }
87+ if (setreuid(pwd->pw_uid, pwd->pw_uid) == -1) {
88+ perror("error: Unable to set real and effective uid");
89+ exit(1);
90+ }
91+
92+ // Close all file descriptors except the standard ones
93+ struct rlimit rlp;
94+ if (getrlimit(RLIMIT_NOFILE, &rlp) == -1) {
95+ perror("error: Unable to determine file descriptor limits");
96+ exit(1);
97+ }
98+ int file_max;
99+ if (rlp.rlim_max == RLIM_INFINITY || rlp.rlim_max > 4096)
100+ file_max = 4096;
101+ else
102+ file_max = rlp.rlim_max;
103+ int file;
104+ for (file = 3; file < file_max; file++) {
105+ close(file);
106+ }
107+
108+ // Set umask to 022
109+ umask(S_IWGRP | S_IWOTH);
110+
111+ if (chdir("/") == -1) {
112+ perror("error: Unable to change working directory");
113+ exit(1);
114+ }
115+
116+ // Run apt-get update
117+ execve(apt_argv[0], apt_argv, apt_envp);
118+ perror("error: Unable to execute apt-get");
119+ return 1;
120+}
121
122=== modified file 'debian/landscape-client.postinst'
123--- debian/landscape-client.postinst 2011-10-07 11:52:47 +0000
124+++ debian/landscape-client.postinst 2011-11-08 11:04:00 +0000
125@@ -102,12 +102,17 @@
126 echo "Old monolithic client data file migrated to new format."
127 fi
128
129- # Add the setuid flag to smart-update and it be executable by users in
130- # the landscape group (that normally means landscape itself)
131+ # Add the setuid flag to smart-update and apt-update, and make them be
132+ # executable by users in the landscape group (that normally means
133+ # landscape itself)
134 smart_update=/usr/lib/landscape/smart-update
135 if ! dpkg-statoverride --list $smart_update >/dev/null 2>&1; then
136 dpkg-statoverride --update --add root landscape 4754 $smart_update
137 fi
138+ apt_update=/usr/lib/landscape/apt-update
139+ if ! dpkg-statoverride --list $apt_update >/dev/null 2>&1; then
140+ dpkg-statoverride --update --add root landscape 4754 $apt_update
141+ fi
142
143 # Remove old cron jobs
144 old_cron_job=/etc/cron.hourly/landscape-client
145
146=== modified file 'debian/landscape-client.prerm'
147--- debian/landscape-client.prerm 2009-06-08 13:29:20 +0000
148+++ debian/landscape-client.prerm 2011-11-08 11:04:00 +0000
149@@ -20,11 +20,15 @@
150 case "$1" in
151 remove|upgrade|deconfigure)
152
153- # Remove statoverride for smart-update
154+ # Remove statoverride for smart-update and apt-update
155 smart_update=/usr/lib/landscape/smart-update
156 if dpkg-statoverride --list $smart_update >/dev/null 2>&1; then
157 dpkg-statoverride --remove $smart_update
158 fi
159+ apt_update=/usr/lib/landscape/apt-update
160+ if dpkg-statoverride --list $apt_update >/dev/null 2>&1; then
161+ dpkg-statoverride --remove $apt_update
162+ fi
163
164 ;;
165
166
167=== modified file 'debian/rules'
168--- debian/rules 2011-04-26 09:47:16 +0000
169+++ debian/rules 2011-11-08 11:04:00 +0000
170@@ -30,6 +30,7 @@
171 sed -i -e "s/^DEBIAN_REVISION = \"\"/DEBIAN_REVISION = \"-$(revision)\"/g" landscape/__init__.py
172 python setup.py build
173 make -C smart-update
174+ make -C apt-update
175 touch build-stamp
176
177 clean:
178@@ -38,6 +39,7 @@
179 rm -f build-stamp
180 rm -rf build
181 make -C smart-update clean
182+ make -C apt-update clean
183 dh_clean
184 debconf-updatepo
185 sed -i -e "s/^DEBIAN_REVISION = .*/DEBIAN_REVISION = \"\"/g" landscape/__init__.py
186@@ -57,6 +59,7 @@
187 install -D -o root -g root -m 755 debian/landscape-sysinfo.wrapper $(root_dir)/usr/share/landscape/landscape-sysinfo.wrapper
188 install -D -o root -g root -m 644 debian/cloud-default.conf $(root_dir)/usr/share/landscape/cloud-default.conf
189 install -D -o root -g root -m 755 smart-update/smart-update $(root_dir)/usr/lib/landscape/smart-update
190+ install -D -o root -g root -m 755 apt-update/apt-update $(root_dir)/usr/lib/landscape/apt-update
191 install -D -o root -g root -m 644 dbus/landscape.conf $(root_dir)/etc/dbus-1/system.d/landscape.conf
192
193 binary-indep:
194
195=== modified file 'landscape/package/reporter.py'
196--- landscape/package/reporter.py 2011-10-19 13:16:30 +0000
197+++ landscape/package/reporter.py 2011-11-08 11:04:00 +0000
198@@ -49,15 +49,20 @@
199
200 smart_update_interval = 60
201 smart_update_filename = "/usr/lib/landscape/smart-update"
202+ apt_update_filename = "/usr/lib/landscape/apt-update"
203 sources_list_filename = "/etc/apt/sources.list"
204 sources_list_directory = "/etc/apt/sources.list.d"
205
206 def run(self):
207 result = Deferred()
208
209- # Run smart-update before anything else, to make sure that
210- # the SmartFacade will load freshly updated channels
211- result.addCallback(lambda x: self.run_smart_update())
212+ if os.environ.get("USE_APT_FACADE"):
213+ # Update APT cache if APT facade is enabled.
214+ result.addCallback(lambda x: self.run_apt_update())
215+ else:
216+ # Run smart-update before anything else, to make sure that
217+ # the SmartFacade will load freshly updated channels
218+ result.addCallback(lambda x: self.run_smart_update())
219
220 # If the appropriate hash=>id db is not there, fetch it
221 result.addCallback(lambda x: self.fetch_hash_id_db())
222@@ -215,6 +220,54 @@
223 result.addCallback(callback)
224 return result
225
226+ def _apt_update_timeout_expired(self, interval):
227+ """Check if the apt-update timeout has passed."""
228+ stamp = self._config.apt_update_stamp_filename
229+
230+ if not os.path.exists(stamp):
231+ return True
232+ # check stamp file mtime
233+ last_update = os.stat(stamp)[8]
234+ now = int(time.time())
235+ return (last_update + interval * 60) < now
236+
237+ def run_apt_update(self):
238+ """Run apt-update and log a warning in case of non-zero exit code.
239+
240+ @return: a deferred returning (out, err, code)
241+ """
242+ if (self._config.force_smart_update or self._apt_sources_have_changed()
243+ or self._apt_update_timeout_expired(self.smart_update_interval)):
244+
245+ result = spawn_process(self.apt_update_filename)
246+
247+ def callback((out, err, code)):
248+ touch_file(self._config.apt_update_stamp_filename)
249+ logging.debug(
250+ "'%s' exited with status %d (out='%s', err='%s')" % (
251+ self.apt_update_filename, code, out, err))
252+
253+ if code != 0:
254+ logging.warning("'%s' exited with status %d (%s)" % (
255+ self.apt_update_filename, code, err))
256+ elif not self._facade.get_channels():
257+ code = 1
258+ err = ("There are no APT sources configured in %s or %s." %
259+ (self.sources_list_filename,
260+ self.sources_list_directory))
261+
262+ deferred = self._broker.call_if_accepted(
263+ "package-reporter-result", self.send_result, code, err)
264+ deferred.addCallback(lambda ignore: (out, err, code))
265+ return deferred
266+
267+ return result.addCallback(callback)
268+
269+ else:
270+ logging.debug("'%s' didn't run, update interval has not passed" %
271+ self.apt_update_filename)
272+ return succeed(("", "", 0))
273+
274 def send_result(self, code, err):
275 """
276 Report the package reporter result to the server in a message.
277
278=== modified file 'landscape/package/taskhandler.py'
279--- landscape/package/taskhandler.py 2011-10-26 13:08:13 +0000
280+++ landscape/package/taskhandler.py 2011-11-08 11:04:00 +0000
281@@ -40,6 +40,11 @@
282 """Get the path to the smart-update stamp file."""
283 return os.path.join(self.package_directory, "smart-update-stamp")
284
285+ @property
286+ def apt_update_stamp_filename(self):
287+ """Get the path to the apt-update stamp file."""
288+ return os.path.join(self.package_directory, "apt-update-stamp")
289+
290
291 class LazyRemoteBroker(object):
292 """Wrapper class around L{RemoteBroker} providing lazy initialization.
293
294=== modified file 'landscape/package/tests/test_reporter.py'
295--- landscape/package/tests/test_reporter.py 2011-10-26 12:55:00 +0000
296+++ landscape/package/tests/test_reporter.py 2011-11-08 11:04:00 +0000
297@@ -1693,6 +1693,262 @@
298 """Make it so that package "name1" is considered installed."""
299 self._install_deb_file(os.path.join(self.repository_dir, PKGNAME1))
300
301+ def _make_fake_apt_update(self, out="output", err="error", code=0):
302+ """Create a fake apt-update executable"""
303+ self.reporter.apt_update_filename = self.makeFile(
304+ "#!/bin/sh\n"
305+ "echo -n %s\n"
306+ "echo -n %s >&2\n"
307+ "exit %d" % (out, err, code))
308+ os.chmod(self.reporter.apt_update_filename, 0755)
309+
310+ def test_run_apt_update(self):
311+ """
312+ The L{PackageReporter.run_apt_update} method should run apt-update.
313+ """
314+ self.reporter.sources_list_filename = "/I/Dont/Exist"
315+ self._make_fake_apt_update()
316+ debug_mock = self.mocker.replace("logging.debug")
317+ debug_mock("'%s' exited with status 0 (out='output', err='error')" %
318+ self.reporter.apt_update_filename)
319+ warning_mock = self.mocker.replace("logging.warning")
320+ self.expect(warning_mock(ANY)).count(0)
321+ self.mocker.replay()
322+ deferred = Deferred()
323+
324+ def do_test():
325+
326+ result = self.reporter.run_apt_update()
327+
328+ def callback((out, err, code)):
329+ self.assertEqual("output", out)
330+ self.assertEqual("error", err)
331+ self.assertEqual(0, code)
332+ result.addCallback(callback)
333+ result.chainDeferred(deferred)
334+
335+ reactor.callWhenRunning(do_test)
336+ return deferred
337+
338+ def test_run_apt_update_with_force_smart_update(self):
339+ """
340+ L{PackageReporter.run_apt_update} forces an apt-update run if the
341+ '--force-smart-update' command line option was passed.
342+ """
343+ self.makeFile("", path=self.config.apt_update_stamp_filename)
344+ self.config.load(["--force-smart-update"])
345+ self._make_fake_apt_update()
346+
347+ deferred = Deferred()
348+
349+ def do_test():
350+ result = self.reporter.run_apt_update()
351+
352+ def callback((out, err, code)):
353+ self.assertEqual("output", out)
354+ result.addCallback(callback)
355+ result.chainDeferred(deferred)
356+
357+ reactor.callWhenRunning(do_test)
358+ return deferred
359+
360+ def test_run_apt_update_with_force_smart_update_if_sources_changed(self):
361+ """
362+ L{PackageReporter.run_apt_update} forces an apt-update run if the APT
363+ sources.list file has changed.
364+
365+ """
366+ self.assertEqual(self.reporter.sources_list_filename,
367+ "/etc/apt/sources.list")
368+ self.reporter.sources_list_filename = self.makeFile("deb ftp://url ./")
369+ self._make_fake_apt_update()
370+
371+ deferred = Deferred()
372+
373+ def do_test():
374+ result = self.reporter.run_apt_update()
375+
376+ def callback((out, err, code)):
377+ self.assertEqual("output", out)
378+ result.addCallback(callback)
379+ result.chainDeferred(deferred)
380+
381+ reactor.callWhenRunning(do_test)
382+ return deferred
383+
384+ def test_run_apt_update_warns_about_failures(self):
385+ """
386+ The L{PackageReporter.run_apt_update} method should log a warning in
387+ case apt-update terminates with a non-zero exit code.
388+ """
389+ self._make_fake_apt_update(code=2)
390+ logging_mock = self.mocker.replace("logging.warning")
391+ logging_mock("'%s' exited with status 2"
392+ " (error)" % self.reporter.apt_update_filename)
393+ self.mocker.replay()
394+ deferred = Deferred()
395+
396+ def do_test():
397+ result = self.reporter.run_apt_update()
398+
399+ def callback((out, err, code)):
400+ self.assertEqual("output", out)
401+ self.assertEqual("error", err)
402+ self.assertEqual(2, code)
403+ result.addCallback(callback)
404+ result.chainDeferred(deferred)
405+
406+ reactor.callWhenRunning(do_test)
407+ return deferred
408+
409+ def test_run_apt_update_report_apt_failure(self):
410+ """
411+ If L{PackageReporter.run_apt_update} fails, a message is sent to the
412+ server reporting the error, to be able to fix the problem centrally.
413+ """
414+ message_store = self.broker_service.message_store
415+ message_store.set_accepted_types(["package-reporter-result"])
416+ self._make_fake_apt_update(code=2)
417+ deferred = Deferred()
418+
419+ def do_test():
420+ result = self.reporter.run_apt_update()
421+
422+ def callback(ignore):
423+ self.assertMessages(message_store.get_pending_messages(),
424+ [{"type": "package-reporter-result",
425+ "code": 2, "err": u"error"}])
426+ result.addCallback(callback)
427+ result.chainDeferred(deferred)
428+
429+ reactor.callWhenRunning(do_test)
430+ return deferred
431+
432+ def test_run_apt_update_report_no_sources(self):
433+ """
434+ L{PackageReporter.run_apt_update} reports a failure if apt succeeds but
435+ there are no APT sources defined. APT doesn't fail if there are no
436+ sources, but we fake a failure in order to re-use the
437+ PackageReporterAlert on the server.
438+ """
439+ self.facade.reset_channels()
440+ message_store = self.broker_service.message_store
441+ message_store.set_accepted_types(["package-reporter-result"])
442+ self._make_fake_apt_update()
443+ deferred = Deferred()
444+
445+ def do_test():
446+ result = self.reporter.run_apt_update()
447+
448+ def callback(ignore):
449+ error = "There are no APT sources configured in %s or %s." % (
450+ self.reporter.sources_list_filename,
451+ self.reporter.sources_list_directory)
452+ self.assertMessages(message_store.get_pending_messages(),
453+ [{"type": "package-reporter-result",
454+ "code": 1, "err": error}])
455+ result.addCallback(callback)
456+ result.chainDeferred(deferred)
457+
458+ reactor.callWhenRunning(do_test)
459+ return deferred
460+
461+ def test_run_apt_update_report_apt_failure_no_sources(self):
462+ """
463+ If L{PackageReporter.run_apt_update} fails and there are no
464+ APT sources configured, the APT error takes precedence.
465+ """
466+ self.facade.reset_channels()
467+ message_store = self.broker_service.message_store
468+ message_store.set_accepted_types(["package-reporter-result"])
469+ self._make_fake_apt_update(code=2)
470+ deferred = Deferred()
471+
472+ def do_test():
473+ result = self.reporter.run_apt_update()
474+
475+ def callback(ignore):
476+ self.assertMessages(message_store.get_pending_messages(),
477+ [{"type": "package-reporter-result",
478+ "code": 2, "err": u"error"}])
479+ result.addCallback(callback)
480+ result.chainDeferred(deferred)
481+
482+ reactor.callWhenRunning(do_test)
483+ return deferred
484+
485+ def test_run_apt_update_report_success(self):
486+ """
487+ L{PackageReporter.run_apt_update} also reports success to be able to
488+ know the proper state of the client.
489+ """
490+ message_store = self.broker_service.message_store
491+ message_store.set_accepted_types(["package-reporter-result"])
492+ self._make_fake_apt_update(err="message")
493+ deferred = Deferred()
494+
495+ def do_test():
496+ result = self.reporter.run_apt_update()
497+
498+ def callback(ignore):
499+ self.assertMessages(message_store.get_pending_messages(),
500+ [{"type": "package-reporter-result",
501+ "code": 0, "err": u"message"}])
502+ result.addCallback(callback)
503+ result.chainDeferred(deferred)
504+
505+ reactor.callWhenRunning(do_test)
506+ return deferred
507+
508+ def test_run_apt_update_no_run_in_interval(self):
509+ """
510+ The L{PackageReporter.run_apt_update} logs a debug message if
511+ apt-update doesn't run because interval has not passed.
512+ """
513+ self.makeFile("", path=self.config.apt_update_stamp_filename)
514+
515+ logging_mock = self.mocker.replace("logging.debug")
516+ logging_mock("'%s' didn't run, update interval has not passed" %
517+ self.reporter.apt_update_filename)
518+ self.mocker.replay()
519+ deferred = Deferred()
520+
521+ def do_test():
522+
523+ result = self.reporter.run_apt_update()
524+
525+ def callback((out, err, code)):
526+ self.assertEqual("", out)
527+ self.assertEqual("", err)
528+ self.assertEqual(0, code)
529+ result.addCallback(callback)
530+ result.chainDeferred(deferred)
531+
532+ reactor.callWhenRunning(do_test)
533+ return deferred
534+
535+ def test_run_apt_update_touches_stamp_file(self):
536+ """
537+ The L{PackageReporter.run_apt_update} method touches a stamp file
538+ after running the apt-update wrapper.
539+ """
540+ self.reporter.sources_list_filename = "/I/Dont/Exist"
541+ self._make_fake_apt_update()
542+ deferred = Deferred()
543+
544+ def do_test():
545+
546+ result = self.reporter.run_apt_update()
547+
548+ def callback(ignored):
549+ self.assertTrue(
550+ os.path.exists(self.config.apt_update_stamp_filename))
551+ result.addCallback(callback)
552+ result.chainDeferred(deferred)
553+
554+ reactor.callWhenRunning(do_test)
555+ return deferred
556+
557
558 class GlobalPackageReporterTestMixin(object):
559
560
561=== modified file 'landscape/package/tests/test_taskhandler.py'
562--- landscape/package/tests/test_taskhandler.py 2011-07-05 05:09:11 +0000
563+++ landscape/package/tests/test_taskhandler.py 2011-11-08 11:04:00 +0000
564@@ -35,6 +35,16 @@
565 config.smart_update_stamp_filename,
566 "/var/lib/landscape/client/package/smart-update-stamp")
567
568+ def test_force_apt_update_option(self):
569+ """
570+ L{PackageReporterConfiguration.apt_update_stamp_filename} points
571+ to the apt-update stamp file.
572+ """
573+ config = PackageTaskHandlerConfiguration()
574+ self.assertEqual(
575+ config.apt_update_stamp_filename,
576+ "/var/lib/landscape/client/package/apt-update-stamp")
577+
578
579 class PackageTaskHandlerTest(LandscapeTest):
580

Subscribers

People subscribed via source and target branches

to all changes: