class ComponentPublisherTest(LandscapeTest):
    def setUp(self):
        super(ComponentPublisherTest, self).setUp()
        reactor = FakeReactor()
        config = Configuration()
        config.data_path = self.makeDir()
        self.makeDir(path=config.sockets_path)
        self.component = TestComponent()
        self.publisher = ComponentPublisher(self.component, reactor, config)
        self.publisher.start()

        self.connector = TestComponentConnector(reactor, config)
        connected = self.connector.connect()
        connected.addCallback(lambda remote: setattr(self, "remote", remote))
        return connected

    def tearDown(self):
        self.connector.disconnect()
        self.publisher.stop()
        super(ComponentPublisherTest, self).tearDown()

    def test_remote_methods(self):
        """Methods decorated with @remote are accessible remotely."""
        result = self.remote.ping()
        return self.assertSuccess(result, True)

    def test_protect_non_remote(self):
        """Methods not decorated with @remote are not accessible remotely."""
        result = self.remote.non_remote()
        failure = self.failureResultOf(result)
        self.assertTrue(failure.check(MethodCallError))
    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()
Exemple #3
0
class RemoteClientHelper(BrokerClientHelper):
    """Setup a connected and registered L{RemoteClient}.

    This helper extends L{BrokerClientHelper} by registering the test
    L{BrokerClient} against the L{BrokerServer} which will then be able to
    talk to it via our AMP-based machinery.
    .
    The following attributes will be set in your test case:

      - C{remote_client}: A C{RemoteClient} connected to a registered client.
    """
    def set_up(self, test_case):
        super(RemoteClientHelper, self).set_up(test_case)
        self._client_publisher = ComponentPublisher(test_case.client,
                                                    test_case.reactor,
                                                    test_case.config)
        self._client_publisher.start()
        test_case.remote.register_client("client")
        test_case.remote_client = test_case.broker.get_client("client")
        self._client_connector = test_case.broker.get_connector("client")

    def tear_down(self, test_case):
        self._client_connector.disconnect()
        self._client_publisher.stop()
        super(RemoteClientHelper, self).tear_down(test_case)
    def __init__(self, config):
        self.persist_filename = os.path.join(
            config.data_path, "%s.bpickle" % (self.service_name, ))
        super(BrokerService, self).__init__(config)

        self.transport = self.transport_factory(self.reactor, config.url,
                                                config.ssl_public_key)
        self.message_store = get_default_message_store(
            self.persist, config.message_store_path)
        self.identity = Identity(self.config, self.persist)
        exchange_store = ExchangeStore(self.config.exchange_store_path)
        self.exchanger = MessageExchange(self.reactor, self.message_store,
                                         self.transport, self.identity,
                                         exchange_store, config)
        self.pinger = self.pinger_factory(self.reactor, self.identity,
                                          self.exchanger, config)
        self.registration = RegistrationHandler(config, self.identity,
                                                self.reactor, self.exchanger,
                                                self.pinger,
                                                self.message_store)
        self.broker = BrokerServer(self.config, self.reactor, self.exchanger,
                                   self.registration, self.message_store,
                                   self.pinger)
        self.publisher = ComponentPublisher(self.broker, self.reactor,
                                            self.config)
    def register(self, registry):
        super(UserMonitor, self).register(registry)

        self.call_on_accepted("users", self._run_detect_changes, None)

        self._publisher = ComponentPublisher(self, self.registry.reactor,
                                             self.registry.config)
        self._publisher.start()
Exemple #6
0
 def set_up(self, test_case):
     super(RemoteClientHelper, self).set_up(test_case)
     self._client_publisher = ComponentPublisher(test_case.client,
                                                 test_case.reactor,
                                                 test_case.config)
     self._client_publisher.start()
     test_case.remote.register_client("client")
     test_case.remote_client = test_case.broker.get_client("client")
     self._client_connector = test_case.broker.get_connector("client")
Exemple #7
0
 def __init__(self, config):
     self.persist_filename = os.path.join(
         config.data_path, "%s.bpickle" % self.service_name)
     super(MonitorService, self).__init__(config)
     self.plugins = self.get_plugins()
     self.monitor = Monitor(self.reactor, self.config, self.persist,
                            persist_filename=self.persist_filename)
     self.publisher = ComponentPublisher(self.monitor, self.reactor,
                                         self.config)
class MonitorService(LandscapeService):
    """
    The core Twisted Service which creates and runs all necessary monitoring
    components when started.
    """

    service_name = Monitor.name

    def __init__(self, config):
        self.persist_filename = os.path.join(config.data_path,
                                             "%s.bpickle" % self.service_name)
        super(MonitorService, self).__init__(config)
        self.plugins = self.get_plugins()
        self.monitor = Monitor(self.reactor,
                               self.config,
                               self.persist,
                               persist_filename=self.persist_filename)
        self.publisher = ComponentPublisher(self.monitor, self.reactor,
                                            self.config)

    def get_plugins(self):
        return [
            namedClass("landscape.client.monitor.%s.%s" %
                       (plugin_name.lower(), plugin_name))()
            for plugin_name in self.config.plugin_factories
        ]

    def startService(self):
        """Start the monitor."""
        super(MonitorService, self).startService()
        self.publisher.start()

        def start_plugins(broker):
            self.broker = broker
            self.monitor.broker = broker
            for plugin in self.plugins:
                self.monitor.add(plugin)
            return self.broker.register_client(self.service_name)

        self.connector = RemoteBrokerConnector(self.reactor, self.config)
        connected = self.connector.connect()
        return connected.addCallback(start_plugins)

    def stopService(self):
        """Stop the monitor.

        The monitor is flushed to ensure that things like persist databases
        get saved to disk.
        """
        deferred = self.publisher.stop()
        self.monitor.flush()
        self.connector.disconnect()
        super(MonitorService, self).stopService()
        return deferred
 def test_disconnect(self):
     """
     It is possible to call L{ComponentConnector.disconnect} multiple times,
     even if the connection has been already closed.
     """
     component = TestComponent()
     publisher = ComponentPublisher(component, self.reactor, self.config)
     publisher.start()
     self.connector.connect()
     self.connector.disconnect()
     self.connector.disconnect()
