~ahasenack/landscape-client/landscape-client-11.02-0ubuntu0.8.04.1

« back to all changes in this revision

Viewing changes to landscape/package/tests/test_changer.py

  • Committer: Andreas Hasenack
  • Date: 2011-05-05 14:12:15 UTC
  • Revision ID: andreas@canonical.com-20110505141215-5ymuyyh5es9pwa6p
Added hardy files.

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
# -*- encoding: utf-8 -*-
 
2
import base64
 
3
import time
 
4
import sys
 
5
import os
 
6
 
 
7
from twisted.internet.defer import Deferred
 
8
 
 
9
from smart.cache import Provides
 
10
 
 
11
from landscape.lib.fs import touch_file
 
12
from landscape.package.changer import (
 
13
    PackageChanger, main, find_changer_command, UNKNOWN_PACKAGE_DATA_TIMEOUT,
 
14
    SUCCESS_RESULT, DEPENDENCY_ERROR_RESULT, POLICY_ALLOW_INSTALLS,
 
15
    POLICY_ALLOW_ALL_CHANGES)
 
16
from landscape.package.store import PackageStore
 
17
from landscape.package.facade import (
 
18
    DependencyError, TransactionError, SmartError)
 
19
from landscape.package.changer import (
 
20
    PackageChangerConfiguration, ChangePackagesResult)
 
21
from landscape.tests.mocker import ANY
 
22
from landscape.tests.helpers import (
 
23
    LandscapeTest, BrokerServiceHelper)
 
24
from landscape.package.tests.helpers import (
 
25
    SmartFacadeHelper, HASH1, HASH2, HASH3, PKGDEB1, PKGDEB2, PKGNAME2)
 
26
from landscape.manager.manager import SUCCEEDED
 
27
 
 
28
 
 
29
class PackageChangerTest(LandscapeTest):
 
30
 
 
31
    helpers = [SmartFacadeHelper, BrokerServiceHelper]
 
32
 
 
33
    def setUp(self):
 
34
 
 
35
        def set_up(ignored):
 
36
 
 
37
            self.store = PackageStore(self.makeFile())
 
38
            self.config = PackageChangerConfiguration()
 
39
            self.config.data_path = self.makeDir()
 
40
            os.mkdir(self.config.package_directory)
 
41
            os.mkdir(self.config.binaries_path)
 
42
            touch_file(self.config.smart_update_stamp_filename)
 
43
            self.changer = PackageChanger(
 
44
                self.store, self.facade, self.remote, self.config)
 
45
            service = self.broker_service
 
46
            service.message_store.set_accepted_types(["change-packages-result",
 
47
                                                      "operation-result"])
 
48
 
 
49
        result = super(PackageChangerTest, self).setUp()
 
50
        return result.addCallback(set_up)
 
51
 
 
52
    def get_pending_messages(self):
 
53
        return self.broker_service.message_store.get_pending_messages()
 
54
 
 
55
    def set_pkg1_installed(self):
 
56
        previous = self.Facade.channels_reloaded
 
57
 
 
58
        def callback(self):
 
59
            previous(self)
 
60
            self.get_packages_by_name("name1")[0].installed = True
 
61
        self.Facade.channels_reloaded = callback
 
62
 
 
63
    def set_pkg2_upgrades_pkg1(self):
 
64
        previous = self.Facade.channels_reloaded
 
65
 
 
66
        def callback(self):
 
67
            from smart.backends.deb.base import DebUpgrades
 
68
            previous(self)
 
69
            pkg2 = self.get_packages_by_name("name2")[0]
 
70
            pkg2.upgrades += (DebUpgrades("name1", "=", "version1-release1"),)
 
71
            self.reload_cache() # Relink relations.
 
72
        self.Facade.channels_reloaded = callback
 
73
 
 
74
    def set_pkg2_satisfied(self):
 
75
        previous = self.Facade.channels_reloaded
 
76
 
 
77
        def callback(self):
 
78
            previous(self)
 
79
            pkg2 = self.get_packages_by_name("name2")[0]
 
80
            pkg2.requires = ()
 
81
            self.reload_cache() # Relink relations.
 
82
        self.Facade.channels_reloaded = callback
 
83
 
 
84
    def set_pkg1_and_pkg2_satisfied(self):
 
85
        previous = self.Facade.channels_reloaded
 
86
 
 
87
        def callback(self):
 
88
            previous(self)
 
89
 
 
90
            provide1 = Provides("prerequirename1", "prerequireversion1")
 
91
            provide2 = Provides("requirename1", "requireversion1")
 
92
            pkg2 = self.get_packages_by_name("name2")[0]
 
93
            pkg2.provides += (provide1, provide2)
 
94
 
 
95
            provide1 = Provides("prerequirename2", "prerequireversion2")
 
96
            provide2 = Provides("requirename2", "requireversion2")
 
97
            pkg1 = self.get_packages_by_name("name1")[0]
 
98
            pkg1.provides += (provide1, provide2)
 
99
 
 
100
            # Ask Smart to reprocess relationships.
 
101
            self.reload_cache()
 
102
        self.Facade.channels_reloaded = callback
 
103
 
 
104
    def test_unknown_package_id_for_dependency(self):
 
105
        self.set_pkg1_and_pkg2_satisfied()
 
106
 
 
107
        # Let's request an operation that would require an answer with a
 
108
        # must-install field with a package for which the id isn't yet
 
109
        # known by the client.
 
110
        self.store.add_task("changer",
 
111
                            {"type": "change-packages", "install": [1],
 
112
                             "operation-id": 123})
 
