11
from twisted.internet.protocol import ProcessProtocol
12
from twisted.internet.defer import succeed, Deferred
13
from twisted.internet.process import Process, ProcessReader
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
24
class ReleaseUpgraderConfiguration(PackageTaskHandlerConfiguration):
25
"""Specialized configuration for the Landscape release-upgrader."""
28
def upgrade_tool_directory(self):
30
The directory where the upgrade-tool files get stored and extracted.
32
return os.path.join(self.package_directory, "upgrade-tool")
35
class ReleaseUpgrader(PackageTaskHandler):
36
"""Perform release upgrades."""
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/"
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,
51
def handle_task(self, task):
52
"""Call the proper handler for the given C{task}."""
54
if message["type"] == "release-upgrade":
55
return self.handle_release_upgrade(message)
57
def handle_release_upgrade(self, message):
58
"""Fetch the upgrade-tool, verify it and run it.
60
@param message: A message of type C{"release-upgrade"}.
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"]
67
if target_code_name == current_code_name:
68
message = self.make_operation_result_message(
70
"The system is already running %s." % target_code_name, 1)
71
logging.info("Queuing message with release upgrade failure to "
73
return self._broker.send_message(message, True)
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)
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
87
directory = self._config.upgrade_tool_directory
88
tarball_filename = url_to_filename(tarball_url,
90
signature_filename = url_to_filename(signature_url,
93
result = self.fetch(tarball_url, signature_url)
94
result.addCallback(lambda x: self.verify(tarball_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)
105
def fetch(self, tarball_url, signature_url):
106
"""Fetch the upgrade-tool files.
108
@param tarball_url: The upgrade-tool tarball URL.
109
@param signature_url: The upgrade-tool signature URL.
111
if not os.path.exists(self._config.upgrade_tool_directory):
112
os.mkdir(self._config.upgrade_tool_directory)
114
result = fetch_to_files([tarball_url, signature_url],
115
self._config.upgrade_tool_directory,
116
logger=logging.warning)
118
def log_success(ignored):
119
logging.info("Successfully fetched upgrade-tool files")
121
def log_failure(failure):
122
logging.warning("Couldn't fetch all upgrade-tool files")
125
result.addCallback(log_success)
126
result.addErrback(log_failure)
129
def verify(self, tarball_filename, signature_filename):
130
"""Verify the upgrade-tool tarball against its signature.
132
@param tarball_filename: The filename of the upgrade-tool tarball.
133
@param signature_filename: The filename of the tarball signature.
135
result = gpg_verify(tarball_filename, signature_filename)
137
def log_success(ignored):
138
logging.info("Successfully verified upgrade-tool tarball")
140
def log_failure(failure):
141
logging.warning("Invalid signature for upgrade-tool tarball: %s"
142
% str(failure.value))
145
result.addCallback(log_success)
146
result.addErrback(log_failure)
149
def extract(self, tarball_filename):
150
"""Extract the upgrade-tool tarball.
152
@param tarball_filename: The filename of the upgrade-tool tarball.
154
tf = tarfile.open(tarball_filename, "r:gz")
155
for member in tf.getmembers():
156
tf.extract(member, path=self._config.upgrade_tool_directory)
159
def tweak(self, current_code_name):
160
"""Tweak the files of the extracted tarballs to workaround known bugs.
162
@param current_code_name: The code-name of the current release.
164
upgrade_tool_directory = self._config.upgrade_tool_directory
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)
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")
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")
185
scripts = config.get("Distro", "PostInstallScripts")
186
scripts += ", ./dbus.sh"
187
config.set("Distro", "PostInstallScripts", scripts)
188
fd = open(config_filename, "w")
192
dbus_sh_filename = os.path.join(upgrade_tool_directory,
194
fd = open(dbus_sh_filename, "w")
195
fd.write("#!/bin/sh\n"
196
"/etc/init.d/dbus start\n"
199
os.chmod(dbus_sh_filename, 0755)
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
205
mirrors_filename = os.path.join(upgrade_tool_directory,
207
fd = open(mirrors_filename, "a")
208
fd.write(self.landscape_ppa_url + "\n")
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.
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.
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"]
229
args.extend(["--mode", mode])
230
env = os.environ.copy()
231
if allow_third_party:
232
env["RELEASE_UPRADER_ALLOW_THIRD_PARTY"] = "True"
234
env["DEBUG_UPDATE_MANAGER"] = "True"
236
from twisted.internet import reactor
238
process_protocol = AllOutputProcessProtocol(result)
239
process = reactor.spawnProcess(process_protocol, upgrade_tool_filename,
241
path=upgrade_tool_directory)
243
def maybeCallProcessEnded():
244
"""A less strict version of Process.maybeCallProcessEnded.
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.
251
@note: Twisted 8.2 now has a processExited hook that could
252
be used in place of this workaround.
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
261
Process.maybeCallProcessEnded(process)
263
process.maybeCallProcessEnded = maybeCallProcessEnded
265
def send_operation_result((out, err, code)):
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)
276
result.addCallback(send_operation_result)
280
"""Clean-up the upgrade-tool files and report about package changes."""
281
shutil.rmtree(self._config.upgrade_tool_directory)
284
uid = pwd.getpwnam("landscape").pw_uid
285
gid = grp.getgrnam("landscape").gr_gid
290
reporter = find_reporter_command()
292
# Force a smart-update run, because the sources.list has changed
293
args = [reporter, "--force-smart-update"]
295
if self._config.config is not None:
296
args.append("--config=%s" % self._config.config)
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)
305
def abort(self, failure, operation_id):
306
"""Abort the task reporting details about the failure."""
308
message = self.make_operation_result_message(
309
operation_id, FAILED, "%s" % str(failure.value), 1)
311
logging.info("Queuing message with release upgrade failure to "
312
"exchange urgently.")
314
return self._broker.send_message(message, True)
318
return find_release_upgrader_command()
321
class AllOutputProcessProtocol(ProcessProtocol):
322
"""A process protocoll for getting stdout, stderr and exit code."""
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
331
def processEnded(self, reason):
332
out = self.outBuf.getvalue()
333
err = self.errBuf.getvalue()
337
self.deferred.errback((out, err, e.signal))
339
self.deferred.callback((out, err, code))
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")
349
if os.getpgrp() != os.getpid():
351
return run_task_handler(ReleaseUpgrader, args)