def test_with_valid_lock(self, mock_kill): """Publisher raises lock error if a valid lock is held.""" sock_path = os.path.join(self.config.sockets_path, u"test.sock") lock_path = u"{}.lock".format(sock_path) # fake a landscape process app = self.makeFile(textwrap.dedent("""\ #!/usr/bin/python3 import time time.sleep(10) """), basename="landscape-manager") os.chmod(app, 0o755) call = subprocess.Popen([app]) self.addCleanup(call.terminate) os.symlink(str(call.pid), lock_path) component = TestComponent() # Test the actual Unix reactor implementation. Fakes won't do. reactor = LandscapeReactor() publisher = ComponentPublisher(component, reactor, self.config) with self.assertRaises(CannotListenError): publisher.start() # ensure lock was not replaced self.assertEqual(str(call.pid), os.readlink(lock_path)) mock_kill.assert_called_with(call.pid, 0) reactor._cleanup()
def test_stale_locks_recycled_pid(self, mock_kill): """Publisher starts with stale lock pointing to recycled process.""" mock_kill.side_effect = [ OSError(errno.EPERM, "Operation not permitted") ] sock_path = os.path.join(self.config.sockets_path, u"test.sock") lock_path = u"{}.lock".format(sock_path) # fake a PID recycled by a known process which isn't landscape (init) os.symlink("1", lock_path) component = TestComponent() # Test the actual Unix reactor implementation. Fakes won't do. reactor = LandscapeReactor() publisher = ComponentPublisher(component, reactor, self.config) # Shouldn't raise the exception. publisher.start() # ensure stale lock was replaced self.assertNotEqual("1", os.readlink(lock_path)) mock_kill.assert_not_called() self.assertFalse(publisher._port.lockFile.clean) publisher.stop() reactor._cleanup()
def register(config, reactor=None, connector_factory=RemoteBrokerConnector, got_connection=got_connection, max_retries=14, on_error=None, results=None): """Instruct the Landscape Broker to register the client. The broker will be instructed to reload its configuration and then to attempt a registration. @param reactor: The reactor to use. This parameter is optional because the client charm does not pass it. @param connector_factory: A callable that accepts a reactor and a configuration object and returns a new remote broker connection. Used primarily for dependency injection. @param got_connection: The handler to trigger when the remote broker connects. Used primarily for dependency injection. @param max_retries: The number of times to retry connecting to the landscape client service. The delay between retries is calculated by Twisted and increases geometrically. @param on_error: A callable that will be passed a non-zero positive integer argument in the case that some error occurs. This is a legacy API provided for use by the client charm. @param results: This parameter provides a mechanism to pre-seed the result of registering. Used for testing. """ if reactor is None: reactor = LandscapeReactor() if results is None: results = [] add_result = results.append connector = connector_factory(reactor, config) connection = connector.connect(max_retries=max_retries, quiet=True) connection.addCallback( partial(got_connection, add_result, connector, reactor)) connection.addErrback( partial(got_error, reactor=reactor, add_result=add_result)) reactor.run() assert len(results) == 1, "We expect exactly one result." # Results will be things like "success" or "ssl-error". result = results[0] if isinstance(result, SystemExit): raise result # If there was an error and the caller requested that errors be reported # to the on_error callable, then do so. if result != "success" and on_error is not None: on_error(1) return result
def __init__(self, store, facade, remote, config, process_factory=reactor, landscape_reactor=None, reboot_required_filename=REBOOT_REQUIRED_FILENAME): super(PackageChanger, self).__init__( store, facade, remote, config, landscape_reactor) self._process_factory = process_factory if landscape_reactor is None: # For testing purposes. from landscape.client.reactor import LandscapeReactor self._landscape_reactor = LandscapeReactor() else: self._landscape_reactor = landscape_reactor self.reboot_required_filename = reboot_required_filename
def get_reactor(self): reactor = LandscapeReactor() # It's not possible to stop the reactor in a Trial test, calling # reactor.crash instead saved_stop = reactor._reactor.stop reactor._reactor.stop = reactor._reactor.crash self.addCleanup(lambda: setattr(reactor._reactor, "stop", saved_stop)) return reactor
def main(args, print=print): """Interact with the user and the server to set up client configuration.""" config = LandscapeSetupConfiguration() try: config.load(args) except ImportOptionError as error: print_text(str(error), error=True) sys.exit(1) if config.is_registered: registration_status = is_registered(config) info_text = registration_info_text(config, registration_status) print(info_text) if registration_status: sys.exit(0) else: sys.exit(EXIT_NOT_REGISTERED) if os.getuid() != 0: sys.exit("landscape-config must be run as root.") if config.init: bootstrap_tree(config) sys.exit(0) # Disable startup on boot and stop the client, if one is running. if config.disable: stop_client_and_disable_init_script() return # Setup client configuration. try: setup(config) except Exception as e: print_text(str(e)) sys.exit("Aborting Landscape configuration") print("Please wait...") # Attempt to register the client. reactor = LandscapeReactor() if config.silent: result = register(config, reactor) report_registration_outcome(result, print=print) sys.exit(determine_exit_code(result)) else: default_answer = not is_registered(config) answer = prompt_yes_no( "\nRequest a new registration for this computer now?", default=default_answer) if answer: result = register(config, reactor) report_registration_outcome(result, print=print) sys.exit(determine_exit_code(result))
def test_stale_locks_with_dead_pid(self, mock_kill): """Publisher starts with stale lock.""" mock_kill.side_effect = [OSError(errno.ESRCH, "No such process")] sock_path = os.path.join(self.config.sockets_path, u"test.sock") lock_path = u"{}.lock".format(sock_path) # fake a PID which does not exist os.symlink("-1", lock_path) component = TestComponent() # Test the actual Unix reactor implementation. Fakes won't do. reactor = LandscapeReactor() publisher = ComponentPublisher(component, reactor, self.config) # Shouldn't raise the exception. publisher.start() # ensure stale lock was replaced self.assertNotEqual("-1", os.readlink(lock_path)) mock_kill.assert_called_with(-1, 0) publisher.stop() reactor._cleanup()
def __init__(self, reactor=reactor, verbose=False, config=None, broker=None, monitor=None, manager=None, enabled_daemons=None): landscape_reactor = LandscapeReactor() if enabled_daemons is None: enabled_daemons = [Broker, Monitor, Manager] if broker is None and Broker in enabled_daemons: broker = Broker(RemoteBrokerConnector(landscape_reactor, config), verbose=verbose, config=config.config) if monitor is None and Monitor in enabled_daemons: monitor = Monitor(RemoteMonitorConnector(landscape_reactor, config), verbose=verbose, config=config.config) if manager is None and Manager in enabled_daemons: manager = Manager(RemoteManagerConnector(landscape_reactor, config), verbose=verbose, config=config.config) self.broker = broker self.monitor = monitor self.manager = manager self.daemons = [ daemon for daemon in [self.broker, self.monitor, self.manager] if daemon ] self.reactor = reactor self._checking = None self._stopping = False signal.signal( signal.SIGUSR1, lambda signal, frame: reactor.callFromThread( self._notify_rotate_logs)) if config is not None and config.clones > 0: options = [ "--clones", str(config.clones), "--start-clones-over", str(config.start_clones_over) ] for daemon in self.daemons: daemon.options = options self._ping_failures = {}
def setUp(self): super(DaemonTestBase, self).setUp() if hasattr(self, "broker_service"): # DaemonBrokerTest self.broker_service.startService() self.config = self.broker_service.config else: # DaemonTest self.config = WatchDogConfiguration() self.config.data_path = self.makeDir() self.makeDir(path=self.config.sockets_path) self.connector = self.connector_factory(LandscapeReactor(), self.config) self.daemon = self.get_daemon()
def run_task_handler(cls, args, reactor=None): if reactor is None: reactor = LandscapeReactor() config = cls.config_factory() config.load(args) for directory in [config.package_directory, config.hash_id_directory]: if not os.path.isdir(directory): os.mkdir(directory) program_name = cls.queue_name lock_filename = os.path.join(config.package_directory, program_name + ".lock") try: lock_path(lock_filename) except LockError: if config.quiet: raise SystemExit() raise SystemExit("error: package %s is already running" % program_name) words = re.findall("[A-Z][a-z]+", cls.__name__) init_logging(config, "-".join(word.lower() for word in words)) # Setup our umask for Apt to use, this needs to setup file permissions to # 0o644 so... os.umask(0o022) package_store = cls.package_store_class(config.store_filename) # Delay importing of the facades so that we don't # import Apt unless we need to. from landscape.lib.apt.package.facade import AptFacade package_facade = AptFacade() def finish(): connector.disconnect() reactor.call_later(0, reactor.stop) def got_error(failure): log_failure(failure) finish() connector = RemoteBrokerConnector(reactor, config, retry_on_reconnect=True) remote = LazyRemoteBroker(connector) handler = cls(package_store, package_facade, remote, config, reactor) result = Deferred() result.addCallback(lambda x: handler.run()) result.addCallback(lambda x: finish()) result.addErrback(got_error) reactor.call_when_running(lambda: result.callback(None)) reactor.run() return result
class PackageChanger(PackageTaskHandler): """Install, remove and upgrade packages.""" config_factory = PackageChangerConfiguration queue_name = "changer" def __init__(self, store, facade, remote, config, process_factory=reactor, landscape_reactor=None, reboot_required_filename=REBOOT_REQUIRED_FILENAME): super(PackageChanger, self).__init__( store, facade, remote, config, landscape_reactor) self._process_factory = process_factory if landscape_reactor is None: # For testing purposes. from landscape.client.reactor import LandscapeReactor self._landscape_reactor = LandscapeReactor() else: self._landscape_reactor = landscape_reactor self.reboot_required_filename = reboot_required_filename def run(self): """ Handle our tasks and spawn the reporter if package data has changed. """ if not self.update_stamp_exists(): logging.warning("The package-reporter hasn't run yet, exiting.") return succeed(None) result = self.use_hash_id_db() result.addCallback(lambda x: self.get_session_id()) result.addCallback(lambda x: self.handle_tasks()) result.addCallback(lambda x: self.run_package_reporter()) return result def run_package_reporter(self): """ Run the L{PackageReporter} if there were successfully completed tasks. """ if self.handled_tasks_count == 0: # Nothing was done return if os.getuid() == 0: os.setgid(grp.getgrnam("landscape").gr_gid) os.setuid(pwd.getpwnam("landscape").pw_uid) command = find_reporter_command(self._config) if self._config.config is not None: command += " -c %s" % self._config.config os.system(command) def handle_task(self, task): """ @param task: A L{PackageTask} carrying a message of type C{"change-packages"}. """ message = task.data if message["type"] == "change-packages": result = maybeDeferred(self.handle_change_packages, message) return result.addErrback(self.unknown_package_data_error, task) if message["type"] == "change-package-locks": return self.handle_change_package_locks(message) def unknown_package_data_error(self, failure, task): """Handle L{UnknownPackageData} data errors. If the task is older than L{UNKNOWN_PACKAGE_DATA_TIMEOUT} seconds, a message is sent to the server to notify the failure of the associated activity and the task will be removed from the queue. Otherwise a L{PackageTaskError} is raised and the task will be picked up again at the next run. """ failure.trap(UnknownPackageData) logging.warning("Package data not yet synchronized with server (%r)" % failure.value.args[0]) if task.timestamp < time.time() - UNKNOWN_PACKAGE_DATA_TIMEOUT: message = {"type": "change-packages-result", "operation-id": task.data["operation-id"], "result-code": ERROR_RESULT, "result-text": "Package data has changed. " "Please retry the operation."} return self._broker.send_message(message, self._session_id) else: raise PackageTaskError() def update_stamp_exists(self): """ Return a boolean indicating if the update-stamp stamp file exists. """ return (os.path.exists(self._config.update_stamp_filename) or os.path.exists(self.update_notifier_stamp)) def _clear_binaries(self): """Remove any binaries and its associated channel.""" binaries_path = self._config.binaries_path for existing_deb_path in os.listdir(binaries_path): # Clean up the binaries we wrote in former runs os.remove(os.path.join(binaries_path, existing_deb_path)) self._facade.clear_channels() def init_channels(self, binaries=()): """Initialize the Apt channels as needed. @param binaries: A possibly empty list of 3-tuples of the form (hash, id, deb), holding the hash, the id and the content of additional Debian packages that should be loaded in the channels. """ binaries_path = self._config.binaries_path # Clean up the binaries we wrote in former runs self._clear_binaries() if binaries: hash_ids = {} for hash, id, deb in binaries: create_binary_file(os.path.join(binaries_path, "%d.deb" % id), base64.decodestring(deb)) hash_ids[hash] = id self._store.set_hash_ids(hash_ids) self._facade.add_channel_deb_dir(binaries_path) self._facade.reload_channels(force_reload_binaries=True) self._facade.ensure_channels_reloaded() def mark_packages(self, upgrade=False, install=(), remove=(), hold=(), remove_hold=(), reset=True): """Mark packages for upgrade, installation or removal. @param upgrade: If C{True} mark all installed packages for upgrade. @param install: A list of package ids to be marked for installation. @param remove: A list of package ids to be marked for removal. @param hold: A list of package ids to be marked for holding. @param remove_hold: A list of package ids to be marked to have a hold removed. @param reset: If C{True} all existing marks will be reset. """ if reset: self._facade.reset_marks() if upgrade: self._facade.mark_global_upgrade() for mark_function, mark_ids in [ (self._facade.mark_install, install), (self._facade.mark_remove, remove), (self._facade.mark_hold, hold), (self._facade.mark_remove_hold, remove_hold)]: for mark_id in mark_ids: hash = self._store.get_id_hash(mark_id) if hash is None: raise UnknownPackageData(mark_id) package = self._facade.get_package_by_hash(hash) if package is None: raise UnknownPackageData(hash) mark_function(package) def change_packages(self, policy): """Perform the requested changes. @param policy: A value indicating what to do in case additional changes beside the ones explicitly requested are needed in order to fulfill the request (see L{complement_changes}). @return: A L{ChangePackagesResult} holding the details about the outcome of the requested changes. """ # Delay importing these so that we don't import Apt unless # we really need to. from landscape.lib.apt.package.facade import ( DependencyError, TransactionError) result = ChangePackagesResult() count = 0 while result.code is None: count += 1 try: result.text = self._facade.perform_changes() except TransactionError as exception: result.code = ERROR_RESULT result.text = exception.args[0] except DependencyError as exception: for package in exception.packages: hash = self._facade.get_package_hash(package) id = self._store.get_hash_id(hash) if id is None: # Will have to wait until the server lets us know about # this id. raise UnknownPackageData(hash) if self._facade.is_package_installed(package): # Package currently installed. Must remove it. result.removals.append(id) else: # Package currently available. Must install it. result.installs.append(id) if count == 1 and self.may_complement_changes(result, policy): # Mark all missing packages and try one more iteration self.mark_packages(install=result.installs, remove=result.removals, reset=False) else: result.code = DEPENDENCY_ERROR_RESULT else: result.code = SUCCESS_RESULT if result.code == SUCCESS_RESULT and result.text is None: result.text = 'No changes required; all changes already performed' return result def may_complement_changes(self, result, policy): """Decide whether or not we should complement the given changes. @param result: A L{PackagesResultObject} holding the details about the missing dependencies needed to complement the given changes. @param policy: It can be one of the following values: - L{POLICY_STRICT}, no additional packages will be marked. - L{POLICY_ALLOW_INSTALLS}, if only additional installs are missing they will be marked for installation. @return: A boolean indicating whether the given policy allows to complement the changes and retry. """ if policy == POLICY_ALLOW_ALL_CHANGES: return True if policy == POLICY_ALLOW_INSTALLS: # Note that package upgrades are one removal and one install, so # are not allowed here. if result.installs and not result.removals: return True return False def handle_change_packages(self, message): """Handle a C{change-packages} message.""" self.init_channels(message.get("binaries", ())) self.mark_packages(upgrade=message.get("upgrade-all", False), install=message.get("install", ()), remove=message.get("remove", ()), hold=message.get("hold", ()), remove_hold=message.get("remove-hold", ())) result = self.change_packages(message.get("policy", POLICY_STRICT)) self._clear_binaries() needs_reboot = (message.get("reboot-if-necessary") and os.path.exists(self.reboot_required_filename)) stop_exchanger = needs_reboot deferred = self._send_response(None, message, result, stop_exchanger=stop_exchanger) if needs_reboot: # Reboot the system after a short delay after the response has been # sent to the broker. This is to allow the broker time to save the # message to its on-disk queue before starting the reboot, which # will stop the landscape-client process. # It would be nice if the Deferred returned from # broker.send_message guaranteed the message was saved to disk # before firing, but that's not the case, so we add an additional # delay. deferred.addCallback(self._reboot_later) return deferred def _reboot_later(self, result): self._landscape_reactor.call_later(5, self._run_reboot) def _run_reboot(self): """ Create a C{ShutdownProcessProtocol} and return its result deferred. """ protocol = ShutdownProcessProtocol() minutes = "now" protocol.set_timeout(self._landscape_reactor) protocol.result.addCallback(self._log_reboot, minutes) protocol.result.addErrback(log_failure, "Reboot failed.") args = ["/sbin/shutdown", "-r", minutes, "Landscape is rebooting the system"] self._process_factory.spawnProcess( protocol, "/sbin/shutdown", args=args) return protocol.result def _log_reboot(self, result, minutes): """Log the reboot.""" logging.warning( "Landscape is rebooting the system in %s minutes" % minutes) def _send_response(self, reboot_result, message, package_change_result, stop_exchanger=False): """ Create a response and dispatch to the broker. """ response = {"type": "change-packages-result", "operation-id": message.get("operation-id")} response["result-code"] = package_change_result.code if package_change_result.text: response["result-text"] = package_change_result.text if package_change_result.installs: response["must-install"] = sorted(package_change_result.installs) if package_change_result.removals: response["must-remove"] = sorted(package_change_result.removals) logging.info("Queuing response with change package results to " "exchange urgently.") deferred = self._broker.send_message(response, self._session_id, True) if stop_exchanger: logging.info("stopping exchanger due to imminent reboot.") deferred.addCallback(lambda _: self._broker.stop_exchanger()) return deferred def handle_change_package_locks(self, message): """Handle a C{change-package-locks} message. Package locks aren't supported anymore. """ response = { "type": "operation-result", "operation-id": message.get("operation-id"), "status": FAILED, "result-text": "This client doesn't support package locks.", "result-code": 1} return self._broker.send_message(response, self._session_id, True) @classmethod def find_command(cls, config=None): """Return the path to the package-changer script. The script's directory is derived from the provided config. If that is None or doesn't have a "bindir" then directory of sys.argv[0] is returned. """ bindir = get_bindir(config) return os.path.join(bindir, "landscape-package-changer")