113
 
 
114
        # In our first try, we should get nothing, because the id of the
 
115
        # dependency (HASH2) isn't known.
 
116
        self.store.set_hash_ids({HASH1: 1})
 
117
        result = self.changer.handle_tasks()
 
118
        self.assertEquals(result.called, True)
 
119
        self.assertMessages(self.get_pending_messages(), [])
 
120
 
 
121
        self.assertIn("Package data not yet synchronized with server (%r)"
 
122
                      % HASH2, self.logfile.getvalue())
 
123
 
 
124
        # So now we'll set it, and advance the reactor to the scheduled
 
125
        # change detection.  We'll get a lot of messages, including the
 
126
        # result of our previous message, which got *postponed*.
 
127
        self.store.set_hash_ids({HASH2: 2})
 
128
        result = self.changer.handle_tasks()
 
129
 
 
130
        def got_result(result):
 
131
            self.assertMessages(self.get_pending_messages(),
 
132
                                [{"must-install": [2],
 
133
                                  "operation-id": 123,
 
134
                                  "result-code": 101,
 
135
                                  "type": "change-packages-result"}])
 
136
        return result.addCallback(got_result)
 
137
 
 
138
    def test_install_unknown_id(self):
 
139
        self.store.add_task("changer",
 
140
                            {"type": "change-packages", "install": [456],
 
141
                             "operation-id": 123})
 
142
 
 
143
        self.changer.handle_tasks()
 
144
 
 
145
        self.assertIn("Package data not yet synchronized with server (456)",
 
146
                      self.logfile.getvalue())
 
147
        self.assertTrue(self.store.get_next_task("changer"))
 
148
 
 
149
    def test_remove_unknown_id(self):
 
150
        self.store.add_task("changer",
 
151
                            {"type": "change-packages", "remove": [456],
 
152
                             "operation-id": 123})
 
153
 
 
154
        self.changer.handle_tasks()
 
155
 
 
156
        self.assertIn("Package data not yet synchronized with server (456)",
 
157
                      self.logfile.getvalue())
 
158
        self.assertTrue(self.store.get_next_task("changer"))
 
159
 
 
160
    def test_install_unknown_package(self):
 
161
        self.store.set_hash_ids({"hash": 456})
 
162
        self.store.add_task("changer",
 
163
                            {"type": "change-packages", "install": [456],
 
164
                             "operation-id": 123})
 
165
 
 
166
        self.changer.handle_tasks()
 
167
 
 
168
        self.assertIn("Package data not yet synchronized with server ('hash')",
 
169
                      self.logfile.getvalue())
 
170
        self.assertTrue(self.store.get_next_task("changer"))
 
171
 
 
172
    def test_remove_unknown_package(self):
 
173
        self.store.set_hash_ids({"hash": 456})
 
174
        self.store.add_task("changer",
 
175
                            {"type": "change-packages", "remove": [456],
 
176
                             "operation-id": 123})
 
177
 
 
178
        self.changer.handle_tasks()
 
179
 
 
180
        self.assertIn("Package data not yet synchronized with server ('hash')",
 
181
                      self.logfile.getvalue())
 
182
        self.assertTrue(self.store.get_next_task("changer"))
 
183
 
 
184
    def test_unknown_data_timeout(self):
 
185
        """After a while, unknown package data is reported as an error.
 
186
 
 
187
        In these cases a warning is logged, and the task is removed.
 
188
        """
 
189
        self.store.add_task("changer",
 
190
                            {"type": "change-packages", "remove": [123],
 
191
                             "operation-id": 123})
 
192
 
 
193
        time_mock = self.mocker.replace("time.time")
 
194
        time_mock()
 
195
        self.mocker.result(time.time() + UNKNOWN_PACKAGE_DATA_TIMEOUT)
 
196
        self.mocker.count(1, None)
 
197
        self.mocker.replay()
 
198
 
 
199
        try:
 
200
            result = self.changer.handle_tasks()
 
201
            self.mocker.verify()
 
202
        finally:
 
203
            # Reset it earlier so that Twisted has the true time function.
 
204
            self.mocker.reset()
 
205
 
 
206
        self.assertIn("Package data not yet synchronized with server (123)",
 
207
                      self.logfile.getvalue())
 
208
 
 
209
        def got_result(result):
 
210
            message = {"type": "change-packages-result",
 
211
                       "operation-id": 123,
 
212
                       "result-code": 100,
 
213
                       "result-text": "Package data has changed. "
 
214
                                      "Please retry the operation."}
 
215
            self.assertMessages(self.get_pending_messages(), [message])
 
216
            self.assertEquals(self.store.get_next_task("changer"), None)
 
217
        return result.addCallback(got_result)
 
218
 
 
219
    def test_dpkg_error(self):
 
220
        """
 
221
        Verify that errors emitted by dpkg are correctly reported to
 
222
        the server as problems.
 
223
        """
 
224
        self.log_helper.ignore_errors(".*dpkg")
 
225
 
 
226
        self.store.set_hash_ids({HASH1: 1})
 
227
        self.store.add_task("changer",
 
228
                            {"type": "change-packages", "remove": [1],
 
229
                             "operation-id": 123})
 
230
 
 
231
        self.set_pkg1_installed()
 
232
 
 
233
        result = self.changer.handle_tasks()
 
234
 
 
235
        def got_result(result):
 
236
            messages = self.get_pending_messages()
 
237
            self.assertEquals(len(messages), 1, "Too many messages")
 
238
            message = messages[0]
 
239
            self.assertEquals(message["operation-id"], 123)
 
240
            self.assertEquals(message["result-code"], 100)
 
