Exemple #1
0
    def __init__(self, conf, logger, servers_per_port):
        self.conf = conf
        self.logger = logger
        self.servers_per_port = servers_per_port
        self.swift_dir = conf.get('swift_dir', '/etc/swift')
        self.ring_check_interval = int(conf.get('ring_check_interval', 15))
        self.port_pid_state = PortPidState(servers_per_port, logger)

        bind_ip = conf.get('bind_ip', '0.0.0.0')
        self.cache = BindPortsCache(self.swift_dir, bind_ip)
Exemple #2
0
    def __init__(self, conf, logger, servers_per_port):
        self.conf = conf
        self.logger = logger
        self.servers_per_port = servers_per_port
        self.swift_dir = conf.get("swift_dir", "/etc/swift")
        self.ring_check_interval = int(conf.get("ring_check_interval", 15))
        self.port_pid_state = PortPidState(servers_per_port, logger)

        bind_ip = conf.get("bind_ip", "0.0.0.0")
        self.cache = BindPortsCache(self.swift_dir, bind_ip)
Exemple #3
0
class ServersPerPortStrategy(object):
    """
    WSGI server management strategy object for an object-server with one listen
    port per unique local port in the storage policy rings.  The
    `servers_per_port` integer config setting determines how many workers are
    run per port.

    Used in :py:func:`run_wsgi`.

    :param dict conf: Server configuration dictionary.
    :param logger: The server's :py:class:`~swift.common.utils.LogAdaptor`
                   object.
    :param int servers_per_port: The number of workers to run per port.
    """

    def __init__(self, conf, logger, servers_per_port):
        self.conf = conf
        self.logger = logger
        self.servers_per_port = servers_per_port
        self.swift_dir = conf.get("swift_dir", "/etc/swift")
        self.ring_check_interval = int(conf.get("ring_check_interval", 15))
        self.port_pid_state = PortPidState(servers_per_port, logger)

        bind_ip = conf.get("bind_ip", "0.0.0.0")
        self.cache = BindPortsCache(self.swift_dir, bind_ip)

    def _reload_bind_ports(self):
        self.bind_ports = self.cache.all_bind_ports_for_node()

    def _bind_port(self, port):
        new_conf = self.conf.copy()
        new_conf["bind_port"] = port
        sock = get_socket(new_conf)
        self.port_pid_state.track_port(port, sock)

    def loop_timeout(self):
        """
        Return timeout before checking for reloaded rings.

        :returns: The time to wait for a child to exit before checking for
                  reloaded rings (new ports).
        """

        return self.ring_check_interval

    def bind_ports(self):
        """
        Bind one listen socket per unique local storage policy ring port.  Then
        do all the work of drop_privileges except the actual dropping of
        privileges (each forked-off worker will do that post-fork in
        :py:meth:`post_fork_hook`).
        """

        self._reload_bind_ports()
        for port in self.bind_ports:
            self._bind_port(port)

        # The workers strategy drops privileges here, which we obviously cannot
        # do if we want to support binding to low ports.  But we do want some
        # of the actions that drop_privileges did.
        try:
            os.setsid()
        except OSError:
            pass
        # In case you need to rmdir where you started the daemon:
        os.chdir("/")
        # Ensure files are created with the correct privileges:
        os.umask(0o22)

    def no_fork_sock(self):
        """
        This strategy does not support running in the foreground.
        """

        pass

    def new_worker_socks(self):
        """
        Yield a sequence of (socket, server_idx) tuples for each server which
        should be forked-off and started.

        Any sockets for "orphaned" ports no longer in any ring will be closed
        (causing their associated workers to gracefully exit) after all new
        sockets have been yielded.

        The server_idx item for each socket will passed into the
        :py:meth:`log_sock_exit` and :py:meth:`register_worker_start` methods.
        """

        self._reload_bind_ports()
        desired_port_index_pairs = set((p, i) for p in self.bind_ports for i in range(self.servers_per_port))

        current_port_index_pairs = self.port_pid_state.port_index_pairs()

        if desired_port_index_pairs != current_port_index_pairs:
            # Orphan ports are ports which had object-server processes running,
            # but which no longer appear in the ring.  We'll kill them after we
            # start missing workers.
            orphan_port_index_pairs = current_port_index_pairs - desired_port_index_pairs

            # Fork off worker(s) for every port who's supposed to have
            # worker(s) but doesn't
            missing_port_index_pairs = desired_port_index_pairs - current_port_index_pairs
            for port, server_idx in sorted(missing_port_index_pairs):
                if self.port_pid_state.not_tracking(port):
                    try:
                        self._bind_port(port)
                    except Exception as e:
                        self.logger.critical("Unable to bind to port %d: %s", port, e)
                        continue
                yield self.port_pid_state.sock_for_port(port), server_idx

            for orphan_pair in orphan_port_index_pairs:
                # For any port in orphan_port_index_pairs, it is guaranteed
                # that there should be no listen socket for that port, so we
                # can close and forget them.
                self.port_pid_state.forget_port(orphan_pair[0])

    def post_fork_hook(self):
        """
        Called in each child process, prior to starting the actual wsgi server,
        to drop privileges.
        """

        drop_privileges(self.conf.get("user", "swift"), call_setsid=False)

    def log_sock_exit(self, sock, server_idx):
        """
        Log a server's exit.
        """

        port = self.port_pid_state.port_for_sock(sock)
        self.logger.notice("Child %d (PID %d, port %d) exiting normally", server_idx, os.getpid(), port)

    def register_worker_start(self, sock, server_idx, pid):
        """
        Called when a new worker is started.

        :param socket sock: The listen socket for the worker just started.
        :param server_idx: The socket's server_idx as yielded by
                           :py:meth:`new_worker_socks`.
        :param int pid: The new worker process' PID
        """
        port = self.port_pid_state.port_for_sock(sock)
        self.logger.notice("Started child %d (PID %d) for port %d", server_idx, pid, port)
        self.port_pid_state.add_pid(port, server_idx, pid)

    def register_worker_exit(self, pid):
        """
        Called when a worker has exited.

        :param int pid: The PID of the worker that exited.
        """

        self.port_pid_state.forget_pid(pid)

    def shutdown_sockets(self):
        """
        Shutdown any listen sockets.
        """

        for sock in self.port_pid_state.all_socks():
            greenio.shutdown_safe(sock)
            sock.close()
    def test_bind_ports_cache(self):
        test_policies = [StoragePolicy(0, 'aay', True),
                         StoragePolicy(1, 'bee', False),
                         StoragePolicy(2, 'cee', False)]

        my_ips = ['1.2.3.4', '2.3.4.5']
        other_ips = ['3.4.5.6', '4.5.6.7']
        bind_ip = my_ips[1]
        devs_by_ring_name1 = {
            'object': [  # 'aay'
                {'id': 0, 'zone': 0, 'region': 1, 'ip': my_ips[0],
                 'port': 6006},
                {'id': 0, 'zone': 0, 'region': 1, 'ip': other_ips[0],
                 'port': 6007},
                {'id': 0, 'zone': 0, 'region': 1, 'ip': my_ips[1],
                 'port': 6008},
                None,
                {'id': 0, 'zone': 0, 'region': 1, 'ip': other_ips[1],
                 'port': 6009}],
            'object-1': [  # 'bee'
                {'id': 0, 'zone': 0, 'region': 1, 'ip': my_ips[1],
                 'port': 6006},  # dupe
                {'id': 0, 'zone': 0, 'region': 1, 'ip': other_ips[0],
                 'port': 6010},
                {'id': 0, 'zone': 0, 'region': 1, 'ip': my_ips[1],
                 'port': 6011},
                {'id': 0, 'zone': 0, 'region': 1, 'ip': other_ips[1],
                 'port': 6012}],
            'object-2': [  # 'cee'
                {'id': 0, 'zone': 0, 'region': 1, 'ip': my_ips[0],
                 'port': 6010},  # on our IP and a not-us IP
                {'id': 0, 'zone': 0, 'region': 1, 'ip': other_ips[0],
                 'port': 6013},
                None,
                {'id': 0, 'zone': 0, 'region': 1, 'ip': my_ips[1],
                 'port': 6014},
                {'id': 0, 'zone': 0, 'region': 1, 'ip': other_ips[1],
                 'port': 6015}],
        }
        devs_by_ring_name2 = {
            'object': [  # 'aay'
                {'id': 0, 'zone': 0, 'region': 1, 'ip': my_ips[0],
                 'port': 6016},
                {'id': 0, 'zone': 0, 'region': 1, 'ip': other_ips[1],
                 'port': 6019}],
            'object-1': [  # 'bee'
                {'id': 0, 'zone': 0, 'region': 1, 'ip': my_ips[1],
                 'port': 6016},  # dupe
                {'id': 0, 'zone': 0, 'region': 1, 'ip': other_ips[1],
                 'port': 6022}],
            'object-2': [  # 'cee'
                {'id': 0, 'zone': 0, 'region': 1, 'ip': my_ips[0],
                 'port': 6020},
                {'id': 0, 'zone': 0, 'region': 1, 'ip': other_ips[1],
                 'port': 6025}],
        }
        ring_files = [ring_name + '.ring.gz'
                      for ring_name in sorted(devs_by_ring_name1)]

        def _fake_load(gz_path, stub_objs, metadata_only=False):
            return RingData(
                devs=stub_objs[os.path.basename(gz_path)[:-8]],
                replica2part2dev_id=[],
                part_shift=24)

        with mock.patch(
                'swift.common.storage_policy.RingData.load'
        ) as mock_ld, \
                patch_policies(test_policies), \
                mock.patch('swift.common.storage_policy.whataremyips') \
                as mock_whataremyips, \
                temptree(ring_files) as tempdir:
            mock_whataremyips.return_value = my_ips

            cache = BindPortsCache(tempdir, bind_ip)

            self.assertEqual([
                mock.call(bind_ip),
            ], mock_whataremyips.mock_calls)
            mock_whataremyips.reset_mock()

            mock_ld.side_effect = partial(_fake_load,
                                          stub_objs=devs_by_ring_name1)
            self.assertEqual(set([
                6006, 6008, 6011, 6010, 6014,
            ]), cache.all_bind_ports_for_node())
            self.assertEqual([
                mock.call(os.path.join(tempdir, ring_files[0]),
                          metadata_only=True),
                mock.call(os.path.join(tempdir, ring_files[1]),
                          metadata_only=True),
                mock.call(os.path.join(tempdir, ring_files[2]),
                          metadata_only=True),
            ], mock_ld.mock_calls)
            mock_ld.reset_mock()

            mock_ld.side_effect = partial(_fake_load,
                                          stub_objs=devs_by_ring_name2)
            self.assertEqual(set([
                6006, 6008, 6011, 6010, 6014,
            ]), cache.all_bind_ports_for_node())
            self.assertEqual([], mock_ld.mock_calls)

            # but when all the file mtimes are made different, it'll
            # reload
            for gz_file in [os.path.join(tempdir, n)
                            for n in ring_files]:
                os.utime(gz_file, (88, 88))

            self.assertEqual(set([
                6016, 6020,
            ]), cache.all_bind_ports_for_node())
            self.assertEqual([
                mock.call(os.path.join(tempdir, ring_files[0]),
                          metadata_only=True),
                mock.call(os.path.join(tempdir, ring_files[1]),
                          metadata_only=True),
                mock.call(os.path.join(tempdir, ring_files[2]),
                          metadata_only=True),
            ], mock_ld.mock_calls)
            mock_ld.reset_mock()

            # Don't do something stupid like crash if a ring file is missing.
            os.unlink(os.path.join(tempdir, 'object-2.ring.gz'))

            self.assertEqual(set([
                6016, 6020,
            ]), cache.all_bind_ports_for_node())
            self.assertEqual([], mock_ld.mock_calls)

        # whataremyips() is only called in the constructor
        self.assertEqual([], mock_whataremyips.mock_calls)