Exemple #10
0
 def test_connect_with_factor(self):
     """
     If C{factor} is passed to the L{ComponentConnector.connect} method,
     then the associated protocol factory will be set to that value.
     """
     component = TestComponent()
     publisher = ComponentPublisher(component, self.reactor, self.config)
     publisher.start()
     deferred = self.connector.connect(factor=1.0)
     remote = self.successResultOf(deferred)
     self.assertEqual(1.0, remote._factory.factor)
Exemple #11
0
    def set_up(self, test_case):
        super(RemoteBrokerHelper, self).set_up(test_case)

        self._publisher = ComponentPublisher(test_case.broker,
                                             test_case.reactor,
                                             test_case.config)
        self._connector = RemoteBrokerConnector(test_case.reactor,
                                                test_case.config)

        self._publisher.start()
        deferred = self._connector.connect()
        test_case.remote = test_case.successResultOf(deferred)
Exemple #12
0
class ManagerService(LandscapeService):
    """
    The core Twisted Service which creates and runs all necessary managing
    components when started.
    """

    service_name = Manager.name

    def __init__(self, config):
        super(ManagerService, self).__init__(config)
        self.plugins = self.get_plugins()
        self.manager = Manager(self.reactor, self.config)
        self.publisher = ComponentPublisher(self.manager, self.reactor,
                                            self.config)

    def get_plugins(self):
        """Return instances of all the plugins enabled in the configuration."""
        return [
            namedClass("landscape.client.manager.%s.%s" %
                       (plugin_name.lower(), plugin_name))()
            for plugin_name in self.config.plugin_factories
        ]

    def startService(self):
        """Start the manager service.

        This method does 3 things, in this order:

          - Start listening for connections on the manager socket.
          - Connect to the broker.
          - Add all configured plugins, that will in turn register themselves.
        """
        super(ManagerService, self).startService()
        self.publisher.start()

        def start_plugins(broker):
            self.broker = broker
            self.manager.broker = broker
            for plugin in self.plugins:
                self.manager.add(plugin)
            return self.broker.register_client(self.service_name)

        self.connector = RemoteBrokerConnector(self.reactor, self.config)
        connected = self.connector.connect()
        return connected.addCallback(start_plugins)

    def stopService(self):
        """Stop the manager and close the connection with the broker."""
        self.connector.disconnect()
        self.publisher.stop()
        super(ManagerService, self).stopService()
Exemple #13
0
    def setUp(self):
        super(ComponentPublisherTest, self).setUp()
        reactor = FakeReactor()
        config = Configuration()
        config.data_path = self.makeDir()
        self.makeDir(path=config.sockets_path)
        self.component = TestComponent()
        self.publisher = ComponentPublisher(self.component, reactor, config)
        self.publisher.start()

        self.connector = TestComponentConnector(reactor, config)
        connected = self.connector.connect()
        connected.addCallback(lambda remote: setattr(self, "remote", remote))
        return connected
Exemple #14
0
    def register(self, registry):
        """
        Schedule reactor events for generic L{Plugin} callbacks, user
        and group management operations, and resynchronization.
        """
        super(UserManager, self).register(registry)
        self._registry = registry

        self._publisher = ComponentPublisher(self, self.registry.reactor,
                                             self.registry.config)
        self._publisher.start()

        for message_type in self._message_types:
            self._registry.register_message(message_type,
                                            self._message_dispatch)
 def setUp(self):
     super(UserMonitorTest, self).setUp()
     self.shadow_file = self.makeFile(
         "jdoe:$1$xFlQvTqe$cBtrNEDOIKMy/BuJoUdeG0:13348:0:99999:7:::\n"
         "psmith:!:13348:0:99999:7:::\n"
         "sam:$1$q7sz09uw$q.A3526M/SHu8vUb.Jo1A/:13349:0:99999:7:::\n")
     self.user_manager = UserManager(shadow_file=self.shadow_file)
     self.publisher = ComponentPublisher(self.user_manager, self.reactor,
                                         self.config)
     self.publisher.start()
     self.provider = FakeUserProvider()
     self.plugin = UserMonitor(self.provider)
     # Part of bug 1048576 remediation:
     self._original_USER_UPDATE_FLAG_FILE = (
         landscape.client.monitor.usermonitor.USER_UPDATE_FLAG_FILE)