241
            self.assertEquals(message["type"], "change-packages-result")
 
242
            text = message["result-text"]
 
243
            # We can't test the actual content of the message because the dpkg
 
244
            # error can be localized
 
245
            self.assertIn("\n[remove] name1_version1-release1\ndpkg: ", text)
 
246
            self.assertIn("ERROR", text)
 
247
            self.assertIn("(2)", text)
 
248
        return result.addCallback(got_result)
 
249
 
 
250
    def test_dependency_error(self):
 
251
        """
 
252
        In this test we hack the facade to simulate the situation where
 
253
        Smart didn't accept to remove the package due to missing
 
254
        dependencies that are present in the system but weren't requested
 
255
        in the message.
 
256
 
 
257
        The client must answer it saying which additional changes are
 
258
        needed to perform the requested operation.
 
259
 
 
260
        It's a slightly hackish approach, since we're returning
 
261
        the full set of packages available as a dependency error, but
 
262
        it serves well for testing this specific feature.
 
263
        """
 
264
        self.store.set_hash_ids({HASH1: 1, HASH2: 2, HASH3: 3})
 
265
        self.store.add_task("changer",
 
266
                            {"type": "change-packages", "install": [2],
 
267
                             "operation-id": 123})
 
268
 
 
269
        self.set_pkg1_installed()
 
270
 
 
271
        def raise_dependency_error(self):
 
272
            raise DependencyError(self.get_packages())
 
273
        self.Facade.perform_changes = raise_dependency_error
 
274
 
 
275
        result = self.changer.handle_tasks()
 
276
 
 
277
        def got_result(result):
 
278
            self.assertMessages(self.get_pending_messages(),
 
279
                                [{"must-install": [2, 3],
 
280
                                  "must-remove": [1],
 
281
                                  "operation-id": 123,
 
282
                                  "result-code": 101,
 
283
                                  "type": "change-packages-result"}])
 
284
        return result.addCallback(got_result)
 
285
 
 
286
    def test_dependency_error_with_binaries(self):
 
287
        """
 
288
        Simulate a failing operation involving server-generated binary
 
289
        packages. The extra changes needed to perform the transaction
 
290
        are sent back to the server.
 
291
        """
 
292
        os.remove(os.path.join(self.repository_dir, PKGNAME2))
 
293
        self.store.set_hash_ids({HASH1: 1, HASH3: 3})
 
294
        self.store.add_task("changer",
 
295
                            {"type": "change-packages",
 
296
                             "install": [2],
 
297
                             "binaries": [(HASH2, 2, PKGDEB2)],
 
298
                             "operation-id": 123})
 
299
 
 
300
        self.set_pkg1_installed()
 
301
 
 
302
        def raise_dependency_error(self):
 
303
            raise DependencyError(self.get_packages())
 
304
        self.Facade.perform_changes = raise_dependency_error
 
305
 
 
306
        result = self.changer.handle_tasks()
 
307
 
 
308
        def got_result(result):
 
309
            self.assertMessages(self.get_pending_messages(),
 
310
                                [{"must-install": [2, 3],
 
311
                                  "must-remove": [1],
 
312
                                  "operation-id": 123,
 
313
                                  "result-code": 101,
 
314
                                  "type": "change-packages-result"}])
 
315
        return result.addCallback(got_result)
 
316
 
 
317
    def test_perform_changes_with_allow_install_policy(self):
 
318
        """
 
319
        The C{POLICY_ALLOW_INSTALLS} policy the makes the changer mark
 
320
        the missing packages for installation.
 
321
        """
 
322
        self.store.set_hash_ids({HASH1: 1})
 
323
        self.facade.reload_channels()
 
324
        package1 = self.facade.get_packages_by_name("name1")[0]
 
325
 
 
326
        self.mocker.order()
 
327
        self.facade.perform_changes = self.mocker.mock()
 
328
        self.facade.perform_changes()
 
329
        self.mocker.throw(DependencyError([package1]))
 
330
 
 
331
        self.facade.mark_install = self.mocker.mock()
 
332
        self.facade.mark_install(package1)
 
333
        self.facade.perform_changes()
 
334
        self.mocker.result("success")
 
335
        self.mocker.replay()
 
336
 
 
337
        result = self.changer.change_packages(POLICY_ALLOW_INSTALLS)
 
338
 
 
339
        self.assertEquals(result.code, SUCCESS_RESULT)
 
340
        self.assertEquals(result.text, "success")
 
341
        self.assertEquals(result.installs, [1])
 
342
        self.assertEquals(result.removals, [])
 
343
 
 
344
    def test_perform_changes_with_allow_install_policy_and_removals(self):
 
345
        """
 
346
        The C{POLICY_ALLOW_INSTALLS} policy doesn't allow additional packages
 
347
        to be removed.
 
348
        """
 
349
        self.store.set_hash_ids({HASH1: 1, HASH2: 2})
 
350
        self.set_pkg1_installed()
 
351
        self.facade.reload_channels()
 
352
 
 
353
        package1 = self.facade.get_packages_by_name("name1")[0]
 
354
        package2 = self.facade.get_packages_by_name("name2")[0]
 
355
        self.facade.perform_changes = self.mocker.mock()
 
356
        self.facade.perform_changes()
 
357
        self.mocker.throw(DependencyError([package1, package2]))
 
358
        self.mocker.replay()
 
359
 
 
360
        result = self.changer.change_packages(POLICY_ALLOW_INSTALLS)
 
361
 
 
362
        self.assertEquals(result.code, DEPENDENCY_ERROR_RESULT)
 
363
        self.assertEquals(result.text, None)
 
