Current Path : /usr/lib/python2.7/dist-packages/landscape/package/ |
Current File : //usr/lib/python2.7/dist-packages/landscape/package/releaseupgrader.py |
import os import sys import grp import pwd import shutil import logging import tarfile import cStringIO from twisted.internet.defer import succeed from landscape.lib.fetch import url_to_filename, fetch_to_files from landscape.lib.lsb_release import parse_lsb_release, LSB_RELEASE_FILENAME from landscape.lib.gpg import gpg_verify from landscape.lib.fs import read_file from landscape.package.taskhandler import ( PackageTaskHandlerConfiguration, PackageTaskHandler, run_task_handler) from landscape.lib.twisted_util import spawn_process from landscape.manager.manager import SUCCEEDED, FAILED from landscape.package.reporter import find_reporter_command class ReleaseUpgraderConfiguration(PackageTaskHandlerConfiguration): """Specialized configuration for the Landscape release-upgrader.""" @property def upgrade_tool_directory(self): """ The directory where the upgrade-tool files get stored and extracted. """ return os.path.join(self.package_directory, "upgrade-tool") class ReleaseUpgrader(PackageTaskHandler): """Perform release upgrades. @cvar config_factory: The configuration class to use to build configuration objects to be passed to our constructor. @cvar queue_name: The queue we pick tasks from. @cvar lsb_release_filename: The path to the LSB data on the file system. @cvar landscape_ppa_url: The URL of the Landscape PPA, if it is present in the computer's sources.list it won't be commented out. @cvar logs_directory: Path to the directory holding the upgrade-tool logs. @cvar logs_limit: When reporting upgrade-tool logs to the server, only the last C{logs_limit} lines will be sent. """ config_factory = ReleaseUpgraderConfiguration queue_name = "release-upgrader" lsb_release_filename = LSB_RELEASE_FILENAME landscape_ppa_url = "http://ppa.launchpad.net/landscape/trunk/ubuntu/" logs_directory = "/var/log/dist-upgrade" logs_limit = 100000 def make_operation_result_message(self, operation_id, status, text, code): """Convenience to create messages of type C{"operation-result"}.""" return {"type": "operation-result", "operation-id": operation_id, "status": status, "result-text": text, "result-code": code} def handle_task(self, task): """Call the proper handler for the given C{task}.""" message = task.data if message["type"] == "release-upgrade": return self.handle_release_upgrade(message) def handle_release_upgrade(self, message): """Fetch the upgrade-tool, verify it and run it. @param message: A message of type C{"release-upgrade"}. """ target_code_name = message["code-name"] operation_id = message["operation-id"] lsb_release_info = parse_lsb_release(self.lsb_release_filename) current_code_name = lsb_release_info["code-name"] if target_code_name == current_code_name: message = self.make_operation_result_message( operation_id, FAILED, "The system is already running %s." % target_code_name, 1) logging.info("Queuing message with release upgrade failure to " "exchange urgently.") return self._send_message(message) tarball_url = message["upgrade-tool-tarball-url"] signature_url = message["upgrade-tool-signature-url"] allow_third_party = message.get("allow-third-party", False) debug = message.get("debug", False) directory = self._config.upgrade_tool_directory tarball_filename = url_to_filename(tarball_url, directory=directory) signature_filename = url_to_filename(signature_url, directory=directory) result = self.fetch(tarball_url, signature_url) result.addCallback(lambda x: self.verify(tarball_filename, signature_filename)) result.addCallback(lambda x: self.extract(tarball_filename)) result.addCallback(lambda x: self.tweak(current_code_name)) result.addCallback(lambda x: self.upgrade( target_code_name, operation_id, allow_third_party=allow_third_party, debug=debug)) result.addCallback(lambda x: self.finish()) result.addErrback(self.abort, operation_id) return result def fetch(self, tarball_url, signature_url): """Fetch the upgrade-tool files. @param tarball_url: The upgrade-tool tarball URL. @param signature_url: The upgrade-tool signature URL. """ if not os.path.exists(self._config.upgrade_tool_directory): os.mkdir(self._config.upgrade_tool_directory) result = fetch_to_files([tarball_url, signature_url], self._config.upgrade_tool_directory, logger=logging.warning) def log_success(ignored): logging.info("Successfully fetched upgrade-tool files") def log_failure(failure): logging.warning("Couldn't fetch all upgrade-tool files") return failure result.addCallback(log_success) result.addErrback(log_failure) return result def verify(self, tarball_filename, signature_filename): """Verify the upgrade-tool tarball against its signature. @param tarball_filename: The filename of the upgrade-tool tarball. @param signature_filename: The filename of the tarball signature. """ result = gpg_verify(tarball_filename, signature_filename) def log_success(ignored): logging.info("Successfully verified upgrade-tool tarball") def log_failure(failure): logging.warning("Invalid signature for upgrade-tool tarball: %s" % str(failure.value)) return failure result.addCallback(log_success) result.addErrback(log_failure) return result def extract(self, tarball_filename): """Extract the upgrade-tool tarball. @param tarball_filename: The filename of the upgrade-tool tarball. """ tf = tarfile.open(tarball_filename, "r:gz") for member in tf.getmembers(): tf.extract(member, path=self._config.upgrade_tool_directory) return succeed(None) def tweak(self, current_code_name): """Tweak the files of the extracted tarballs to workaround known bugs. @param current_code_name: The code-name of the current release. """ upgrade_tool_directory = self._config.upgrade_tool_directory # On some releases the upgrade-tool doesn't support the allow third # party environment variable, so this trick is needed to make it # possible to upgrade against testing client packages from the # Landscape PPA mirrors_filename = os.path.join(upgrade_tool_directory, "mirrors.cfg") fd = open(mirrors_filename, "a") fd.write(self.landscape_ppa_url + "\n") fd.close() return succeed(None) def make_operation_result_text(self, out, err): """Return the operation result text to be sent to the server. @param out: The standard output of the upgrade-tool process. @param err: The standard error of the upgrade-tool process. @return: A text aggregating the process output, error and log files. """ buf = cStringIO.StringIO() for label, content in [("output", out), ("error", err)]: if content: buf.write("=== Standard %s ===\n\n%s\n\n" % (label, content)) for basename in sorted(os.listdir(self.logs_directory)): if not basename.endswith(".log"): continue filename = os.path.join(self.logs_directory, basename) content = read_file(filename, - self.logs_limit) buf.write("=== %s ===\n\n%s\n\n" % (basename, content)) return buf.getvalue() def upgrade(self, code_name, operation_id, allow_third_party=False, debug=False): """Run the upgrade-tool command and send a report of the results. @param code_name: The code-name of the release to upgrade to. @param operation_id: The activity id for this task. @param allow_third_party: Whether to enable non-official APT repo. @param debug: Whether to turn on debug level logging. """ upgrade_tool_directory = self._config.upgrade_tool_directory upgrade_tool_filename = os.path.join(upgrade_tool_directory, code_name) args = ["--frontend", "DistUpgradeViewNonInteractive"] env = os.environ.copy() if allow_third_party: env["RELEASE_UPRADER_ALLOW_THIRD_PARTY"] = "True" if debug: env["DEBUG_UPDATE_MANAGER"] = "True" result = spawn_process(upgrade_tool_filename, args=args, env=env, path=upgrade_tool_directory, wait_pipes=False) def send_operation_result((out, err, code)): if code == 0: status = SUCCEEDED else: status = FAILED text = self.make_operation_result_text(out, err) message = self.make_operation_result_message(operation_id, status, text, code) logging.info("Queuing message with release upgrade results to " "exchange urgently.") return self._send_message(message) result.addCallback(send_operation_result) return result def finish(self): """Clean-up the upgrade-tool files and report about package changes.""" shutil.rmtree(self._config.upgrade_tool_directory) if os.getuid() == 0: uid = pwd.getpwnam("landscape").pw_uid gid = grp.getgrnam("landscape").gr_gid else: uid = None gid = None reporter = find_reporter_command() # Force an apt-update run, because the sources.list has changed args = ["--force-apt-update"] if self._config.config is not None: args.append("--config=%s" % self._config.config) return spawn_process(reporter, args=args, uid=uid, gid=gid, path=os.getcwd(), env=os.environ) def abort(self, failure, operation_id): """Abort the task reporting details about the failure.""" message = self.make_operation_result_message( operation_id, FAILED, "%s" % str(failure.value), 1) logging.info("Queuing message with release upgrade failure to " "exchange urgently.") return self._send_message(message) @staticmethod def find_command(): return find_release_upgrader_command() def _send_message(self, message): """Acquire a session ID and send the given message.""" deferred = self.get_session_id() def send(_): self._broker.send_message(message, self._session_id, True) deferred.addCallback(send) return deferred def find_release_upgrader_command(): """Return the path to the landscape-release-upgrader script.""" dirname = os.path.dirname(os.path.abspath(sys.argv[0])) return os.path.join(dirname, "landscape-release-upgrader") def main(args): if os.getpgrp() != os.getpid(): os.setsid() return run_task_handler(ReleaseUpgrader, args)