Exemple #16
0
    def test_reconnect_fires_event(self):
        """
        An event is fired whenever the connection is established again after
        it has been lost.
        """
        reconnects = []
        self.reactor.call_on("test-reconnect", lambda: reconnects.append(True))

        component = TestComponent()
        publisher = ComponentPublisher(component, self.reactor, self.config)
        publisher.start()
        deferred = self.connector.connect()
        self.successResultOf(deferred)
        self.connector._connector.disconnect()  # Simulate a disconnection
        self.assertEqual([], reconnects)
        self.reactor._reactor.advance(10)
        self.assertEqual([True], reconnects)
    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()
Exemple #18
0
class RemoteBrokerHelper(BrokerServerHelper):
    """Setup a connected L{RemoteBroker}.

    This helper extends L{BrokerServerHelper}.by adding a L{RemoteBroker} which
    exposes the L{BrokerServer} instance remotely via our AMP-based machinery.

    IMPORTANT: note that the connection is created using a *real* Unix socket,
    calling L{FakeReactor.call_unix} which in turn defers to the *real* Twisted
    reactor. This means that all calls to the L{RemoteBroker} instance will
    be truly asynchronous and tests will need to return deferreds in order to
    let the reactor run. See also::

        http://twistedmatrix.com/documents/current/core/howto/testing.html

    and the "Leave the Reactor as you found it" paragraph to understand how
    to write tests interacting with the reactor.

    The following attributes will be set in your test case:

      - C{remote}: A C{RemoteObject} connected to the broker server.
    """
    def set_up(self, test_case):
        super(RemoteBrokerHelper, self).set_up(test_case)

        self._publisher = ComponentPublisher(test_case.broker,
                                             test_case.reactor,
                                             test_case.config)
        self._connector = RemoteBrokerConnector(test_case.reactor,
                                                test_case.config)

        self._publisher.start()
        deferred = self._connector.connect()
        test_case.remote = test_case.successResultOf(deferred)

    def tear_down(self, test_case):
        self._connector.disconnect()
        self._publisher.stop()
        super(RemoteBrokerHelper, self).tear_down(test_case)
    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()
class UserMonitor(MonitorPlugin):
    """
    A plugin which monitors the system user databases.
    """

    persist_name = "users"
    scope = "users"
    run_interval = 3600  # 1 hour
    name = "usermonitor"

    def __init__(self, provider=None):
        if provider is None:
            provider = UserProvider()
        self._provider = provider
        self._publisher = None

    def register(self, registry):
        super(UserMonitor, self).register(registry)

        self.call_on_accepted("users", self._run_detect_changes, None)

        self._publisher = ComponentPublisher(self, self.registry.reactor,
                                             self.registry.config)
        self._publisher.start()

    def stop(self):
        """Stop listening for incoming AMP connections."""
        if self._publisher:
            self._publisher.stop()
            self._publisher = None

    def _resynchronize(self, scopes=None):
        """Reset user and group data."""
        deferred = super(UserMonitor, self)._resynchronize(scopes=scopes)
        # Wait for the superclass' asynchronous _resynchronize method to
        # complete, so we have a new session ID at hand and we can craft a
        # valid message (l.broker.client.BrokerClientPlugin._resynchronize).
        deferred.addCallback(lambda _: self._run_detect_changes())
        return deferred

    @remote
    def detect_changes(self, operation_id=None):
        return self.registry.broker.call_if_accepted("users",
                                                     self._run_detect_changes,
                                                     operation_id)

    run = detect_changes

    def _run_detect_changes(self, operation_id=None):
        """
        If changes are detected an C{urgent-exchange} is fired to send
        updates to the server immediately.

        @param operation_id: When present it will be included in the
            C{operation-id} field.
        """
        from landscape.client.manager.usermanager import (
            RemoteUserManagerConnector)
        user_manager_connector = RemoteUserManagerConnector(
            self.registry.reactor, self.registry.config)

        # We'll skip checking the locked users if we're in monitor-only mode.
        if getattr(self.registry.config, "monitor_only", False):
            result = maybeDeferred(self._detect_changes, [], operation_id)
        else:

            def get_locked_usernames(user_manager):
                return user_manager.get_locked_usernames()

            def disconnect(locked_usernames):
                user_manager_connector.disconnect()
                return locked_usernames

            result = user_manager_connector.connect()
            result.addCallback(get_locked_usernames)
            result.addCallback(disconnect)
            result.addCallback(self._detect_changes, operation_id)
            result.addErrback(lambda f: self._detect_changes([], operation_id))
        return result

    def _detect_changes(self,
                        locked_users,
                        operation_id=None,
                        UserChanges=UserChanges):
        def update_snapshot(result):
            changes.snapshot()
            return result

        def log_error(result):
            log_failure(
                result, "Error occured calling send_message in "
                "_detect_changes")

        self._provider.locked_users = locked_users
        changes = UserChanges(self._persist, self._provider)

        # Part of bug 1048576 remediation: If the flag file exists, we need to
        # do a full update of user data.
        full_refresh = os.path.exists(self.user_update_flag_file_path)
        if full_refresh:
            # Clear the record of what changes have been sent to the server in
            # order to force sending of all user data which will do one of two
            # things server side:  either the server has no user data at all,
            # in which case it will now have a complete copy, otherwise it
            # will have at least some user data which this message will
            # duplicate, provoking the server to note the inconsistency and
            # request a full resync of the user data.  Either way, the result
            # is the same: the client and server will be in sync with regard
            # to users.
            changes.clear()

        message = changes.create_diff()

        if message:
            message["type"] = "users"
            if operation_id:
                message["operation-id"] = operation_id
            result = self.registry.broker.send_message(message,
                                                       self._session_id,
                                                       urgent=True)
            result.addCallback(update_snapshot)

            # Part of bug 1048576 remediation:
            if full_refresh:
                # If we are doing a full refresh, we want to remove the flag
                # file that triggered the refresh if it completes successfully.
                result.addCallback(lambda _: self._remove_update_flag_file())

            result.addErrback(log_error)
            return result

    def _remove_update_flag_file(self):
        """Remove the full update flag file, logging any errors.

        This is part of the bug 1048576 remediation.
        """
        try:
            os.remove(self.user_update_flag_file_path)
        except OSError:
            logging.exception("Error removing user update flag file.")

    @property
    def user_update_flag_file_path(self):
        """Location of the user update flag file.

        This is part of the bug 1048576 remediation.
        """
        return os.path.join(self.registry.config.data_path,
                            USER_UPDATE_FLAG_FILE)