364
        self.assertEquals(result.installs, [2])
 
365
        self.assertEquals(result.removals, [1])
 
366
 
 
367
    def test_perform_changes_with_max_retries(self):
 
368
        """
 
369
        After having complemented the requested changes to handle a dependency
 
370
        error, the L{PackageChanger.change_packages} will try to perform the
 
371
        requested changes again only once.
 
372
        """
 
373
        self.store.set_hash_ids({HASH1: 1, HASH2: 2})
 
374
        self.facade.reload_channels()
 
375
 
 
376
        package1 = self.facade.get_packages_by_name("name1")[0]
 
377
        package2 = self.facade.get_packages_by_name("name2")[0]
 
378
 
 
379
        self.facade.perform_changes = self.mocker.mock()
 
380
        self.facade.perform_changes()
 
381
        self.mocker.throw(DependencyError([package1]))
 
382
        self.facade.perform_changes()
 
383
        self.mocker.throw(DependencyError([package2]))
 
384
        self.mocker.replay()
 
385
 
 
386
        result = self.changer.change_packages(POLICY_ALLOW_INSTALLS)
 
387
 
 
388
        self.assertEquals(result.code, DEPENDENCY_ERROR_RESULT)
 
389
        self.assertEquals(result.text, None)
 
390
        self.assertEquals(result.installs, [1, 2])
 
391
        self.assertEquals(result.removals, [])
 
392
 
 
393
    def test_handle_change_packages_with_policy(self):
 
394
        """
 
395
        The C{change-packages} message can have an optional C{policy}
 
396
        field that will be passed to the C{perform_changes} method.
 
397
        """
 
398
        self.store.set_hash_ids({HASH1: 1})
 
399
        self.store.add_task("changer",
 
400
                            {"type": "change-packages",
 
401
                             "install": [1],
 
402
                             "policy": POLICY_ALLOW_INSTALLS,
 
403
                             "operation-id": 123})
 
404
        self.changer.change_packages = self.mocker.mock()
 
405
        self.changer.change_packages(POLICY_ALLOW_INSTALLS)
 
406
        result = ChangePackagesResult()
 
407
        result.code = SUCCESS_RESULT
 
408
        self.mocker.result(result)
 
409
        self.mocker.replay()
 
410
        return self.changer.handle_tasks()
 
411
 
 
412
    def test_perform_changes_with_policy_allow_all_changes(self):
 
413
        """
 
414
        The C{POLICY_ALLOW_ALL_CHANGES} policy allows any needed additional
 
415
        package to be installed or removed.
 
416
        """
 
417
        self.store.set_hash_ids({HASH1: 1, HASH2: 2})
 
418
        self.set_pkg1_installed()
 
419
        self.facade.reload_channels()
 
420
 
 
421
        self.mocker.order()
 
422
        package1 = self.facade.get_packages_by_name("name1")[0]
 
423
        package2 = self.facade.get_packages_by_name("name2")[0]
 
424
        self.facade.perform_changes = self.mocker.mock()
 
425
        self.facade.perform_changes()
 
426
        self.mocker.throw(DependencyError([package1, package2]))
 
427
        self.facade.mark_install = self.mocker.mock()
 
428
        self.facade.mark_remove = self.mocker.mock()
 
429
        self.facade.mark_install(package2)
 
430
        self.facade.mark_remove(package1)
 
431
        self.facade.perform_changes()
 
432
        self.mocker.result("success")
 
433
        self.mocker.replay()
 
434
 
 
435
        result = self.changer.change_packages(POLICY_ALLOW_ALL_CHANGES)
 
436
 
 
437
        self.assertEquals(result.code, SUCCESS_RESULT)
 
438
        self.assertEquals(result.text, "success")
 
439
        self.assertEquals(result.installs, [2])
 
440
        self.assertEquals(result.removals, [1])
 
441
 
 
442
    def test_transaction_error(self):
 
443
        """
 
444
        In this case, the package we're trying to install declared some
 
445
        dependencies that can't be satisfied in the client because they
 
446
        don't exist at all.  The client must answer the request letting
 
447
        the server know about the unsolvable problem.
 
448
        """
 
449
        self.store.set_hash_ids({HASH1: 1})
 
450
        self.store.add_task("changer",
 
451
                            {"type": "change-packages", "install": [1],
 
452
                             "operation-id": 123})
 
453
 
 
454
        result = self.changer.handle_tasks()
 
455
 
 
456
        def got_result(result):
 
457
            result_text = ("requirename1 = requireversion1")
 
458
            messages = self.get_pending_messages()
 
459
            self.assertEquals(len(messages), 1)
 
460
            message = messages[0]
 
461
            self.assertEquals(message["operation-id"], 123)
 
462
            self.assertEquals(message["result-code"], 100)
 
463
            self.assertIn(result_text, message["result-text"])
 
464
            self.assertEquals(message["type"], "change-packages-result")
 
465
        return result.addCallback(got_result)
 
466
 
 
467
    def test_tasks_are_isolated(self):
 
468
        """
 
469
        Changes attempted on one task should be reset before the next
 
470
        task is run.  In this test, we try to run two different
 
471
        operations, first installing package 2, then upgrading
 
472
        anything available.  The first installation will fail for lack
 
473
        of superuser privileges, and the second one will succeed since
 
474
        there's nothing to upgrade.  If tasks are mixed up, the second
 
475
        operation will fail too, because the installation of package 2
 
476
        is still queued.
 
477
        """
 
478
        self.log_helper.ignore_errors(".*dpkg")
 
479
 
 
480
        self.store.set_hash_ids({HASH1: 1, HASH2: 2})
 
