def eval_master(self,
                    opts,
                    timeout=60,
                    safe=True,
                    failed=False,
                    failback=False):
        '''
        Evaluates and returns a tuple of the current master address and the pub_channel.

        In standard mode, just creates a pub_channel with the given master address.

        With master_type=func evaluates the current master address from the given
        module and then creates a pub_channel.

        With master_type=failover takes the list of masters and loops through them.
        The first one that allows the minion to create a pub_channel is then
        returned. If this function is called outside the minions initialization
        phase (for example from the minions main event-loop when a master connection
        loss was detected), 'failed' should be set to True. The current
        (possibly failed) master will then be removed from the list of masters.
        '''
        # return early if we are not connecting to a master
        if opts['master_type'] == 'disable':
            log.warning('Master is set to disable, skipping connection')
            self.connected = False
            raise tornado.gen.Return((None, None))

        # Run masters discovery over SSDP. This may modify the whole configuration,
        # depending of the networking and sets of masters.
        # if we are using multimaster, discovery can only happen at start time
        # because MinionManager handles it. by eval_master time the minion doesn't
        # know about other siblings currently running
        if isinstance(self.opts['discovery'], dict) and not self.opts['discovery'].get('multimaster'):
            self._discover_masters()

        # check if master_type was altered from its default
        if opts['master_type'] != 'str' and opts['__role'] != 'syndic':
            # check for a valid keyword
            if opts['master_type'] == 'func':
                eval_master_func(opts)

            # if failover or distributed is set, master has to be of type list
            elif opts['master_type'] in ('failover', 'distributed'):
                if isinstance(opts['master'], list):
                    log.info(
                        'Got list of available master addresses: %s',
                        opts['master']
                    )

                    if opts['master_type'] == 'distributed':
                        master_len = len(opts['master'])
                        if master_len > 1:
                            secondary_masters = opts['master'][1:]
                            master_idx = crc32(opts['id']) % master_len
                            try:
                                preferred_masters = opts['master']
                                preferred_masters[0] = opts['master'][master_idx]
                                preferred_masters[1:] = [m for m in opts['master'] if m != preferred_masters[0]]
                                opts['master'] = preferred_masters
                                log.info('Distributed to the master at \'%s\'.', opts['master'][0])
                            except (KeyError, AttributeError, TypeError):
                                log.warning('Failed to distribute to a specific master.')
                        else:
                            log.warning('master_type = distributed needs more than 1 master.')

                    if opts['master_shuffle']:
                        log.warning(
                            'Use of \'master_shuffle\' detected. \'master_shuffle\' is deprecated in favor '
                            'of \'random_master\'. Please update your minion config file.'
                        )
                        opts['random_master'] = opts['master_shuffle']

                    opts['auth_tries'] = 0
                    if opts['master_failback'] and opts['master_failback_interval'] == 0:
                        opts['master_failback_interval'] = opts['master_alive_interval']
                # if opts['master'] is a str and we have never created opts['master_list']
                elif isinstance(opts['master'], six.string_types) and ('master_list' not in opts):
                    # We have a string, but a list was what was intended. Convert.
                    # See issue 23611 for details
                    opts['master'] = [opts['master']]
                elif opts['__role'] == 'syndic':
                    log.info('Syndic setting master_syndic to \'%s\'', opts['master'])

                # if failed=True, the minion was previously connected
                # we're probably called from the minions main-event-loop
                # because a master connection loss was detected. remove
                # the possibly failed master from the list of masters.
                elif failed:
                    if failback:
                        # failback list of masters to original config
                        opts['master'] = opts['master_list']
                    else:
                        log.info(
                            'Moving possibly failed master %s to the end of '
                            'the list of masters', opts['master']
                        )
                        if opts['master'] in opts['local_masters']:
                            # create new list of master with the possibly failed
                            # one moved to the end
                            failed_master = opts['master']
                            opts['master'] = [x for x in opts['local_masters'] if opts['master'] != x]
                            opts['master'].append(failed_master)
                        else:
                            opts['master'] = opts['master_list']
                else:
                    msg = ('master_type set to \'failover\' but \'master\' '
                           'is not of type list but of type '
                           '{0}'.format(type(opts['master'])))
                    log.error(msg)
                    sys.exit(salt.defaults.exitcodes.EX_GENERIC)
                # If failover is set, minion have to failover on DNS errors instead of retry DNS resolve.
                # See issue 21082 for details
                if opts['retry_dns'] and opts['master_type'] == 'failover':
                    msg = ('\'master_type\' set to \'failover\' but \'retry_dns\' is not 0. '
                           'Setting \'retry_dns\' to 0 to failover to the next master on DNS errors.')
                    log.critical(msg)
                    opts['retry_dns'] = 0
            else:
                msg = ('Invalid keyword \'{0}\' for variable '
                       '\'master_type\''.format(opts['master_type']))
                log.error(msg)
                sys.exit(salt.defaults.exitcodes.EX_GENERIC)

        # FIXME: if SMinion don't define io_loop, it can't switch master see #29088
        # Specify kwargs for the channel factory so that SMinion doesn't need to define an io_loop
        # (The channel factories will set a default if the kwarg isn't passed)
        factory_kwargs = {'timeout': timeout, 'safe': safe}
        if getattr(self, 'io_loop', None):
            factory_kwargs['io_loop'] = self.io_loop  # pylint: disable=no-member

        tries = opts.get('master_tries', 1)
        attempts = 0

        # if we have a list of masters, loop through them and be
        # happy with the first one that allows us to connect
        if isinstance(opts['master'], list):
            conn = False
            last_exc = None
            opts['master_uri_list'] = []
            opts['local_masters'] = copy.copy(opts['master'])

            # shuffle the masters and then loop through them
            if opts['random_master']:
                # master_failback is only used when master_type is set to failover
                if opts['master_type'] == 'failover' and opts['master_failback']:
                    secondary_masters = opts['local_masters'][1:]
                    shuffle(secondary_masters)
                    opts['local_masters'][1:] = secondary_masters
                else:
                    shuffle(opts['local_masters'])

            # This sits outside of the connection loop below because it needs to set
            # up a list of master URIs regardless of which masters are available
            # to connect _to_. This is primarily used for masterless mode, when
            # we need a list of master URIs to fire calls back to.
            for master in opts['local_masters']:
                opts['master'] = master
                opts.update(prep_ip_port(opts))
                opts['master_uri_list'].append(resolve_dns(opts)['master_uri'])

            while True:
                if attempts != 0:
                    # Give up a little time between connection attempts
                    # to allow the IOLoop to run any other scheduled tasks.
                    yield tornado.gen.sleep(opts['acceptance_wait_time'])
                attempts += 1
                if tries > 0:
                    log.debug(
                        'Connecting to master. Attempt %s of %s',
                        attempts, tries
                    )
                else:
                    log.debug(
                        'Connecting to master. Attempt %s (infinite attempts)',
                        attempts
                    )
                for master in opts['local_masters']:
                    opts['master'] = master
                    opts.update(prep_ip_port(opts))
                    opts.update(resolve_dns(opts))

                    # on first run, update self.opts with the whole master list
                    # to enable a minion to re-use old masters if they get fixed
                    if 'master_list' not in opts:
                        opts['master_list'] = copy.copy(opts['local_masters'])

                    self.opts = opts

                    pub_channel = salt.transport.client.AsyncPubChannel.factory(opts, **factory_kwargs)
                    try:
                        yield pub_channel.connect()
                        conn = True
                        break
                    except SaltClientError as exc:
                        last_exc = exc
                        if exc.strerror.startswith('Could not access'):
                            msg = (
                                'Failed to initiate connection with Master '
                                '%s: check ownership/permissions. Error '
                                'message: %s', opts['master'], exc
                            )
                        else:
                            msg = ('Master %s could not be reached, trying next '
                                   'next master (if any)', opts['master'])
                        log.info(msg)
                        continue

                if not conn:
                    if attempts == tries:
                        # Exhausted all attempts. Return exception.
                        self.connected = False
                        self.opts['master'] = copy.copy(self.opts['local_masters'])
                        log.error(
                            'No master could be reached or all masters '
                            'denied the minion\'s connection attempt.'
                        )
                        # If the code reaches this point, 'last_exc'
                        # should already be set.
                        raise last_exc  # pylint: disable=E0702
                else:
                    self.tok = pub_channel.auth.gen_token(b'salt')
                    self.connected = True
                    raise tornado.gen.Return((opts['master'], pub_channel))

        # single master sign in
        else:
            if opts['random_master']:
                log.warning('random_master is True but there is only one master specified. Ignoring.')
            while True:
                if attempts != 0:
                    # Give up a little time between connection attempts
                    # to allow the IOLoop to run any other scheduled tasks.
                    yield tornado.gen.sleep(opts['acceptance_wait_time'])
                attempts += 1
                if tries > 0:
                    log.debug(
                        'Connecting to master. Attempt %s of %s',
                        attempts, tries
                    )
                else:
                    log.debug(
                        'Connecting to master. Attempt %s (infinite attempts)',
                        attempts
                    )
                opts.update(prep_ip_port(opts))
                opts.update(resolve_dns(opts))
                try:
                    if self.opts['transport'] == 'detect':
                        self.opts['detect_mode'] = True
                        for trans in ('zeromq', 'tcp'):
                            if trans == 'zeromq' and not zmq:
                                continue
                            self.opts['transport'] = trans
                            pub_channel = salt.transport.client.AsyncPubChannel.factory(self.opts, **factory_kwargs)
                            yield pub_channel.connect()
                            if not pub_channel.auth.authenticated:
                                continue
                            del self.opts['detect_mode']
                            break
                    else:
                        pub_channel = salt.transport.client.AsyncPubChannel.factory(self.opts, **factory_kwargs)
                        yield pub_channel.connect()
                    self.tok = pub_channel.auth.gen_token(b'salt')
                    self.connected = True
                    raise tornado.gen.Return((opts['master'], pub_channel))
                except SaltClientError as exc:
                    if attempts == tries:
                        # Exhausted all attempts. Return exception.
                        self.connected = False
                        raise exc