Exemple #21
0
class BrokerService(LandscapeService):
    """The core C{Service} of the Landscape Broker C{Application}.

    The Landscape broker service handles all the communication between the
    client and server.  When started it creates and runs all necessary
    components to exchange messages with the Landscape server.

    @cvar service_name: C{broker}

    @ivar persist_filename: Path to broker-specific persistent data.
    @ivar persist: A L{Persist} object saving and loading data from
        C{self.persist_filename}.
    @ivar message_store: A L{MessageStore} used by the C{exchanger} to
        queue outgoing messages.
    @ivar transport: An L{HTTPTransport} used by the C{exchanger} to deliver
        messages.
    @ivar identity: The L{Identity} of the Landscape client the broker runs on.
    @ivar exchanger: The L{MessageExchange} exchanges messages with the server.
    @ivar pinger: The L{Pinger} checks if the server has new messages for us.
    @ivar registration: The L{RegistrationHandler} performs the initial
        registration.

    @param config: A L{BrokerConfiguration}.
    """

    transport_factory = HTTPTransport
    pinger_factory = Pinger
    service_name = BrokerServer.name

    def __init__(self, config):
        self.persist_filename = os.path.join(
            config.data_path, "%s.bpickle" % (self.service_name,))
        super(BrokerService, self).__init__(config)

        self.transport = self.transport_factory(
            self.reactor, config.url, config.ssl_public_key)
        self.message_store = get_default_message_store(
            self.persist, config.message_store_path)
        self.identity = Identity(self.config, self.persist)
        exchange_store = ExchangeStore(self.config.exchange_store_path)
        self.exchanger = MessageExchange(
            self.reactor, self.message_store, self.transport, self.identity,
            exchange_store, config)
        self.pinger = self.pinger_factory(
            self.reactor, self.identity, self.exchanger, config)
        self.registration = RegistrationHandler(
            config, self.identity, self.reactor, self.exchanger, self.pinger,
            self.message_store)
        self.broker = BrokerServer(self.config, self.reactor, self.exchanger,
                                   self.registration, self.message_store,
                                   self.pinger)
        self.publisher = ComponentPublisher(self.broker, self.reactor,
                                            self.config)

    def startService(self):
        """Start the broker.

        Create a L{BrokerServer} listening on C{broker_socket_path} for clients
        connecting with the L{BrokerServerConnector}, and start the
        L{MessageExchange} and L{Pinger} services.
        """
        super(BrokerService, self).startService()
        self.publisher.start()
        self.exchanger.start()
        self.pinger.start()

    def stopService(self):
        """Stop the broker."""
        self.publisher.stop()
        self.exchanger.stop()
        self.pinger.stop()
        super(BrokerService, self).stopService()
 def __init__(self, config):
     super(ManagerService, self).__init__(config)
     self.plugins = self.get_plugins()
     self.manager = Manager(self.reactor, self.config)
     self.publisher = ComponentPublisher(self.manager, self.reactor,
                                         self.config)