481
 
 
482
        self.store.add_task("changer",
 
483
                            {"type": "change-packages", "install": [2],
 
484
                             "operation-id": 123})
 
485
        self.store.add_task("changer",
 
486
                            {"type": "change-packages", "upgrade-all": True,
 
487
                             "operation-id": 124})
 
488
 
 
489
        self.set_pkg2_satisfied()
 
490
        self.set_pkg1_installed()
 
491
 
 
492
        result = self.changer.handle_tasks()
 
493
 
 
494
        def got_result(result):
 
495
            self.assertMessage(self.get_pending_messages()[1],
 
496
                               {"operation-id": 124,
 
497
                                "result-code": 1,
 
498
                                "type": "change-packages-result"})
 
499
 
 
500
        return result.addCallback(got_result)
 
501
 
 
502
    def test_successful_operation(self):
 
503
        """Simulate a *very* successful operation.
 
504
 
 
505
        We'll do that by hacking perform_changes(), and returning our
 
506
        *very* successful operation result.
 
507
        """
 
508
        self.store.set_hash_ids({HASH1: 1, HASH2: 2, HASH3: 3})
 
509
        self.store.add_task("changer",
 
510
                            {"type": "change-packages", "install": [2],
 
511
                             "operation-id": 123})
 
512
 
 
513
        self.set_pkg1_installed()
 
514
 
 
515
        def return_good_result(self):
 
516
            return "Yeah, I did whatever you've asked for!"
 
517
        self.Facade.perform_changes = return_good_result
 
518
 
 
519
        result = self.changer.handle_tasks()
 
520
 
 
521
        def got_result(result):
 
522
            self.assertMessages(self.get_pending_messages(),
 
523
                                [{"operation-id": 123,
 
524
                                  "result-code": 1,
 
525
                                  "result-text": "Yeah, I did whatever you've "
 
526
                                                 "asked for!",
 
527
                                  "type": "change-packages-result"}])
 
528
        return result.addCallback(got_result)
 
529
 
 
530
    def test_successful_operation_with_binaries(self):
 
531
        """
 
532
        Simulate a successful operation involving server-generated binary
 
533
        packages.
 
534
        """
 
535
        self.store.set_hash_ids({HASH3: 3})
 
536
        self.store.add_task("changer",
 
537
                            {"type": "change-packages", "install": [2, 3],
 
538
                             "binaries": [(HASH2, 2, PKGDEB2)],
 
539
                             "operation-id": 123})
 
540
 
 
541
        def return_good_result(self):
 
542
            return "Yeah, I did whatever you've asked for!"
 
543
        self.Facade.perform_changes = return_good_result
 
544
 
 
545
        result = self.changer.handle_tasks()
 
546
 
 
547
        def got_result(result):
 
548
            self.assertMessages(self.get_pending_messages(),
 
549
                                [{"operation-id": 123,
 
550
                                  "result-code": 1,
 
551
                                  "result-text": "Yeah, I did whatever you've "
 
552
                                                 "asked for!",
 
553
                                  "type": "change-packages-result"}])
 
554
        return result.addCallback(got_result)
 
555
 
 
556
    def test_global_upgrade(self):
 
557
        """
 
558
        Besides asking for individual changes, the server may also request
 
559
        the client to perform a global upgrade.  This would be the equivalent
 
560
        of a "smart upgrade" command being executed in the command line.
 
561
        """
 
562
        self.store.set_hash_ids({HASH1: 1, HASH2: 2})
 
563
 
 
564
        self.store.add_task("changer",
 
565
                            {"type": "change-packages", "upgrade-all": True,
 
566
                             "operation-id": 123})
 
567
 
 
568
        self.set_pkg2_upgrades_pkg1()
 
569
        self.set_pkg2_satisfied()
 
570
        self.set_pkg1_installed()
 
571
 
 
572
        result = self.changer.handle_tasks()
 
573
 
 
574
        def got_result(result):
 
575
            self.assertMessages(self.get_pending_messages(),
 
576
                                [{"operation-id": 123,
 
577
                                  "must-install": [2],
 
578
                                  "result-code": 101,
 
579
                                  "type": "change-packages-result"}])
 
580
 
 
581
        return result.addCallback(got_result)
 
582
 
 
583
    def test_global_upgrade_with_nothing_to_do(self):
 
584
 
 
585
        self.store.add_task("changer",
 
586
                            {"type": "change-packages", "upgrade-all": True,
 
587
                             "operation-id": 123})
 
588
 
 
589
        result = self.changer.handle_tasks()
 
590
 
 
591
        def got_result(result):
 
592
            self.assertMessages(self.get_pending_messages(),
 
593
                                [{"operation-id": 123,
 
594
                                  "result-code": 1,
 
595
                                  "type": "change-packages-result"}])
 
596
 
 
597
        return result.addCallback(got_result)
 
598
 
 
599
    def test_run_with_no_smart_update_stamp(self):
 
600
        """
 
601
        If the smart-update stamp file is not there yet, the package changer
 
602
        just exists.
 
603
        """
 
604
        os.remove(self.config.smart_update_stamp_filename)
 
605
 
 
606
        def assert_log(ignored):
 
607
            self.assertIn("The package-reporter hasn't run yet, exiting.",
 
608
                          self.logfile.getvalue())
 
609
 
 
610
        result = self.changer.run()
 
611
        return result.addCallback(assert_log)
 
612
 
 
613
    def test_spawn_reporter_after_running(self):
 
614
        output_filename = self.makeFile("REPORTER NOT RUN")
 
