docs: 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