Exemple #23
0
class UserManager(ManagerPlugin):

    name = "usermanager"

    def __init__(self, management=None, shadow_file="/etc/shadow"):
        self._management = management or UserManagement()
        self._shadow_file = shadow_file
        self._message_types = {
            "add-user": self._add_user,
            "edit-user": self._edit_user,
            "lock-user": self._lock_user,
            "unlock-user": self._unlock_user,
            "remove-user": self._remove_user,
            "add-group": self._add_group,
            "edit-group": self._edit_group,
            "remove-group": self._remove_group,
            "add-group-member": self._add_group_member,
            "remove-group-member": self._remove_group_member
        }
        self._publisher = None

    def register(self, registry):
        """
        Schedule reactor events for generic L{Plugin} callbacks, user
        and group management operations, and resynchronization.
        """
        super(UserManager, self).register(registry)
        self._registry = registry

        self._publisher = ComponentPublisher(self, self.registry.reactor,
                                             self.registry.config)
        self._publisher.start()

        for message_type in self._message_types:
            self._registry.register_message(message_type,
                                            self._message_dispatch)

    def stop(self):
        """Stop listening for incoming AMP connections."""
        if self._publisher:
            self._publisher.stop()
            self._publisher = None

    @remote
    def get_locked_usernames(self):
        """Return a list of usernames with locked system accounts."""
        locked_users = []
        if self._shadow_file:
            try:
                shadow_file = open(self._shadow_file, "r")
                for line in shadow_file:
                    parts = line.split(":")
                    if len(parts) > 1:
                        if parts[1].startswith("!"):
                            locked_users.append(parts[0].strip())
            except IOError as e:
                logging.error("Error reading shadow file. %s" % e)
        return locked_users

    def _message_dispatch(self, message):
        """Dispatch the given user-change request to the correct handler.

        @param message: The request we got from the server.
        """
        user_monitor_connector = RemoteUserMonitorConnector(
            self.registry.reactor, self.registry.config)

        def detect_changes(user_monitor):
            self._user_monitor = user_monitor
            return user_monitor.detect_changes()

        result = user_monitor_connector.connect()
        result.addCallback(detect_changes)
        result.addCallback(self._perform_operation, message)
        result.addCallback(self._send_changes, message)
        result.addCallback(lambda x: user_monitor_connector.disconnect())
        return result

    def _perform_operation(self, result, message):
        message_type = message["type"]
        message_method = self._message_types[message_type]
        return self.call_with_operation_result(message, message_method,
                                               message)

    def _send_changes(self, result, message):
        return self._user_monitor.detect_changes(message["operation-id"])

    def _add_user(self, message):
        """Run an C{add-user} operation."""
        return self._management.add_user(
            message["username"], message["name"], message["password"],
            message["require-password-reset"], message["primary-group-name"],
            message["location"], message["work-number"],
            message["home-number"])

    def _edit_user(self, message):
        """Run an C{edit-user} operation."""
        return self._management.set_user_details(
            message["username"],
            password=message["password"],
            name=message["name"],
            location=message["location"],
            work_number=message["work-number"],
            home_number=message["home-number"],
            primary_group_name=message["primary-group-name"])

    def _lock_user(self, message):
        """Run a C{lock-user} operation."""
        return self._management.lock_user(message["username"])

    def _unlock_user(self, message):
        """Run an C{unlock-user} operation."""
        return self._management.unlock_user(message["username"])

    def _remove_user(self, message):
        """Run a C{remove-user} operation."""
        return self._management.remove_user(message["username"],
                                            message["delete-home"])

    def _add_group(self, message):
        """Run an C{add-group} operation."""
        return self._management.add_group(message["groupname"])

    def _edit_group(self, message):
        """Run an C{edit-group} operation."""
        return self._management.set_group_details(message["groupname"],
                                                  message["new-name"])

    def _add_group_member(self, message):
        """Run an C{add-group-member} operation."""
        return self._management.add_group_member(message["username"],
                                                 message["groupname"])

    def _remove_group_member(self, message):
        """Run a C{remove-group-member} operation."""
        return self._management.remove_group_member(message["username"],
                                                    message["groupname"])

    def _remove_group(self, message):
        """Run an C{remove-group} operation."""
        return self._management.remove_group(message["groupname"])