615
        reporter_filename = self.makeFile("#!/bin/sh\necho REPORTER RUN > %s" %
 
616
                                          output_filename)
 
617
        os.chmod(reporter_filename, 0755)
 
618
 
 
619
        find_command_mock = self.mocker.replace(
 
620
            "landscape.package.reporter.find_reporter_command")
 
621
        find_command_mock()
 
622
        self.mocker.result(reporter_filename)
 
623
        self.mocker.replay()
 
624
 
 
625
        # Add a task that will do nothing besides producing an answer.
 
626
        # The reporter is only spawned if at least one task was handled.
 
627
        self.store.add_task("changer", {"type": "change-packages",
 
628
                                        "operation-id": 123})
 
629
 
 
630
        result = self.changer.run()
 
631
 
 
632
        def got_result(result):
 
633
            self.assertEquals(open(output_filename).read().strip(),
 
634
                              "REPORTER RUN")
 
635
        return result.addCallback(got_result)
 
636
 
 
637
    def test_spawn_reporter_after_running_with_config(self):
 
638
        """The changer passes the config to the reporter when running it."""
 
639
        self.config.config = "test.conf"
 
640
        output_filename = self.makeFile("REPORTER NOT RUN")
 
641
        reporter_filename = self.makeFile("#!/bin/sh\necho ARGS $@ > %s" %
 
642
                                          output_filename)
 
643
        os.chmod(reporter_filename, 0755)
 
644
 
 
645
        find_command_mock = self.mocker.replace(
 
646
            "landscape.package.reporter.find_reporter_command")
 
647
        find_command_mock()
 
648
        self.mocker.result(reporter_filename)
 
649
        self.mocker.replay()
 
650
 
 
651
        # Add a task that will do nothing besides producing an answer.
 
652
        # The reporter is only spawned if at least one task was handled.
 
653
        self.store.add_task("changer", {"type": "change-packages",
 
654
                                        "operation-id": 123})
 
655
 
 
656
        result = self.changer.run()
 
657
 
 
658
        def got_result(result):
 
659
            self.assertEquals(open(output_filename).read().strip(),
 
660
                              "ARGS -c test.conf")
 
661
        return result.addCallback(got_result)
 
662
 
 
663
    def test_set_effective_uid_and_gid_when_running_as_root(self):
 
664
        """
 
665
        After the package changer has run, we want the package-reporter to run
 
666
        to report the recent changes.  If we're running as root, we want to
 
667
        change to the "landscape" user and "landscape" group. We also want to
 
668
        deinitialize Smart to let the reporter run smart-update cleanly.
 
669
        """
 
670
 
 
671
        # We are running as root
 
672
        getuid_mock = self.mocker.replace("os.getuid")
 
673
        getuid_mock()
 
674
        self.mocker.result(0)
 
675
 
 
676
        # The order matters (first smart then gid and finally uid)
 
677
        self.mocker.order()
 
678
 
 
679
        # Deinitialize smart
 
680
        facade_mock = self.mocker.patch(self.facade)
 
681
        facade_mock.deinit()
 
682
 
 
683
        # We want to return a known gid
 
684
        grnam_mock = self.mocker.replace("grp.getgrnam")
 
685
        grnam_mock("landscape")
 
686
 
 
687
        class FakeGroup(object):
 
688
            gr_gid = 199
 
689
 
 
690
        self.mocker.result(FakeGroup())
 
691
 
 
692
        # First the changer should change the group
 
693
        setgid_mock = self.mocker.replace("os.setgid")
 
694
        setgid_mock(199)
 
695
 
 
696
        # And a known uid as well
 
697
        pwnam_mock = self.mocker.replace("pwd.getpwnam")
 
698
        pwnam_mock("landscape")
 
699
 
 
700
        class FakeUser(object):
 
701
            pw_uid = 199
 
702
 
 
703
        self.mocker.result(FakeUser())
 
704
 
 
705
        # And now the user as well
 
706
        setuid_mock = self.mocker.replace("os.setuid")
 
707
        setuid_mock(199)
 
708
 
 
709
        # Finally, we don't really want the package reporter to run.
 
710
        system_mock = self.mocker.replace("os.system")
 
711
        system_mock(ANY)
 
712
 
 
713
        self.mocker.replay()
 
714
 
 
715
        # Add a task that will do nothing besides producing an answer.
 
716
        # The reporter is only spawned if at least one task was handled.
 
717
        self.store.add_task("changer", {"type": "change-packages",
 
718
                                        "operation-id": 123})
 
719
        return self.changer.run()
 
720
 
 
721
    def test_run(self):
 
722
        changer_mock = self.mocker.patch(self.changer)
 
723
 
 
724
        self.mocker.order()
 
725
 
 
726
        results = [Deferred() for i in range(2)]
 
727
 
 
728
        changer_mock.use_hash_id_db()
 
729
        self.mocker.result(results[0])
 
730
 
 
731
        changer_mock.handle_tasks()
 
732
        self.mocker.result(results[1])
 
733
 
 
734
        self.mocker.replay()
 
735
 
 
736
        self.changer.run()
 
737
 
 
738
        # It must raise an error because deferreds weren't yet fired.
 
739
        self.assertRaises(AssertionError, self.mocker.verify)
 
740
 
 
741
        for deferred in reversed(results):
 
742
            deferred.callback(None)
 
743
 
 
744
    def test_dont_spawn_reporter_after_running_if_nothing_done(self):
 
745
        output_filename = self.makeFile("REPORTER NOT RUN")
 
746
        reporter_filename = self.makeFile("#!/bin/sh\necho REPORTER RUN > %s" %
 
747
                                          output_filename)
 