class ServersPerPortStrategy(object):
    """
    WSGI server management strategy object for an object-server with one listen
    port per unique local port in the storage policy rings.  The
    `servers_per_port` integer config setting determines how many workers are
    run per port.

    Used in :py:func:`run_wsgi`.

    :param dict conf: Server configuration dictionary.
    :param logger: The server's :py:class:`~swift.common.utils.LogAdaptor`
                   object.
    :param int servers_per_port: The number of workers to run per port.
    """
    def __init__(self, conf, logger, servers_per_port):
        self.conf = conf
        self.logger = logger
        self.servers_per_port = servers_per_port
        self.swift_dir = conf.get('swift_dir', '/etc/swift')
        self.ring_check_interval = int(conf.get('ring_check_interval', 15))
        self.port_pid_state = PortPidState(servers_per_port, logger)

        bind_ip = conf.get('bind_ip', '0.0.0.0')
        self.cache = BindPortsCache(self.swift_dir, bind_ip)

    def _reload_bind_ports(self):
        self.bind_ports = self.cache.all_bind_ports_for_node()

    def _bind_port(self, port):
        new_conf = self.conf.copy()
        new_conf['bind_port'] = port
        sock = get_socket(new_conf)
        self.port_pid_state.track_port(port, sock)

    def loop_timeout(self):
        """
        Return timeout before checking for reloaded rings.

        :returns: The time to wait for a child to exit before checking for
                  reloaded rings (new ports).
        """

        return self.ring_check_interval

    def bind_ports(self):
        """
        Bind one listen socket per unique local storage policy ring port.  Then
        do all the work of drop_privileges except the actual dropping of
        privileges (each forked-off worker will do that post-fork in
        :py:meth:`post_fork_hook`).
        """

        self._reload_bind_ports()
        for port in self.bind_ports:
            self._bind_port(port)

        # The workers strategy drops privileges here, which we obviously cannot
        # do if we want to support binding to low ports.  But we do want some
        # of the actions that drop_privileges did.
        try:
            os.setsid()
        except OSError:
            pass
        # In case you need to rmdir where you started the daemon:
        os.chdir('/')
        # Ensure files are created with the correct privileges:
        os.umask(0o22)

    def no_fork_sock(self):
        """
        This strategy does not support running in the foreground.
        """

        pass

    def new_worker_socks(self):
        """
        Yield a sequence of (socket, server_idx) tuples for each server which
        should be forked-off and started.

        Any sockets for "orphaned" ports no longer in any ring will be closed
        (causing their associated workers to gracefully exit) after all new
        sockets have been yielded.

        The server_idx item for each socket will passed into the
        :py:meth:`log_sock_exit` and :py:meth:`register_worker_start` methods.
        """

        self._reload_bind_ports()
        desired_port_index_pairs = set((p, i) for p in self.bind_ports
                                       for i in range(self.servers_per_port))

        current_port_index_pairs = self.port_pid_state.port_index_pairs()

        if desired_port_index_pairs != current_port_index_pairs:
            # Orphan ports are ports which had object-server processes running,
            # but which no longer appear in the ring.  We'll kill them after we
            # start missing workers.
            orphan_port_index_pairs = current_port_index_pairs - \
                desired_port_index_pairs

            # Fork off worker(s) for every port who's supposed to have
            # worker(s) but doesn't
            missing_port_index_pairs = desired_port_index_pairs - \
                current_port_index_pairs
            for port, server_idx in sorted(missing_port_index_pairs):
                if self.port_pid_state.not_tracking(port):
                    try:
                        self._bind_port(port)
                    except Exception as e:
                        self.logger.critical('Unable to bind to port %d: %s',
                                             port, e)
                        continue
                yield self.port_pid_state.sock_for_port(port), server_idx

            for orphan_pair in orphan_port_index_pairs:
                # For any port in orphan_port_index_pairs, it is guaranteed
                # that there should be no listen socket for that port, so we
                # can close and forget them.
                self.port_pid_state.forget_port(orphan_pair[0])

    def post_fork_hook(self):
        """
        Called in each child process, prior to starting the actual wsgi server,
        to drop privileges.
        """

        drop_privileges(self.conf.get('user', 'swift'), call_setsid=False)

    def log_sock_exit(self, sock, server_idx):
        """
        Log a server's exit.
        """

        port = self.port_pid_state.port_for_sock(sock)
        self.logger.notice('Child %d (PID %d, port %d) exiting normally',
                           server_idx, os.getpid(), port)

    def register_worker_start(self, sock, server_idx, pid):
        """
        Called when a new worker is started.

        :param socket sock: The listen socket for the worker just started.
        :param server_idx: The socket's server_idx as yielded by
                           :py:meth:`new_worker_socks`.
        :param int pid: The new worker process' PID
        """
        port = self.port_pid_state.port_for_sock(sock)
        self.logger.notice('Started child %d (PID %d) for port %d', server_idx,
                           pid, port)
        self.port_pid_state.add_pid(port, server_idx, pid)

    def register_worker_exit(self, pid):
        """
        Called when a worker has exited.

        :param int pid: The PID of the worker that exited.
        """

        self.port_pid_state.forget_pid(pid)

    def shutdown_sockets(self):
        """
        Shutdown any listen sockets.
        """

        for sock in self.port_pid_state.all_socks():
            greenio.shutdown_safe(sock)
            sock.close()
    def test_bind_ports_cache(self):
        test_policies = [StoragePolicy(0, "aay", True), StoragePolicy(1, "bee", False), StoragePolicy(2, "cee", False)]

        my_ips = ["1.2.3.4", "2.3.4.5"]
        other_ips = ["3.4.5.6", "4.5.6.7"]
        bind_ip = my_ips[1]
        devs_by_ring_name1 = {
            "object": [  # 'aay'
                {"id": 0, "zone": 0, "region": 1, "ip": my_ips[0], "port": 6006},
                {"id": 0, "zone": 0, "region": 1, "ip": other_ips[0], "port": 6007},
                {"id": 0, "zone": 0, "region": 1, "ip": my_ips[1], "port": 6008},
                None,
                {"id": 0, "zone": 0, "region": 1, "ip": other_ips[1], "port": 6009},
            ],
            "object-1": [  # 'bee'
                {"id": 0, "zone": 0, "region": 1, "ip": my_ips[1], "port": 6006},  # dupe
                {"id": 0, "zone": 0, "region": 1, "ip": other_ips[0], "port": 6010},
                {"id": 0, "zone": 0, "region": 1, "ip": my_ips[1], "port": 6011},
                {"id": 0, "zone": 0, "region": 1, "ip": other_ips[1], "port": 6012},
            ],
            "object-2": [  # 'cee'
                {"id": 0, "zone": 0, "region": 1, "ip": my_ips[0], "port": 6010},  # on our IP and a not-us IP
                {"id": 0, "zone": 0, "region": 1, "ip": other_ips[0], "port": 6013},
                None,
                {"id": 0, "zone": 0, "region": 1, "ip": my_ips[1], "port": 6014},
                {"id": 0, "zone": 0, "region": 1, "ip": other_ips[1], "port": 6015},
            ],
        }
        devs_by_ring_name2 = {
            "object": [  # 'aay'
                {"id": 0, "zone": 0, "region": 1, "ip": my_ips[0], "port": 6016},
                {"id": 0, "zone": 0, "region": 1, "ip": other_ips[1], "port": 6019},
            ],
            "object-1": [  # 'bee'
                {"id": 0, "zone": 0, "region": 1, "ip": my_ips[1], "port": 6016},  # dupe
                {"id": 0, "zone": 0, "region": 1, "ip": other_ips[1], "port": 6022},
            ],
            "object-2": [  # 'cee'
                {"id": 0, "zone": 0, "region": 1, "ip": my_ips[0], "port": 6020},
                {"id": 0, "zone": 0, "region": 1, "ip": other_ips[1], "port": 6025},
            ],
        }
        ring_files = [ring_name + ".ring.gz" for ring_name in sorted(devs_by_ring_name1)]

        def _fake_load(gz_path, stub_objs, metadata_only=False):
            return RingData(devs=stub_objs[os.path.basename(gz_path)[:-8]], replica2part2dev_id=[], part_shift=24)

        with mock.patch("swift.common.storage_policy.RingData.load") as mock_ld, patch_policies(
            test_policies
        ), mock.patch("swift.common.storage_policy.whataremyips") as mock_whataremyips, temptree(ring_files) as tempdir:
            mock_whataremyips.return_value = my_ips

            cache = BindPortsCache(tempdir, bind_ip)

            self.assertEqual([mock.call(bind_ip)], mock_whataremyips.mock_calls)
            mock_whataremyips.reset_mock()

            mock_ld.side_effect = partial(_fake_load, stub_objs=devs_by_ring_name1)
            self.assertEqual(set([6006, 6008, 6011, 6010, 6014]), cache.all_bind_ports_for_node())
            self.assertEqual(
                [
                    mock.call(os.path.join(tempdir, ring_files[0]), metadata_only=True),
                    mock.call(os.path.join(tempdir, ring_files[1]), metadata_only=True),
                    mock.call(os.path.join(tempdir, ring_files[2]), metadata_only=True),
                ],
                mock_ld.mock_calls,
            )
            mock_ld.reset_mock()

            mock_ld.side_effect = partial(_fake_load, stub_objs=devs_by_ring_name2)
            self.assertEqual(set([6006, 6008, 6011, 6010, 6014]), cache.all_bind_ports_for_node())
            self.assertEqual([], mock_ld.mock_calls)

            # but when all the file mtimes are made different, it'll
            # reload
            for gz_file in [os.path.join(tempdir, n) for n in ring_files]:
                os.utime(gz_file, (88, 88))

            self.assertEqual(set([6016, 6020]), cache.all_bind_ports_for_node())
            self.assertEqual(
                [
                    mock.call(os.path.join(tempdir, ring_files[0]), metadata_only=True),
                    mock.call(os.path.join(tempdir, ring_files[1]), metadata_only=True),
                    mock.call(os.path.join(tempdir, ring_files[2]), metadata_only=True),
                ],
                mock_ld.mock_calls,
            )
            mock_ld.reset_mock()

            # Don't do something stupid like crash if a ring file is missing.
            os.unlink(os.path.join(tempdir, "object-2.ring.gz"))

            self.assertEqual(set([6016, 6020]), cache.all_bind_ports_for_node())
            self.assertEqual([], mock_ld.mock_calls)

        # whataremyips() is only called in the constructor
        self.assertEqual([], mock_whataremyips.mock_calls)