class UserMonitorTest(LandscapeTest):

    helpers = [MonitorHelper]

    def setUp(self):
        super(UserMonitorTest, self).setUp()
        self.shadow_file = self.makeFile(
            "jdoe:$1$xFlQvTqe$cBtrNEDOIKMy/BuJoUdeG0:13348:0:99999:7:::\n"
            "psmith:!:13348:0:99999:7:::\n"
            "sam:$1$q7sz09uw$q.A3526M/SHu8vUb.Jo1A/:13349:0:99999:7:::\n")
        self.user_manager = UserManager(shadow_file=self.shadow_file)
        self.publisher = ComponentPublisher(self.user_manager, self.reactor,
                                            self.config)
        self.publisher.start()
        self.provider = FakeUserProvider()
        self.plugin = UserMonitor(self.provider)
        # Part of bug 1048576 remediation:
        self._original_USER_UPDATE_FLAG_FILE = (
            landscape.client.monitor.usermonitor.USER_UPDATE_FLAG_FILE)

    def tearDown(self):
        self.publisher.stop()
        self.plugin.stop()
        # Part of bug 1048576 remediation:
        landscape.client.monitor.usermonitor.USER_UPDATE_FLAG_FILE = (
            self._original_USER_UPDATE_FLAG_FILE)
        return super(UserMonitorTest, self).tearDown()

    def test_constants(self):
        """
        L{UserMonitor.persist_name} and
        L{UserMonitor.run_interval} need to be present for
        L{Plugin} to work properly.
        """
        self.assertEqual(self.plugin.persist_name, "users")
        self.assertEqual(self.plugin.run_interval, 3600)

    def test_wb_resynchronize_event(self):
        """
        When a C{resynchronize} event, with 'users' scope, occurs any cached
        L{UserChange} snapshots should be cleared and a new message with users
        generated.
        """
        self.provider.users = [("jdoe", "x", 1000, 1000, "JD,,,,",
                                "/home/jdoe", "/bin/sh")]
        self.provider.groups = [("webdev", "x", 1000, ["jdoe"])]
        self.broker_service.message_store.set_accepted_types(["users"])
        self.monitor.add(self.plugin)
        self.successResultOf(self.plugin.run())
        persist = self.plugin._persist
        self.assertTrue(persist.get("users"))
        self.assertTrue(persist.get("groups"))
        self.assertMessages(
                self.broker_service.message_store.get_pending_messages(),
                [{"create-group-members": {u"webdev": [u"jdoe"]},
                  "create-groups": [{"gid": 1000, "name": u"webdev"}],
                  "create-users": [{"enabled": True, "home-phone": None,
                                    "location": None, "name": u"JD",
                                    "primary-gid": 1000, "uid": 1000,
                                    "username": u"jdoe", "work-phone": None}],
                  "type": "users"}])
        self.broker_service.message_store.delete_all_messages()
        deferred = self.monitor.reactor.fire(
            "resynchronize", scopes=["users"])[0]
        self.successResultOf(deferred)
        self.assertMessages(
            self.broker_service.message_store.get_pending_messages(),
            [{"create-group-members": {u"webdev": [u"jdoe"]},
              "create-groups": [{"gid": 1000, "name": u"webdev"}],
              "create-users": [{"enabled": True, "home-phone": None,
                                "location": None, "name": u"JD",
                                "primary-gid": 1000, "uid": 1000,
                                "username": u"jdoe",
                                "work-phone": None}],
              "type": "users"}])

    def test_new_message_after_resynchronize_event(self):
        """
        When a 'resynchronize' reactor event is fired, a new session is
        created and the UserMonitor creates a new message.
        """
        self.provider.users = [("jdoe", "x", 1000, 1000, "JD,,,,",
                                "/home/jdoe", "/bin/sh")]
        self.provider.groups = [("webdev", "x", 1000, ["jdoe"])]
        self.broker_service.message_store.set_accepted_types(["users"])
        self.monitor.add(self.plugin)
        self.plugin.client.broker.message_store.drop_session_ids()
        deferred = self.reactor.fire("resynchronize")[0]
        self.successResultOf(deferred)
        self.assertMessages(
            self.broker_service.message_store.get_pending_messages(),
            [{"create-group-members": {u"webdev": [u"jdoe"]},
              "create-groups": [{"gid": 1000, "name": u"webdev"}],
              "create-users": [{"enabled": True, "home-phone": None,
                                "location": None, "name": u"JD",
                                "primary-gid": 1000, "uid": 1000,
                                "username": u"jdoe", "work-phone": None}],
              "type": "users"}])

    def test_wb_resynchronize_event_with_global_scope(self):
        """
        When a C{resynchronize} event, with global scope, occurs we act exactly
        as if it had 'users' scope.
        """
        self.provider.users = [("jdoe", "x", 1000, 1000, "JD,,,,",
                                "/home/jdoe", "/bin/sh")]
        self.provider.groups = [("webdev", "x", 1000, ["jdoe"])]
        self.broker_service.message_store.set_accepted_types(["users"])
        self.monitor.add(self.plugin)
        self.successResultOf(self.plugin.run())
        persist = self.plugin._persist
        self.assertTrue(persist.get("users"))
        self.assertTrue(persist.get("groups"))
        self.assertMessages(
                self.broker_service.message_store.get_pending_messages(),
                [{"create-group-members": {u"webdev": [u"jdoe"]},
                  "create-groups": [{"gid": 1000, "name": u"webdev"}],
                  "create-users": [{"enabled": True, "home-phone": None,
                                    "location": None, "name": u"JD",
                                    "primary-gid": 1000, "uid": 1000,
                                    "username": u"jdoe", "work-phone": None}],
                  "type": "users"}])
        self.broker_service.message_store.delete_all_messages()
        deferred = self.monitor.reactor.fire("resynchronize")[0]
        self.successResultOf(deferred)
        self.assertMessages(
            self.broker_service.message_store.get_pending_messages(),
            [{"create-group-members": {u"webdev": [u"jdoe"]},
              "create-groups": [{"gid": 1000, "name": u"webdev"}],
              "create-users": [{"enabled": True, "home-phone": None,
                                "location": None, "name": u"JD",
                                "primary-gid": 1000, "uid": 1000,
                                "username": u"jdoe",
                                "work-phone": None}],
              "type": "users"}])

    def test_do_not_resynchronize_with_other_scope(self):
        """
        When a C{resynchronize} event, with an irrelevant scope, occurs we do
        nothing.
        """
        self.provider.users = [("jdoe", "x", 1000, 1000, "JD,,,,",
                                "/home/jdoe", "/bin/sh")]
        self.provider.groups = [("webdev", "x", 1000, ["jdoe"])]
        self.broker_service.message_store.set_accepted_types(["users"])
        self.monitor.add(self.plugin)
        self.successResultOf(self.plugin.run())
        persist = self.plugin._persist
        self.assertTrue(persist.get("users"))
        self.assertTrue(persist.get("groups"))
        self.assertMessages(
                self.broker_service.message_store.get_pending_messages(),
                [{"create-group-members": {u"webdev": [u"jdoe"]},
                  "create-groups": [{"gid": 1000, "name": u"webdev"}],
                  "create-users": [{"enabled": True, "home-phone": None,
                                    "location": None, "name": u"JD",
                                    "primary-gid": 1000, "uid": 1000,
                                    "username": u"jdoe", "work-phone": None}],
                  "type": "users"}])
        self.broker_service.message_store.delete_all_messages()
        self.monitor.reactor.fire("resynchronize", scopes=["disk"])[0]
        self.assertMessages(
            self.broker_service.message_store.get_pending_messages(),
            [])

    def test_run(self):
        """
        The L{UserMonitor} should have message run which should enqueue a
        message with  a diff-like representation of changes since the last
        run.
        """

        def got_result(result):
            self.assertMessages(
                self.broker_service.message_store.get_pending_messages(),
                [{"create-group-members": {u"webdev": [u"jdoe"]},
                  "create-groups": [{"gid": 1000, "name": u"webdev"}],
                  "create-users": [{"enabled": True, "home-phone": None,
                                    "location": None, "name": u"JD",
                                    "primary-gid": 1000, "uid": 1000,
                                    "username": u"jdoe", "work-phone": None}],
                  "type": "users"}])

        self.provider.users = [("jdoe", "x", 1000, 1000, "JD,,,,",
                                "/home/jdoe", "/bin/sh")]
        self.provider.groups = [("webdev", "x", 1000, ["jdoe"])]
        self.broker_service.message_store.set_accepted_types(["users"])
        self.monitor.add(self.plugin)
        result = self.plugin.run()
        result.addCallback(got_result)
        return result

    def test_run_interval(self):
        """
        L{UserMonitor.register} calls the C{register} method on it's
        super class, which sets up a looping call to run the plugin
        every L{UserMonitor.run_interval} seconds.
        """
        self.plugin.run = Mock()

        self.monitor.add(self.plugin)
        self.broker_service.message_store.set_accepted_types(["users"])
        self.reactor.advance(self.plugin.run_interval * 5)

        self.assertEqual(self.plugin.run.call_count, 5)

    def test_run_with_operation_id(self):
        """
        The L{UserMonitor} should have message run which should enqueue a
        message with  a diff-like representation of changes since the last
        run.
        """

        def got_result(result):
            self.assertMessages(
                self.broker_service.message_store.get_pending_messages(),
                [{"create-group-members": {u"webdev": [u"jdoe"]},
                  "create-groups": [{"gid": 1000, "name": u"webdev"}],
                  "create-users": [{"enabled": True, "home-phone": None,
                                    "location": None, "name": u"JD",
                                    "primary-gid": 1000, "uid": 1000,
                                    "username": u"jdoe", "work-phone": None}],
                  "operation-id": 1001,
                  "type": "users"}])

        self.provider.users = [("jdoe", "x", 1000, 1000, "JD,,,,",
                                "/home/jdoe", "/bin/sh")]
        self.provider.groups = [("webdev", "x", 1000, ["jdoe"])]
        self.monitor.add(self.plugin)
        self.broker_service.message_store.set_accepted_types(["users"])
        result = self.plugin.run(1001)
        result.addCallback(got_result)
        return result

    def test_detect_changes(self):

        def got_result(result):
            self.assertMessages(
                self.broker_service.message_store.get_pending_messages(),
                [{"create-group-members": {u"webdev": [u"jdoe"]},
                  "create-groups": [{"gid": 1000, "name": u"webdev"}],
                  "create-users": [{"enabled": True, "home-phone": None,
                                    "location": None, "name": u"JD",
                                    "primary-gid": 1000, "uid": 1000,
                                    "username": u"jdoe", "work-phone": None}],
                  "type": "users"}])

        self.broker_service.message_store.set_accepted_types(["users"])
        self.provider.users = [("jdoe", "x", 1000, 1000, "JD,,,,",
                                "/home/jdoe", "/bin/sh")]
        self.provider.groups = [("webdev", "x", 1000, ["jdoe"])]

        self.monitor.add(self.plugin)
        connector = RemoteUserMonitorConnector(self.reactor, self.config)
        result = connector.connect()
        result.addCallback(lambda remote: remote.detect_changes())
        result.addCallback(got_result)
        result.addCallback(lambda x: connector.disconnect())
        return result

    def test_detect_changes_with_operation_id(self):
        """
        The L{UserMonitor} should expose a remote
        C{remote_run} method which should call the remote
        """

        def got_result(result):
            self.assertMessages(
                self.broker_service.message_store.get_pending_messages(),
                [{"create-group-members": {u"webdev": [u"jdoe"]},
                  "create-groups": [{"gid": 1000, "name": u"webdev"}],
                  "create-users": [{"enabled": True, "home-phone": None,
                                    "location": None, "name": u"JD",
                                    "primary-gid": 1000, "uid": 1000,
                                    "username": u"jdoe", "work-phone": None}],
                  "operation-id": 1001,
                  "type": "users"}])

        self.broker_service.message_store.set_accepted_types(["users"])
        self.provider.users = [("jdoe", "x", 1000, 1000, "JD,,,,",
                                "/home/jdoe", "/bin/sh")]
        self.provider.groups = [("webdev", "x", 1000, ["jdoe"])]
        self.monitor.add(self.plugin)
        connector = RemoteUserMonitorConnector(self.reactor, self.config)
        result = connector.connect()
        result.addCallback(lambda remote: remote.detect_changes(1001))
        result.addCallback(got_result)
        result.addCallback(lambda x: connector.disconnect())
        return result

    def test_detect_changes_clears_user_provider_if_flag_file_exists(self):
        """
        Temporary bug 1508110 remediation: If a special flag file exists,
        cached user data is dumped and a complete refresh of all user data is
        transmitted.
        """
        self.monitor.add(self.plugin)

        class FauxUserChanges(object):
            cleared = False

            def __init__(self, *args):
                pass

            def create_diff(self):
                return None

            def clear(self):
                self.__class__.cleared = True

        # Create the (temporary, test) user update flag file.
        landscape.client.monitor.usermonitor.USER_UPDATE_FLAG_FILE = \
            update_flag_file = self.makeFile("")
        self.addCleanup(lambda: os.remove(update_flag_file))

        # Trigger a detect changes.
        self.plugin._persist = None
        self.plugin._detect_changes([], UserChanges=FauxUserChanges)

        # The clear() method was called.
        self.assertTrue(FauxUserChanges.cleared)

    def test_detect_changes_deletes_flag_file(self):
        """
        Temporary bug 1508110 remediation: The total user data refresh flag
        file is deleted once the data has been sent.
        """

        def got_result(result):
            # The flag file has been deleted.
            self.assertFalse(os.path.exists(update_flag_file))

        # Create the (temporary, test) user update flag file.
        landscape.client.monitor.usermonitor.USER_UPDATE_FLAG_FILE = \
            update_flag_file = self.makeFile("")

        self.broker_service.message_store.set_accepted_types(["users"])
        self.provider.users = [("jdoe", "x", 1000, 1000, "JD,,,,",
                                "/home/jdoe", "/bin/sh")]
        self.provider.groups = []

        self.monitor.add(self.plugin)
        connector = RemoteUserMonitorConnector(self.reactor, self.config)
        result = connector.connect()
        result.addCallback(lambda remote: remote.detect_changes())
        result.addCallback(got_result)
        result.addCallback(lambda x: connector.disconnect())
        return result

    def test_no_message_if_not_accepted(self):
        """
        Don't add any messages at all if the broker isn't currently
        accepting their type.
        """

        def got_result(result):
            mstore = self.broker_service.message_store
            self.assertMessages(list(mstore.get_pending_messages()), [])
            mstore.set_accepted_types(["users"])
            self.assertMessages(list(mstore.get_pending_messages()), [])

        self.broker_service.message_store.set_accepted_types([])
        self.provider.users = [("jdoe", "x", 1000, 1000, "JD,,,,",
                                "/home/jdoe", "/bin/sh")]
        self.provider.groups = [("webdev", "x", 1000, ["jdoe"])]
        self.monitor.add(self.plugin)
        connector = RemoteUserMonitorConnector(self.reactor, self.config)
        result = connector.connect()
        result.addCallback(lambda remote: remote.detect_changes(1001))
        result.addCallback(got_result)
        result.addCallback(lambda x: connector.disconnect())
        return result

    def test_call_on_accepted(self):

        def got_result(result):
            mstore = self.broker_service.message_store
            self.assertMessages(
                mstore.get_pending_messages(),
                [{"create-group-members": {u"webdev": [u"jdoe"]},
                  "create-groups": [{"gid": 1000, "name": u"webdev"}],
                  "create-users": [{"enabled": True, "home-phone": None,
                                    "location": None, "name": u"JD",
                                    "primary-gid": 1000, "uid": 1000,
                                    "username": u"jdoe", "work-phone": None}],
                  "type": "users"}])

        self.provider.users = [("jdoe", "x", 1000, 1000, "JD,,,,",
                                "/home/jdoe", "/bin/sh")]
        self.provider.groups = [("webdev", "x", 1000, ["jdoe"])]
        self.monitor.add(self.plugin)

        self.broker_service.message_store.set_accepted_types(["users"])
        result = self.reactor.fire(
            ("message-type-acceptance-changed", "users"), True)
        result = [x for x in result if x][0]
        result.addCallback(got_result)
        return result

    def test_do_not_persist_changes_when_send_message_fails(self):
        """
        When the plugin is run it persists data that it uses on
        subsequent checks to calculate the delta to send.  It should
        only persist data when the broker confirms that the message
        sent by the plugin has been sent.
        """
        self.log_helper.ignore_errors(RuntimeError)

        def got_result(result):
            persist = self.plugin._persist
            mstore = self.broker_service.message_store
            self.assertMessages(mstore.get_pending_messages(), [])
            self.assertFalse(persist.get("users"))
            self.assertFalse(persist.get("groups"))

        self.broker_service.message_store.set_accepted_types(["users"])

        self.monitor.broker.send_message = Mock(return_value=fail(
            RuntimeError()))

        self.provider.users = [("jdoe", "x", 1000, 1000, "JD,,,,",
                                "/home/jdoe", "/bin/sh")]
        self.provider.groups = [("webdev", "x", 1000, ["jdoe"])]
        self.monitor.add(self.plugin)
        connector = RemoteUserMonitorConnector(self.reactor, self.config)
        result = connector.connect()
        result.addCallback(lambda remote: remote.detect_changes(1001))
        result.addCallback(got_result)
        result.addCallback(lambda x: connector.disconnect())
        self.monitor.broker.send_message.assert_called_once_with(
            ANY, ANY, urgent=True)