docs: def isScheduleValid(self, schedule, node_srvs, force) -> (bool, str):
          """
          Validates schedule of planned node upgrades
  
          :param schedule: dictionary of node ids and upgrade times
          :param node_srvs: dictionary of node ids and services
          :return: a 2-tuple of whether schedule valid or not and the reason
          """
  
          # flag "force=True" ignore basic checks! only datetime format is
          # checked
          times = []
          non_demoted_nodes = set([k for k, v in node_srvs.items() if v])
          if not force and set(schedule.keys()) != non_demoted_nodes:
              return False, 'Schedule should contain id of all nodes'
          now = datetime.utcnow().replace(tzinfo=dateutil.tz.tzutc())
          for dateStr in schedule.values():
              try:
                  when = dateutil.parser.parse(dateStr)
                  if when <= now and not force:
                      return False, '{} is less than current time'.format(when)
                  times.append(when)
              except ValueError:
                  return False, '{} cannot be parsed to a time'.format(dateStr)
          if force:
              return True, ''
          times = sorted(times)
          for i in range(len(times) - 1):
              diff = (times[i + 1] - times[i]).total_seconds()
              if diff < self.config.MinSepBetweenNodeUpgrades:
                  return False, 'time span between upgrades is {} ' \
                                'seconds which is less than specified ' \
                                'in the config'.format(diff)
          return True, ''