748
        os.chmod(reporter_filename, 0755)
 
749
 
 
750
        find_command_mock = self.mocker.replace(
 
751
            "landscape.package.reporter.find_reporter_command")
 
752
        find_command_mock()
 
753
        self.mocker.result(reporter_filename)
 
754
        self.mocker.count(0, None)
 
755
        self.mocker.replay()
 
756
 
 
757
        result = self.changer.run()
 
758
 
 
759
        def got_result(result):
 
760
            self.assertEquals(open(output_filename).read().strip(),
 
761
                              "REPORTER NOT RUN")
 
762
        return result.addCallback(got_result)
 
763
 
 
764
    def test_main(self):
 
765
        self.mocker.order()
 
766
 
 
767
        run_task_handler = self.mocker.replace("landscape.package.taskhandler"
 
768
                                               ".run_task_handler",
 
769
                                               passthrough=False)
 
770
        getpgrp = self.mocker.replace("os.getpgrp")
 
771
        self.expect(getpgrp()).result(os.getpid() + 1)
 
772
        setsid = self.mocker.replace("os.setsid")
 
773
        setsid()
 
774
        run_task_handler(PackageChanger, ["ARGS"])
 
775
        self.mocker.result("RESULT")
 
776
 
 
777
        self.mocker.replay()
 
778
 
 
779
        self.assertEquals(main(["ARGS"]), "RESULT")
 
780
 
 
781
    def test_main_run_from_shell(self):
 
782
        """
 
783
        We want the getpid and getpgrp to return the same process id
 
784
        this simulates the case where the process is already the process
 
785
        session leader, in this case the os.setsid would fail.
 
786
        """
 
787
        getpgrp = self.mocker.replace("os.getpgrp")
 
788
        getpgrp()
 
789
        self.mocker.result(os.getpid())
 
790
 
 
791
        setsid = self.mocker.replace("os.setsid")
 
792
        setsid()
 
793
        self.mocker.count(0, 0)
 
794
 
 
795
        run_task_handler = self.mocker.replace("landscape.package.taskhandler"
 
796
                                               ".run_task_handler",
 
797
                                               passthrough=False)
 
798
        run_task_handler(PackageChanger, ["ARGS"])
 
799
        self.mocker.replay()
 
800
 
 
801
        main(["ARGS"])
 
802
 
 
803
    def test_find_changer_command(self):
 
804
        dirname = self.makeDir()
 
805
        filename = self.makeFile("", dirname=dirname,
 
806
                                 basename="landscape-package-changer")
 
807
 
 
808
        saved_argv = sys.argv
 
809
        try:
 
810
            sys.argv = [os.path.join(dirname, "landscape-monitor")]
 
811
 
 
812
            command = find_changer_command()
 
813
 
 
814
            self.assertEquals(command, filename)
 
815
        finally:
 
816
            sys.argv = saved_argv
 
817
 
 
818
    def test_transaction_error_with_unicode_data(self):
 
819
        self.store.set_hash_ids({HASH1: 1})
 
820
        self.store.add_task("changer",
 
821
                            {"type": "change-packages", "install": [1],
 
822
                             "operation-id": 123})
 
823
 
 
824
        def raise_error(self):
 
825
            raise TransactionError(u"áéíóú")
 
826
        self.Facade.perform_changes = raise_error
 
827
 
 
828
        result = self.changer.handle_tasks()
 
829
 
 
830
        def got_result(result):
 
831
            self.assertMessages(self.get_pending_messages(),
 
832
                                [{"operation-id": 123,
 
833
                                  "result-code": 100,
 
834
                                  "result-text": u"áéíóú",
 
835
                                  "type": "change-packages-result"}])
 
836
        return result.addCallback(got_result)
 
837
 
 
838
    def test_smart_error_with_unicode_data(self):
 
839
        self.store.set_hash_ids({HASH1: 1})
 
840
        self.store.add_task("changer",
 
841
                            {"type": "change-packages", "install": [1],
 
842
                             "operation-id": 123})
 
843
 
 
844
        def raise_error(self):
 
845
            raise SmartError(u"áéíóú")
 
846
        self.Facade.perform_changes = raise_error
 
847
 
 
848
        result = self.changer.handle_tasks()
 
849
 
 
850
        def got_result(result):
 
851
            self.assertMessages(self.get_pending_messages(),
 
852
                                [{"operation-id": 123,
 
853
                                  "result-code": 100,
 
854
                                  "result-text": u"áéíóú",
 
855
                                  "type": "change-packages-result"}])
 
856
        return result.addCallback(got_result)
 
857
 
 
858
    def test_smart_update_stamp_exists(self):
 
859
        """
 
860
        L{PackageChanger.smart_update_exists} returns C{True} if the
 
861
        smart-update stamp file is there, C{False} otherwise.
 
862
        """
 
863
        self.assertTrue(self.changer.smart_update_stamp_exists())
 
864
        os.remove(self.config.smart_update_stamp_filename)
 
865
        self.assertFalse(self.changer.smart_update_stamp_exists())
 
866
 
 
867
    def test_binaries_path(self):
 
868
        self.assertEquals(
 
869
            self.config.binaries_path,
 
870
            os.path.join(self.config.data_path, "package", "binaries"))
 
871
 
 
872
    def test_init_channels(self):
 
873
        """
 
874
        The L{PackageChanger.init_channels} method makes the given
 
875
        Debian packages available in a C{deb-dir} Smart channel.
 
876
        """
 
877
        binaries = [(HASH1, 111, PKGDEB1), (HASH2, 222, PKGDEB2)]
 