Exemple #7
0
    def test_bind_ports_cache(self):
        test_policies = [
            StoragePolicy(0, 'aay', True),
            StoragePolicy(1, 'bee', False),
            StoragePolicy(2, 'cee', False)
        ]

        my_ips = ['1.2.3.4', '2.3.4.5']
        other_ips = ['3.4.5.6', '4.5.6.7']
        bind_ip = my_ips[1]
        devs_by_ring_name1 = {
            'object': [  # 'aay'
                {
                    'id': 0,
                    'zone': 0,
                    'region': 1,
                    'ip': my_ips[0],
                    'port': 6006
                }, {
                    'id': 0,
                    'zone': 0,
                    'region': 1,
                    'ip': other_ips[0],
                    'port': 6007
                }, {
                    'id': 0,
                    'zone': 0,
                    'region': 1,
                    'ip': my_ips[1],
                    'port': 6008
                }, None, {
                    'id': 0,
                    'zone': 0,
                    'region': 1,
                    'ip': other_ips[1],
                    'port': 6009
                }
            ],
            'object-1': [  # 'bee'
                {
                    'id': 0,
                    'zone': 0,
                    'region': 1,
                    'ip': my_ips[1],
                    'port': 6006
                },  # dupe
                {
                    'id': 0,
                    'zone': 0,
                    'region': 1,
                    'ip': other_ips[0],
                    'port': 6010
                },
                {
                    'id': 0,
                    'zone': 0,
                    'region': 1,
                    'ip': my_ips[1],
                    'port': 6011
                },
                {
                    'id': 0,
                    'zone': 0,
                    'region': 1,
                    'ip': other_ips[1],
                    'port': 6012
                }
            ],
            'object-2': [  # 'cee'
                {
                    'id': 0,
                    'zone': 0,
                    'region': 1,
                    'ip': my_ips[0],
                    'port': 6010
                },  # on our IP and a not-us IP
                {
                    'id': 0,
                    'zone': 0,
                    'region': 1,
                    'ip': other_ips[0],
                    'port': 6013
                },
                None,
                {
                    'id': 0,
                    'zone': 0,
                    'region': 1,
                    'ip': my_ips[1],
                    'port': 6014
                },
                {
                    'id': 0,
                    'zone': 0,
                    'region': 1,
                    'ip': other_ips[1],
                    'port': 6015
                }
            ],
        }
        devs_by_ring_name2 = {
            'object': [  # 'aay'
                {
                    'id': 0,
                    'zone': 0,
                    'region': 1,
                    'ip': my_ips[0],
                    'port': 6016
                }, {
                    'id': 0,
                    'zone': 0,
                    'region': 1,
                    'ip': other_ips[1],
                    'port': 6019
                }
            ],
            'object-1': [  # 'bee'
                {
                    'id': 0,
                    'zone': 0,
                    'region': 1,
                    'ip': my_ips[1],
                    'port': 6016
                },  # dupe
                {
                    'id': 0,
                    'zone': 0,
                    'region': 1,
                    'ip': other_ips[1],
                    'port': 6022
                }
            ],
            'object-2': [  # 'cee'
                {
                    'id': 0,
                    'zone': 0,
                    'region': 1,
                    'ip': my_ips[0],
                    'port': 6020
                }, {
                    'id': 0,
                    'zone': 0,
                    'region': 1,
                    'ip': other_ips[1],
                    'port': 6025
                }
            ],
        }
        ring_files = [
            ring_name + '.ring.gz' for ring_name in sorted(devs_by_ring_name1)
        ]

        def _fake_load(gz_path, stub_objs, metadata_only=False):
            return RingData(devs=stub_objs[os.path.basename(gz_path)[:-8]],
                            replica2part2dev_id=[],
                            part_shift=24)

        with mock.patch(
            'swift.common.storage_policy.RingData.load'
        ) as mock_ld, \
                patch_policies(test_policies), \
                mock.patch('swift.common.storage_policy.whataremyips') \
                as mock_whataremyips, \
                temptree(ring_files) as tempdir:
            mock_whataremyips.return_value = my_ips

            cache = BindPortsCache(tempdir, bind_ip)

            self.assertEqual([
                mock.call(bind_ip),
            ], mock_whataremyips.mock_calls)
            mock_whataremyips.reset_mock()

            mock_ld.side_effect = partial(_fake_load,
                                          stub_objs=devs_by_ring_name1)
            self.assertEqual(set([
                6006,
                6008,
                6011,
                6010,
                6014,
            ]), cache.all_bind_ports_for_node())
            self.assertEqual([
                mock.call(os.path.join(tempdir, ring_files[0]),
                          metadata_only=True),
                mock.call(os.path.join(tempdir, ring_files[1]),
                          metadata_only=True),
                mock.call(os.path.join(tempdir, ring_files[2]),
                          metadata_only=True),
            ], mock_ld.mock_calls)
            mock_ld.reset_mock()

            mock_ld.side_effect = partial(_fake_load,
                                          stub_objs=devs_by_ring_name2)
            self.assertEqual(set([
                6006,
                6008,
                6011,
                6010,
                6014,
            ]), cache.all_bind_ports_for_node())
            self.assertEqual([], mock_ld.mock_calls)

            # but when all the file mtimes are made different, it'll
            # reload
            for gz_file in [os.path.join(tempdir, n) for n in ring_files]:
                os.utime(gz_file, (88, 88))

            self.assertEqual(set([
                6016,
                6020,
            ]), cache.all_bind_ports_for_node())
            self.assertEqual([
                mock.call(os.path.join(tempdir, ring_files[0]),
                          metadata_only=True),
                mock.call(os.path.join(tempdir, ring_files[1]),
                          metadata_only=True),
                mock.call(os.path.join(tempdir, ring_files[2]),
                          metadata_only=True),
            ], mock_ld.mock_calls)
            mock_ld.reset_mock()

            # Don't do something stupid like crash if a ring file is missing.
            os.unlink(os.path.join(tempdir, 'object-2.ring.gz'))

            self.assertEqual(set([
                6016,
                6020,
            ]), cache.all_bind_ports_for_node())
            self.assertEqual([], mock_ld.mock_calls)

        # whataremyips() is only called in the constructor
        self.assertEqual([], mock_whataremyips.mock_calls)