Merge lp:~barry/ubuntu-system-image/citrain-2.1 into lp:~ubuntu-managed-branches/ubuntu-system-image/system-image
- citrain-2.1
- Merge into system-image
Status: | Merged | ||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
Merged at revision: | 229 | ||||||||||||||||
Proposed branch: | lp:~barry/ubuntu-system-image/citrain-2.1 | ||||||||||||||||
Merge into: | lp:~ubuntu-managed-branches/ubuntu-system-image/system-image | ||||||||||||||||
Diff against target: |
2123 lines (+901/-146) 65 files modified
MANIFEST.in (+1/-0) NEWS.rst (+48/-1) PKG-INFO (+1/-1) cli-manpage.rst (+1/-1) dbus-manpage.rst (+1/-1) debian/changelog (+32/-0) debian/control (+5/-4) debian/patches/01_send_ack_on_applyupdate.diff (+0/-16) debian/patches/lp1284217.patch (+106/-0) debian/patches/series (+1/-1) debian/rules (+1/-0) ini-manpage.rst (+1/-1) setup.cfg (+2/-2) setup.py (+1/-1) system_image.egg-info/PKG-INFO (+1/-1) system_image.egg-info/SOURCES.txt (+1/-1) systemimage/api.py (+5/-1) systemimage/bag.py (+1/-1) systemimage/bindings.py (+1/-1) systemimage/candidates.py (+1/-1) systemimage/channel.py (+1/-1) systemimage/config.py (+1/-1) systemimage/dbus.py (+51/-16) systemimage/device.py (+1/-1) systemimage/docs/conf.py (+1/-1) systemimage/download.py (+36/-2) systemimage/gpg.py (+95/-1) systemimage/helpers.py (+1/-1) systemimage/image.py (+1/-1) systemimage/index.py (+1/-1) systemimage/keyring.py (+3/-4) systemimage/logging.py (+29/-8) systemimage/main.py (+1/-1) systemimage/reactor.py (+1/-1) systemimage/reboot.py (+1/-1) systemimage/scores.py (+1/-1) systemimage/service.py (+14/-6) systemimage/settings.py (+1/-1) systemimage/state.py (+8/-10) systemimage/testing/controller.py (+17/-8) systemimage/testing/dbus.py (+8/-2) systemimage/testing/demo.py (+1/-1) systemimage/testing/helpers.py (+25/-7) systemimage/testing/nose.py (+21/-2) systemimage/tests/data/config_03.ini (+1/-1) systemimage/tests/data/index_24.json (+36/-0) systemimage/tests/test_api.py (+1/-1) systemimage/tests/test_bag.py (+1/-1) systemimage/tests/test_candidates.py (+1/-1) systemimage/tests/test_channel.py (+1/-1) systemimage/tests/test_config.py (+1/-1) systemimage/tests/test_dbus.py (+98/-8) systemimage/tests/test_download.py (+1/-1) systemimage/tests/test_gpg.py (+175/-2) systemimage/tests/test_helpers.py (+1/-1) systemimage/tests/test_image.py (+1/-1) systemimage/tests/test_index.py (+1/-1) systemimage/tests/test_keyring.py (+20/-5) systemimage/tests/test_main.py (+25/-1) systemimage/tests/test_scores.py (+1/-1) systemimage/tests/test_settings.py (+1/-1) systemimage/tests/test_state.py (+1/-1) systemimage/tests/test_winner.py (+1/-1) systemimage/version.txt (+1/-1) tox.ini (+1/-1) |
||||||||||||||||
To merge this branch: | bzr merge lp:~barry/ubuntu-system-image/citrain-2.1 | ||||||||||||||||
Related bugs: |
|
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Stéphane Graber | Pending | ||
Review via email: mp+207702@code.launchpad.net |
Commit message
New upstream release, along with some packaging changes. See changelog and NEWS.rst for details.
Description of the change
[ Stéphane Graber ]
* New upstream release.
* Set X-Auto-Uploader to no-rewrite-version
* Set Vcs-Bzr to the new target branch
[ Barry Warsaw ]
* New upstream release.
- LP: #1279056 - Internal improvements to SignatureError for
better debugging.
- LP: #1277589 - Better protection against race conditions.
- LP: #1260768 - Return empty string from ApplyUpdate D-Bus method.
- Request ubuntu-
with atomic rename.
- More detailed logging.
- Fixed D-Bus error logging.
- Added -L flag to nose2 tests for explicitly setting log file path.
- Added SYSTEMIMAGE_
which can be used to give virtualized buildds a fighting chance.
* d/patches/
* d/control: Bump Standards-Version to 3.9.5 with no other changes necessary.
- 234. By Barry Warsaw
-
* d/control:
- Bump Standards-Version to 3.9.5 with no other changes necessary.
- Add python3-psutil as Depends to system-image-dev. - 235. By Barry Warsaw
-
d/rules: Set SYSTEMIMAGE_
DBUS_DAEMON_ HUP_SLEEP_ SECONDS to 1 to deal with
buildd dbus-daemon SIGHUP timing issues. - 236. By Barry Warsaw
-
- LP: #1284217 - Send UpdateAvailable
Status during auto-downloading
from a previous CheckForUpdate, if cached status is available.
* d/patches/01_send_ ack_on_ applyupdate. diff: Removed; applied upstream.
* d/patches/lp1284217. patch: Added (see above). - 237. By Barry Warsaw
-
Update patch
Preview Diff
1 | === modified file 'MANIFEST.in' | |||
2 | --- MANIFEST.in 2013-12-13 13:55:51 +0000 | |||
3 | +++ MANIFEST.in 2014-02-25 17:45:25 +0000 | |||
4 | @@ -3,4 +3,5 @@ | |||
5 | 3 | prune build | 3 | prune build |
6 | 4 | prune dist | 4 | prune dist |
7 | 5 | prune .tox | 5 | prune .tox |
8 | 6 | prune .bzr | ||
9 | 6 | exclude .bzrignore | 7 | exclude .bzrignore |
10 | 7 | 8 | ||
11 | === modified file 'NEWS.rst' | |||
12 | --- NEWS.rst 2013-12-13 13:55:51 +0000 | |||
13 | +++ NEWS.rst 2014-02-25 17:45:25 +0000 | |||
14 | @@ -2,7 +2,54 @@ | |||
15 | 2 | NEWS for system-image updater | 2 | NEWS for system-image updater |
16 | 3 | ============================= | 3 | ============================= |
17 | 4 | 4 | ||
19 | 5 | 2.0.3 (2013-XX-XX) | 5 | 2.1 (2014-02-20) |
20 | 6 | ================ | ||
21 | 7 | * Internal improvements to SignatureError for better debugging. (LP: #1279056) | ||
22 | 8 | * Better protection against several possible race conditions during | ||
23 | 9 | `CheckForUpdate()` (LP: #1277589) | ||
24 | 10 | - Use a threading.Lock instance as the internal "checking for update" | ||
25 | 11 | barrier instead of a boolean. This should eliminate the race window | ||
26 | 12 | between testing and acquiring the checking lock. | ||
27 | 13 | - Put an exclusive claim on the `com.canonical.SystemImage` system dbus | ||
28 | 14 | name, and if we cannot get that claim, exit with an error code 2. This | ||
29 | 15 | prevents multiple instances of the D-Bus system service from running at | ||
30 | 16 | the same time. | ||
31 | 17 | * Return the empty string from `ApplyUpdate()` D-Bus method. This restores | ||
32 | 18 | the original API (patch merged from Ubuntu package, given by Didier | ||
33 | 19 | Roche). (LP: #1260768) | ||
34 | 20 | * Request ubuntu-download-manager to download all files to temporary | ||
35 | 21 | destinations, then atomically rename them into place. This avoids | ||
36 | 22 | clobbering by multiple processes and mimics changes coming in u-d-m. | ||
37 | 23 | * Provide much more detailed logging. | ||
38 | 24 | - `Mediator` instances have a helpful `repr` which also includes the id of | ||
39 | 25 | the `State` object. | ||
40 | 26 | - More logging during state transitions. | ||
41 | 27 | - All emitted D-Bus signals are also logged (at debug level). | ||
42 | 28 | * Added `-L` flag to nose test runner, which can be used to specify an | ||
43 | 29 | explicit log file path for debugging. | ||
44 | 30 | * Fixed D-Bus error logging. | ||
45 | 31 | - Don't initialize the root logger, since this can interfere with | ||
46 | 32 | python-dbus, which doesn't initialize its loggers correctly. | ||
47 | 33 | - Only use `.format()` based interpolation for `systemimage` logs. | ||
48 | 34 | * Give virtualized buildds a fighting chance against D-Bus by | ||
49 | 35 | - using `org.freedesktop.DBus`s `ReloadConfig()` interface instead of | ||
50 | 36 | SIGHUP. | ||
51 | 37 | - add a configurable sleep call after the `ReloadConfig()`. This defaults | ||
52 | 38 | to 0 since de-virtualized and local builds do not need them. Set the | ||
53 | 39 | environment variable `SYSTEMIMAGE_DBUS_DAEMON_HUP_SLEEP_SECONDS` to | ||
54 | 40 | override. | ||
55 | 41 | * Run the tox test suite for both Python 3.3 and 3.4. | ||
56 | 42 | |||
57 | 43 | 2.0.5 (2014-01-30) | ||
58 | 44 | ================== | ||
59 | 45 | * MANIFEST.in: Make sure the .bzr directory doesn't end up in the | ||
60 | 46 | sdist tarball. | ||
61 | 47 | |||
62 | 48 | 2.0.4 (2014-01-30) | ||
63 | 49 | ================== | ||
64 | 50 | * No change release to test the new landing process. | ||
65 | 51 | |||
66 | 52 | 2.0.3 (2013-12-11) | ||
67 | 6 | ================== | 53 | ================== |
68 | 7 | * More attempted DEP-8 test failure fixes. | 54 | * More attempted DEP-8 test failure fixes. |
69 | 8 | 55 | ||
70 | 9 | 56 | ||
71 | === modified file 'PKG-INFO' | |||
72 | --- PKG-INFO 2013-12-13 13:55:51 +0000 | |||
73 | +++ PKG-INFO 2014-02-25 17:45:25 +0000 | |||
74 | @@ -1,6 +1,6 @@ | |||
75 | 1 | Metadata-Version: 1.0 | 1 | Metadata-Version: 1.0 |
76 | 2 | Name: system-image | 2 | Name: system-image |
78 | 3 | Version: 2.0.3 | 3 | Version: 2.1 |
79 | 4 | Summary: Ubuntu System Image Based Upgrades | 4 | Summary: Ubuntu System Image Based Upgrades |
80 | 5 | Home-page: UNKNOWN | 5 | Home-page: UNKNOWN |
81 | 6 | Author: Barry Warsaw | 6 | Author: Barry Warsaw |
82 | 7 | 7 | ||
83 | === modified file 'cli-manpage.rst' | |||
84 | --- cli-manpage.rst 2013-12-13 13:55:51 +0000 | |||
85 | +++ cli-manpage.rst 2014-02-25 17:45:25 +0000 | |||
86 | @@ -8,7 +8,7 @@ | |||
87 | 8 | 8 | ||
88 | 9 | :Author: Barry Warsaw <barry@ubuntu.com> | 9 | :Author: Barry Warsaw <barry@ubuntu.com> |
89 | 10 | :Date: 2013-10-23 | 10 | :Date: 2013-10-23 |
91 | 11 | :Copyright: 2013 Canonical Ltd. | 11 | :Copyright: 2013-2014 Canonical Ltd. |
92 | 12 | :Version: 2.0 | 12 | :Version: 2.0 |
93 | 13 | :Manual section: 1 | 13 | :Manual section: 1 |
94 | 14 | 14 | ||
95 | 15 | 15 | ||
96 | === modified file 'dbus-manpage.rst' | |||
97 | --- dbus-manpage.rst 2013-12-13 13:55:51 +0000 | |||
98 | +++ dbus-manpage.rst 2014-02-25 17:45:25 +0000 | |||
99 | @@ -8,7 +8,7 @@ | |||
100 | 8 | 8 | ||
101 | 9 | :Author: Barry Warsaw <barry@ubuntu.com> | 9 | :Author: Barry Warsaw <barry@ubuntu.com> |
102 | 10 | :Date: 2013-07-31 | 10 | :Date: 2013-07-31 |
104 | 11 | :Copyright: 2013 Canonical Ltd. | 11 | :Copyright: 2013-2014 Canonical Ltd. |
105 | 12 | :Version: 1.0 | 12 | :Version: 1.0 |
106 | 13 | :Manual section: 8 | 13 | :Manual section: 8 |
107 | 14 | 14 | ||
108 | 15 | 15 | ||
109 | === modified file 'debian/changelog' | |||
110 | --- debian/changelog 2013-12-13 13:55:51 +0000 | |||
111 | +++ debian/changelog 2014-02-25 17:45:25 +0000 | |||
112 | @@ -1,3 +1,35 @@ | |||
113 | 1 | system-image (2.1-0ubuntu4) UNRELEASED; urgency=medium | ||
114 | 2 | |||
115 | 3 | [ Stéphane Graber ] | ||
116 | 4 | * New upstream release. | ||
117 | 5 | * Set X-Auto-Uploader to no-rewrite-version | ||
118 | 6 | * Set Vcs-Bzr to the new target branch | ||
119 | 7 | |||
120 | 8 | [ Barry Warsaw ] | ||
121 | 9 | * New upstream release. | ||
122 | 10 | - LP: #1279056 - Internal improvements to SignatureError for | ||
123 | 11 | better debugging. | ||
124 | 12 | - LP: #1277589 - Better protection against race conditions. | ||
125 | 13 | - LP: #1260768 - Return empty string from ApplyUpdate D-Bus method. | ||
126 | 14 | - LP: #1284217 - Send UpdateAvailableStatus during auto-downloading | ||
127 | 15 | from a previous CheckForUpdate, if cached status is available. | ||
128 | 16 | - Request ubuntu-download-manager to download to a temporary location, | ||
129 | 17 | with atomic rename. | ||
130 | 18 | - More detailed logging. | ||
131 | 19 | - Fixed D-Bus error logging. | ||
132 | 20 | - Added -L flag to nose2 tests for explicitly setting log file path. | ||
133 | 21 | - Added SYSTEMIMAGE_DBUS_DAEMON_HUP_SLEEP_SECONDS environment variable | ||
134 | 22 | which can be used to give virtualized buildds a fighting chance. | ||
135 | 23 | * d/patches/01_send_ack_on_applyupdate.diff: Removed; applied upstream. | ||
136 | 24 | * d/patches/lp1284217.patch: Added (see above). | ||
137 | 25 | * d/control: | ||
138 | 26 | - Bump Standards-Version to 3.9.5 with no other changes necessary. | ||
139 | 27 | - Add python3-psutil as Depends to system-image-dev. | ||
140 | 28 | * d/rules: Set SYSTEMIMAGE_DBUS_DAEMON_HUP_SLEEP_SECONDS to 1 to deal with | ||
141 | 29 | buildd dbus-daemon SIGHUP timing issues. | ||
142 | 30 | |||
143 | 31 | -- Barry Warsaw <barry@ubuntu.com> Thu, 20 Feb 2014 18:03:10 -0500 | ||
144 | 32 | |||
145 | 1 | system-image (2.0.3-0ubuntu2) trusty; urgency=low | 33 | system-image (2.0.3-0ubuntu2) trusty; urgency=low |
146 | 2 | 34 | ||
147 | 3 | * Fix ApplyUpdate() to return an empty string as per spec if the update | 35 | * Fix ApplyUpdate() to return an empty string as per spec if the update |
148 | 4 | 36 | ||
149 | === modified file 'debian/control' | |||
150 | --- debian/control 2013-12-13 13:55:51 +0000 | |||
151 | +++ debian/control 2014-02-25 17:45:25 +0000 | |||
152 | @@ -18,10 +18,11 @@ | |||
153 | 18 | python3-psutil, | 18 | python3-psutil, |
154 | 19 | python3-setuptools, | 19 | python3-setuptools, |
155 | 20 | ubuntu-download-manager | 20 | ubuntu-download-manager |
157 | 21 | Standards-Version: 3.9.4 | 21 | Standards-Version: 3.9.5 |
158 | 22 | XS-Testsuite: autopkgtest | 22 | XS-Testsuite: autopkgtest |
161 | 23 | Vcs-Bzr: https://code.launchpad.net/~ubuntu-system-image/ubuntu-system-image/client.pkg | 23 | Vcs-Bzr: https://code.launchpad.net/~ubuntu-managed-branches/ubuntu-system-image/system-image |
162 | 24 | Vcs-Browser: http://bazaar.launchpad.net/~ubuntu-system-image/ubuntu-system-image/client.pkg/files | 24 | Vcs-Browser: http://bazaar.launchpad.net/~ubuntu-managed-branches/ubuntu-system-image/system-image/files |
163 | 25 | X-Auto-Uploader: no-rewrite-version | ||
164 | 25 | 26 | ||
165 | 26 | Package: system-image-cli | 27 | Package: system-image-cli |
166 | 27 | Architecture: all | 28 | Architecture: all |
167 | @@ -53,7 +54,7 @@ | |||
168 | 53 | 54 | ||
169 | 54 | Package: system-image-dev | 55 | Package: system-image-dev |
170 | 55 | Architecture: all | 56 | Architecture: all |
172 | 56 | Depends: python3-gnupg, ${misc:Depends}, ${python3:Depends} | 57 | Depends: python3-gnupg, python3-psutil, ${misc:Depends}, ${python3:Depends} |
173 | 57 | Description: Ubuntu system image updater development | 58 | Description: Ubuntu system image updater development |
174 | 58 | This is the development bits for the Ubuntu system image updater. | 59 | This is the development bits for the Ubuntu system image updater. |
175 | 59 | Install this package if you want to run the tests. | 60 | Install this package if you want to run the tests. |
176 | 60 | 61 | ||
177 | === removed file 'debian/patches/01_send_ack_on_applyupdate.diff' | |||
178 | --- debian/patches/01_send_ack_on_applyupdate.diff 2013-12-13 13:55:51 +0000 | |||
179 | +++ debian/patches/01_send_ack_on_applyupdate.diff 1970-01-01 00:00:00 +0000 | |||
180 | @@ -1,16 +0,0 @@ | |||
181 | 1 | Description: Send ack on apply diff | ||
182 | 2 | Fix ApplyUpdate() to return an empty string as per spec if the update | ||
183 | 3 | is successfull (LP: #1260712) | ||
184 | 4 | Author: Didier Roche <didrocks@ubuntu.com> | ||
185 | 5 | Bug-Ubuntu: https://bugs.launchpad.net/bugs/1260712 | ||
186 | 6 | |||
187 | 7 | --- system-image-2.0.3.orig/systemimage/dbus.py | ||
188 | 8 | +++ system-image-2.0.3/systemimage/dbus.py | ||
189 | 9 | @@ -233,6 +233,7 @@ class Service(Object): | ||
190 | 10 | def ApplyUpdate(self): | ||
191 | 11 | """Apply the update, rebooting the device.""" | ||
192 | 12 | GLib.timeout_add(50, self._apply_update) | ||
193 | 13 | + return "" | ||
194 | 14 | |||
195 | 15 | @method('com.canonical.SystemImage', out_signature='isssa{ss}') | ||
196 | 16 | def Info(self): | ||
197 | 17 | 0 | ||
198 | === added file 'debian/patches/lp1284217.patch' | |||
199 | --- debian/patches/lp1284217.patch 1970-01-01 00:00:00 +0000 | |||
200 | +++ debian/patches/lp1284217.patch 2014-02-25 17:45:25 +0000 | |||
201 | @@ -0,0 +1,106 @@ | |||
202 | 1 | Description: Backport of fix for LP: #1284217. This adds an additional | ||
203 | 2 | UpdateAvailableStatus signal when a second CheckForUpdate is called while an | ||
204 | 3 | auto-download is in progress, but only if we have cached status available. | ||
205 | 4 | Origin: http://bazaar.launchpad.net/~ubuntu-system-image/ubuntu-system-image/client/revision/240?start_revid=240 | ||
206 | 5 | Bug: http://pad.lv/1284217 | ||
207 | 6 | Forwarded: not-needed | ||
208 | 7 | |||
209 | 8 | === modified file 'systemimage/dbus.py' | ||
210 | 9 | --- old/systemimage/dbus.py 2014-02-18 22:31:55 +0000 | ||
211 | 10 | +++ new/systemimage/dbus.py 2014-02-25 17:27:14 +0000 | ||
212 | 11 | @@ -131,7 +131,20 @@ | ||
213 | 12 | # Check-and-acquire the lock. | ||
214 | 13 | log.info('test and acquire checking lock') | ||
215 | 14 | if not self._checking.acquire(blocking=False): | ||
216 | 15 | - # Check is already in progress, so there's nothing more to do. | ||
217 | 16 | + # Check is already in progress, so there's nothing more to do. If | ||
218 | 17 | + # there's status available (i.e. we are in the auto-downloading | ||
219 | 18 | + # phase of the last CFU), then send the status. | ||
220 | 19 | + if self._update is not None: | ||
221 | 20 | + self.UpdateAvailableStatus( | ||
222 | 21 | + self._update.is_available, | ||
223 | 22 | + self._downloading, | ||
224 | 23 | + self._update.version, | ||
225 | 24 | + self._update.size, | ||
226 | 25 | + self._update.last_update_date, | ||
227 | 26 | + # XXX 2013-08-22 - the u/i cannot currently currently | ||
228 | 27 | + # handle the array of dictionaries data type. LP: | ||
229 | 28 | + # #1215586 self._update.descriptions, | ||
230 | 29 | + "") | ||
231 | 30 | log.info('checking lock not acquired') | ||
232 | 31 | return | ||
233 | 32 | log.info('checking lock acquired') | ||
234 | 33 | |||
235 | 34 | === modified file 'systemimage/tests/test_dbus.py' | ||
236 | 35 | --- old/systemimage/tests/test_dbus.py 2014-02-18 20:02:59 +0000 | ||
237 | 36 | +++ new/systemimage/tests/test_dbus.py 2014-02-25 17:27:14 +0000 | ||
238 | 37 | @@ -23,13 +23,13 @@ | ||
239 | 38 | 'TestDBusGetSet', | ||
240 | 39 | 'TestDBusInfo', | ||
241 | 40 | 'TestDBusInfoNoDetails', | ||
242 | 41 | - 'TestDBusLP1277589', | ||
243 | 42 | 'TestDBusMockFailApply', | ||
244 | 43 | 'TestDBusMockFailPause', | ||
245 | 44 | 'TestDBusMockFailResume', | ||
246 | 45 | 'TestDBusMockNoUpdate', | ||
247 | 46 | 'TestDBusMockUpdateAutoSuccess', | ||
248 | 47 | 'TestDBusMockUpdateManualSuccess', | ||
249 | 48 | + 'TestDBusMultipleChecksInFlight', | ||
250 | 49 | 'TestDBusPauseResume', | ||
251 | 50 | 'TestDBusProgress', | ||
252 | 51 | 'TestDBusRegressions', | ||
253 | 52 | @@ -133,6 +133,7 @@ | ||
254 | 53 | self.schedule(self.iface.CheckForUpdate) | ||
255 | 54 | |||
256 | 55 | def _do_UpdateAvailableStatus(self, signal, path, *args, **kws): | ||
257 | 56 | + # We'll keep doing this until we get the UpdateDownloaded signal. | ||
258 | 57 | self.uas_signals.append(args) | ||
259 | 58 | self.schedule(self.iface.CheckForUpdate) | ||
260 | 59 | |||
261 | 60 | @@ -1563,7 +1564,7 @@ | ||
262 | 61 | """) | ||
263 | 62 | |||
264 | 63 | |||
265 | 64 | -class TestDBusLP1277589(_LiveTesting): | ||
266 | 65 | +class TestDBusMultipleChecksInFlight(_LiveTesting): | ||
267 | 66 | def test_multiple_check_for_updates(self): | ||
268 | 67 | # Log analysis of LP: #1277589 appears to show the following scenario, | ||
269 | 68 | # reproduced in this test case: | ||
270 | 69 | @@ -1588,18 +1589,23 @@ | ||
271 | 70 | # signal, we'll immediately issue *another* CheckForUpdate, which | ||
272 | 71 | # should run while the auto-download is working. | ||
273 | 72 | # | ||
274 | 73 | - # At the end, we should not get another UpdateAvailableStatus signal, | ||
275 | 74 | - # but we should get the UpdateDownloaded signal. | ||
276 | 75 | + # As per LP: #1284217, we will get a second UpdateAvailableStatus | ||
277 | 76 | + # signal, since the status is available even while the original | ||
278 | 77 | + # request is being downloaded. | ||
279 | 78 | reactor = DoubleCheckingReactor(self.iface) | ||
280 | 79 | reactor.run() | ||
281 | 80 | - self.assertEqual(len(reactor.uas_signals), 1) | ||
282 | 81 | - (is_available, downloading, available_version, update_size, | ||
283 | 82 | - last_update_date, | ||
284 | 83 | - #descriptions, | ||
285 | 84 | - error_reason) = reactor.uas_signals[0] | ||
286 | 85 | - self.assertTrue(is_available) | ||
287 | 86 | - self.assertTrue(downloading) | ||
288 | 87 | - self.assertEqual(available_version, '1600') | ||
289 | 88 | - self.assertEqual(update_size, 314572800) | ||
290 | 89 | - self.assertEqual(last_update_date, 'Unknown') | ||
291 | 90 | - self.assertEqual(error_reason, '') | ||
292 | 91 | + # We need to have received at least 2 signals, but due to timing | ||
293 | 92 | + # issues it could possibly be more. | ||
294 | 93 | + self.assertGreater(len(reactor.uas_signals), 1) | ||
295 | 94 | + # All received signals should have the same information. | ||
296 | 95 | + for signal in reactor.uas_signals: | ||
297 | 96 | + (is_available, downloading, available_version, update_size, | ||
298 | 97 | + last_update_date, | ||
299 | 98 | + #descriptions, | ||
300 | 99 | + error_reason) = signal | ||
301 | 100 | + self.assertTrue(is_available) | ||
302 | 101 | + self.assertTrue(downloading) | ||
303 | 102 | + self.assertEqual(available_version, '1600') | ||
304 | 103 | + self.assertEqual(update_size, 314572800) | ||
305 | 104 | + self.assertEqual(last_update_date, 'Unknown') | ||
306 | 105 | + self.assertEqual(error_reason, '') | ||
307 | 106 | |||
308 | 0 | 107 | ||
309 | === modified file 'debian/patches/series' | |||
310 | --- debian/patches/series 2013-12-13 13:55:51 +0000 | |||
311 | +++ debian/patches/series 2014-02-25 17:45:25 +0000 | |||
312 | @@ -1,1 +1,1 @@ | |||
314 | 1 | 01_send_ack_on_applyupdate.diff | 1 | lp1284217.patch |
315 | 2 | 2 | ||
316 | === modified file 'debian/rules' | |||
317 | --- debian/rules 2013-12-13 13:55:51 +0000 | |||
318 | +++ debian/rules 2014-02-25 17:45:25 +0000 | |||
319 | @@ -16,6 +16,7 @@ | |||
320 | 16 | test-python%: | 16 | test-python%: |
321 | 17 | unset http_proxy; unset https_proxy; export HOME=/tmp; \ | 17 | unset http_proxy; unset https_proxy; export HOME=/tmp; \ |
322 | 18 | export SYSTEMIMAGE_REACTOR_TIMEOUT=1200; \ | 18 | export SYSTEMIMAGE_REACTOR_TIMEOUT=1200; \ |
323 | 19 | export SYSTEMIMAGE_DBUS_DAEMON_HUP_SLEEP_SECONDS=2; \ | ||
324 | 19 | nodot=$(shell echo $* | cut --complement -b 2); \ | 20 | nodot=$(shell echo $* | cut --complement -b 2); \ |
325 | 20 | tox -e py$${nodot} | 21 | tox -e py$${nodot} |
326 | 21 | 22 | ||
327 | 22 | 23 | ||
328 | === modified file 'ini-manpage.rst' | |||
329 | --- ini-manpage.rst 2013-12-13 13:55:51 +0000 | |||
330 | +++ ini-manpage.rst 2014-02-25 17:45:25 +0000 | |||
331 | @@ -9,7 +9,7 @@ | |||
332 | 9 | 9 | ||
333 | 10 | :Author: Barry Warsaw <barry@ubuntu.com> | 10 | :Author: Barry Warsaw <barry@ubuntu.com> |
334 | 11 | :Date: 2013-10-11 | 11 | :Date: 2013-10-11 |
336 | 12 | :Copyright: 2013 Canonical Ltd. | 12 | :Copyright: 2013-2014 Canonical Ltd. |
337 | 13 | :Version: 1.9 | 13 | :Version: 1.9 |
338 | 14 | :Manual section: 5 | 14 | :Manual section: 5 |
339 | 15 | 15 | ||
340 | 16 | 16 | ||
341 | === modified file 'setup.cfg' | |||
342 | --- setup.cfg 2013-12-13 13:55:51 +0000 | |||
343 | +++ setup.cfg 2014-02-25 17:45:25 +0000 | |||
344 | @@ -4,7 +4,7 @@ | |||
345 | 4 | logging-filter = systemimage | 4 | logging-filter = systemimage |
346 | 5 | 5 | ||
347 | 6 | [egg_info] | 6 | [egg_info] |
348 | 7 | tag_svn_revision = 0 | ||
349 | 8 | tag_date = 0 | ||
350 | 7 | tag_build = | 9 | tag_build = |
351 | 8 | tag_date = 0 | ||
352 | 9 | tag_svn_revision = 0 | ||
353 | 10 | 10 | ||
354 | 11 | 11 | ||
355 | === modified file 'setup.py' | |||
356 | --- setup.py 2013-12-13 13:55:51 +0000 | |||
357 | +++ setup.py 2014-02-25 17:45:25 +0000 | |||
358 | @@ -1,4 +1,4 @@ | |||
360 | 1 | # Copyright (C) 2013 Canonical Ltd. | 1 | # Copyright (C) 2013-2014 Canonical Ltd. |
361 | 2 | # Author: Barry Warsaw <barry@ubuntu.com> | 2 | # Author: Barry Warsaw <barry@ubuntu.com> |
362 | 3 | 3 | ||
363 | 4 | # This program is free software: you can redistribute it and/or modify | 4 | # This program is free software: you can redistribute it and/or modify |
364 | 5 | 5 | ||
365 | === modified file 'system_image.egg-info/PKG-INFO' | |||
366 | --- system_image.egg-info/PKG-INFO 2013-12-13 13:55:51 +0000 | |||
367 | +++ system_image.egg-info/PKG-INFO 2014-02-25 17:45:25 +0000 | |||
368 | @@ -1,6 +1,6 @@ | |||
369 | 1 | Metadata-Version: 1.0 | 1 | Metadata-Version: 1.0 |
370 | 2 | Name: system-image | 2 | Name: system-image |
372 | 3 | Version: 2.0.3 | 3 | Version: 2.1 |
373 | 4 | Summary: Ubuntu System Image Based Upgrades | 4 | Summary: Ubuntu System Image Based Upgrades |
374 | 5 | Home-page: UNKNOWN | 5 | Home-page: UNKNOWN |
375 | 6 | Author: Barry Warsaw | 6 | Author: Barry Warsaw |
376 | 7 | 7 | ||
377 | === modified file 'system_image.egg-info/SOURCES.txt' | |||
378 | --- system_image.egg-info/SOURCES.txt 2013-12-13 13:55:51 +0000 | |||
379 | +++ system_image.egg-info/SOURCES.txt 2014-02-25 17:45:25 +0000 | |||
380 | @@ -7,7 +7,6 @@ | |||
381 | 7 | setup.py | 7 | setup.py |
382 | 8 | tox.ini | 8 | tox.ini |
383 | 9 | unittest.cfg | 9 | unittest.cfg |
384 | 10 | .bzr/branch/branch.conf | ||
385 | 11 | system_image.egg-info/PKG-INFO | 10 | system_image.egg-info/PKG-INFO |
386 | 12 | system_image.egg-info/SOURCES.txt | 11 | system_image.egg-info/SOURCES.txt |
387 | 13 | system_image.egg-info/dependency_links.txt | 12 | system_image.egg-info/dependency_links.txt |
388 | @@ -126,6 +125,7 @@ | |||
389 | 126 | systemimage/tests/data/index_21.json | 125 | systemimage/tests/data/index_21.json |
390 | 127 | systemimage/tests/data/index_22.json | 126 | systemimage/tests/data/index_22.json |
391 | 128 | systemimage/tests/data/index_23.json | 127 | systemimage/tests/data/index_23.json |
392 | 128 | systemimage/tests/data/index_24.json | ||
393 | 129 | systemimage/tests/data/key.pem | 129 | systemimage/tests/data/key.pem |
394 | 130 | systemimage/tests/data/master-secring.gpg | 130 | systemimage/tests/data/master-secring.gpg |
395 | 131 | systemimage/tests/data/nasty_cert.pem | 131 | systemimage/tests/data/nasty_cert.pem |
396 | 132 | 132 | ||
397 | === modified file 'systemimage/api.py' | |||
398 | --- systemimage/api.py 2013-12-13 13:55:51 +0000 | |||
399 | +++ systemimage/api.py 2014-02-25 17:45:25 +0000 | |||
400 | @@ -1,4 +1,4 @@ | |||
402 | 1 | # Copyright (C) 2013 Canonical Ltd. | 1 | # Copyright (C) 2013-2014 Canonical Ltd. |
403 | 2 | # Author: Barry Warsaw <barry@ubuntu.com> | 2 | # Author: Barry Warsaw <barry@ubuntu.com> |
404 | 3 | 3 | ||
405 | 4 | # This program is free software: you can redistribute it and/or modify | 4 | # This program is free software: you can redistribute it and/or modify |
406 | @@ -73,6 +73,10 @@ | |||
407 | 73 | self._update = None | 73 | self._update = None |
408 | 74 | self._callback = callback | 74 | self._callback = callback |
409 | 75 | 75 | ||
410 | 76 | def __repr__(self): | ||
411 | 77 | return '<Mediator at 0x{:x} | State at 0x{:x}>'.format( | ||
412 | 78 | id(self), id(self._state)) | ||
413 | 79 | |||
414 | 76 | def cancel(self): | 80 | def cancel(self): |
415 | 77 | self._state.downloader.cancel() | 81 | self._state.downloader.cancel() |
416 | 78 | 82 | ||
417 | 79 | 83 | ||
418 | === modified file 'systemimage/bag.py' | |||
419 | --- systemimage/bag.py 2013-12-13 13:55:51 +0000 | |||
420 | +++ systemimage/bag.py 2014-02-25 17:45:25 +0000 | |||
421 | @@ -1,4 +1,4 @@ | |||
423 | 1 | # Copyright (C) 2013 Canonical Ltd. | 1 | # Copyright (C) 2013-2014 Canonical Ltd. |
424 | 2 | # Author: Barry Warsaw <barry@ubuntu.com> | 2 | # Author: Barry Warsaw <barry@ubuntu.com> |
425 | 3 | 3 | ||
426 | 4 | # This program is free software: you can redistribute it and/or modify | 4 | # This program is free software: you can redistribute it and/or modify |
427 | 5 | 5 | ||
428 | === modified file 'systemimage/bindings.py' | |||
429 | --- systemimage/bindings.py 2013-12-13 13:55:51 +0000 | |||
430 | +++ systemimage/bindings.py 2014-02-25 17:45:25 +0000 | |||
431 | @@ -1,4 +1,4 @@ | |||
433 | 1 | # Copyright (C) 2013 Canonical Ltd. | 1 | # Copyright (C) 2013-2014 Canonical Ltd. |
434 | 2 | # Author: Barry Warsaw <barry@ubuntu.com> | 2 | # Author: Barry Warsaw <barry@ubuntu.com> |
435 | 3 | 3 | ||
436 | 4 | # This program is free software: you can redistribute it and/or modify | 4 | # This program is free software: you can redistribute it and/or modify |
437 | 5 | 5 | ||
438 | === modified file 'systemimage/candidates.py' | |||
439 | --- systemimage/candidates.py 2013-12-13 13:55:51 +0000 | |||
440 | +++ systemimage/candidates.py 2014-02-25 17:45:25 +0000 | |||
441 | @@ -1,4 +1,4 @@ | |||
443 | 1 | # Copyright (C) 2013 Canonical Ltd. | 1 | # Copyright (C) 2013-2014 Canonical Ltd. |
444 | 2 | # Author: Barry Warsaw <barry@ubuntu.com> | 2 | # Author: Barry Warsaw <barry@ubuntu.com> |
445 | 3 | 3 | ||
446 | 4 | # This program is free software: you can redistribute it and/or modify | 4 | # This program is free software: you can redistribute it and/or modify |
447 | 5 | 5 | ||
448 | === modified file 'systemimage/channel.py' | |||
449 | --- systemimage/channel.py 2013-12-13 13:55:51 +0000 | |||
450 | +++ systemimage/channel.py 2014-02-25 17:45:25 +0000 | |||
451 | @@ -1,4 +1,4 @@ | |||
453 | 1 | # Copyright (C) 2013 Canonical Ltd. | 1 | # Copyright (C) 2013-2014 Canonical Ltd. |
454 | 2 | # Author: Barry Warsaw <barry@ubuntu.com> | 2 | # Author: Barry Warsaw <barry@ubuntu.com> |
455 | 3 | 3 | ||
456 | 4 | # This program is free software: you can redistribute it and/or modify | 4 | # This program is free software: you can redistribute it and/or modify |
457 | 5 | 5 | ||
458 | === modified file 'systemimage/config.py' | |||
459 | --- systemimage/config.py 2013-12-13 13:55:51 +0000 | |||
460 | +++ systemimage/config.py 2014-02-25 17:45:25 +0000 | |||
461 | @@ -1,4 +1,4 @@ | |||
463 | 1 | # Copyright (C) 2013 Canonical Ltd. | 1 | # Copyright (C) 2013-2014 Canonical Ltd. |
464 | 2 | # Author: Barry Warsaw <barry@ubuntu.com> | 2 | # Author: Barry Warsaw <barry@ubuntu.com> |
465 | 3 | 3 | ||
466 | 4 | # This program is free software: you can redistribute it and/or modify | 4 | # This program is free software: you can redistribute it and/or modify |
467 | 5 | 5 | ||
468 | === modified file 'systemimage/dbus.py' | |||
469 | --- systemimage/dbus.py 2013-12-13 13:55:51 +0000 | |||
470 | +++ systemimage/dbus.py 2014-02-25 17:45:25 +0000 | |||
471 | @@ -1,4 +1,4 @@ | |||
473 | 1 | # Copyright (C) 2013 Canonical Ltd. | 1 | # Copyright (C) 2013-2014 Canonical Ltd. |
474 | 2 | # Author: Barry Warsaw <barry@ubuntu.com> | 2 | # Author: Barry Warsaw <barry@ubuntu.com> |
475 | 3 | 3 | ||
476 | 4 | # This program is free software: you can redistribute it and/or modify | 4 | # This program is free software: you can redistribute it and/or modify |
477 | @@ -23,6 +23,7 @@ | |||
478 | 23 | 23 | ||
479 | 24 | import os | 24 | import os |
480 | 25 | import sys | 25 | import sys |
481 | 26 | import logging | ||
482 | 26 | import traceback | 27 | import traceback |
483 | 27 | 28 | ||
484 | 28 | from dbus.service import Object, method, signal | 29 | from dbus.service import Object, method, signal |
485 | @@ -31,9 +32,11 @@ | |||
486 | 31 | from systemimage.config import config | 32 | from systemimage.config import config |
487 | 32 | from systemimage.helpers import last_update_date, version_detail | 33 | from systemimage.helpers import last_update_date, version_detail |
488 | 33 | from systemimage.settings import Settings | 34 | from systemimage.settings import Settings |
489 | 35 | from threading import Lock | ||
490 | 34 | 36 | ||
491 | 35 | 37 | ||
492 | 36 | EMPTYSTRING = '' | 38 | EMPTYSTRING = '' |
493 | 39 | log = logging.getLogger('systemimage') | ||
494 | 37 | 40 | ||
495 | 38 | 41 | ||
496 | 39 | class Loop: | 42 | class Loop: |
497 | @@ -68,7 +71,8 @@ | |||
498 | 68 | super().__init__(bus, object_path) | 71 | super().__init__(bus, object_path) |
499 | 69 | self._loop = loop | 72 | self._loop = loop |
500 | 70 | self._api = Mediator(self._progress_callback) | 73 | self._api = Mediator(self._progress_callback) |
502 | 71 | self._checking = False | 74 | log.info('Mediator created {}', self._api) |
503 | 75 | self._checking = Lock() | ||
504 | 72 | self._update = None | 76 | self._update = None |
505 | 73 | self._downloading = False | 77 | self._downloading = False |
506 | 74 | self._paused = False | 78 | self._paused = False |
507 | @@ -78,17 +82,22 @@ | |||
508 | 78 | 82 | ||
509 | 79 | def _check_for_update(self): | 83 | def _check_for_update(self): |
510 | 80 | # Asynchronous method call. | 84 | # Asynchronous method call. |
511 | 85 | log.info('Checking for update') | ||
512 | 81 | self._update = self._api.check_for_update() | 86 | self._update = self._api.check_for_update() |
513 | 82 | # Do we have an update and can we auto-download it? | 87 | # Do we have an update and can we auto-download it? |
514 | 83 | downloading = False | 88 | downloading = False |
515 | 84 | if self._update.is_available: | 89 | if self._update.is_available: |
516 | 85 | settings = Settings() | 90 | settings = Settings() |
517 | 86 | auto = settings.get('auto_download') | 91 | auto = settings.get('auto_download') |
518 | 92 | log.info('Update available; auto-download: {}', auto) | ||
519 | 87 | if auto in ('1', '2'): | 93 | if auto in ('1', '2'): |
520 | 88 | # XXX When we have access to the download service, we can | 94 | # XXX When we have access to the download service, we can |
521 | 89 | # check if we're on the wifi (auto == '1'). | 95 | # check if we're on the wifi (auto == '1'). |
523 | 90 | GLib.timeout_add(50, self._download) | 96 | GLib.timeout_add(50, self._download, self._checking.release) |
524 | 91 | downloading = True | 97 | downloading = True |
525 | 98 | else: | ||
526 | 99 | log.info('release checking lock from _check_for_update()') | ||
527 | 100 | self._checking.release() | ||
528 | 92 | self.UpdateAvailableStatus( | 101 | self.UpdateAvailableStatus( |
529 | 93 | self._update.is_available, | 102 | self._update.is_available, |
530 | 94 | downloading, | 103 | downloading, |
531 | @@ -99,7 +108,6 @@ | |||
532 | 99 | # array of dictionaries data type. LP: #1215586 | 108 | # array of dictionaries data type. LP: #1215586 |
533 | 100 | #self._update.descriptions, | 109 | #self._update.descriptions, |
534 | 101 | "") | 110 | "") |
535 | 102 | self._checking = False | ||
536 | 103 | # Stop GLib from calling this method again. | 111 | # Stop GLib from calling this method again. |
537 | 104 | return False | 112 | return False |
538 | 105 | 113 | ||
539 | @@ -120,13 +128,17 @@ | |||
540 | 120 | whether the update is available or not. | 128 | whether the update is available or not. |
541 | 121 | """ | 129 | """ |
542 | 122 | self._loop.keepalive() | 130 | self._loop.keepalive() |
544 | 123 | if self._checking: | 131 | # Check-and-acquire the lock. |
545 | 132 | log.info('test and acquire checking lock') | ||
546 | 133 | if not self._checking.acquire(blocking=False): | ||
547 | 124 | # Check is already in progress, so there's nothing more to do. | 134 | # Check is already in progress, so there's nothing more to do. |
548 | 135 | log.info('checking lock not acquired') | ||
549 | 125 | return | 136 | return |
553 | 126 | self._checking = True | 137 | log.info('checking lock acquired') |
554 | 127 | # Reset any failure or in-progress state. Get a new mediator to reset | 138 | # We've now acquired the lock. Reset any failure or in-progress |
555 | 128 | # any of its state. | 139 | # state. Get a new mediator to reset any of its state. |
556 | 129 | self._api = Mediator(self._progress_callback) | 140 | self._api = Mediator(self._progress_callback) |
557 | 141 | log.info('Mediator recreated {}', self._api) | ||
558 | 130 | self._failure_count = 0 | 142 | self._failure_count = 0 |
559 | 131 | self._last_error = '' | 143 | self._last_error = '' |
560 | 132 | # Arrange for the actual check to happen in a little while, so that | 144 | # Arrange for the actual check to happen in a little while, so that |
561 | @@ -141,24 +153,28 @@ | |||
562 | 141 | eta = 0 | 153 | eta = 0 |
563 | 142 | self.UpdateProgress(percentage, eta) | 154 | self.UpdateProgress(percentage, eta) |
564 | 143 | 155 | ||
566 | 144 | def _download(self): | 156 | def _download(self, release_checking=None): |
567 | 145 | if self._downloading and self._paused: | 157 | if self._downloading and self._paused: |
568 | 146 | self._api.resume() | 158 | self._api.resume() |
569 | 147 | self._paused = False | 159 | self._paused = False |
570 | 160 | log.info('Download previously paused') | ||
571 | 148 | return | 161 | return |
575 | 149 | if (self._downloading # Already in progress. | 162 | if (self._downloading # Already in progress. |
576 | 150 | or self._update is None # Not yet checked. | 163 | or self._update is None # Not yet checked. |
577 | 151 | or not self._update.is_available # No update available. | 164 | or not self._update.is_available # No update available. |
578 | 152 | ): | 165 | ): |
579 | 166 | log.info('Download already in progress or not available') | ||
580 | 153 | return | 167 | return |
581 | 154 | if self._failure_count > 0: | 168 | if self._failure_count > 0: |
582 | 155 | self._failure_count += 1 | 169 | self._failure_count += 1 |
583 | 156 | self.UpdateFailed(self._failure_count, self._last_error) | 170 | self.UpdateFailed(self._failure_count, self._last_error) |
584 | 171 | log.info('Update failure count: {}', self._failure_count) | ||
585 | 157 | return | 172 | return |
586 | 158 | self._downloading = True | 173 | self._downloading = True |
587 | 174 | log.info('Update is downloading') | ||
588 | 159 | try: | 175 | try: |
591 | 160 | # Always start by sending a UpdateProgress(0, 0). This is enough | 176 | # Always start by sending a UpdateProgress(0, 0). This is |
592 | 161 | # to get the u/i's attention. | 177 | # enough to get the u/i's attention. |
593 | 162 | self.UpdateProgress(0, 0) | 178 | self.UpdateProgress(0, 0) |
594 | 163 | self._api.download() | 179 | self._api.download() |
595 | 164 | except Exception: | 180 | except Exception: |
596 | @@ -167,13 +183,20 @@ | |||
597 | 167 | # value, but not the traceback. | 183 | # value, but not the traceback. |
598 | 168 | self._last_error = EMPTYSTRING.join( | 184 | self._last_error = EMPTYSTRING.join( |
599 | 169 | traceback.format_exception_only(*sys.exc_info()[:2])) | 185 | traceback.format_exception_only(*sys.exc_info()[:2])) |
600 | 186 | log.info('Update failed: {}', self._last_error) | ||
601 | 170 | self.UpdateFailed(self._failure_count, self._last_error) | 187 | self.UpdateFailed(self._failure_count, self._last_error) |
602 | 171 | else: | 188 | else: |
603 | 189 | log.info('Update downloaded') | ||
604 | 172 | self.UpdateDownloaded() | 190 | self.UpdateDownloaded() |
605 | 173 | self._failure_count = 0 | 191 | self._failure_count = 0 |
606 | 174 | self._last_error = '' | 192 | self._last_error = '' |
607 | 175 | self._rebootable = True | 193 | self._rebootable = True |
608 | 176 | self._downloading = False | 194 | self._downloading = False |
609 | 195 | log.info('release checking lock from _download()') | ||
610 | 196 | if release_checking is not None: | ||
611 | 197 | # We were auto-downloading, so we now have to release the checking | ||
612 | 198 | # lock. If we were manually downloading, there would be no lock. | ||
613 | 199 | release_checking() | ||
614 | 177 | # Stop GLib from calling this method again. | 200 | # Stop GLib from calling this method again. |
615 | 178 | return False | 201 | return False |
616 | 179 | 202 | ||
617 | @@ -216,8 +239,6 @@ | |||
618 | 216 | return '' | 239 | return '' |
619 | 217 | 240 | ||
620 | 218 | def _apply_update(self): | 241 | def _apply_update(self): |
621 | 219 | # This signal may or may not get sent. We're racing against the | ||
622 | 220 | # system reboot procedure. | ||
623 | 221 | self._loop.keepalive() | 242 | self._loop.keepalive() |
624 | 222 | if not self._rebootable: | 243 | if not self._rebootable: |
625 | 223 | command_file = os.path.join( | 244 | command_file = os.path.join( |
626 | @@ -227,12 +248,16 @@ | |||
627 | 227 | self.Rebooting(False) | 248 | self.Rebooting(False) |
628 | 228 | return | 249 | return |
629 | 229 | self._api.reboot() | 250 | self._api.reboot() |
630 | 251 | # This code may or may not run. We're racing against the system | ||
631 | 252 | # reboot procedure. | ||
632 | 253 | self._rebootable = False | ||
633 | 230 | self.Rebooting(True) | 254 | self.Rebooting(True) |
634 | 231 | 255 | ||
635 | 232 | @method('com.canonical.SystemImage') | 256 | @method('com.canonical.SystemImage') |
636 | 233 | def ApplyUpdate(self): | 257 | def ApplyUpdate(self): |
637 | 234 | """Apply the update, rebooting the device.""" | 258 | """Apply the update, rebooting the device.""" |
638 | 235 | GLib.timeout_add(50, self._apply_update) | 259 | GLib.timeout_add(50, self._apply_update) |
639 | 260 | return '' | ||
640 | 236 | 261 | ||
641 | 237 | @method('com.canonical.SystemImage', out_signature='isssa{ss}') | 262 | @method('com.canonical.SystemImage', out_signature='isssa{ss}') |
642 | 238 | def Info(self): | 263 | def Info(self): |
643 | @@ -294,31 +319,40 @@ | |||
644 | 294 | #descriptions, | 319 | #descriptions, |
645 | 295 | error_reason): | 320 | error_reason): |
646 | 296 | """Signal sent in response to a CheckForUpdate().""" | 321 | """Signal sent in response to a CheckForUpdate().""" |
647 | 322 | log.debug('EMIT UpdateAvailableStatus({}, {}, {}, {}, {}, {})', | ||
648 | 323 | is_available, downloading, available_version, update_size, | ||
649 | 324 | last_update_date, repr(error_reason)) | ||
650 | 297 | self._loop.keepalive() | 325 | self._loop.keepalive() |
651 | 298 | 326 | ||
652 | 299 | @signal('com.canonical.SystemImage', signature='id') | 327 | @signal('com.canonical.SystemImage', signature='id') |
653 | 300 | def UpdateProgress(self, percentage, eta): | 328 | def UpdateProgress(self, percentage, eta): |
654 | 301 | """Download progress.""" | 329 | """Download progress.""" |
655 | 330 | log.debug('EMIT UpdateProgress({}, {})', percentage, eta) | ||
656 | 302 | self._loop.keepalive() | 331 | self._loop.keepalive() |
657 | 303 | 332 | ||
658 | 304 | @signal('com.canonical.SystemImage') | 333 | @signal('com.canonical.SystemImage') |
659 | 305 | def UpdateDownloaded(self): | 334 | def UpdateDownloaded(self): |
660 | 306 | """The update has been successfully downloaded.""" | 335 | """The update has been successfully downloaded.""" |
661 | 336 | log.debug('EMIT UpdateDownloaded()') | ||
662 | 307 | self._loop.keepalive() | 337 | self._loop.keepalive() |
663 | 308 | 338 | ||
664 | 309 | @signal('com.canonical.SystemImage', signature='is') | 339 | @signal('com.canonical.SystemImage', signature='is') |
665 | 310 | def UpdateFailed(self, consecutive_failure_count, last_reason): | 340 | def UpdateFailed(self, consecutive_failure_count, last_reason): |
666 | 311 | """The update failed for some reason.""" | 341 | """The update failed for some reason.""" |
667 | 342 | log.debug('EMIT UpdateFailed({}, {})', | ||
668 | 343 | consecutive_failure_count, repr(last_reason)) | ||
669 | 312 | self._loop.keepalive() | 344 | self._loop.keepalive() |
670 | 313 | 345 | ||
671 | 314 | @signal('com.canonical.SystemImage', signature='i') | 346 | @signal('com.canonical.SystemImage', signature='i') |
672 | 315 | def UpdatePaused(self, percentage): | 347 | def UpdatePaused(self, percentage): |
673 | 316 | """The download got paused.""" | 348 | """The download got paused.""" |
674 | 349 | log.debug('EMIT UpdatePaused({})', percentage) | ||
675 | 317 | self._loop.keepalive() | 350 | self._loop.keepalive() |
676 | 318 | 351 | ||
677 | 319 | @signal('com.canonical.SystemImage', signature='ss') | 352 | @signal('com.canonical.SystemImage', signature='ss') |
678 | 320 | def SettingChanged(self, key, new_value): | 353 | def SettingChanged(self, key, new_value): |
679 | 321 | """A setting value has change.""" | 354 | """A setting value has change.""" |
680 | 355 | log.debug('EMIT SettingChanged({}, {})', repr(key), repr(new_value)) | ||
681 | 322 | self._loop.keepalive() | 356 | self._loop.keepalive() |
682 | 323 | 357 | ||
683 | 324 | @signal('com.canonical.SystemImage', signature='b') | 358 | @signal('com.canonical.SystemImage', signature='b') |
684 | @@ -326,3 +360,4 @@ | |||
685 | 326 | """The system is rebooting.""" | 360 | """The system is rebooting.""" |
686 | 327 | # We don't need to keep the loop alive since we're probably just going | 361 | # We don't need to keep the loop alive since we're probably just going |
687 | 328 | # to shutdown anyway. | 362 | # to shutdown anyway. |
688 | 363 | log.debug('EMIT Rebooting({})', status) | ||
689 | 329 | 364 | ||
690 | === modified file 'systemimage/device.py' | |||
691 | --- systemimage/device.py 2013-12-13 13:55:51 +0000 | |||
692 | +++ systemimage/device.py 2014-02-25 17:45:25 +0000 | |||
693 | @@ -1,4 +1,4 @@ | |||
695 | 1 | # Copyright (C) 2013 Canonical Ltd. | 1 | # Copyright (C) 2013-2014 Canonical Ltd. |
696 | 2 | # Author: Barry Warsaw <barry@ubuntu.com> | 2 | # Author: Barry Warsaw <barry@ubuntu.com> |
697 | 3 | 3 | ||
698 | 4 | # This program is free software: you can redistribute it and/or modify | 4 | # This program is free software: you can redistribute it and/or modify |
699 | 5 | 5 | ||
700 | === modified file 'systemimage/docs/conf.py' | |||
701 | --- systemimage/docs/conf.py 2013-12-13 13:55:51 +0000 | |||
702 | +++ systemimage/docs/conf.py 2014-02-25 17:45:25 +0000 | |||
703 | @@ -41,7 +41,7 @@ | |||
704 | 41 | 41 | ||
705 | 42 | # General information about the project. | 42 | # General information about the project. |
706 | 43 | project = u'Image Update Resolver' | 43 | project = u'Image Update Resolver' |
708 | 44 | copyright = u'2013, Canonical Ltd.' | 44 | copyright = u'2013-2014, Canonical Ltd.' |
709 | 45 | 45 | ||
710 | 46 | # The version info for the project you're documenting, acts as replacement for | 46 | # The version info for the project you're documenting, acts as replacement for |
711 | 47 | # |version| and |release|, also used in various other places throughout the | 47 | # |version| and |release|, also used in various other places throughout the |
712 | 48 | 48 | ||
713 | === modified file 'systemimage/download.py' | |||
714 | --- systemimage/download.py 2013-12-13 13:55:51 +0000 | |||
715 | +++ systemimage/download.py 2014-02-25 17:45:25 +0000 | |||
716 | @@ -1,4 +1,4 @@ | |||
718 | 1 | # Copyright (C) 2013 Canonical Ltd. | 1 | # Copyright (C) 2013-2014 Canonical Ltd. |
719 | 2 | # Author: Barry Warsaw <barry@ubuntu.com> | 2 | # Author: Barry Warsaw <barry@ubuntu.com> |
720 | 3 | 3 | ||
721 | 4 | # This program is free software: you can redistribute it and/or modify | 4 | # This program is free software: you can redistribute it and/or modify |
722 | @@ -22,12 +22,16 @@ | |||
723 | 22 | ] | 22 | ] |
724 | 23 | 23 | ||
725 | 24 | 24 | ||
726 | 25 | import os | ||
727 | 25 | import dbus | 26 | import dbus |
728 | 26 | import logging | 27 | import logging |
729 | 28 | import tempfile | ||
730 | 27 | 29 | ||
731 | 30 | from contextlib import ExitStack | ||
732 | 28 | from io import StringIO | 31 | from io import StringIO |
733 | 29 | from pprint import pformat | 32 | from pprint import pformat |
734 | 30 | from systemimage.config import config | 33 | from systemimage.config import config |
735 | 34 | from systemimage.helpers import safe_remove | ||
736 | 31 | from systemimage.reactor import Reactor | 35 | from systemimage.reactor import Reactor |
737 | 32 | 36 | ||
738 | 33 | 37 | ||
739 | @@ -191,8 +195,25 @@ | |||
740 | 191 | for url, dst in downloads: | 195 | for url, dst in downloads: |
741 | 192 | print('\t{} -> {}'.format(url, dst), file=fp) | 196 | print('\t{} -> {}'.format(url, dst), file=fp) |
742 | 193 | log.info('{}'.format(fp.getvalue())) | 197 | log.info('{}'.format(fp.getvalue())) |
743 | 198 | # As a workaround for LP: #1277589, ask u-d-m to download the files to | ||
744 | 199 | # .tmp files, and if they succeed, then atomically move them into | ||
745 | 200 | # their real location. | ||
746 | 201 | renames = [] | ||
747 | 202 | requests = [] | ||
748 | 203 | for url, dst in downloads: | ||
749 | 204 | head, tail = os.path.split(dst) | ||
750 | 205 | fd, path = tempfile.mkstemp(suffix='.tmp', prefix='', dir=head) | ||
751 | 206 | os.close(fd) | ||
752 | 207 | renames.append((path, dst)) | ||
753 | 208 | requests.append((url, path, '')) | ||
754 | 209 | # mkstemp() creates the file system path, but if the files exist when | ||
755 | 210 | # the group download is requested, ubuntu-download-manager will | ||
756 | 211 | # complain and return an error. So, delete all temporary files now so | ||
757 | 212 | # udm has a clear path to download to. | ||
758 | 213 | for path, dst in renames: | ||
759 | 214 | os.remove(path) | ||
760 | 194 | object_path = iface.createDownloadGroup( | 215 | object_path = iface.createDownloadGroup( |
762 | 195 | [(url, dst, '') for url, dst in downloads], | 216 | requests, # The temporary requests. |
763 | 196 | '', # No hashes yet. | 217 | '', # No hashes yet. |
764 | 197 | False, # Don't allow GSM yet. | 218 | False, # Don't allow GSM yet. |
765 | 198 | # https://bugs.freedesktop.org/show_bug.cgi?id=55594 | 219 | # https://bugs.freedesktop.org/show_bug.cgi?id=55594 |
766 | @@ -220,6 +241,19 @@ | |||
767 | 220 | raise Canceled | 241 | raise Canceled |
768 | 221 | if reactor.timed_out: | 242 | if reactor.timed_out: |
769 | 222 | raise TimeoutError | 243 | raise TimeoutError |
770 | 244 | # Now that everything succeeded, rename the temporary files. Just to | ||
771 | 245 | # be extra cautious, set up a context manager to safely remove all | ||
772 | 246 | # temporary files in case of an error. If there are no errors, then | ||
773 | 247 | # there will be nothing to remove. | ||
774 | 248 | with ExitStack() as resources: | ||
775 | 249 | for tmp, dst in renames: | ||
776 | 250 | resources.callback(safe_remove, tmp) | ||
777 | 251 | for tmp, dst in renames: | ||
778 | 252 | os.rename(tmp, dst) | ||
779 | 253 | # We only get here if all the renames succeeded, so there will be | ||
780 | 254 | # no temporary files to remove, so we can throw away the new | ||
781 | 255 | # ExitStack, which holds all the removals. | ||
782 | 256 | resources.pop_all() | ||
783 | 223 | 257 | ||
784 | 224 | def cancel(self): | 258 | def cancel(self): |
785 | 225 | """Cancel any current downloads.""" | 259 | """Cancel any current downloads.""" |
786 | 226 | 260 | ||
787 | === modified file 'systemimage/gpg.py' | |||
788 | --- systemimage/gpg.py 2013-12-13 13:55:51 +0000 | |||
789 | +++ systemimage/gpg.py 2014-02-25 17:45:25 +0000 | |||
790 | @@ -1,4 +1,4 @@ | |||
792 | 1 | # Copyright (C) 2013 Canonical Ltd. | 1 | # Copyright (C) 2013-2014 Canonical Ltd. |
793 | 2 | # Author: Barry Warsaw <barry@ubuntu.com> | 2 | # Author: Barry Warsaw <barry@ubuntu.com> |
794 | 3 | 3 | ||
795 | 4 | # This program is free software: you can redistribute it and/or modify | 4 | # This program is free software: you can redistribute it and/or modify |
796 | @@ -23,6 +23,7 @@ | |||
797 | 23 | 23 | ||
798 | 24 | import os | 24 | import os |
799 | 25 | import gnupg | 25 | import gnupg |
800 | 26 | import hashlib | ||
801 | 26 | import tarfile | 27 | import tarfile |
802 | 27 | 28 | ||
803 | 28 | from contextlib import ExitStack | 29 | from contextlib import ExitStack |
804 | @@ -37,10 +38,70 @@ | |||
805 | 37 | always returns a boolean. This exception is used by other functions to | 38 | always returns a boolean. This exception is used by other functions to |
806 | 38 | signal that a .asc file did not match. | 39 | signal that a .asc file did not match. |
807 | 39 | """ | 40 | """ |
808 | 41 | def __init__(self, signature_path, data_path, | ||
809 | 42 | keyrings=None, blacklist=None): | ||
810 | 43 | super().__init__() | ||
811 | 44 | self.signature_path = signature_path | ||
812 | 45 | self.data_path = data_path | ||
813 | 46 | self.keyrings = ([] if keyrings is None else keyrings) | ||
814 | 47 | self.blacklist = blacklist | ||
815 | 48 | # We have to calculate the checksums now, because it's possible that | ||
816 | 49 | # the files will be temporary/atomic files, deleted when a context | ||
817 | 50 | # manager exits. I.e. the files aren't guaranteed to exist after this | ||
818 | 51 | # constructor runs. | ||
819 | 52 | # | ||
820 | 53 | # Also, md5 is fine; this is not a security critical context, we just | ||
821 | 54 | # want to be able to quickly and easily compare the file on disk | ||
822 | 55 | # against the file on the server. | ||
823 | 56 | with open(self.signature_path, 'rb') as fp: | ||
824 | 57 | self.signature_checksum = hashlib.md5(fp.read()).hexdigest() | ||
825 | 58 | with open(self.data_path, 'rb') as fp: | ||
826 | 59 | self.data_checksum = hashlib.md5(fp.read()).hexdigest() | ||
827 | 60 | self.keyring_checksums = [] | ||
828 | 61 | for path in self.keyrings: | ||
829 | 62 | with open(path, 'rb') as fp: | ||
830 | 63 | checksum = hashlib.md5(fp.read()).hexdigest() | ||
831 | 64 | self.keyring_checksums.append(checksum) | ||
832 | 65 | if self.blacklist is None: | ||
833 | 66 | self.blacklist_checksum = None | ||
834 | 67 | else: | ||
835 | 68 | with open(self.blacklist, 'rb') as fp: | ||
836 | 69 | self.blacklist_checksum = hashlib.md5(fp.read()).hexdigest() | ||
837 | 70 | |||
838 | 71 | def __str__(self): | ||
839 | 72 | if self.blacklist is None: | ||
840 | 73 | checksum_str = 'no blacklist' | ||
841 | 74 | path_str = '' | ||
842 | 75 | else: | ||
843 | 76 | checksum_str = self.blacklist_checksum | ||
844 | 77 | path_str = self.blacklist | ||
845 | 78 | return """ | ||
846 | 79 | sig path : {0.signature_checksum} | ||
847 | 80 | {0.signature_path} | ||
848 | 81 | data path: {0.data_checksum} | ||
849 | 82 | {0.data_path} | ||
850 | 83 | keyrings : {0.keyring_checksums} | ||
851 | 84 | {1} | ||
852 | 85 | blacklist: {2} {3} | ||
853 | 86 | """.format(self, list(self.keyrings), checksum_str, path_str) | ||
854 | 87 | |||
855 | 40 | 88 | ||
856 | 41 | 89 | ||
857 | 42 | class Context: | 90 | class Context: |
858 | 43 | def __init__(self, *keyrings, blacklist=None): | 91 | def __init__(self, *keyrings, blacklist=None): |
859 | 92 | """Create a GPG signature verification context. | ||
860 | 93 | |||
861 | 94 | :param keyrings: The list of keyrings to use for validating the | ||
862 | 95 | signature on data files. | ||
863 | 96 | :type keyrings: Sequence of .tar.xz keyring files, which will be | ||
864 | 97 | unpacked to retrieve the actual .gpg keyring file. | ||
865 | 98 | :param blacklist: The blacklist keyring, from which fingerprints to | ||
866 | 99 | explicitly disallow are retrieved. | ||
867 | 100 | :type blacklist: A .tar.xz keyring file, which will be unpacked to | ||
868 | 101 | retrieve the actual .gpg keyring file. | ||
869 | 102 | """ | ||
870 | 103 | self.keyring_paths = keyrings | ||
871 | 104 | self.blacklist_path = blacklist | ||
872 | 44 | self._ctx = None | 105 | self._ctx = None |
873 | 45 | self._stack = ExitStack() | 106 | self._stack = ExitStack() |
874 | 46 | self._keyrings = [] | 107 | self._keyrings = [] |
875 | @@ -112,6 +173,21 @@ | |||
876 | 112 | return set(info['keyid'] for info in self._ctx.list_keys()) | 173 | return set(info['keyid'] for info in self._ctx.list_keys()) |
877 | 113 | 174 | ||
878 | 114 | def verify(self, signature_path, data_path): | 175 | def verify(self, signature_path, data_path): |
879 | 176 | """Verify a GPG signature. | ||
880 | 177 | |||
881 | 178 | This verifies that the data file signature is valid, given the | ||
882 | 179 | keyrings and blacklist specified in the constructor. Specifically, we | ||
883 | 180 | use GPG to extract the fingerprint in the signature path, and compare | ||
884 | 181 | it against the fingerprints in the keyrings, subtracting any | ||
885 | 182 | fingerprints in the blacklist. | ||
886 | 183 | |||
887 | 184 | :param signature_path: The file system path to the detached signature | ||
888 | 185 | file for the data file. | ||
889 | 186 | :type signature_path: str | ||
890 | 187 | :param data_path: The file system path to the data file. | ||
891 | 188 | :type data_path: str | ||
892 | 189 | :return: bool | ||
893 | 190 | """ | ||
894 | 115 | with open(signature_path, 'rb') as sig_fp: | 191 | with open(signature_path, 'rb') as sig_fp: |
895 | 116 | verified = self._ctx.verify_file(sig_fp, data_path) | 192 | verified = self._ctx.verify_file(sig_fp, data_path) |
896 | 117 | # If the file is properly signed, we'll be able to get back a set of | 193 | # If the file is properly signed, we'll be able to get back a set of |
897 | @@ -120,3 +196,21 @@ | |||
898 | 120 | # loaded-up keyrings. If so, the signature succeeds. | 196 | # loaded-up keyrings. If so, the signature succeeds. |
899 | 121 | return verified.fingerprint in (self.fingerprints - | 197 | return verified.fingerprint in (self.fingerprints - |
900 | 122 | self._blacklisted_fingerprints) | 198 | self._blacklisted_fingerprints) |
901 | 199 | |||
902 | 200 | def validate(self, signature_path, data_path): | ||
903 | 201 | """Like .verify() but raises a SignatureError when invalid. | ||
904 | 202 | |||
905 | 203 | :param signature_path: The file system path to the detached signature | ||
906 | 204 | file for the data file. | ||
907 | 205 | :type signature_path: str | ||
908 | 206 | :param data_path: The file system path to the data file. | ||
909 | 207 | :type data_path: str | ||
910 | 208 | :return: None | ||
911 | 209 | :raises SignatureError: when the signature cannot be verified. Note | ||
912 | 210 | that the exception will contain extra information, namely the | ||
913 | 211 | keyrings involved in the verification, as well as the blacklist | ||
914 | 212 | file if there is one. | ||
915 | 213 | """ | ||
916 | 214 | if not self.verify(signature_path, data_path): | ||
917 | 215 | raise SignatureError(signature_path, data_path, | ||
918 | 216 | self.keyring_paths, self.blacklist_path) | ||
919 | 123 | 217 | ||
920 | === modified file 'systemimage/helpers.py' | |||
921 | --- systemimage/helpers.py 2013-12-13 13:55:51 +0000 | |||
922 | +++ systemimage/helpers.py 2014-02-25 17:45:25 +0000 | |||
923 | @@ -1,4 +1,4 @@ | |||
925 | 1 | # Copyright (C) 2013 Canonical Ltd. | 1 | # Copyright (C) 2013-2014 Canonical Ltd. |
926 | 2 | # Author: Barry Warsaw <barry@ubuntu.com> | 2 | # Author: Barry Warsaw <barry@ubuntu.com> |
927 | 3 | 3 | ||
928 | 4 | # This program is free software: you can redistribute it and/or modify | 4 | # This program is free software: you can redistribute it and/or modify |
929 | 5 | 5 | ||
930 | === modified file 'systemimage/image.py' | |||
931 | --- systemimage/image.py 2013-12-13 13:55:51 +0000 | |||
932 | +++ systemimage/image.py 2014-02-25 17:45:25 +0000 | |||
933 | @@ -1,4 +1,4 @@ | |||
935 | 1 | # Copyright (C) 2013 Canonical Ltd. | 1 | # Copyright (C) 2013-2014 Canonical Ltd. |
936 | 2 | # Author: Barry Warsaw <barry@ubuntu.com> | 2 | # Author: Barry Warsaw <barry@ubuntu.com> |
937 | 3 | 3 | ||
938 | 4 | # This program is free software: you can redistribute it and/or modify | 4 | # This program is free software: you can redistribute it and/or modify |
939 | 5 | 5 | ||
940 | === modified file 'systemimage/index.py' | |||
941 | --- systemimage/index.py 2013-12-13 13:55:51 +0000 | |||
942 | +++ systemimage/index.py 2014-02-25 17:45:25 +0000 | |||
943 | @@ -1,4 +1,4 @@ | |||
945 | 1 | # Copyright (C) 2013 Canonical Ltd. | 1 | # Copyright (C) 2013-2014 Canonical Ltd. |
946 | 2 | # Author: Barry Warsaw <barry@ubuntu.com> | 2 | # Author: Barry Warsaw <barry@ubuntu.com> |
947 | 3 | 3 | ||
948 | 4 | # This program is free software: you can redistribute it and/or modify | 4 | # This program is free software: you can redistribute it and/or modify |
949 | 5 | 5 | ||
950 | === modified file 'systemimage/keyring.py' | |||
951 | --- systemimage/keyring.py 2013-12-13 13:55:51 +0000 | |||
952 | +++ systemimage/keyring.py 2014-02-25 17:45:25 +0000 | |||
953 | @@ -1,4 +1,4 @@ | |||
955 | 1 | # Copyright (C) 2013 Canonical Ltd. | 1 | # Copyright (C) 2013-2014 Canonical Ltd. |
956 | 2 | # Author: Barry Warsaw <barry@ubuntu.com> | 2 | # Author: Barry Warsaw <barry@ubuntu.com> |
957 | 3 | 3 | ||
958 | 4 | # This program is free software: you can redistribute it and/or modify | 4 | # This program is free software: you can redistribute it and/or modify |
959 | @@ -31,7 +31,7 @@ | |||
960 | 31 | from datetime import datetime, timezone | 31 | from datetime import datetime, timezone |
961 | 32 | from systemimage.config import config | 32 | from systemimage.config import config |
962 | 33 | from systemimage.download import DBusDownloadManager | 33 | from systemimage.download import DBusDownloadManager |
964 | 34 | from systemimage.gpg import Context, SignatureError | 34 | from systemimage.gpg import Context |
965 | 35 | from systemimage.helpers import makedirs, safe_remove | 35 | from systemimage.helpers import makedirs, safe_remove |
966 | 36 | from urllib.parse import urljoin | 36 | from urllib.parse import urljoin |
967 | 37 | 37 | ||
968 | @@ -110,8 +110,7 @@ | |||
969 | 110 | stack.callback(os.remove, ascxz_dst) | 110 | stack.callback(os.remove, ascxz_dst) |
970 | 111 | signing_keyring = getattr(config.gpg, sigkr.replace('-', '_')) | 111 | signing_keyring = getattr(config.gpg, sigkr.replace('-', '_')) |
971 | 112 | with Context(signing_keyring, blacklist=blacklist) as ctx: | 112 | with Context(signing_keyring, blacklist=blacklist) as ctx: |
974 | 113 | if not ctx.verify(ascxz_dst, tarxz_dst): | 113 | ctx.validate(ascxz_dst, tarxz_dst) |
973 | 114 | raise SignatureError | ||
975 | 115 | # The signature is good, so now unpack the tarball, load the json file | 114 | # The signature is good, so now unpack the tarball, load the json file |
976 | 116 | # and verify its contents. | 115 | # and verify its contents. |
977 | 117 | keyring_gpg = os.path.join(config.tempdir, 'keyring.gpg') | 116 | keyring_gpg = os.path.join(config.tempdir, 'keyring.gpg') |
978 | 118 | 117 | ||
979 | === modified file 'systemimage/logging.py' | |||
980 | --- systemimage/logging.py 2013-12-13 13:55:51 +0000 | |||
981 | +++ systemimage/logging.py 2014-02-25 17:45:25 +0000 | |||
982 | @@ -1,4 +1,4 @@ | |||
984 | 1 | # Copyright (C) 2013 Canonical Ltd. | 1 | # Copyright (C) 2013-2014 Canonical Ltd. |
985 | 2 | # Author: Barry Warsaw <barry@ubuntu.com> | 2 | # Author: Barry Warsaw <barry@ubuntu.com> |
986 | 3 | 3 | ||
987 | 4 | # This program is free software: you can redistribute it and/or modify | 4 | # This program is free software: you can redistribute it and/or modify |
988 | @@ -36,12 +36,35 @@ | |||
989 | 36 | LOGFILE_PERMISSIONS = stat.S_IRUSR | stat.S_IWUSR | 36 | LOGFILE_PERMISSIONS = stat.S_IRUSR | stat.S_IWUSR |
990 | 37 | 37 | ||
991 | 38 | 38 | ||
992 | 39 | # We want to support {}-style logging for all systemimage child loggers. One | ||
993 | 40 | # way to do this is with a LogRecord factory, but to play nice with third | ||
994 | 41 | # party loggers which might be using %-style, we have to make sure that we use | ||
995 | 42 | # the default factory for everything else. | ||
996 | 43 | # | ||
997 | 44 | # This actually isn't the best way to do this because it still makes a global | ||
998 | 45 | # change and we don't know how this will interact with other third party | ||
999 | 46 | # loggers. A marginally better way to do this is to pass class instances to | ||
1000 | 47 | # the logging calls. Those instances would have a __str__() method that does | ||
1001 | 48 | # the .format() conversion. The problem with that is that it's a bit less | ||
1002 | 49 | # convenient to make the logging calls because you can't pass strings | ||
1003 | 50 | # directly. One such suggestion at <http://tinyurl.com/pjjwjxq> is to import | ||
1004 | 51 | # the class as __ (i.e. double underscore) so your logging calls would look | ||
1005 | 52 | # like: log.error(__('Message with {} {}'), foo, bar) | ||
1006 | 53 | |||
1007 | 39 | class FormattingLogRecord(logging.LogRecord): | 54 | class FormattingLogRecord(logging.LogRecord): |
1008 | 55 | def __init__(self, name, *args, **kws): | ||
1009 | 56 | logger_path = name.split('.') | ||
1010 | 57 | self._use_format = (logger_path[0] == 'systemimage') | ||
1011 | 58 | super().__init__(name, *args, **kws) | ||
1012 | 59 | |||
1013 | 40 | def getMessage(self): | 60 | def getMessage(self): |
1018 | 41 | msg = str(self.msg) | 61 | if self._use_format: |
1019 | 42 | if self.args: | 62 | msg = str(self.msg) |
1020 | 43 | msg = msg.format(*self.args) | 63 | if self.args: |
1021 | 44 | return msg | 64 | msg = msg.format(*self.args) |
1022 | 65 | return msg | ||
1023 | 66 | else: | ||
1024 | 67 | return super().getMessage() | ||
1025 | 45 | 68 | ||
1026 | 46 | 69 | ||
1027 | 47 | def initialize(*, verbosity=0): | 70 | def initialize(*, verbosity=0): |
1028 | @@ -53,9 +76,7 @@ | |||
1029 | 53 | 3: logging.CRITICAL, | 76 | 3: logging.CRITICAL, |
1030 | 54 | }.get(verbosity, logging.ERROR) | 77 | }.get(verbosity, logging.ERROR) |
1031 | 55 | level = min(level, config.system.loglevel) | 78 | level = min(level, config.system.loglevel) |
1035 | 56 | # We're not going to propagate to the root logger anyway. | 79 | # Make sure our library's logging uses {}-style messages. |
1033 | 57 | logging.basicConfig(style='{') | ||
1034 | 58 | # Make sure logging uses {}-style messages. | ||
1036 | 59 | logging.setLogRecordFactory(FormattingLogRecord) | 80 | logging.setLogRecordFactory(FormattingLogRecord) |
1037 | 60 | # Now configure the application level logger based on the ini file. | 81 | # Now configure the application level logger based on the ini file. |
1038 | 61 | log = logging.getLogger('systemimage') | 82 | log = logging.getLogger('systemimage') |
1039 | 62 | 83 | ||
1040 | === modified file 'systemimage/main.py' | |||
1041 | --- systemimage/main.py 2013-12-13 13:55:51 +0000 | |||
1042 | +++ systemimage/main.py 2014-02-25 17:45:25 +0000 | |||
1043 | @@ -1,4 +1,4 @@ | |||
1045 | 1 | # Copyright (C) 2013 Canonical Ltd. | 1 | # Copyright (C) 2013-2014 Canonical Ltd. |
1046 | 2 | # Author: Barry Warsaw <barry@ubuntu.com> | 2 | # Author: Barry Warsaw <barry@ubuntu.com> |
1047 | 3 | 3 | ||
1048 | 4 | # This program is free software: you can redistribute it and/or modify | 4 | # This program is free software: you can redistribute it and/or modify |
1049 | 5 | 5 | ||
1050 | === modified file 'systemimage/reactor.py' | |||
1051 | --- systemimage/reactor.py 2013-12-13 13:55:51 +0000 | |||
1052 | +++ systemimage/reactor.py 2014-02-25 17:45:25 +0000 | |||
1053 | @@ -1,4 +1,4 @@ | |||
1055 | 1 | # Copyright (C) 2013 Canonical Ltd. | 1 | # Copyright (C) 2013-2014 Canonical Ltd. |
1056 | 2 | # Author: Barry Warsaw <barry@ubuntu.com> | 2 | # Author: Barry Warsaw <barry@ubuntu.com> |
1057 | 3 | 3 | ||
1058 | 4 | # This program is free software: you can redistribute it and/or modify | 4 | # This program is free software: you can redistribute it and/or modify |
1059 | 5 | 5 | ||
1060 | === modified file 'systemimage/reboot.py' | |||
1061 | --- systemimage/reboot.py 2013-12-13 13:55:51 +0000 | |||
1062 | +++ systemimage/reboot.py 2014-02-25 17:45:25 +0000 | |||
1063 | @@ -1,4 +1,4 @@ | |||
1065 | 1 | # Copyright (C) 2013 Canonical Ltd. | 1 | # Copyright (C) 2013-2014 Canonical Ltd. |
1066 | 2 | # Author: Barry Warsaw <barry@ubuntu.com> | 2 | # Author: Barry Warsaw <barry@ubuntu.com> |
1067 | 3 | 3 | ||
1068 | 4 | # This program is free software: you can redistribute it and/or modify | 4 | # This program is free software: you can redistribute it and/or modify |
1069 | 5 | 5 | ||
1070 | === modified file 'systemimage/scores.py' | |||
1071 | --- systemimage/scores.py 2013-12-13 13:55:51 +0000 | |||
1072 | +++ systemimage/scores.py 2014-02-25 17:45:25 +0000 | |||
1073 | @@ -1,4 +1,4 @@ | |||
1075 | 1 | # Copyright (C) 2013 Canonical Ltd. | 1 | # Copyright (C) 2013-2014 Canonical Ltd. |
1076 | 2 | # Author: Barry Warsaw <barry@ubuntu.com> | 2 | # Author: Barry Warsaw <barry@ubuntu.com> |
1077 | 3 | 3 | ||
1078 | 4 | # This program is free software: you can redistribute it and/or modify | 4 | # This program is free software: you can redistribute it and/or modify |
1079 | 5 | 5 | ||
1080 | === modified file 'systemimage/service.py' | |||
1081 | --- systemimage/service.py 2013-12-13 13:55:51 +0000 | |||
1082 | +++ systemimage/service.py 2014-02-25 17:45:25 +0000 | |||
1083 | @@ -1,4 +1,4 @@ | |||
1085 | 1 | # Copyright (C) 2013 Canonical Ltd. | 1 | # Copyright (C) 2013-2014 Canonical Ltd. |
1086 | 2 | # Author: Barry Warsaw <barry@ubuntu.com> | 2 | # Author: Barry Warsaw <barry@ubuntu.com> |
1087 | 3 | 3 | ||
1088 | 4 | # This program is free software: you can redistribute it and/or modify | 4 | # This program is free software: you can redistribute it and/or modify |
1089 | @@ -28,10 +28,9 @@ | |||
1090 | 28 | 28 | ||
1091 | 29 | from contextlib import ExitStack | 29 | from contextlib import ExitStack |
1092 | 30 | from dbus.mainloop.glib import DBusGMainLoop | 30 | from dbus.mainloop.glib import DBusGMainLoop |
1093 | 31 | from dbus.service import BusName | ||
1094 | 32 | from pkg_resources import resource_string as resource_bytes | 31 | from pkg_resources import resource_string as resource_bytes |
1095 | 33 | from systemimage.config import config | 32 | from systemimage.config import config |
1097 | 34 | from systemimage.dbus import Loop, Service | 33 | from systemimage.dbus import Loop |
1098 | 35 | from systemimage.helpers import makedirs | 34 | from systemimage.helpers import makedirs |
1099 | 36 | from systemimage.logging import initialize | 35 | from systemimage.logging import initialize |
1100 | 37 | from systemimage.main import DEFAULT_CONFIG_FILE | 36 | from systemimage.main import DEFAULT_CONFIG_FILE |
1101 | @@ -92,12 +91,20 @@ | |||
1102 | 92 | initialize(verbosity=args.verbose) | 91 | initialize(verbosity=args.verbose) |
1103 | 93 | log = logging.getLogger('systemimage') | 92 | log = logging.getLogger('systemimage') |
1104 | 94 | 93 | ||
1105 | 95 | log.info('SystemImage dbus main loop started [{}/{}]', | ||
1106 | 96 | config.channel, config.device) | ||
1107 | 97 | DBusGMainLoop(set_as_default=True) | 94 | DBusGMainLoop(set_as_default=True) |
1108 | 98 | 95 | ||
1109 | 99 | system_bus = dbus.SystemBus() | 96 | system_bus = dbus.SystemBus() |
1111 | 100 | bus_name = BusName('com.canonical.SystemImage', system_bus) | 97 | # Ensure we're the only owner of this bus name. |
1112 | 98 | code = system_bus.request_name( | ||
1113 | 99 | 'com.canonical.SystemImage', | ||
1114 | 100 | dbus.bus.NAME_FLAG_DO_NOT_QUEUE) | ||
1115 | 101 | if code == dbus.bus.REQUEST_NAME_REPLY_EXISTS: | ||
1116 | 102 | # Another instance already owns this name. Exit. | ||
1117 | 103 | log.error('Cannot get exclusive ownership of bus name.') | ||
1118 | 104 | sys.exit(2) | ||
1119 | 105 | |||
1120 | 106 | log.info('SystemImage dbus main loop starting [{}/{}]', | ||
1121 | 107 | config.channel, config.device) | ||
1122 | 101 | 108 | ||
1123 | 102 | with ExitStack() as stack: | 109 | with ExitStack() as stack: |
1124 | 103 | loop = Loop() | 110 | loop = Loop() |
1125 | @@ -107,6 +114,7 @@ | |||
1126 | 107 | config.dbus_service = get_service( | 114 | config.dbus_service = get_service( |
1127 | 108 | testing_mode, system_bus, '/Service', loop) | 115 | testing_mode, system_bus, '/Service', loop) |
1128 | 109 | else: | 116 | else: |
1129 | 117 | from systemimage.dbus import Service | ||
1130 | 110 | config.dbus_service = Service(system_bus, '/Service', loop) | 118 | config.dbus_service = Service(system_bus, '/Service', loop) |
1131 | 111 | try: | 119 | try: |
1132 | 112 | loop.run() | 120 | loop.run() |
1133 | 113 | 121 | ||
1134 | === modified file 'systemimage/settings.py' | |||
1135 | --- systemimage/settings.py 2013-12-13 13:55:51 +0000 | |||
1136 | +++ systemimage/settings.py 2014-02-25 17:45:25 +0000 | |||
1137 | @@ -1,4 +1,4 @@ | |||
1139 | 1 | # Copyright (C) 2013 Canonical Ltd. | 1 | # Copyright (C) 2013-2014 Canonical Ltd. |
1140 | 2 | # Author: Barry Warsaw <barry@ubuntu.com> | 2 | # Author: Barry Warsaw <barry@ubuntu.com> |
1141 | 3 | 3 | ||
1142 | 4 | # This program is free software: you can redistribute it and/or modify | 4 | # This program is free software: you can redistribute it and/or modify |
1143 | 5 | 5 | ||
1144 | === modified file 'systemimage/state.py' | |||
1145 | --- systemimage/state.py 2013-12-13 13:55:51 +0000 | |||
1146 | +++ systemimage/state.py 2014-02-25 17:45:25 +0000 | |||
1147 | @@ -1,4 +1,4 @@ | |||
1149 | 1 | # Copyright (C) 2013 Canonical Ltd. | 1 | # Copyright (C) 2013-2014 Canonical Ltd. |
1150 | 2 | # Author: Barry Warsaw <barry@ubuntu.com> | 2 | # Author: Barry Warsaw <barry@ubuntu.com> |
1151 | 3 | 3 | ||
1152 | 4 | # This program is free software: you can redistribute it and/or modify | 4 | # This program is free software: you can redistribute it and/or modify |
1153 | @@ -217,7 +217,7 @@ | |||
1154 | 217 | urljoin(config.service.https_base, url))) | 217 | urljoin(config.service.https_base, url))) |
1155 | 218 | get_keyring('blacklist', url, 'image-master') | 218 | get_keyring('blacklist', url, 'image-master') |
1156 | 219 | except SignatureError: | 219 | except SignatureError: |
1158 | 220 | log.info('No signed blacklist found') | 220 | log.exception('No signed blacklist found') |
1159 | 221 | # The blacklist wasn't signed by the system image master. Maybe | 221 | # The blacklist wasn't signed by the system image master. Maybe |
1160 | 222 | # there's a new system image master key? Let's find out. | 222 | # there's a new system image master key? Let's find out. |
1161 | 223 | self._next.appendleft(self._get_master_key) | 223 | self._next.appendleft(self._get_master_key) |
1162 | @@ -293,14 +293,16 @@ | |||
1163 | 293 | # SIGNING key. There may or may not be a blacklist. | 293 | # SIGNING key. There may or may not be a blacklist. |
1164 | 294 | ctx = stack.enter_context( | 294 | ctx = stack.enter_context( |
1165 | 295 | Context(config.gpg.image_signing, blacklist=self.blacklist)) | 295 | Context(config.gpg.image_signing, blacklist=self.blacklist)) |
1167 | 296 | if not ctx.verify(asc_path, channels_path): | 296 | try: |
1168 | 297 | ctx.validate(asc_path, channels_path) | ||
1169 | 298 | except SignatureError: | ||
1170 | 297 | # The signature on the channels.json file did not match. | 299 | # The signature on the channels.json file did not match. |
1171 | 298 | # Maybe there's a new image signing key on the server. If | 300 | # Maybe there's a new image signing key on the server. If |
1172 | 299 | # we've already downloaded a new image signing key, then | 301 | # we've already downloaded a new image signing key, then |
1173 | 300 | # there's nothing more to do but raise an exception. | 302 | # there's nothing more to do but raise an exception. |
1174 | 301 | # Otherwise, if a new key *is* found, retry the current step. | 303 | # Otherwise, if a new key *is* found, retry the current step. |
1175 | 302 | if count > 0: | 304 | if count > 0: |
1177 | 303 | raise SignatureError(channels_path) | 305 | raise |
1178 | 304 | self._next.appendleft(self._get_signing_key) | 306 | self._next.appendleft(self._get_signing_key) |
1179 | 305 | log.info('channels.json not properly signed') | 307 | log.info('channels.json not properly signed') |
1180 | 306 | return | 308 | return |
1181 | @@ -398,10 +400,7 @@ | |||
1182 | 398 | keyrings.append(config.gpg.device_signing) | 400 | keyrings.append(config.gpg.device_signing) |
1183 | 399 | ctx = stack.enter_context( | 401 | ctx = stack.enter_context( |
1184 | 400 | Context(*keyrings, blacklist=self.blacklist)) | 402 | Context(*keyrings, blacklist=self.blacklist)) |
1189 | 401 | if not ctx.verify(asc_path, index_path): | 403 | ctx.validate(asc_path, index_path) |
1186 | 402 | log.error('index.json signature failure: {} {}', | ||
1187 | 403 | index_path, asc_path) | ||
1188 | 404 | raise SignatureError(index_path) | ||
1190 | 405 | # The signature was good. | 404 | # The signature was good. |
1191 | 406 | with open(index_path, encoding='utf-8') as fp: | 405 | with open(index_path, encoding='utf-8') as fp: |
1192 | 407 | self.index = Index.from_json(fp.read()) | 406 | self.index = Index.from_json(fp.read()) |
1193 | @@ -512,8 +511,7 @@ | |||
1194 | 512 | # Verify the signatures on all the downloaded files. | 511 | # Verify the signatures on all the downloaded files. |
1195 | 513 | with Context(*keyrings, blacklist=self.blacklist) as ctx: | 512 | with Context(*keyrings, blacklist=self.blacklist) as ctx: |
1196 | 514 | for dst, asc in signatures: | 513 | for dst, asc in signatures: |
1199 | 515 | if not ctx.verify(asc, dst): | 514 | ctx.validate(asc, dst) |
1198 | 516 | raise SignatureError(dst) | ||
1200 | 517 | # Verify the checksums. | 515 | # Verify the checksums. |
1201 | 518 | for dst, checksum in checksums: | 516 | for dst, checksum in checksums: |
1202 | 519 | with open(dst, 'rb') as fp: | 517 | with open(dst, 'rb') as fp: |
1203 | 520 | 518 | ||
1204 | === modified file 'systemimage/testing/controller.py' | |||
1205 | --- systemimage/testing/controller.py 2013-12-13 13:55:51 +0000 | |||
1206 | +++ systemimage/testing/controller.py 2014-02-25 17:45:25 +0000 | |||
1207 | @@ -1,4 +1,4 @@ | |||
1209 | 1 | # Copyright (C) 2013 Canonical Ltd. | 1 | # Copyright (C) 2013-2014 Canonical Ltd. |
1210 | 2 | # Author: Barry Warsaw <barry@ubuntu.com> | 2 | # Author: Barry Warsaw <barry@ubuntu.com> |
1211 | 3 | 3 | ||
1212 | 4 | # This program is free software: you can redistribute it and/or modify | 4 | # This program is free software: you can redistribute it and/or modify |
1213 | @@ -25,8 +25,8 @@ | |||
1214 | 25 | import pwd | 25 | import pwd |
1215 | 26 | import sys | 26 | import sys |
1216 | 27 | import dbus | 27 | import dbus |
1217 | 28 | import time | ||
1218 | 28 | import psutil | 29 | import psutil |
1219 | 29 | import signal | ||
1220 | 30 | import subprocess | 30 | import subprocess |
1221 | 31 | 31 | ||
1222 | 32 | from contextlib import ExitStack | 32 | from contextlib import ExitStack |
1223 | @@ -38,6 +38,8 @@ | |||
1224 | 38 | 38 | ||
1225 | 39 | 39 | ||
1226 | 40 | SPACE = ' ' | 40 | SPACE = ' ' |
1227 | 41 | OVERRIDE = os.environ.get('SYSTEMIMAGE_DBUS_DAEMON_HUP_SLEEP_SECONDS') | ||
1228 | 42 | HUP_SLEEP = (0 if OVERRIDE is None else int(OVERRIDE)) | ||
1229 | 41 | 43 | ||
1230 | 42 | 44 | ||
1231 | 43 | def start_system_image(controller): | 45 | def start_system_image(controller): |
1232 | @@ -125,7 +127,7 @@ | |||
1233 | 125 | class Controller: | 127 | class Controller: |
1234 | 126 | """Start and stop D-Bus service under test.""" | 128 | """Start and stop D-Bus service under test.""" |
1235 | 127 | 129 | ||
1237 | 128 | def __init__(self): | 130 | def __init__(self, logfile=None): |
1238 | 129 | # Non-public. | 131 | # Non-public. |
1239 | 130 | self._stack = ExitStack() | 132 | self._stack = ExitStack() |
1240 | 131 | self._stoppers = [] | 133 | self._stoppers = [] |
1241 | @@ -148,21 +150,22 @@ | |||
1242 | 148 | # We need a client.ini file for the subprocess. | 150 | # We need a client.ini file for the subprocess. |
1243 | 149 | ini_tmpdir = self._stack.enter_context(temporary_directory()) | 151 | ini_tmpdir = self._stack.enter_context(temporary_directory()) |
1244 | 150 | ini_vardir = self._stack.enter_context(temporary_directory()) | 152 | ini_vardir = self._stack.enter_context(temporary_directory()) |
1245 | 153 | ini_logfile = (os.path.join(ini_tmpdir, 'client.log') | ||
1246 | 154 | if logfile is None | ||
1247 | 155 | else logfile) | ||
1248 | 151 | self.ini_path = os.path.join(self.tmpdir, 'client.ini') | 156 | self.ini_path = os.path.join(self.tmpdir, 'client.ini') |
1249 | 152 | template = resource_bytes( | 157 | template = resource_bytes( |
1250 | 153 | 'systemimage.tests.data', 'config_03.ini').decode('utf-8') | 158 | 'systemimage.tests.data', 'config_03.ini').decode('utf-8') |
1251 | 154 | with open(self.ini_path, 'w', encoding='utf-8') as fp: | 159 | with open(self.ini_path, 'w', encoding='utf-8') as fp: |
1253 | 155 | print(template.format(tmpdir=ini_tmpdir, vardir=ini_vardir), | 160 | print(template.format(tmpdir=ini_tmpdir, vardir=ini_vardir, |
1254 | 161 | logfile=ini_logfile), | ||
1255 | 156 | file=fp) | 162 | file=fp) |
1256 | 157 | 163 | ||
1257 | 158 | def _configure_services(self): | 164 | def _configure_services(self): |
1260 | 159 | # If the daemon is already running, kill all the children and HUP the | 165 | # If the dbus-daemon is already running, kill all the children. |
1259 | 160 | # daemon to reset dbus activation. | ||
1261 | 161 | if self.daemon_pid is not None: | 166 | if self.daemon_pid is not None: |
1262 | 162 | for stopper in self._stoppers: | 167 | for stopper in self._stoppers: |
1263 | 163 | stopper(self) | 168 | stopper(self) |
1264 | 164 | process = psutil.Process(self.daemon_pid) | ||
1265 | 165 | process.send_signal(signal.SIGHUP) | ||
1266 | 166 | del self._stoppers[:] | 169 | del self._stoppers[:] |
1267 | 167 | # Now we have to set up the .service files. We use the Python | 170 | # Now we have to set up the .service files. We use the Python |
1268 | 168 | # executable used to run the tests, executing the entry point as would | 171 | # executable used to run the tests, executing the entry point as would |
1269 | @@ -178,6 +181,12 @@ | |||
1270 | 178 | with open(service_path, 'w', encoding='utf-8') as fp: | 181 | with open(service_path, 'w', encoding='utf-8') as fp: |
1271 | 179 | fp.write(config) | 182 | fp.write(config) |
1272 | 180 | self._stoppers.append(stopper) | 183 | self._stoppers.append(stopper) |
1273 | 184 | # If the dbus-daemon is running, reload its configuration files. | ||
1274 | 185 | if self.daemon_pid is not None: | ||
1275 | 186 | service = dbus.SystemBus().get_object('org.freedesktop.DBus', '/') | ||
1276 | 187 | iface = dbus.Interface(service, 'org.freedesktop.DBus') | ||
1277 | 188 | iface.ReloadConfig() | ||
1278 | 189 | time.sleep(HUP_SLEEP) | ||
1279 | 181 | 190 | ||
1280 | 182 | def set_mode(self, *, cert_pem=None, service_mode=''): | 191 | def set_mode(self, *, cert_pem=None, service_mode=''): |
1281 | 183 | self.mode = service_mode | 192 | self.mode = service_mode |
1282 | 184 | 193 | ||
1283 | === modified file 'systemimage/testing/dbus.py' | |||
1284 | --- systemimage/testing/dbus.py 2013-12-13 13:55:51 +0000 | |||
1285 | +++ systemimage/testing/dbus.py 2014-02-25 17:45:25 +0000 | |||
1286 | @@ -1,4 +1,4 @@ | |||
1288 | 1 | # Copyright (C) 2013 Canonical Ltd. | 1 | # Copyright (C) 2013-2014 Canonical Ltd. |
1289 | 2 | # Author: Barry Warsaw <barry@ubuntu.com> | 2 | # Author: Barry Warsaw <barry@ubuntu.com> |
1290 | 3 | 3 | ||
1291 | 4 | # This program is free software: you can redistribute it and/or modify | 4 | # This program is free software: you can redistribute it and/or modify |
1292 | @@ -31,6 +31,8 @@ | |||
1293 | 31 | from systemimage.helpers import makedirs, safe_remove | 31 | from systemimage.helpers import makedirs, safe_remove |
1294 | 32 | from unittest.mock import patch | 32 | from unittest.mock import patch |
1295 | 33 | 33 | ||
1296 | 34 | from systemimage.testing.helpers import debug | ||
1297 | 35 | |||
1298 | 34 | 36 | ||
1299 | 35 | SPACE = ' ' | 37 | SPACE = ' ' |
1300 | 36 | SIGNAL_DELAY_SECS = 5 | 38 | SIGNAL_DELAY_SECS = 5 |
1301 | @@ -65,7 +67,11 @@ | |||
1302 | 65 | @method('com.canonical.SystemImage') | 67 | @method('com.canonical.SystemImage') |
1303 | 66 | def Reset(self): | 68 | def Reset(self): |
1304 | 67 | self._api = Mediator() | 69 | self._api = Mediator() |
1306 | 68 | self._checking = False | 70 | try: |
1307 | 71 | self._checking.release() | ||
1308 | 72 | except RuntimeError: | ||
1309 | 73 | # Lock is already released. | ||
1310 | 74 | pass | ||
1311 | 69 | self._update = None | 75 | self._update = None |
1312 | 70 | self._downloading = False | 76 | self._downloading = False |
1313 | 71 | self._rebootable = False | 77 | self._rebootable = False |
1314 | 72 | 78 | ||
1315 | === modified file 'systemimage/testing/demo.py' | |||
1316 | --- systemimage/testing/demo.py 2013-12-13 13:55:51 +0000 | |||
1317 | +++ systemimage/testing/demo.py 2014-02-25 17:45:25 +0000 | |||
1318 | @@ -1,4 +1,4 @@ | |||
1320 | 1 | # Copyright (C) 2013 Canonical Ltd. | 1 | # Copyright (C) 2013-2014 Canonical Ltd. |
1321 | 2 | # Author: Barry Warsaw <barry@ubuntu.com> | 2 | # Author: Barry Warsaw <barry@ubuntu.com> |
1322 | 3 | 3 | ||
1323 | 4 | # This program is free software: you can redistribute it and/or modify | 4 | # This program is free software: you can redistribute it and/or modify |
1324 | 5 | 5 | ||
1325 | === modified file 'systemimage/testing/helpers.py' | |||
1326 | --- systemimage/testing/helpers.py 2013-12-13 13:55:51 +0000 | |||
1327 | +++ systemimage/testing/helpers.py 2014-02-25 17:45:25 +0000 | |||
1328 | @@ -1,4 +1,4 @@ | |||
1330 | 1 | # Copyright (C) 2013 Canonical Ltd. | 1 | # Copyright (C) 2013-2014 Canonical Ltd. |
1331 | 2 | # Author: Barry Warsaw <barry@ubuntu.com> | 2 | # Author: Barry Warsaw <barry@ubuntu.com> |
1332 | 3 | 3 | ||
1333 | 4 | # This program is free software: you can redistribute it and/or modify | 4 | # This program is free software: you can redistribute it and/or modify |
1334 | @@ -107,6 +107,12 @@ | |||
1335 | 107 | # Please shut up. | 107 | # Please shut up. |
1336 | 108 | pass | 108 | pass |
1337 | 109 | 109 | ||
1338 | 110 | def handle_one_request(self): | ||
1339 | 111 | try: | ||
1340 | 112 | super().handle_one_request() | ||
1341 | 113 | except ConnectionResetError: | ||
1342 | 114 | super().handle_one_request() | ||
1343 | 115 | |||
1344 | 110 | def do_GET(self): | 116 | def do_GET(self): |
1345 | 111 | # If we requested the magic 'user-agent.txt' file, send back the | 117 | # If we requested the magic 'user-agent.txt' file, send back the |
1346 | 112 | # value of the User-Agent header. Otherwise, vend as normal. | 118 | # value of the User-Agent header. Otherwise, vend as normal. |
1347 | @@ -166,7 +172,16 @@ | |||
1348 | 166 | for conn in connections: | 172 | for conn in connections: |
1349 | 167 | if conn.fileno() != -1: | 173 | if conn.fileno() != -1: |
1350 | 168 | # Disallow sends and receives. | 174 | # Disallow sends and receives. |
1352 | 169 | conn.shutdown(SHUT_RDWR) | 175 | try: |
1353 | 176 | conn.shutdown(SHUT_RDWR) | ||
1354 | 177 | except OSError: | ||
1355 | 178 | # I'm ignoring all OSErrors here, although the only | ||
1356 | 179 | # one I've seen semi-consistency is ENOTCONN [107] | ||
1357 | 180 | # "Transport endpoint is not connected". I don't know | ||
1358 | 181 | # why this happens, but it tells me that the client | ||
1359 | 182 | # has already exited. We're shutting down, so who | ||
1360 | 183 | # cares? (Or am I masking a real error?) | ||
1361 | 184 | pass | ||
1362 | 170 | conn.close() | 185 | conn.close() |
1363 | 171 | server.shutdown() | 186 | server.shutdown() |
1364 | 172 | thread.join() | 187 | thread.join() |
1365 | @@ -332,7 +347,7 @@ | |||
1366 | 332 | setup_keyring_txz(keyring + '.gpg', signing_kr, json_data, dst) | 347 | setup_keyring_txz(keyring + '.gpg', signing_kr, json_data, dst) |
1367 | 333 | 348 | ||
1368 | 334 | 349 | ||
1370 | 335 | def setup_index(index, todir, keyring): | 350 | def setup_index(index, todir, keyring, write_callback=None): |
1371 | 336 | for image in get_index(index).images: | 351 | for image in get_index(index).images: |
1372 | 337 | for filerec in image.files: | 352 | for filerec in image.files: |
1373 | 338 | path = (filerec.path[1:] | 353 | path = (filerec.path[1:] |
1374 | @@ -340,10 +355,13 @@ | |||
1375 | 340 | else filerec.path) | 355 | else filerec.path) |
1376 | 341 | dst = os.path.join(todir, path) | 356 | dst = os.path.join(todir, path) |
1377 | 342 | makedirs(os.path.dirname(dst)) | 357 | makedirs(os.path.dirname(dst)) |
1382 | 343 | contents = EMPTYSTRING.join( | 358 | if write_callback is None: |
1383 | 344 | os.path.splitext(filerec.path)[0].split('/')) | 359 | contents = EMPTYSTRING.join( |
1384 | 345 | with open(dst, 'w', encoding='utf-8') as fp: | 360 | os.path.splitext(filerec.path)[0].split('/')) |
1385 | 346 | fp.write(contents) | 361 | with open(dst, 'w', encoding='utf-8') as fp: |
1386 | 362 | fp.write(contents) | ||
1387 | 363 | else: | ||
1388 | 364 | write_callback(dst) | ||
1389 | 347 | # Sign with the specified signing key. | 365 | # Sign with the specified signing key. |
1390 | 348 | sign(dst, keyring) | 366 | sign(dst, keyring) |
1391 | 349 | 367 | ||
1392 | 350 | 368 | ||
1393 | === modified file 'systemimage/testing/nose.py' | |||
1394 | --- systemimage/testing/nose.py 2013-12-13 13:55:51 +0000 | |||
1395 | +++ systemimage/testing/nose.py 2014-02-25 17:45:25 +0000 | |||
1396 | @@ -1,4 +1,4 @@ | |||
1398 | 1 | # Copyright (C) 2013 Canonical Ltd. | 1 | # Copyright (C) 2013-2014 Canonical Ltd. |
1399 | 2 | # Author: Barry Warsaw <barry@ubuntu.com> | 2 | # Author: Barry Warsaw <barry@ubuntu.com> |
1400 | 3 | 3 | ||
1401 | 4 | # This program is free software: you can redistribute it and/or modify | 4 | # This program is free software: you can redistribute it and/or modify |
1402 | @@ -75,15 +75,24 @@ | |||
1403 | 75 | super().__init__() | 75 | super().__init__() |
1404 | 76 | self.patterns = [] | 76 | self.patterns = [] |
1405 | 77 | self.verbosity = 0 | 77 | self.verbosity = 0 |
1406 | 78 | self.log_file = None | ||
1407 | 78 | self.addArgument(self.patterns, 'P', 'pattern', | 79 | self.addArgument(self.patterns, 'P', 'pattern', |
1408 | 79 | 'Add a test matching pattern') | 80 | 'Add a test matching pattern') |
1409 | 80 | def bump(ignore): | 81 | def bump(ignore): |
1410 | 81 | self.verbosity += 1 | 82 | self.verbosity += 1 |
1411 | 82 | self.addFlag(bump, 'V', 'Verbosity', | 83 | self.addFlag(bump, 'V', 'Verbosity', |
1412 | 83 | 'Increase system-image verbosity') | 84 | 'Increase system-image verbosity') |
1413 | 85 | def set_log_file(path): | ||
1414 | 86 | self.log_file = path[0] | ||
1415 | 87 | self.addOption(set_log_file, 'L', 'logfile', | ||
1416 | 88 | 'Set the log file for the test run', | ||
1417 | 89 | nargs=1) | ||
1418 | 84 | 90 | ||
1419 | 85 | @configuration | 91 | @configuration |
1420 | 86 | def startTestRun(self, event): | 92 | def startTestRun(self, event): |
1421 | 93 | from systemimage.config import config | ||
1422 | 94 | if self.log_file is not None: | ||
1423 | 95 | config.system.logfile = self.log_file | ||
1424 | 87 | DBusGMainLoop(set_as_default=True) | 96 | DBusGMainLoop(set_as_default=True) |
1425 | 88 | initialize(verbosity=self.verbosity) | 97 | initialize(verbosity=self.verbosity) |
1426 | 89 | # We need to set up the dbus service controller, since all the tests | 98 | # We need to set up the dbus service controller, since all the tests |
1427 | @@ -92,7 +101,7 @@ | |||
1428 | 92 | # individual services, and we can write new dbus configuration files | 101 | # individual services, and we can write new dbus configuration files |
1429 | 93 | # and HUP the dbus-launch to re-read them, but we cannot change bus | 102 | # and HUP the dbus-launch to re-read them, but we cannot change bus |
1430 | 94 | # addresses after the initial one is set. | 103 | # addresses after the initial one is set. |
1432 | 95 | SystemImagePlugin.controller = Controller() | 104 | SystemImagePlugin.controller = Controller(self.log_file) |
1433 | 96 | SystemImagePlugin.controller.start() | 105 | SystemImagePlugin.controller.start() |
1434 | 97 | atexit.register(SystemImagePlugin.controller.stop) | 106 | atexit.register(SystemImagePlugin.controller.stop) |
1435 | 98 | 107 | ||
1436 | @@ -123,3 +132,13 @@ | |||
1437 | 123 | SystemImagePlugin.controller.stop() | 132 | SystemImagePlugin.controller.stop() |
1438 | 124 | # Let other plugins continue printing. | 133 | # Let other plugins continue printing. |
1439 | 125 | return None | 134 | return None |
1440 | 135 | |||
1441 | 136 | ## def startTest(self, event): | ||
1442 | 137 | ## from systemimage.testing.helpers import debug | ||
1443 | 138 | ## with debug() as dlog: | ||
1444 | 139 | ## dlog('vvvvv', event.test) | ||
1445 | 140 | |||
1446 | 141 | ## def stopTest(self, event): | ||
1447 | 142 | ## from systemimage.testing.helpers import debug | ||
1448 | 143 | ## with debug() as dlog: | ||
1449 | 144 | ## dlog('^^^^^', event.test) | ||
1450 | 126 | 145 | ||
1451 | === modified file 'systemimage/tests/data/config_03.ini' | |||
1452 | --- systemimage/tests/data/config_03.ini 2013-12-13 13:55:51 +0000 | |||
1453 | +++ systemimage/tests/data/config_03.ini 2014-02-25 17:45:25 +0000 | |||
1454 | @@ -14,7 +14,7 @@ | |||
1455 | 14 | timeout: 1s | 14 | timeout: 1s |
1456 | 15 | build_file: {tmpdir}/ubuntu-build | 15 | build_file: {tmpdir}/ubuntu-build |
1457 | 16 | tempdir: {tmpdir}/tmp | 16 | tempdir: {tmpdir}/tmp |
1459 | 17 | logfile: {tmpdir}/client.log | 17 | logfile: {logfile} |
1460 | 18 | loglevel: info | 18 | loglevel: info |
1461 | 19 | settings_db: {vardir}/settings.db | 19 | settings_db: {vardir}/settings.db |
1462 | 20 | 20 | ||
1463 | 21 | 21 | ||
1464 | === added file 'systemimage/tests/data/index_24.json' | |||
1465 | --- systemimage/tests/data/index_24.json 1970-01-01 00:00:00 +0000 | |||
1466 | +++ systemimage/tests/data/index_24.json 2014-02-25 17:45:25 +0000 | |||
1467 | @@ -0,0 +1,36 @@ | |||
1468 | 1 | { | ||
1469 | 2 | "global": { | ||
1470 | 3 | "generated_at": "Thu Aug 01 08:01:00 UTC 2013" | ||
1471 | 4 | }, | ||
1472 | 5 | "images": [ | ||
1473 | 6 | { | ||
1474 | 7 | "description": "Full", | ||
1475 | 8 | "files": [ | ||
1476 | 9 | { | ||
1477 | 10 | "checksum": "5b05b298e974f3b9e40f0a1a8188f50984a4f18fb329e050324296632d3d9dfc", | ||
1478 | 11 | "order": 3, | ||
1479 | 12 | "path": "/3/4/5.txt", | ||
1480 | 13 | "signature": "/3/4/5.txt.asc", | ||
1481 | 14 | "size": 104857600 | ||
1482 | 15 | }, | ||
1483 | 16 | { | ||
1484 | 17 | "checksum": "5b05b298e974f3b9e40f0a1a8188f50984a4f18fb329e050324296632d3d9dfc", | ||
1485 | 18 | "order": 1, | ||
1486 | 19 | "path": "/4/5/6.txt", | ||
1487 | 20 | "signature": "/4/5/6.txt.asc", | ||
1488 | 21 | "size": 104857600 | ||
1489 | 22 | }, | ||
1490 | 23 | { | ||
1491 | 24 | "checksum": "5b05b298e974f3b9e40f0a1a8188f50984a4f18fb329e050324296632d3d9dfc", | ||
1492 | 25 | "order": 2, | ||
1493 | 26 | "path": "/5/6/7.txt", | ||
1494 | 27 | "signature": "/5/6/7.txt.asc", | ||
1495 | 28 | "size": 104857600 | ||
1496 | 29 | } | ||
1497 | 30 | ], | ||
1498 | 31 | "type": "full", | ||
1499 | 32 | "version": 1600, | ||
1500 | 33 | "bootme": true | ||
1501 | 34 | } | ||
1502 | 35 | ] | ||
1503 | 36 | } | ||
1504 | 0 | 37 | ||
1505 | === modified file 'systemimage/tests/test_api.py' | |||
1506 | --- systemimage/tests/test_api.py 2013-12-13 13:55:51 +0000 | |||
1507 | +++ systemimage/tests/test_api.py 2014-02-25 17:45:25 +0000 | |||
1508 | @@ -1,4 +1,4 @@ | |||
1510 | 1 | # Copyright (C) 2013 Canonical Ltd. | 1 | # Copyright (C) 2013-2014 Canonical Ltd. |
1511 | 2 | # Author: Barry Warsaw <barry@ubuntu.com> | 2 | # Author: Barry Warsaw <barry@ubuntu.com> |
1512 | 3 | 3 | ||
1513 | 4 | # This program is free software: you can redistribute it and/or modify | 4 | # This program is free software: you can redistribute it and/or modify |
1514 | 5 | 5 | ||
1515 | === modified file 'systemimage/tests/test_bag.py' | |||
1516 | --- systemimage/tests/test_bag.py 2013-12-13 13:55:51 +0000 | |||
1517 | +++ systemimage/tests/test_bag.py 2014-02-25 17:45:25 +0000 | |||
1518 | @@ -1,4 +1,4 @@ | |||
1520 | 1 | # Copyright (C) 2013 Canonical Ltd. | 1 | # Copyright (C) 2013-2014 Canonical Ltd. |
1521 | 2 | # Author: Barry Warsaw <barry@ubuntu.com> | 2 | # Author: Barry Warsaw <barry@ubuntu.com> |
1522 | 3 | 3 | ||
1523 | 4 | # This program is free software: you can redistribute it and/or modify | 4 | # This program is free software: you can redistribute it and/or modify |
1524 | 5 | 5 | ||
1525 | === modified file 'systemimage/tests/test_candidates.py' | |||
1526 | --- systemimage/tests/test_candidates.py 2013-12-13 13:55:51 +0000 | |||
1527 | +++ systemimage/tests/test_candidates.py 2014-02-25 17:45:25 +0000 | |||
1528 | @@ -1,4 +1,4 @@ | |||
1530 | 1 | # Copyright (C) 2013 Canonical Ltd. | 1 | # Copyright (C) 2013-2014 Canonical Ltd. |
1531 | 2 | # Author: Barry Warsaw <barry@ubuntu.com> | 2 | # Author: Barry Warsaw <barry@ubuntu.com> |
1532 | 3 | 3 | ||
1533 | 4 | # This program is free software: you can redistribute it and/or modify | 4 | # This program is free software: you can redistribute it and/or modify |
1534 | 5 | 5 | ||
1535 | === modified file 'systemimage/tests/test_channel.py' | |||
1536 | --- systemimage/tests/test_channel.py 2013-12-13 13:55:51 +0000 | |||
1537 | +++ systemimage/tests/test_channel.py 2014-02-25 17:45:25 +0000 | |||
1538 | @@ -1,4 +1,4 @@ | |||
1540 | 1 | # Copyright (C) 2013 Canonical Ltd. | 1 | # Copyright (C) 2013-2014 Canonical Ltd. |
1541 | 2 | # Author: Barry Warsaw <barry@ubuntu.com> | 2 | # Author: Barry Warsaw <barry@ubuntu.com> |
1542 | 3 | 3 | ||
1543 | 4 | # This program is free software: you can redistribute it and/or modify | 4 | # This program is free software: you can redistribute it and/or modify |
1544 | 5 | 5 | ||
1545 | === modified file 'systemimage/tests/test_config.py' | |||
1546 | --- systemimage/tests/test_config.py 2013-12-13 13:55:51 +0000 | |||
1547 | +++ systemimage/tests/test_config.py 2014-02-25 17:45:25 +0000 | |||
1548 | @@ -1,4 +1,4 @@ | |||
1550 | 1 | # Copyright (C) 2013 Canonical Ltd. | 1 | # Copyright (C) 2013-2014 Canonical Ltd. |
1551 | 2 | # Author: Barry Warsaw <barry@ubuntu.com> | 2 | # Author: Barry Warsaw <barry@ubuntu.com> |
1552 | 3 | 3 | ||
1553 | 4 | # This program is free software: you can redistribute it and/or modify | 4 | # This program is free software: you can redistribute it and/or modify |
1554 | 5 | 5 | ||
1555 | === modified file 'systemimage/tests/test_dbus.py' | |||
1556 | --- systemimage/tests/test_dbus.py 2013-12-13 13:55:51 +0000 | |||
1557 | +++ systemimage/tests/test_dbus.py 2014-02-25 17:45:25 +0000 | |||
1558 | @@ -1,4 +1,4 @@ | |||
1560 | 1 | # Copyright (C) 2013 Canonical Ltd. | 1 | # Copyright (C) 2013-2014 Canonical Ltd. |
1561 | 2 | # Author: Barry Warsaw <barry@ubuntu.com> | 2 | # Author: Barry Warsaw <barry@ubuntu.com> |
1562 | 3 | 3 | ||
1563 | 4 | # This program is free software: you can redistribute it and/or modify | 4 | # This program is free software: you can redistribute it and/or modify |
1564 | @@ -23,6 +23,7 @@ | |||
1565 | 23 | 'TestDBusGetSet', | 23 | 'TestDBusGetSet', |
1566 | 24 | 'TestDBusInfo', | 24 | 'TestDBusInfo', |
1567 | 25 | 'TestDBusInfoNoDetails', | 25 | 'TestDBusInfoNoDetails', |
1568 | 26 | 'TestDBusLP1277589', | ||
1569 | 26 | 'TestDBusMockFailApply', | 27 | 'TestDBusMockFailApply', |
1570 | 27 | 'TestDBusMockFailPause', | 28 | 'TestDBusMockFailPause', |
1571 | 28 | 'TestDBusMockFailResume', | 29 | 'TestDBusMockFailResume', |
1572 | @@ -122,6 +123,23 @@ | |||
1573 | 122 | self.quit() | 123 | self.quit() |
1574 | 123 | 124 | ||
1575 | 124 | 125 | ||
1576 | 126 | class DoubleCheckingReactor(Reactor): | ||
1577 | 127 | def __init__(self, iface): | ||
1578 | 128 | super().__init__(dbus.SystemBus()) | ||
1579 | 129 | self.iface = iface | ||
1580 | 130 | self.uas_signals = [] | ||
1581 | 131 | self.react_to('UpdateAvailableStatus') | ||
1582 | 132 | self.react_to('UpdateDownloaded') | ||
1583 | 133 | self.schedule(self.iface.CheckForUpdate) | ||
1584 | 134 | |||
1585 | 135 | def _do_UpdateAvailableStatus(self, signal, path, *args, **kws): | ||
1586 | 136 | self.uas_signals.append(args) | ||
1587 | 137 | self.schedule(self.iface.CheckForUpdate) | ||
1588 | 138 | |||
1589 | 139 | def _do_UpdateDownloaded(self, *args, **kws): | ||
1590 | 140 | self.quit() | ||
1591 | 141 | |||
1592 | 142 | |||
1593 | 125 | class _TestBase(unittest.TestCase): | 143 | class _TestBase(unittest.TestCase): |
1594 | 126 | """Base class for all DBus testing.""" | 144 | """Base class for all DBus testing.""" |
1595 | 127 | 145 | ||
1596 | @@ -262,13 +280,14 @@ | |||
1597 | 262 | safe_remove(self.reboot_log) | 280 | safe_remove(self.reboot_log) |
1598 | 263 | super().tearDown() | 281 | super().tearDown() |
1599 | 264 | 282 | ||
1601 | 265 | def _prepare_index(self, index_file): | 283 | def _prepare_index(self, index_file, write_callback=None): |
1602 | 266 | serverdir = SystemImagePlugin.controller.serverdir | 284 | serverdir = SystemImagePlugin.controller.serverdir |
1603 | 267 | index_path = os.path.join(serverdir, 'stable', 'nexus7', 'index.json') | 285 | index_path = os.path.join(serverdir, 'stable', 'nexus7', 'index.json') |
1604 | 268 | head, tail = os.path.split(index_path) | 286 | head, tail = os.path.split(index_path) |
1605 | 269 | copy(index_file, head, tail) | 287 | copy(index_file, head, tail) |
1606 | 270 | sign(index_path, 'device-signing.gpg') | 288 | sign(index_path, 'device-signing.gpg') |
1608 | 271 | setup_index(index_file, serverdir, 'device-signing.gpg') | 289 | setup_index(index_file, serverdir, 'device-signing.gpg', |
1609 | 290 | write_callback) | ||
1610 | 272 | 291 | ||
1611 | 273 | def _touch_build(self, version): | 292 | def _touch_build(self, version): |
1612 | 274 | # Unlike the touch_build() helper, this one uses our own config object | 293 | # Unlike the touch_build() helper, this one uses our own config object |
1613 | @@ -374,6 +393,23 @@ | |||
1614 | 374 | self.assertEqual(last_update_date, '2013-01-20 12:01:45') | 393 | self.assertEqual(last_update_date, '2013-01-20 12:01:45') |
1615 | 375 | # All other values are undefined. | 394 | # All other values are undefined. |
1616 | 376 | 395 | ||
1617 | 396 | def test_check_for_update_twice(self): | ||
1618 | 397 | # Issue two CheckForUpdate calls immediate after each other. | ||
1619 | 398 | self.download_always() | ||
1620 | 399 | reactor = SignalCapturingReactor('UpdateAvailableStatus') | ||
1621 | 400 | def two_calls(): | ||
1622 | 401 | self.iface.CheckForUpdate() | ||
1623 | 402 | self.iface.CheckForUpdate() | ||
1624 | 403 | reactor.run(two_calls) | ||
1625 | 404 | self.assertEqual(len(reactor.signals), 1) | ||
1626 | 405 | # There's one boolean argument to the result. | ||
1627 | 406 | (is_available, downloading, available_version, update_size, | ||
1628 | 407 | last_update_date, | ||
1629 | 408 | # descriptions, | ||
1630 | 409 | error_reason) = reactor.signals[0] | ||
1631 | 410 | self.assertTrue(is_available) | ||
1632 | 411 | self.assertTrue(downloading) | ||
1633 | 412 | |||
1634 | 377 | @unittest.skip('LP: #1215586') | 413 | @unittest.skip('LP: #1215586') |
1635 | 378 | def test_get_multilingual_descriptions(self): | 414 | def test_get_multilingual_descriptions(self): |
1636 | 379 | # The descriptions are multilingual. | 415 | # The descriptions are multilingual. |
1637 | @@ -1368,7 +1404,9 @@ | |||
1638 | 1368 | def setUp(self): | 1404 | def setUp(self): |
1639 | 1369 | super().setUp() | 1405 | super().setUp() |
1640 | 1370 | # We have to hack the files to be rather large so that the download | 1406 | # We have to hack the files to be rather large so that the download |
1642 | 1371 | # doesn't complete before we get a chance to pause it. | 1407 | # doesn't complete before we get a chance to pause it. Of course, |
1643 | 1408 | # this breaks the signatures because we're changing the file contents | ||
1644 | 1409 | # after the .asc files have been written. | ||
1645 | 1372 | for path in ('3/4/5.txt', '4/5/6.txt', '5/6/7.txt'): | 1410 | for path in ('3/4/5.txt', '4/5/6.txt', '5/6/7.txt'): |
1646 | 1373 | full_path = os.path.join( | 1411 | full_path = os.path.join( |
1647 | 1374 | SystemImagePlugin.controller.serverdir, path) | 1412 | SystemImagePlugin.controller.serverdir, path) |
1648 | @@ -1391,15 +1429,25 @@ | |||
1649 | 1391 | self.assertTrue(reactor.paused) | 1429 | self.assertTrue(reactor.paused) |
1650 | 1392 | # Now let's resume the download. Because we intentionally corrupted | 1430 | # Now let's resume the download. Because we intentionally corrupted |
1651 | 1393 | # the downloaded files, we'll get an UpdateFailed signal instead of | 1431 | # the downloaded files, we'll get an UpdateFailed signal instead of |
1653 | 1394 | # the successful UpdateDownloaded signal. We can ignore that. | 1432 | # the successful UpdateDownloaded signal. |
1654 | 1395 | reactor = SignalCapturingReactor('UpdateFailed') | 1433 | reactor = SignalCapturingReactor('UpdateFailed') |
1655 | 1396 | reactor.run(self.iface.DownloadUpdate, timeout=60) | 1434 | reactor.run(self.iface.DownloadUpdate, timeout=60) |
1656 | 1397 | self.assertEqual(len(reactor.signals), 1) | 1435 | self.assertEqual(len(reactor.signals), 1) |
1658 | 1398 | # We've gotten one error and the first file that failed is 5.txt. | 1436 | # The error message will include lots of details on the SignatureError |
1659 | 1437 | # that results. The key thing is that it's 5.txt that is the first | ||
1660 | 1438 | # file to fail its signature check. | ||
1661 | 1399 | failure_count, last_error = reactor.signals[0] | 1439 | failure_count, last_error = reactor.signals[0] |
1662 | 1400 | self.assertEqual(failure_count, 1) | 1440 | self.assertEqual(failure_count, 1) |
1665 | 1401 | # Watch out for the trailing newline. | 1441 | check_next = False |
1666 | 1402 | self.assertEqual(os.path.basename(last_error[:-1]), '5.txt') | 1442 | for line in last_error.splitlines(): |
1667 | 1443 | line = line.strip() | ||
1668 | 1444 | if check_next: | ||
1669 | 1445 | self.assertEqual(os.path.basename(line), '5.txt') | ||
1670 | 1446 | break | ||
1671 | 1447 | if line.startswith('data path:'): | ||
1672 | 1448 | check_next = True | ||
1673 | 1449 | else: | ||
1674 | 1450 | raise AssertionError('Did not find expected error output') | ||
1675 | 1403 | 1451 | ||
1676 | 1404 | 1452 | ||
1677 | 1405 | class TestDBusUseCache(_LiveTesting): | 1453 | class TestDBusUseCache(_LiveTesting): |
1678 | @@ -1513,3 +1561,45 @@ | |||
1679 | 1513 | update 5.txt 5.txt.asc | 1561 | update 5.txt 5.txt.asc |
1680 | 1514 | unmount system | 1562 | unmount system |
1681 | 1515 | """) | 1563 | """) |
1682 | 1564 | |||
1683 | 1565 | |||
1684 | 1566 | class TestDBusLP1277589(_LiveTesting): | ||
1685 | 1567 | def test_multiple_check_for_updates(self): | ||
1686 | 1568 | # Log analysis of LP: #1277589 appears to show the following scenario, | ||
1687 | 1569 | # reproduced in this test case: | ||
1688 | 1570 | # | ||
1689 | 1571 | # * Automatic updates are enabled. | ||
1690 | 1572 | # * No image signing or image master keys are present. | ||
1691 | 1573 | # * A full update is checked. | ||
1692 | 1574 | # - A new image master key and image signing key is downloaded. | ||
1693 | 1575 | # - Update is available | ||
1694 | 1576 | # | ||
1695 | 1577 | # Start by creating some big files which will take a while to | ||
1696 | 1578 | # download. | ||
1697 | 1579 | def write_callback(dst): | ||
1698 | 1580 | # Write a 100 MiB sized file. | ||
1699 | 1581 | with open(dst, 'wb') as fp: | ||
1700 | 1582 | for i in range(25600): | ||
1701 | 1583 | fp.write(b'x' * 4096) | ||
1702 | 1584 | self._prepare_index('index_24.json', write_callback) | ||
1703 | 1585 | # Create a reactor that will exit when the UpdateDownloaded signal is | ||
1704 | 1586 | # received. We're going to issue a CheckForUpdate with automatic | ||
1705 | 1587 | # updates enabled. As soon as we receive the UpdateAvailableStatus | ||
1706 | 1588 | # signal, we'll immediately issue *another* CheckForUpdate, which | ||
1707 | 1589 | # should run while the auto-download is working. | ||
1708 | 1590 | # | ||
1709 | 1591 | # At the end, we should not get another UpdateAvailableStatus signal, | ||
1710 | 1592 | # but we should get the UpdateDownloaded signal. | ||
1711 | 1593 | reactor = DoubleCheckingReactor(self.iface) | ||
1712 | 1594 | reactor.run() | ||
1713 | 1595 | self.assertEqual(len(reactor.uas_signals), 1) | ||
1714 | 1596 | (is_available, downloading, available_version, update_size, | ||
1715 | 1597 | last_update_date, | ||
1716 | 1598 | #descriptions, | ||
1717 | 1599 | error_reason) = reactor.uas_signals[0] | ||
1718 | 1600 | self.assertTrue(is_available) | ||
1719 | 1601 | self.assertTrue(downloading) | ||
1720 | 1602 | self.assertEqual(available_version, '1600') | ||
1721 | 1603 | self.assertEqual(update_size, 314572800) | ||
1722 | 1604 | self.assertEqual(last_update_date, 'Unknown') | ||
1723 | 1605 | self.assertEqual(error_reason, '') | ||
1724 | 1516 | 1606 | ||
1725 | === modified file 'systemimage/tests/test_download.py' | |||
1726 | --- systemimage/tests/test_download.py 2013-12-13 13:55:51 +0000 | |||
1727 | +++ systemimage/tests/test_download.py 2014-02-25 17:45:25 +0000 | |||
1728 | @@ -1,4 +1,4 @@ | |||
1730 | 1 | # Copyright (C) 2013 Canonical Ltd. | 1 | # Copyright (C) 2013-2014 Canonical Ltd. |
1731 | 2 | # Author: Barry Warsaw <barry@ubuntu.com> | 2 | # Author: Barry Warsaw <barry@ubuntu.com> |
1732 | 3 | 3 | ||
1733 | 4 | # This program is free software: you can redistribute it and/or modify | 4 | # This program is free software: you can redistribute it and/or modify |
1734 | 5 | 5 | ||
1735 | === modified file 'systemimage/tests/test_gpg.py' | |||
1736 | --- systemimage/tests/test_gpg.py 2013-12-13 13:55:51 +0000 | |||
1737 | +++ systemimage/tests/test_gpg.py 2014-02-25 17:45:25 +0000 | |||
1738 | @@ -1,4 +1,4 @@ | |||
1740 | 1 | # Copyright (C) 2013 Canonical Ltd. | 1 | # Copyright (C) 2013-2014 Canonical Ltd. |
1741 | 2 | # Author: Barry Warsaw <barry@ubuntu.com> | 2 | # Author: Barry Warsaw <barry@ubuntu.com> |
1742 | 3 | 3 | ||
1743 | 4 | # This program is free software: you can redistribute it and/or modify | 4 | # This program is free software: you can redistribute it and/or modify |
1744 | @@ -18,15 +18,20 @@ | |||
1745 | 18 | __all__ = [ | 18 | __all__ = [ |
1746 | 19 | 'TestKeyrings', | 19 | 'TestKeyrings', |
1747 | 20 | 'TestSignature', | 20 | 'TestSignature', |
1748 | 21 | 'TestSignatureError', | ||
1749 | 21 | ] | 22 | ] |
1750 | 22 | 23 | ||
1751 | 23 | 24 | ||
1752 | 24 | import os | 25 | import os |
1753 | 26 | import sys | ||
1754 | 27 | import hashlib | ||
1755 | 25 | import unittest | 28 | import unittest |
1756 | 29 | import traceback | ||
1757 | 26 | 30 | ||
1758 | 27 | from contextlib import ExitStack | 31 | from contextlib import ExitStack |
1759 | 32 | from io import StringIO | ||
1760 | 28 | from systemimage.config import config | 33 | from systemimage.config import config |
1762 | 29 | from systemimage.gpg import Context | 34 | from systemimage.gpg import Context, SignatureError |
1763 | 30 | from systemimage.helpers import temporary_directory | 35 | from systemimage.helpers import temporary_directory |
1764 | 31 | from systemimage.testing.helpers import ( | 36 | from systemimage.testing.helpers import ( |
1765 | 32 | configuration, copy, setup_keyring_txz, setup_keyrings, sign) | 37 | configuration, copy, setup_keyring_txz, setup_keyrings, sign) |
1766 | @@ -330,3 +335,171 @@ | |||
1767 | 330 | with Context(keyring_1, keyring_2, blacklist=blacklist) as ctx: | 335 | with Context(keyring_1, keyring_2, blacklist=blacklist) as ctx: |
1768 | 331 | self.assertFalse( | 336 | self.assertFalse( |
1769 | 332 | ctx.verify(channels_json + '.asc', channels_json)) | 337 | ctx.verify(channels_json + '.asc', channels_json)) |
1770 | 338 | |||
1771 | 339 | @configuration | ||
1772 | 340 | def test_good_validation(self): | ||
1773 | 341 | # The .validate() method does nothing if the signature is good. | ||
1774 | 342 | channels_json = os.path.join(self._tmpdir, 'channels.json') | ||
1775 | 343 | copy('channels_01.json', self._tmpdir, dst=channels_json) | ||
1776 | 344 | sign(channels_json, 'image-signing.gpg') | ||
1777 | 345 | with temporary_directory() as tmpdir: | ||
1778 | 346 | keyring = os.path.join(tmpdir, 'image-signing.tar.xz') | ||
1779 | 347 | setup_keyring_txz('image-signing.gpg', 'image-master.gpg', | ||
1780 | 348 | dict(type='image-signing'), keyring) | ||
1781 | 349 | with Context(keyring) as ctx: | ||
1782 | 350 | self.assertIsNone( | ||
1783 | 351 | ctx.validate(channels_json + '.asc', channels_json)) | ||
1784 | 352 | |||
1785 | 353 | |||
1786 | 354 | class TestSignatureError(unittest.TestCase): | ||
1787 | 355 | def setUp(self): | ||
1788 | 356 | self._stack = ExitStack() | ||
1789 | 357 | self._tmpdir = self._stack.enter_context(temporary_directory()) | ||
1790 | 358 | |||
1791 | 359 | def tearDown(self): | ||
1792 | 360 | self._stack.close() | ||
1793 | 361 | |||
1794 | 362 | def test_extra_data(self): | ||
1795 | 363 | # A SignatureError includes extra information about the path to the | ||
1796 | 364 | # signature file, and the path to the data file. You also get the md5 | ||
1797 | 365 | # checksums of those two paths. | ||
1798 | 366 | signature_path = os.path.join(self._tmpdir, 'signature') | ||
1799 | 367 | data_path = os.path.join(self._tmpdir, 'data') | ||
1800 | 368 | with open(signature_path, 'wb') as fp: | ||
1801 | 369 | fp.write(b'012345') | ||
1802 | 370 | with open(data_path, 'wb') as fp: | ||
1803 | 371 | fp.write(b'67890a') | ||
1804 | 372 | error = SignatureError(signature_path, data_path) | ||
1805 | 373 | self.assertEqual(error.signature_path, signature_path) | ||
1806 | 374 | self.assertEqual(error.data_path, data_path) | ||
1807 | 375 | self.assertEqual( | ||
1808 | 376 | error.signature_checksum, 'd6a9a933c8aafc51e55ac0662b6e4d4a') | ||
1809 | 377 | self.assertEqual( | ||
1810 | 378 | error.data_checksum, 'e82780258de250078f7ad3f595d71f6d') | ||
1811 | 379 | |||
1812 | 380 | @configuration | ||
1813 | 381 | def test_signature_invalid(self): | ||
1814 | 382 | # The .validate() method raises a SignatureError exception with extra | ||
1815 | 383 | # information when the signature is invalid. | ||
1816 | 384 | channels_json = os.path.join(self._tmpdir, 'channels.json') | ||
1817 | 385 | copy('channels_01.json', self._tmpdir, dst=channels_json) | ||
1818 | 386 | sign(channels_json, 'device-signing.gpg') | ||
1819 | 387 | # Verify the signature with the pubkey. | ||
1820 | 388 | with temporary_directory() as tmpdir: | ||
1821 | 389 | dst = os.path.join(tmpdir, 'image-signing.tar.xz') | ||
1822 | 390 | setup_keyring_txz('image-signing.gpg', 'image-master.gpg', | ||
1823 | 391 | dict(type='image-signing'), dst) | ||
1824 | 392 | # Get the dst's checksum now, because the file will get deleted | ||
1825 | 393 | # when the tmpdir context manager exits. | ||
1826 | 394 | with open(dst, 'rb') as fp: | ||
1827 | 395 | dst_checksum = hashlib.md5(fp.read()).hexdigest() | ||
1828 | 396 | with Context(dst) as ctx: | ||
1829 | 397 | with self.assertRaises(SignatureError) as cm: | ||
1830 | 398 | ctx.validate(channels_json + '.asc', channels_json) | ||
1831 | 399 | error = cm.exception | ||
1832 | 400 | basename = os.path.basename | ||
1833 | 401 | self.assertEqual(basename(error.signature_path), 'channels.json.asc') | ||
1834 | 402 | self.assertEqual(basename(error.data_path), 'channels.json') | ||
1835 | 403 | # The contents of the signature file are not predictable. | ||
1836 | 404 | with open(channels_json + '.asc', 'rb') as fp: | ||
1837 | 405 | checksum = hashlib.md5(fp.read()).hexdigest() | ||
1838 | 406 | self.assertEqual(error.signature_checksum, checksum) | ||
1839 | 407 | self.assertEqual( | ||
1840 | 408 | error.data_checksum, '715c63fecbf44b62f9fa04a82dfa7d29') | ||
1841 | 409 | basenames = [basename(path) for path in error.keyrings] | ||
1842 | 410 | self.assertEqual(basenames, ['image-signing.tar.xz']) | ||
1843 | 411 | self.assertIsNone(error.blacklist) | ||
1844 | 412 | self.assertEqual(error.keyring_checksums, [dst_checksum]) | ||
1845 | 413 | self.assertIsNone(error.blacklist_checksum) | ||
1846 | 414 | |||
1847 | 415 | @configuration | ||
1848 | 416 | def test_signature_invalid_due_to_blacklist(self): | ||
1849 | 417 | # Like above, but we put the device signing key id in the blacklist. | ||
1850 | 418 | channels_json = os.path.join(self._tmpdir, 'channels.json') | ||
1851 | 419 | copy('channels_01.json', self._tmpdir, dst=channels_json) | ||
1852 | 420 | sign(channels_json, 'device-signing.gpg') | ||
1853 | 421 | # Verify the signature with the pubkey. | ||
1854 | 422 | with temporary_directory() as tmpdir: | ||
1855 | 423 | keyring_1 = os.path.join(tmpdir, 'image-signing.tar.xz') | ||
1856 | 424 | keyring_2 = os.path.join(tmpdir, 'device-signing.tar.xz') | ||
1857 | 425 | blacklist = os.path.join(tmpdir, 'blacklist.tar.xz') | ||
1858 | 426 | setup_keyring_txz('image-signing.gpg', 'image-master.gpg', | ||
1859 | 427 | dict(type='image-signing'), keyring_1) | ||
1860 | 428 | setup_keyring_txz('device-signing.gpg', 'image-signing.gpg', | ||
1861 | 429 | dict(type='device-signing'), keyring_2) | ||
1862 | 430 | # We're letting the device signing pubkey stand in for a blacklist. | ||
1863 | 431 | setup_keyring_txz('device-signing.gpg', 'image-master.gpg', | ||
1864 | 432 | dict(type='blacklist'), blacklist) | ||
1865 | 433 | # Get the keyring checksums now, because the files will get | ||
1866 | 434 | # deleted when the tmpdir context manager exits. | ||
1867 | 435 | keyring_checksums = [] | ||
1868 | 436 | for path in (keyring_1, keyring_2): | ||
1869 | 437 | with open(path, 'rb') as fp: | ||
1870 | 438 | checksum = hashlib.md5(fp.read()).hexdigest() | ||
1871 | 439 | keyring_checksums.append(checksum) | ||
1872 | 440 | with open(blacklist, 'rb') as fp: | ||
1873 | 441 | blacklist_checksum = hashlib.md5(fp.read()).hexdigest() | ||
1874 | 442 | with Context(keyring_1, keyring_2, blacklist=blacklist) as ctx: | ||
1875 | 443 | with self.assertRaises(SignatureError) as cm: | ||
1876 | 444 | ctx.validate(channels_json + '.asc', channels_json) | ||
1877 | 445 | error = cm.exception | ||
1878 | 446 | basename = os.path.basename | ||
1879 | 447 | self.assertEqual(basename(error.signature_path), 'channels.json.asc') | ||
1880 | 448 | self.assertEqual(basename(error.data_path), 'channels.json') | ||
1881 | 449 | # The contents of the signature file are not predictable. | ||
1882 | 450 | with open(channels_json + '.asc', 'rb') as fp: | ||
1883 | 451 | checksum = hashlib.md5(fp.read()).hexdigest() | ||
1884 | 452 | self.assertEqual(error.signature_checksum, checksum) | ||
1885 | 453 | self.assertEqual( | ||
1886 | 454 | error.data_checksum, '715c63fecbf44b62f9fa04a82dfa7d29') | ||
1887 | 455 | basenames = [basename(path) for path in error.keyrings] | ||
1888 | 456 | self.assertEqual(basenames, ['image-signing.tar.xz', | ||
1889 | 457 | 'device-signing.tar.xz']) | ||
1890 | 458 | self.assertEqual(basename(error.blacklist), 'blacklist.tar.xz') | ||
1891 | 459 | self.assertEqual(error.keyring_checksums, keyring_checksums) | ||
1892 | 460 | self.assertEqual(error.blacklist_checksum, blacklist_checksum) | ||
1893 | 461 | |||
1894 | 462 | @configuration | ||
1895 | 463 | def test_signature_error_logging(self): | ||
1896 | 464 | # The repr/str of the SignatureError should contain lots of useful | ||
1897 | 465 | # information that will make debugging easier. | ||
1898 | 466 | channels_json = os.path.join(self._tmpdir, 'channels.json') | ||
1899 | 467 | copy('channels_01.json', self._tmpdir, dst=channels_json) | ||
1900 | 468 | sign(channels_json, 'device-signing.gpg') | ||
1901 | 469 | # Verify the signature with the pubkey. | ||
1902 | 470 | tmpdir = self._stack.enter_context(temporary_directory()) | ||
1903 | 471 | dst = os.path.join(tmpdir, 'image-signing.tar.xz') | ||
1904 | 472 | setup_keyring_txz('image-signing.gpg', 'image-master.gpg', | ||
1905 | 473 | dict(type='image-signing'), dst) | ||
1906 | 474 | output = StringIO() | ||
1907 | 475 | with Context(dst) as ctx: | ||
1908 | 476 | try: | ||
1909 | 477 | ctx.validate(channels_json + '.asc', channels_json) | ||
1910 | 478 | except SignatureError: | ||
1911 | 479 | # For our purposes, log.exception() is essentially a wrapper | ||
1912 | 480 | # around this traceback call. We don't really care about the | ||
1913 | 481 | # full stack trace though. | ||
1914 | 482 | e = sys.exc_info() | ||
1915 | 483 | traceback.print_exception(e[0], e[1], e[2], | ||
1916 | 484 | limit=0, file=output) | ||
1917 | 485 | # 2014-02-12 BAW: Yuck, but I can't get assertRegex() to work properly. | ||
1918 | 486 | for i, line in enumerate(output.getvalue().splitlines()): | ||
1919 | 487 | if i == 0: | ||
1920 | 488 | self.assertEqual(line, 'Traceback (most recent call last):') | ||
1921 | 489 | elif i == 1: | ||
1922 | 490 | self.assertEqual(line, 'systemimage.gpg.SignatureError: ') | ||
1923 | 491 | elif i == 2: | ||
1924 | 492 | self.assertTrue(line.startswith(' sig path :')) | ||
1925 | 493 | elif i == 3: | ||
1926 | 494 | self.assertTrue(line.endswith('/channels.json.asc')) | ||
1927 | 495 | elif i == 4: | ||
1928 | 496 | self.assertEqual( | ||
1929 | 497 | line, ' data path: 715c63fecbf44b62f9fa04a82dfa7d29') | ||
1930 | 498 | elif i == 5: | ||
1931 | 499 | self.assertTrue(line.endswith('/channels.json')) | ||
1932 | 500 | elif i == 6: | ||
1933 | 501 | self.assertTrue(line.startswith(' keyrings :')) | ||
1934 | 502 | elif i == 7: | ||
1935 | 503 | self.assertTrue(line.endswith("/image-signing.tar.xz']")) | ||
1936 | 504 | elif i == 8: | ||
1937 | 505 | self.assertEqual(line, ' blacklist: no blacklist ') | ||
1938 | 333 | 506 | ||
1939 | === modified file 'systemimage/tests/test_helpers.py' | |||
1940 | --- systemimage/tests/test_helpers.py 2013-12-13 13:55:51 +0000 | |||
1941 | +++ systemimage/tests/test_helpers.py 2014-02-25 17:45:25 +0000 | |||
1942 | @@ -1,4 +1,4 @@ | |||
1944 | 1 | # Copyright (C) 2013 Canonical Ltd. | 1 | # Copyright (C) 2013-2014 Canonical Ltd. |
1945 | 2 | # Author: Barry Warsaw <barry@ubuntu.com> | 2 | # Author: Barry Warsaw <barry@ubuntu.com> |
1946 | 3 | 3 | ||
1947 | 4 | # This program is free software: you can redistribute it and/or modify | 4 | # This program is free software: you can redistribute it and/or modify |
1948 | 5 | 5 | ||
1949 | === modified file 'systemimage/tests/test_image.py' | |||
1950 | --- systemimage/tests/test_image.py 2013-12-13 13:55:51 +0000 | |||
1951 | +++ systemimage/tests/test_image.py 2014-02-25 17:45:25 +0000 | |||
1952 | @@ -1,4 +1,4 @@ | |||
1954 | 1 | # Copyright (C) 2013 Canonical Ltd. | 1 | # Copyright (C) 2013-2014 Canonical Ltd. |
1955 | 2 | # Author: Barry Warsaw <barry@ubuntu.com> | 2 | # Author: Barry Warsaw <barry@ubuntu.com> |
1956 | 3 | 3 | ||
1957 | 4 | # This program is free software: you can redistribute it and/or modify | 4 | # This program is free software: you can redistribute it and/or modify |
1958 | 5 | 5 | ||
1959 | === modified file 'systemimage/tests/test_index.py' | |||
1960 | --- systemimage/tests/test_index.py 2013-12-13 13:55:51 +0000 | |||
1961 | +++ systemimage/tests/test_index.py 2014-02-25 17:45:25 +0000 | |||
1962 | @@ -1,4 +1,4 @@ | |||
1964 | 1 | # Copyright (C) 2013 Canonical Ltd. | 1 | # Copyright (C) 2013-2014 Canonical Ltd. |
1965 | 2 | # Author: Barry Warsaw <barry@ubuntu.com> | 2 | # Author: Barry Warsaw <barry@ubuntu.com> |
1966 | 3 | 3 | ||
1967 | 4 | # This program is free software: you can redistribute it and/or modify | 4 | # This program is free software: you can redistribute it and/or modify |
1968 | 5 | 5 | ||
1969 | === modified file 'systemimage/tests/test_keyring.py' | |||
1970 | --- systemimage/tests/test_keyring.py 2013-12-13 13:55:51 +0000 | |||
1971 | +++ systemimage/tests/test_keyring.py 2014-02-25 17:45:25 +0000 | |||
1972 | @@ -1,4 +1,4 @@ | |||
1974 | 1 | # Copyright (C) 2013 Canonical Ltd. | 1 | # Copyright (C) 2013-2014 Canonical Ltd. |
1975 | 2 | # Author: Barry Warsaw <barry@ubuntu.com> | 2 | # Author: Barry Warsaw <barry@ubuntu.com> |
1976 | 3 | 3 | ||
1977 | 4 | # This program is free software: you can redistribute it and/or modify | 4 | # This program is free software: you can redistribute it and/or modify |
1978 | @@ -22,6 +22,7 @@ | |||
1979 | 22 | 22 | ||
1980 | 23 | 23 | ||
1981 | 24 | import os | 24 | import os |
1982 | 25 | import hashlib | ||
1983 | 25 | import unittest | 26 | import unittest |
1984 | 26 | 27 | ||
1985 | 27 | from contextlib import ExitStack | 28 | from contextlib import ExitStack |
1986 | @@ -174,11 +175,25 @@ | |||
1987 | 174 | setup_keyrings() | 175 | setup_keyrings() |
1988 | 175 | # Use the spare key as the blacklist, signed by itself. Since this | 176 | # Use the spare key as the blacklist, signed by itself. Since this |
1989 | 176 | # won't match the image-signing key, the check will fail. | 177 | # won't match the image-signing key, the check will fail. |
1990 | 178 | server_path = os.path.join(self._serverdir, 'gpg', 'blacklist.tar.xz') | ||
1991 | 177 | setup_keyring_txz( | 179 | setup_keyring_txz( |
1996 | 178 | 'spare.gpg', 'spare.gpg', dict(type='blacklist'), | 180 | 'spare.gpg', 'spare.gpg', dict(type='blacklist'), server_path) |
1997 | 179 | os.path.join(self._serverdir, 'gpg', 'blacklist.tar.xz')) | 181 | with self.assertRaises(SignatureError) as cm: |
1998 | 180 | self.assertRaises(SignatureError, get_keyring, | 182 | get_keyring('blacklist', 'gpg/blacklist.tar.xz', 'image-master') |
1999 | 181 | 'blacklist', 'gpg/blacklist.tar.xz', 'image-master') | 183 | error = cm.exception |
2000 | 184 | # The local file name will be keyring.tar.xz in the cache directory. | ||
2001 | 185 | basename = os.path.basename | ||
2002 | 186 | self.assertEqual(basename(error.data_path), 'keyring.tar.xz') | ||
2003 | 187 | self.assertEqual(basename(error.signature_path), 'keyring.tar.xz.asc') | ||
2004 | 188 | # The crafted blacklist.tar.xz file will have an unpredictable | ||
2005 | 189 | # checksum due to tarfile variablility. | ||
2006 | 190 | with open(server_path, 'rb') as fp: | ||
2007 | 191 | checksum = hashlib.md5(fp.read()).hexdigest() | ||
2008 | 192 | self.assertEqual(error.data_checksum, checksum) | ||
2009 | 193 | # The signature file's checksum is also unpredictable. | ||
2010 | 194 | with open(server_path + '.asc', 'rb') as fp: | ||
2011 | 195 | checksum = hashlib.md5(fp.read()).hexdigest() | ||
2012 | 196 | self.assertEqual(error.signature_checksum, checksum) | ||
2013 | 182 | 197 | ||
2014 | 183 | @configuration | 198 | @configuration |
2015 | 184 | def test_blacklisted_signature(self): | 199 | def test_blacklisted_signature(self): |
2016 | 185 | 200 | ||
2017 | === modified file 'systemimage/tests/test_main.py' | |||
2018 | --- systemimage/tests/test_main.py 2013-12-13 13:55:51 +0000 | |||
2019 | +++ systemimage/tests/test_main.py 2014-02-25 17:45:25 +0000 | |||
2020 | @@ -1,4 +1,4 @@ | |||
2022 | 1 | # Copyright (C) 2013 Canonical Ltd. | 1 | # Copyright (C) 2013-2014 Canonical Ltd. |
2023 | 2 | # Author: Barry Warsaw <barry@ubuntu.com> | 2 | # Author: Barry Warsaw <barry@ubuntu.com> |
2024 | 3 | 3 | ||
2025 | 4 | # This program is free software: you can redistribute it and/or modify | 4 | # This program is free software: you can redistribute it and/or modify |
2026 | @@ -26,11 +26,13 @@ | |||
2027 | 26 | 26 | ||
2028 | 27 | 27 | ||
2029 | 28 | import os | 28 | import os |
2030 | 29 | import sys | ||
2031 | 29 | import dbus | 30 | import dbus |
2032 | 30 | import stat | 31 | import stat |
2033 | 31 | import time | 32 | import time |
2034 | 32 | import shutil | 33 | import shutil |
2035 | 33 | import unittest | 34 | import unittest |
2036 | 35 | import subprocess | ||
2037 | 34 | 36 | ||
2038 | 35 | from contextlib import ExitStack, contextmanager | 37 | from contextlib import ExitStack, contextmanager |
2039 | 36 | from datetime import datetime | 38 | from datetime import datetime |
2040 | @@ -781,3 +783,25 @@ | |||
2041 | 781 | self.assertEqual(stat.filemode(mode), 'drwx--S---') | 783 | self.assertEqual(stat.filemode(mode), 'drwx--S---') |
2042 | 782 | mode = os.stat(config.system.logfile).st_mode | 784 | mode = os.stat(config.system.logfile).st_mode |
2043 | 783 | self.assertEqual(stat.filemode(mode), '-rw-------') | 785 | self.assertEqual(stat.filemode(mode), '-rw-------') |
2044 | 786 | |||
2045 | 787 | def test_single_instance(self): | ||
2046 | 788 | # Only one instance of the system-image-dbus service is allowed to | ||
2047 | 789 | # remain active on a single system bus. | ||
2048 | 790 | self.assertIsNone(find_dbus_process(self.ini_path)) | ||
2049 | 791 | self._activate() | ||
2050 | 792 | proc = find_dbus_process(self.ini_path) | ||
2051 | 793 | # Attempt to start a second process on the same system bus. | ||
2052 | 794 | env = dict( | ||
2053 | 795 | DBUS_SYSTEM_BUS_ADDRESS=os.environ['DBUS_SYSTEM_BUS_ADDRESS']) | ||
2054 | 796 | args = (sys.executable, '-m', 'systemimage.service', | ||
2055 | 797 | '-C', self.ini_path) | ||
2056 | 798 | second = subprocess.Popen(args, universal_newlines=True, env=env) | ||
2057 | 799 | # Allow a TimeoutExpired exception to fail the test. | ||
2058 | 800 | try: | ||
2059 | 801 | code = second.wait(timeout=10) | ||
2060 | 802 | except subprocess.TimeoutExpired: | ||
2061 | 803 | second.kill() | ||
2062 | 804 | second.communicate() | ||
2063 | 805 | raise | ||
2064 | 806 | self.assertNotEqual(second.pid, proc) | ||
2065 | 807 | self.assertEqual(code, 2) | ||
2066 | 784 | 808 | ||
2067 | === modified file 'systemimage/tests/test_scores.py' | |||
2068 | --- systemimage/tests/test_scores.py 2013-12-13 13:55:51 +0000 | |||
2069 | +++ systemimage/tests/test_scores.py 2014-02-25 17:45:25 +0000 | |||
2070 | @@ -1,4 +1,4 @@ | |||
2072 | 1 | # Copyright (C) 2013 Canonical Ltd. | 1 | # Copyright (C) 2013-2014 Canonical Ltd. |
2073 | 2 | # Author: Barry Warsaw <barry@ubuntu.com> | 2 | # Author: Barry Warsaw <barry@ubuntu.com> |
2074 | 3 | 3 | ||
2075 | 4 | # This program is free software: you can redistribute it and/or modify | 4 | # This program is free software: you can redistribute it and/or modify |
2076 | 5 | 5 | ||
2077 | === modified file 'systemimage/tests/test_settings.py' | |||
2078 | --- systemimage/tests/test_settings.py 2013-12-13 13:55:51 +0000 | |||
2079 | +++ systemimage/tests/test_settings.py 2014-02-25 17:45:25 +0000 | |||
2080 | @@ -1,4 +1,4 @@ | |||
2082 | 1 | # Copyright (C) 2013 Canonical Ltd. | 1 | # Copyright (C) 2013-2014 Canonical Ltd. |
2083 | 2 | # Author: Barry Warsaw <barry@ubuntu.com> | 2 | # Author: Barry Warsaw <barry@ubuntu.com> |
2084 | 3 | 3 | ||
2085 | 4 | # This program is free software: you can redistribute it and/or modify | 4 | # This program is free software: you can redistribute it and/or modify |
2086 | 5 | 5 | ||
2087 | === modified file 'systemimage/tests/test_state.py' | |||
2088 | --- systemimage/tests/test_state.py 2013-12-13 13:55:51 +0000 | |||
2089 | +++ systemimage/tests/test_state.py 2014-02-25 17:45:25 +0000 | |||
2090 | @@ -1,4 +1,4 @@ | |||
2092 | 1 | # Copyright (C) 2013 Canonical Ltd. | 1 | # Copyright (C) 2013-2014 Canonical Ltd. |
2093 | 2 | # Author: Barry Warsaw <barry@ubuntu.com> | 2 | # Author: Barry Warsaw <barry@ubuntu.com> |
2094 | 3 | 3 | ||
2095 | 4 | # This program is free software: you can redistribute it and/or modify | 4 | # This program is free software: you can redistribute it and/or modify |
2096 | 5 | 5 | ||
2097 | === modified file 'systemimage/tests/test_winner.py' | |||
2098 | --- systemimage/tests/test_winner.py 2013-12-13 13:55:51 +0000 | |||
2099 | +++ systemimage/tests/test_winner.py 2014-02-25 17:45:25 +0000 | |||
2100 | @@ -1,4 +1,4 @@ | |||
2102 | 1 | # Copyright (C) 2013 Canonical Ltd. | 1 | # Copyright (C) 2013-2014 Canonical Ltd. |
2103 | 2 | # Author: Barry Warsaw <barry@ubuntu.com> | 2 | # Author: Barry Warsaw <barry@ubuntu.com> |
2104 | 3 | 3 | ||
2105 | 4 | # This program is free software: you can redistribute it and/or modify | 4 | # This program is free software: you can redistribute it and/or modify |
2106 | 5 | 5 | ||
2107 | === modified file 'systemimage/version.txt' | |||
2108 | --- systemimage/version.txt 2013-12-13 13:55:51 +0000 | |||
2109 | +++ systemimage/version.txt 2014-02-25 17:45:25 +0000 | |||
2110 | @@ -1,1 +1,1 @@ | |||
2112 | 1 | 2.0.3 | 1 | 2.1 |
2113 | 2 | 2 | ||
2114 | === modified file 'tox.ini' | |||
2115 | --- tox.ini 2013-12-13 13:55:51 +0000 | |||
2116 | +++ tox.ini 2014-02-25 17:45:25 +0000 | |||
2117 | @@ -1,5 +1,5 @@ | |||
2118 | 1 | [tox] | 1 | [tox] |
2120 | 2 | envlist = py33 | 2 | envlist = py33, py34 |
2121 | 3 | 3 | ||
2122 | 4 | [testenv] | 4 | [testenv] |
2123 | 5 | commands = python -m nose2 -v | 5 | commands = python -m nose2 -v |