~free.ekanayaka/landscape-client/lucid-1.5.0-0ubuntu0.10.04.0

« back to all changes in this revision

Viewing changes to landscape/package/releaseupgrader.py

  • Committer: Bazaar Package Importer
  • Author(s): Free Ekanayaka
  • Date: 2009-12-16 10:50:05 UTC
  • mfrom: (1.1.12 upstream)
  • Revision ID: james.westby@ubuntu.com-20091216105005-svplwdorkgz6vja7
Tags: 1.4.0-0ubuntu0.10.04.0
* New upstream release with several bug fixes:
  - Fix landscape daemons fail to start when too many groups are
    available (LP: #456124)
  - Fix landscape programs wake up far too much. (LP: #340843)
  - Fix Package manager fails with 'no such table: task' (LP #465846)
  - Fix test suite leaving temporary files around (LP #476418)
  - Fix the 1hr long wait for user data to be uploaded following a
    resynchronisation (LP #369000)

* Add support for Ubuntu release upgrades:
  - Add helper function to fetch many files at once (LP: #450629)
  - Handle release-upgrade messages in the packagemanager
    plugin (LP: #455217)
  - Add a release-upgrader task handler (LP: #462543)
  - Support upgrade-tool environment variables (LP: #463321)

* Add initial support for Smart package locking:
  - Detect and report changes about Smart package locks (#488108)

* Packaging fixes:
  - Turn unnecessary Pre-Depends on python-gobject into a regular Depends
  - If it's empty, remove /etc/landscape upon purge

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
import os
 
2
import sys
 
3
import grp
 
4
import pwd
 
5
import shutil
 
6
import logging
 
7
import tarfile
 
8
import cStringIO
 
9
import ConfigParser
 
10
 
 
11
from twisted.internet.protocol import ProcessProtocol
 
12
from twisted.internet.defer import succeed, Deferred
 
13
from twisted.internet.process import Process, ProcessReader
 
14
 
 
15
from landscape.lib.fetch import url_to_filename, fetch_to_files
 
16
from landscape.lib.lsb_release import parse_lsb_release, LSB_RELEASE_FILENAME
 
17
from landscape.lib.gpg import gpg_verify
 
18
from landscape.package.taskhandler import (
 
19
    PackageTaskHandlerConfiguration, PackageTaskHandler, run_task_handler)
 
20
from landscape.manager.manager import SUCCEEDED, FAILED
 
21
from landscape.package.reporter import find_reporter_command
 
22
 
 
23
 
 
24
class ReleaseUpgraderConfiguration(PackageTaskHandlerConfiguration):
 
25
    """Specialized configuration for the Landscape release-upgrader."""
 
26
 
 
27
    @property
 
28
    def upgrade_tool_directory(self):
 
29
        """
 
30
        The directory where the upgrade-tool files get stored and extracted.
 
31
        """
 
32
        return os.path.join(self.package_directory, "upgrade-tool")
 
33
 
 
34
 
 
35
class ReleaseUpgrader(PackageTaskHandler):
 
36
    """Perform release upgrades."""
 
37
 
 
38
    config_factory = ReleaseUpgraderConfiguration
 
39
    queue_name = "release-upgrader"
 
40
    lsb_release_filename = LSB_RELEASE_FILENAME
 
41
    landscape_ppa_url = "http://ppa.launchpad.net/landscape/ppa/ubuntu/"
 
42
 
 
43
    def make_operation_result_message(self, operation_id, status, text, code):
 
44
        """Convenience to create messages of type C{"operation-result"}."""
 
45
        return {"type": "operation-result",
 
46
                "operation-id": operation_id,
 
47
                "status": status,
 
48
                "result-text": text,
 
49
                "result-code": code}
 
50
 
 
51
    def handle_task(self, task):
 
52
        """Call the proper handler for the given C{task}."""
 
53
        message = task.data
 
54
        if message["type"] == "release-upgrade":
 
55
            return self.handle_release_upgrade(message)
 
56
 
 
57
    def handle_release_upgrade(self, message):
 
58
        """Fetch the upgrade-tool, verify it and run it.
 
59
 
 
60
        @param message: A message of type C{"release-upgrade"}.
 
61
        """
 
62
        target_code_name = message["code-name"]
 
63
        operation_id = message["operation-id"]
 
64
        lsb_release_info = parse_lsb_release(self.lsb_release_filename)
 
65
        current_code_name = lsb_release_info["code-name"]
 
66
 
 
67
        if target_code_name == current_code_name:
 
68
            message = self.make_operation_result_message(
 
69
                operation_id, FAILED,
 
70
                "The system is already running %s." % target_code_name, 1)
 
71
            logging.info("Queuing message with release upgrade failure to "
 
72
                         "exchange urgently.")
 
73
            return self._broker.send_message(message, True)
 
74
 
 
75
        tarball_url = message["upgrade-tool-tarball-url"]
 
76
        signature_url = message["upgrade-tool-signature-url"]
 
77
        allow_third_party = message.get("allow-third-party", False)
 
78
        debug = message.get("debug", False)
 
79
        mode = None
 
80
        if current_code_name == "dapper":
 
81
            # On Dapper the upgrade tool must be passed "--mode server"
 
82
            # when run on a server system. As there is no simple and
 
83
            # reliable way to detect if a system is a desktop one, and as
 
84
            # the desktop edition is no longer supported, we default to server
 
85
            # mode.
 
86
            mode = "server"
 
87
        directory = self._config.upgrade_tool_directory
 
88
        tarball_filename = url_to_filename(tarball_url,
 
89
                                           directory=directory)
 
90
        signature_filename = url_to_filename(signature_url,
 
91
                                             directory=directory)
 
92
 
 
93
        result = self.fetch(tarball_url, signature_url)
 
94
        result.addCallback(lambda x: self.verify(tarball_filename,
 
95
                                                 signature_filename))
 
96
        result.addCallback(lambda x: self.extract(tarball_filename))
 
97
        result.addCallback(lambda x: self.tweak(current_code_name))
 
98
        result.addCallback(lambda x: self.upgrade(
 
99
            target_code_name, operation_id,
 
100
            allow_third_party=allow_third_party, debug=debug, mode=mode))
 
101
        result.addCallback(lambda x: self.finish())
 
102
        result.addErrback(self.abort, operation_id)
 
103
        return result
 
104
 
 
105
    def fetch(self, tarball_url, signature_url):
 
106
        """Fetch the upgrade-tool files.
 
107
 
 
108
        @param tarball_url: The upgrade-tool tarball URL.
 
109
        @param signature_url: The upgrade-tool signature URL.
 
110
        """
 
111
        if not os.path.exists(self._config.upgrade_tool_directory):
 
112
            os.mkdir(self._config.upgrade_tool_directory)
 
113
 
 
114
        result = fetch_to_files([tarball_url, signature_url],
 
115
                                self._config.upgrade_tool_directory,
 
116
                                logger=logging.warning)
 
117
 
 
118
        def log_success(ignored):
 
119
            logging.info("Successfully fetched upgrade-tool files")
 
120
 
 
121
        def log_failure(failure):
 
122
            logging.warning("Couldn't fetch all upgrade-tool files")
 
123
            return failure
 
124
 
 
125
        result.addCallback(log_success)
 
126
        result.addErrback(log_failure)
 
127
        return result
 
128
 
 
129
    def verify(self, tarball_filename, signature_filename):
 
130
        """Verify the upgrade-tool tarball against its signature.
 
131
 
 
132
        @param tarball_filename: The filename of the upgrade-tool tarball.
 
133
        @param signature_filename: The filename of the tarball signature.
 
134
        """
 
135
        result = gpg_verify(tarball_filename, signature_filename)
 
136
 
 
137
        def log_success(ignored):
 
138
            logging.info("Successfully verified upgrade-tool tarball")
 
139
 
 
140
        def log_failure(failure):
 
141
            logging.warning("Invalid signature for upgrade-tool tarball: %s"
 
142
                            % str(failure.value))
 
143
            return failure
 
144
 
 
145
        result.addCallback(log_success)
 
146
        result.addErrback(log_failure)
 
147
        return result
 
148
 
 
149
    def extract(self, tarball_filename):
 
150
        """Extract the upgrade-tool tarball.
 
151
 
 
152
        @param tarball_filename: The filename of the upgrade-tool tarball.
 
153
        """
 
154
        tf = tarfile.open(tarball_filename, "r:gz")
 
155
        for member in tf.getmembers():
 
156
            tf.extract(member, path=self._config.upgrade_tool_directory)
 
157
        return succeed(None)
 
158
 
 
159
    def tweak(self, current_code_name):
 
160
        """Tweak the files of the extracted tarballs to workaround known bugs.
 
161
 
 
162
        @param current_code_name: The code-name of the current release.
 
163
        """
 
164
        upgrade_tool_directory = self._config.upgrade_tool_directory
 
165
 
 
166
        if current_code_name == "dapper":
 
167
            config_filename = os.path.join(upgrade_tool_directory,
 
168
                                           "DistUpgrade.cfg.dapper")
 
169
            config = ConfigParser.ConfigParser()
 
170
            config.read(config_filename)
 
171
 
 
172
            # Fix a bug in the DistUpgrade.cfg.dapper file contained in
 
173
            # the upgrade tool tarball
 
174
            if not config.has_section("NonInteractive"):
 
175
                config.add_section("NonInteractive")
 
176
                config.set("NonInteractive", "ForceOverwrite", "no")
 
177
 
 
178
            # Workaround for Bug #174148, which prevents dbus from restarting
 
179
            # after a dapper->hardy upgrade
 
180
            if not config.has_section("Distro"):
 
181
                config.add_section("Distro")
 
182
            if not config.has_option("Distro", "PostInstallScripts"):
 
183
                config.set("Distro", "PostInstallScripts", "./dbus/sh")
 
184
            else:
 
185
                scripts = config.get("Distro", "PostInstallScripts")
 
186
                scripts += ", ./dbus.sh"
 
187
                config.set("Distro", "PostInstallScripts", scripts)
 
188
            fd = open(config_filename, "w")
 
189
            config.write(fd)
 
190
            fd.close()
 
191
 
 
192
            dbus_sh_filename = os.path.join(upgrade_tool_directory,
 
193
                                            "dbus.sh")
 
194
            fd = open(dbus_sh_filename, "w")
 
195
            fd.write("#!/bin/sh\n"
 
196
                     "/etc/init.d/dbus start\n"
 
197
                     "sleep 10\n")
 
198
            fd.close()
 
199
            os.chmod(dbus_sh_filename, 0755)
 
200
 
 
201
        # On some releases the upgrade-tool doesn't support the allow third
 
202
        # party environment variable, so this trick is needed to make it
 
203
        # possible to upgrade against testing client packages from the
 
204
        # Landscape PPA
 
205
        mirrors_filename = os.path.join(upgrade_tool_directory,
 
206
                                        "mirrors.cfg")
 
207
        fd = open(mirrors_filename, "a")
 
208
        fd.write(self.landscape_ppa_url + "\n")
 
209
        fd.close()
 
210
 
 
211
        return succeed(None)
 
212
 
 
213
    def upgrade(self, code_name, operation_id, allow_third_party=False,
 
214
                debug=False, mode=None):
 
215
        """Run the upgrade-tool command and send a report of the results.
 
216
 
 
217
        @param code_name: The code-name of the release to upgrade to.
 
218
        @param operation_id: The activity id for this task.
 
219
        @param allow_third_party: Whether to enable non-official APT repo.
 
220
        @param debug: Whether to turn on debug level logging.
 
221
        @param mode: Optionally, the mode to run the upgrade-tool as. It
 
222
            can be "server" or "desktop", and it's relevant only for dapper.
 
223
        """
 
224
        upgrade_tool_directory = self._config.upgrade_tool_directory
 
225
        upgrade_tool_filename = os.path.join(upgrade_tool_directory, code_name)
 
226
        args = [upgrade_tool_filename, "--frontend",
 
227
                "DistUpgradeViewNonInteractive"]
 
228
        if mode:
 
229
            args.extend(["--mode", mode])
 
230
        env = os.environ.copy()
 
231
        if allow_third_party:
 
232
            env["RELEASE_UPRADER_ALLOW_THIRD_PARTY"] = "True"
 
233
        if debug:
 
234
            env["DEBUG_UPDATE_MANAGER"] = "True"
 
235
 
 
236
        from twisted.internet import reactor
 
237
        result = Deferred()
 
238
        process_protocol = AllOutputProcessProtocol(result)
 
239
        process = reactor.spawnProcess(process_protocol, upgrade_tool_filename,
 
240
                                       args=args, env=env,
 
241
                                       path=upgrade_tool_directory)
 
242
 
 
243
        def maybeCallProcessEnded():
 
244
            """A less strict version of Process.maybeCallProcessEnded.
 
245
 
 
246
            This behaves exactly like the original method, but in case the
 
247
            process has ended already and sent us a SIGCHLD, it doesn't wait
 
248
            for the stdin/stdout pipes to close, because the child process
 
249
            itself might have passed them to its own child processes.
 
250
 
 
251
            @note: Twisted 8.2 now has a processExited hook that could
 
252
                be used in place of this workaround.
 
253
            """
 
254
            if process.pipes and not process.pid:
 
255
                for pipe in process.pipes.itervalues():
 
256
                    if isinstance(pipe, ProcessReader):
 
257
                        # Read whatever is left
 
258
                        pipe.doRead()
 
259
                    pipe.stopReading()
 
260
                process.pipes = {}
 
261
            Process.maybeCallProcessEnded(process)
 
262
 
 
263
        process.maybeCallProcessEnded = maybeCallProcessEnded
 
264
 
 
265
        def send_operation_result((out, err, code)):
 
266
            if code == 0:
 
267
                status = SUCCEEDED
 
268
            else:
 
269
                status = FAILED
 
270
            message = self.make_operation_result_message(
 
271
                operation_id, status, "%s%s" % (out, err), code)
 
272
            logging.info("Queuing message with release upgrade results to "
 
273
                         "exchange urgently.")
 
274
            return self._broker.send_message(message, True)
 
275
 
 
276
        result.addCallback(send_operation_result)
 
277
        return result
 
278
 
 
279
    def finish(self):
 
280
        """Clean-up the upgrade-tool files and report about package changes."""
 
281
        shutil.rmtree(self._config.upgrade_tool_directory)
 
282
 
 
283
        if os.getuid() == 0:
 
284
            uid = pwd.getpwnam("landscape").pw_uid
 
285
            gid = grp.getgrnam("landscape").gr_gid
 
286
        else:
 
287
            uid = None
 
288
            gid = None
 
289
 
 
290
        reporter = find_reporter_command()
 
291
 
 
292
        # Force a smart-update run, because the sources.list has changed
 
293
        args = [reporter, "--force-smart-update"]
 
294
 
 
295
        if self._config.config is not None:
 
296
            args.append("--config=%s" % self._config.config)
 
297
 
 
298
        result = Deferred()
 
299
        process_protocol = AllOutputProcessProtocol(result)
 
300
        from twisted.internet import reactor
 
301
        reactor.spawnProcess(process_protocol, reporter, args=args, uid=uid,
 
302
                             gid=gid, path=os.getcwd(), env=os.environ)
 
303
        return result
 
304
 
 
305
    def abort(self, failure, operation_id):
 
306
        """Abort the task reporting details about the failure."""
 
307
 
 
308
        message = self.make_operation_result_message(
 
309
            operation_id, FAILED, "%s" % str(failure.value), 1)
 
310
 
 
311
        logging.info("Queuing message with release upgrade failure to "
 
312
                     "exchange urgently.")
 
313
 
 
314
        return self._broker.send_message(message, True)
 
315
 
 
316
    @staticmethod
 
317
    def find_command():
 
318
        return find_release_upgrader_command()
 
319
 
 
320
 
 
321
class AllOutputProcessProtocol(ProcessProtocol):
 
322
    """A process protocoll for getting stdout, stderr and exit code."""
 
323
 
 
324
    def __init__(self, deferred):
 
325
        self.deferred = deferred
 
326
        self.outBuf = cStringIO.StringIO()
 
327
        self.errBuf = cStringIO.StringIO()
 
328
        self.outReceived = self.outBuf.write
 
329
        self.errReceived = self.errBuf.write
 
330
 
 
331
    def processEnded(self, reason):
 
332
        out = self.outBuf.getvalue()
 
333
        err = self.errBuf.getvalue()
 
334
        e = reason.value
 
335
        code = e.exitCode
 
336
        if e.signal:
 
337
            self.deferred.errback((out, err, e.signal))
 
338
        else:
 
339
            self.deferred.callback((out, err, code))
 
340
 
 
341
 
 
342
def find_release_upgrader_command():
 
343
    """Return the path to the landscape-release-upgrader script."""
 
344
    dirname = os.path.dirname(os.path.abspath(sys.argv[0]))
 
345
    return os.path.join(dirname, "landscape-release-upgrader")
 
346
 
 
347
 
 
348
def main(args):
 
349
    if os.getpgrp() != os.getpid():
 
350
        os.setsid()
 
351
    return run_task_handler(ReleaseUpgrader, args)