878
 
 
879
        self.facade.reset_channels()
 
880
        self.changer.init_channels(binaries)
 
881
 
 
882
        binaries_path = self.config.binaries_path
 
883
        self.assertFileContent(os.path.join(binaries_path, "111.deb"),
 
884
                               base64.decodestring(PKGDEB1))
 
885
        self.assertFileContent(os.path.join(binaries_path, "222.deb"),
 
886
                               base64.decodestring(PKGDEB2))
 
887
        self.assertEquals(self.facade.get_channels(),
 
888
                          {binaries_path: {"type": "deb-dir",
 
889
                                            "path": binaries_path}})
 
890
 
 
891
        self.assertEquals(self.store.get_hash_ids(), {HASH1: 111, HASH2: 222})
 
892
 
 
893
        self.facade.ensure_channels_reloaded()
 
894
        [pkg1, pkg2] = sorted(self.facade.get_packages(),
 
895
                              key=lambda pkg: pkg.name)
 
896
        self.assertEquals(self.facade.get_package_hash(pkg1), HASH1)
 
897
        self.assertEquals(self.facade.get_package_hash(pkg2), HASH2)
 
898
 
 
899
    def test_init_channels_with_existing_hash_id_map(self):
 
900
        """
 
901
        The L{PackageChanger.init_channels} behaves well even if the
 
902
        hash->id mapping for a given deb is already in the L{PackageStore}.
 
903
        """
 
904
        self.store.set_hash_ids({HASH1: 111})
 
905
        self.changer.init_channels([(HASH1, 111, PKGDEB1)])
 
906
        self.assertEquals(self.store.get_hash_ids(), {HASH1: 111})
 
907
 
 
908
    def test_init_channels_with_existing_binaries(self):
 
909
        """
 
910
        The L{PackageChanger.init_channels} removes Debian packages
 
911
        from previous runs.
 
912
        """
 
913
        existing_deb_path = os.path.join(self.config.binaries_path, "123.deb")
 
914
        self.makeFile(basename=existing_deb_path, content="foo")
 
915
        self.changer.init_channels([])
 
916
        self.assertFalse(os.path.exists(existing_deb_path))
 
917
 
 
918
    def test_change_package_locks(self):
 
919
        """
 
920
        The L{PackageChanger.handle_tasks} method appropriately creates and
 
921
        deletes package locks as requested by the C{change-package-locks}
 
922
        message.
 
923
        """
 
924
        self.facade.set_package_lock("bar")
 
925
        self.store.add_task("changer", {"type": "change-package-locks",
 
926
                                        "create": [("foo", ">=", "1.0")],
 
927
                                        "delete": [("bar", None, None)],
 
928
                                        "operation-id": 123})
 
929
 
 
930
        def assert_result(result):
 
931
            self.facade.deinit()
 
932
            self.assertEquals(self.facade.get_package_locks(),
 
933
                              [("foo", ">=", "1.0")])
 
934
            self.assertIn("Queuing message with change package locks results "
 
935
                          "to exchange urgently.", self.logfile.getvalue())
 
936
            self.assertMessages(self.get_pending_messages(),
 
937
                                [{"type": "operation-result",
 
938
                                  "operation-id": 123,
 
939
                                  "status": SUCCEEDED,
 
940
                                  "result-text": "Package locks successfully"
 
941
                                                 " changed.",
 
942
                                  "result-code": 0}])
 
943
 
 
944
        result = self.changer.handle_tasks()
 
945
        return result.addCallback(assert_result)
 
946
 
 
947
    def test_change_package_locks_create_with_already_existing(self):
 
948
        """
 
949
        The L{PackageChanger.handle_tasks} method gracefully handles requests
 
950
        for creating package locks that already exist.
 
951
        """
 
952
        self.facade.set_package_lock("foo")
 
953
        self.store.add_task("changer", {"type": "change-package-locks",
 
954
                                        "create": [("foo", None, None)],
 
955
                                        "operation-id": 123})
 
956
 
 
957
        def assert_result(result):
 
958
            self.facade.deinit()
 
959
            self.assertEquals(self.facade.get_package_locks(),
 
960
                              [("foo", "", "")])
 
961
            self.assertMessages(self.get_pending_messages(),
 
962
                                [{"type": "operation-result",
 
963
                                  "operation-id": 123,
 
964
                                  "status": SUCCEEDED,
 
965
                                  "result-text": "Package locks successfully"
 
966
                                                 " changed.",
 
967
                                  "result-code": 0}])
 
968
 
 
969
        result = self.changer.handle_tasks()
 
970
        return result.addCallback(assert_result)
 
971
 
 
972
    def test_change_package_locks_delete_without_already_existing(self):
 
973
        """
 
974
        The L{PackageChanger.handle_tasks} method gracefully handles requests
 
975
        for deleting package locks that don't exist.
 
976
        """
 
977
        self.store.add_task("changer", {"type": "change-package-locks",
 
978
                                        "delete": [("foo", ">=", "1.0")],
 
979
                                        "operation-id": 123})
 
980
 
 
981
        def assert_result(result):
 
982
            self.facade.deinit()
 
983
            self.assertEquals(self.facade.get_package_locks(), [])
 
984
            self.assertMessages(self.get_pending_messages(),
 
985
                                [{"type": "operation-result",
 
986
                                  "operation-id": 123,
 
987
                                  "status": SUCCEEDED,
 
988
                                  "result-text": "Package locks successfully"
 
989
                                                 " changed.",
 
990
                                  "result-code": 0}])
 
991
 
 
992
        result = self.changer.handle_tasks()
 
993
        return result.addCallback(assert_result)