Exemple #1
0
def fail_if_platform_and_host_conflict(task_conf, task_name):
    """Raise an error if task spec contains platform and forbidden host items.

    Args:
        task_conf(dict, OrderedDictWithDefaults):
            A specification to be checked.
        task_name(string):
            A name to be given in an error.

    Raises:
        PlatformLookupError - if platform and host items conflict

    """
    if 'platform' in task_conf and task_conf['platform']:
        fail_items = ''
        for section, key, _ in FORBIDDEN_WITH_PLATFORM:
            if (section in task_conf and key in task_conf[section]
                    and task_conf[section][key] is not None):
                fail_items += (
                    f' * platform = {task_conf["platform"]} AND'
                    f' [{section}]{key} = {task_conf[section][key]}\n')
        if fail_items != '':
            raise PlatformLookupError(
                f"A mixture of Cylc 7 (host) and Cylc 8 (platform) "
                f"logic should not be used. In this case the task "
                f"\"{task_name}\" has the following settings which "
                f"are not compatible:\n{fail_items}")
Exemple #2
0
def remote_clean(reg, platform_names, timeout):
    """Run subprocesses to clean workflows on remote install targets
    (skip localhost), given a set of platform names to look up.

    Args:
        reg (str): Workflow name.
        platform_names (list): List of platform names to look up in the global
            config, in order to determine the install targets to clean on.
        timeout (str): Number of seconds to wait before cancelling.
    """
    try:
        install_targets_map = (
            get_install_target_to_platforms_map(platform_names))
    except PlatformLookupError as exc:
        raise PlatformLookupError(
            "Cannot clean on remote platforms as the workflow database is "
            f"out of date/inconsistent with the global config - {exc}")

    pool = []
    for target, platforms in install_targets_map.items():
        if target == get_localhost_install_target():
            continue
        shuffle(platforms)
        LOG.info(
            f"Cleaning on install target: {platforms[0]['install target']}")
        # Issue ssh command:
        pool.append(
            (_remote_clean_cmd(reg, platforms[0], timeout), target, platforms)
        )
    failed_targets = []
    # Handle subproc pool results almost concurrently:
    while pool:
        for proc, target, platforms in pool:
            ret_code = proc.poll()
            if ret_code is None:  # proc still running
                continue
            pool.remove((proc, target, platforms))
            out, err = (f.decode() for f in proc.communicate())
            if out:
                LOG.debug(out)
            if ret_code:
                # Try again using the next platform for this install target:
                this_platform = platforms.pop(0)
                excn = TaskRemoteMgmtError(
                    TaskRemoteMgmtError.MSG_TIDY, this_platform['name'],
                    " ".join(proc.args), ret_code, out, err)
                LOG.debug(excn)
                if platforms:
                    pool.append(
                        (_remote_clean_cmd(reg, platforms[0], timeout),
                         target, platforms)
                    )
                else:  # Exhausted list of platforms
                    failed_targets.append(target)
            elif err:
                LOG.debug(err)
        time.sleep(0.2)
    if failed_targets:
        raise CylcError(
            f"Could not clean on install targets: {', '.join(failed_targets)}")
Exemple #3
0
def get_random_platform_for_install_target(
        install_target: str) -> Dict[str, Any]:
    """Return a randomly selected platform (dict) for given install target."""
    platforms = get_all_platforms_for_install_target(install_target)
    try:
        return random.choice(platforms)  # nosec (not crypto related)
    except IndexError:
        # No platforms to choose from
        raise PlatformLookupError(
            f'Could not select platform for install target: {install_target}')
Exemple #4
0
def is_platform_definition_subshell(value: str) -> bool:
    """Is the platform definition using subshell? E.g. platform = $(foo)

    Raise PlatformLookupError if using backticks.
    """
    if PLATFORM_REC_COMMAND.match(value):
        return True
    if HOST_REC_COMMAND.match(value):
        raise PlatformLookupError(
            f"platform = {value}: backticks are not supported; please use $()")
    return False
Exemple #5
0
def forward_lookup(platforms, job_platform):
    """
    Find out which job platform to use given a list of possible platforms and
    a task platform string.

    Verifies selected platform is present in global.rc file and returns it,
    raises error if platfrom is not in global.rc or returns 'localhost' if
    no platform is initally selected.

    Args:
        job_platform (str):
            platform item from config [runtime][TASK]platform
        platforms (dictionary):
            list of possible platforms defined by global.rc

    Returns:
        platform (str):
            string representing a platform from the global config.

    Example:
    >>> platforms = {
    ...     'suite server platform': None,
    ...     'desktop[0-9][0-9]|laptop[0-9][0-9]': None,
    ...     'sugar': {
    ...         'remote hosts': 'localhost',
    ...         'batch system': 'slurm'
    ...     },
    ...     'hpc': {
    ...         'remote hosts': ['hpc1', 'hpc2'],
    ...         'batch system': 'pbs'
    ...     },
    ...     'hpc1-bg': {
    ...         'remote hosts': 'hpc1',
    ...         'batch system': 'background'
    ...     },
    ...     'hpc2-bg': {
    ...         'remote hosts': 'hpc2',
    ...         'batch system': 'background'
    ...     }
    ... }
    >>> job_platform = 'desktop22'
    >>> forward_lookup(platforms, job_platform)
    'desktop22'
    """
    if job_platform is None:
        return 'localhost'
    for platform in reversed(list(platforms)):
        if re.fullmatch(platform, job_platform):
            return job_platform

    raise PlatformLookupError(
        f"No matching platform \"{job_platform}\" found")
def get_platform_from_task_def(flow: str, task: str) -> Dict[str, Any]:
    """Return the platform dictionary for a particular task.

    Uses the flow definition - designed to be used with tasks
    with unsubmitted jobs.

    Args:
        flow: The name of the Cylc flow to be queried.
        task: The name of the task to be queried.

    Returns:
        Platform Dictionary.
    """
    _, _, flow_file = parse_id(flow, constraint='workflows', src=True)
    config = WorkflowConfig(flow, flow_file, Values())
    # Get entire task spec to allow Cylc 7 platform from host guessing.
    task_spec = config.pcfg.get(['runtime', task])
    platform = get_platform(task_spec)
    if platform is None:
        raise PlatformLookupError(
            'Platform lookup failed; platform is a subshell to be evaluated: '
            f' Task: {task}, platform: {task_spec["platform"]}.')
    return platform
Exemple #7
0
def platform_from_job_info(platforms: Dict[str, Any], job: Dict[str, Any],
                           remote: Dict[str, Any]) -> str:
    """
    Find out which job platform to use given a list of possible platforms
    and the task dictionary with cylc 7 definitions in it.

    (Note: "batch system" (Cylc 7) and "job runner" (Cylc 8)
    mean the same thing)

          +------------+ Yes    +-----------------------+
    +-----> Tried all  +------->+ RAISE                 |
    |     | platforms? |        | PlatformNotFoundError |
    |     +------------+        +-----------------------+
    |              No|
    |     +----------v---+
    |     | Examine next |
    |     | platform     |
    |     +--------------+
    |                |
    |     +----------v----------------+
    |     | Do all items other than   |
    +<----+ "host" and "batch system" |
    |   No| match for this plaform    |
    |     +---------------------------+
    |                           |Yes
    |                +----------v----------------+ No
    |                | Task host is 'localhost'? +--+
    |                +---------------------------+  |
    |                           |Yes                |
    |              No+----------v----------------+  |
    |            +---+ Task batch system is      |  |
    |            |   | 'background'?             |  |
    |            |   +---------------------------+  |
    |            |              |Yes                |
    |            |   +----------v----------------+  |
    |            |   | RETURN 'localhost'        |  |
    |            |   +---------------------------+  |
    |            |                                  |
    |    +-------v-------------+     +--------------v-------+
    |  No| batch systems match |  Yes| batch system and     |
    +<---+ and 'localhost' in  |  +--+ host both match      |
    |    | platform hosts?     |  |  +----------------------+
         +---------------------+  |                 |No
    |            |Yes             |  +--------------v-------+
    |    +-------v--------------+ |  | batch system match   |
    |    | RETURN this platform <-+--+ and regex of platform|
    |    +----------------------+ Yes| name matches host    |
    |                                +----------------------+
    |                                  |No
    +<---------------------------------+

    Args:
        job: Workflow config [runtime][TASK][job] section.
        remote: Workflow config [runtime][TASK][remote] section.
        platforms: Dictionary containing platform definitions.

    Returns:
        platform: string representing a platform from the global config.

    Raises:
        PlatformLookupError:
            If no matching platform can be a found an error is raised.

    Example:
        >>> platforms = {
        ...         'desktop[0-9][0-9]|laptop[0-9][0-9]': {},
        ...         'sugar': {
        ...             'hosts': 'localhost',
        ...             'job runner': 'slurm'
        ...         }
        ... }
        >>> job = {'batch system': 'slurm'}
        >>> remote = {'host': 'localhost'}
        >>> platform_from_job_info(platforms, job, remote)
        'sugar'
        >>> remote = {}
        >>> platform_from_job_info(platforms, job, remote)
        'sugar'
        >>> remote ={'host': 'desktop92'}
        >>> job = {}
        >>> platform_from_job_info(platforms, job, remote)
        'desktop92'
    """

    # These settings are removed from the incoming dictionaries for special
    # handling later - we want more than a simple match:
    #   - In the case of "host" we also want a regex match to the platform name
    #   - In the case of "batch system" we want to match the name of the
    #     system/job runner to a platform when host is localhost.
    if 'host' in remote and remote['host']:
        task_host = remote['host']
    else:
        task_host = 'localhost'
    if 'batch system' in job and job['batch system']:
        task_job_runner = job['batch system']
    else:
        # Necessary? Perhaps not if batch system default is 'background'
        task_job_runner = 'background'
    # Riffle through the platforms looking for a match to our task settings.
    # reverse dict order so that user config platforms added last are examined
    # before site config platforms.
    for platform_name, platform_spec in reversed(list(platforms.items())):
        # Handle all the items requiring an exact match.
        # All items other than batch system and host must be an exact match
        if not generic_items_match(platform_spec, job, remote):
            continue
        # We have some special logic to identify whether task host and task
        # batch system match the platform in question.
        if (not is_remote_host(task_host) and task_job_runner == 'background'):
            return 'localhost'

        elif ('hosts' in platform_spec and task_host in platform_spec['hosts']
              and task_job_runner == platform_spec['job runner']):
            # If we have localhost with a non-background batch system we
            # use the batch system to give a sensible guess at the platform
            return platform_name

        elif (re.fullmatch(platform_name, task_host)
              and ((task_job_runner == 'background'
                    and 'job runner' not in platform_spec)
                   or task_job_runner == platform_spec['job runner'])):
            return task_host

    raise PlatformLookupError('No platform found matching your task')
Exemple #8
0
def platform_from_name(platform_name: Optional[str] = None,
                       platforms: Optional[Dict[str, Dict[str, Any]]] = None,
                       bad_hosts: Optional[Set[str]] = None) -> Dict[str, Any]:
    """
    Find out which job platform to use given a list of possible platforms and
    a task platform string.

    Verifies selected platform is present in global.cylc file and returns it,
    raises error if platform is not in global.cylc or returns 'localhost' if
    no platform is initially selected.

    Args:
        platform_name: name of platform to be retrieved.
        platforms: global.cylc platforms given as a dict.

    Returns:
        platform: object containing settings for a platform, loaded from
            Global Config.
    """
    if platforms is None:
        platforms = glbl_cfg().get(['platforms'])
    platform_groups = glbl_cfg().get(['platform groups'])

    if platform_name is None:
        platform_name = 'localhost'

    platform_group = None
    for platform_name_re in reversed(list(platform_groups)):
        # Platform is member of a group.
        if re.fullmatch(platform_name_re, platform_name):
            platform_name = get_platform_from_group(
                platform_groups[platform_name_re],
                group_name=platform_name,
                bad_hosts=bad_hosts)

    # The list is reversed to allow user-set platforms (which are loaded
    # later than site set platforms) to be matched first and override site
    # defined platforms.
    for platform_name_re in reversed(list(platforms)):
        # We substitue commas with or without spaces to
        # allow lists of platforms
        if (re.fullmatch(
                re.sub(r'\s*(?!{[\s\d]*),(?![\s\d]*})\s*', '|',
                       platform_name_re), platform_name)):
            # Deepcopy prevents contaminating platforms with data
            # from other platforms matching platform_name_re
            platform_data = deepcopy(platforms[platform_name_re])

            # If hosts are not filled in make remote
            # hosts the platform name.
            # Example: `[platforms][workplace_vm_123]<nothing>`
            #   should create a platform where
            #   `remote_hosts = ['workplace_vm_123']`
            if ('hosts' not in platform_data.keys()
                    or not platform_data['hosts']):
                platform_data['hosts'] = [platform_name]
            # Fill in the "private" name field.
            platform_data['name'] = platform_name
            if platform_group:
                platform_data['group'] = platform_group
            return platform_data

    raise PlatformLookupError(
        f"No matching platform \"{platform_name}\" found")
Exemple #9
0
def get_platform(task_conf=None, task_id='unknown task', warn_only=False):
    """Get a platform.

    Looking at a task config this method decides whether to get platform from
    name, or Cylc7 config items.

    Args:
        task_conf (str, dict or dict-like such as OrderedDictWithDefaults):
            If str this is assumed to be the platform name, otherwise this
            should be a configuration for a task.
        task_id (str):
            Task identification string - help produce more helpful error
            messages.
        warn_only(bool):
            If true, warnings about tasks requiring upgrade will be returned.

    Returns:
        platform (platform, or string):
            Actually it returns either get_platform() or
            platform_from_job_info(), but to the user these look the same.
            When working in warn_only mode, warnings are returned as strings.

    TODO:
        At Cylc 9 remove all Cylc7 upgrade logic.
    """

    if task_conf is None:
        # Just a simple way of accessing localhost items.
        output = platform_from_name()

    elif isinstance(task_conf, str):
        # If task_conf is str assume that it is a platform name.
        output = platform_from_name(task_conf)

    elif 'platform' in task_conf and task_conf['platform']:
        if PLATFORM_REC_COMMAND.match(task_conf['platform']) and warn_only:
            # In warning mode this function might have been passed an
            # un-expanded platform string - warn that they won't deal with
            # with this until job submit.
            return None
        if HOST_REC_COMMAND.match(task_conf['platform']) and warn_only:
            raise PlatformLookupError(f"platform = {task_conf['platform']}: "
                                      "backticks are not supported; "
                                      "please use $()")

        # Check whether task has conflicting Cylc7 items.
        fail_if_platform_and_host_conflict(task_conf, task_id)

        # If platform name exists and doesn't clash with Cylc7 Config items.
        output = platform_from_name(task_conf['platform'])

    else:
        # If forbidden items present calculate platform else platform is
        # local
        platform_is_localhost = True

        warn_msgs = []
        for section, key, exceptions in FORBIDDEN_WITH_PLATFORM:
            # if section not in task_conf:
            #     task_conf[section] = {}
            if (section in task_conf and key in task_conf[section]
                    and task_conf[section][key] not in exceptions):
                platform_is_localhost = False
                if warn_only:
                    warn_msgs.append(f"[runtime][{task_id}][{section}]{key} = "
                                     f"{task_conf[section][key]}\n")

        if platform_is_localhost:
            output = platform_from_name()

        elif warn_only:
            output = (
                f'Task {task_id} Deprecated "host" and "batch system" will be '
                'removed at Cylc 9 - upgrade to platform:'
                f'\n{"".join(warn_msgs)}')

        else:
            task_job_section, task_remote_section = {}, {}
            if 'job' in task_conf:
                task_job_section = task_conf['job']
            if 'remote' in task_conf:
                task_remote_section = task_conf['remote']
            output = platform_from_name(
                platform_from_job_info(
                    glbl_cfg(cached=False).get(['platforms']),
                    task_job_section, task_remote_section))

    return output
Exemple #10
0
def platform_from_name(platform_name=None, platforms=None):
    """
    Find out which job platform to use given a list of possible platforms and
    a task platform string.

    Verifies selected platform is present in global.cylc file and returns it,
    raises error if platfrom is not in global.cylc or returns 'localhost' if
    no platform is initally selected.

    Args:
        platform_name (str):
            name of platform to be retrieved.
        platforms ():
            global.cylc platforms given as a dict for logic testing purposes

    Returns:
        platform (dict):
            object containing settings for a platform, loaded from
            Global Config.
    """
    if platforms is None:
        platforms = glbl_cfg().get(['platforms'])
    platform_groups = glbl_cfg().get(['platform groups'])

    if platform_name is None:
        platform_data = deepcopy(platforms['localhost'])
        platform_data['name'] = 'localhost'
        return platform_data

    platform_group = None
    for platform_name_re in reversed(list(platform_groups)):
        if re.fullmatch(platform_name_re, platform_name):
            platform_group = deepcopy(platform_name)
            platform_name = random.choice(
                platform_groups[platform_name_re]['platforms'])

    # The list is reversed to allow user-set platforms (which are loaded
    # later than site set platforms) to be matched first and override site
    # defined platforms.
    for platform_name_re in reversed(list(platforms)):
        if re.fullmatch(platform_name_re, platform_name):
            # Deepcopy prevents contaminating platforms with data
            # from other platforms matching platform_name_re
            platform_data = deepcopy(platforms[platform_name_re])

            # If hosts are not filled in make remote
            # hosts the platform name.
            # Example: `[platforms][workplace_vm_123]<nothing>`
            #   should create a platform where
            #   `remote_hosts = ['workplace_vm_123']`
            if ('hosts' not in platform_data.keys()
                    or not platform_data['hosts']):
                platform_data['hosts'] = [platform_name]
            # Fill in the "private" name field.
            platform_data['name'] = platform_name
            if platform_group:
                platform_data['group'] = platform_group
            return platform_data

    raise PlatformLookupError(
        f"No matching platform \"{platform_name}\" found")
Exemple #11
0
def host_to_platform_upgrader(cfg):
    """Upgrade a config with host settings to a config with platform settings
    if it is appropriate to do so.

                       +-------------------------------+
                       | Is platform set in this       |
                       | [runtime][TASK]?              |
                       +-------------------------------+
                          |YES                      |NO
                          |                         |
    +---------------------v---------+      +--------+--------------+
    | Are any forbidden items set   |      | host == $(function)?  |
    | in any [runtime][TASK]        |      +-+---------------------+
    | [job] or [remote] section     |     NO |          |YES
    |                               |        |  +-------v------------------+
    +-------------------------------+        |  | Log - evaluate at task   |
              |YES            |NO            |  | submit                   |
              |               +-------+      |  |                          |
              |                       |      |  +--------------------------+
    +---------v---------------------+ |      |
    | Fail Loudly                   | |    +-v-----------------------------+
    +-------------------------------+ |    | * Run reverse_lookup()        |
                                      |    | * handle reverse lookup fail  |
                                      |    | * add platform                |
                                      |    | * delete forbidden settings   |
                                      |    +-------------------------------+
                                      |
                                      |    +-------------------------------+
                                      +----> Return without changes        |
                                           +-------------------------------+

    Args (cfg):
        config object to be upgraded

    Returns (cfg):
        upgraded config object
    """
    # If platform and old settings are set fail
    # and remote should be added to this forbidden list
    forbidden_with_platform = {
        'host', 'batch system', 'batch submit command template'
    }

    for task_name, task_spec in cfg['runtime'].items():
        # if task_name == 'delta':
        #     breakpoint(header=f"task_name = {task_name}")

        if ('platform' in task_spec and 'job' in task_spec
                or 'platform' in task_spec and 'remote' in task_spec):
            if ('platform' in task_spec and forbidden_with_platform
                    & {*task_spec['job'], *task_spec['remote']}):
                # Fail Loudly and Horribly
                raise PlatformLookupError(
                    f"A mixture of Cylc 7 (host) and Cylc 8 (platform logic)"
                    f" should not be used. Task {task_name} set platform "
                    f"and item in {forbidden_with_platform}")

        elif 'platform' in task_spec:
            # Return config unchanged
            continue

        else:
            # Add empty dicts if appropriate sections not present.
            if 'job' in task_spec:
                task_spec_job = task_spec['job']
            else:
                task_spec_job = {}
            if 'remote' in task_spec:
                task_spec_remote = task_spec['remote']
            else:
                task_spec_remote = {}

            # Deal with case where host is a function and we cannot auto
            # upgrade at the time of loading the config.
            if ('host' in task_spec_remote
                    and REC_COMMAND.match(task_spec['remote']['host'])):
                LOG.debug(
                    f"The host setting of '{task_name}' is a function: "
                    f"Cylc will try to upgrade this task on job submission.")
                continue

            # Attempt to use the reverse lookup
            try:
                platform = reverse_lookup(
                    glbl_cfg(cached=False).get(['job platforms']),
                    task_spec_job, task_spec_remote)
            except PlatformLookupError as exc:
                raise PlatformLookupError(f"for task {task_name}: {exc}")
            else:
                # Set platform in config
                cfg['runtime'][task_name].update({'platform': platform})
                LOG.warning(f"Platform {platform} auto selected from ")
                # Remove deprecated items from config
                for old_spec_item in forbidden_with_platform:
                    for task_section in ['job', 'remote']:
                        if (task_section in cfg['runtime'][task_name]
                                and old_spec_item in cfg['runtime'][task_name]
                            [task_section].keys()):
                            poppable = cfg['runtime'][task_name][task_section]
                            poppable.pop(old_spec_item)
                    LOG.warning(f"Cylc 7 {old_spec_item} removed.")
    return cfg
Exemple #12
0
def reverse_lookup(platforms, job, remote):
    """
    Find out which job platform to use given a list of possible platforms
    and the task dictionary with cylc 7 definitions in it.

          +------------+ Yes    +-----------------------+
    +-----> Tried all  +------->+ RAISE                 |
    |     | platforms? |        | PlatformNotFoundError |
    |     +------------+        +-----------------------+
    |              No|
    |     +----------v---+
    |     | Examine next |
    |     | platform     |
    |     +--------------+
    |                |
    |     +----------v----------------+
    |     | Do all items other than   |
    +<----+ "host" and "batch system" |
    |   No| match for this plaform    |
    |     +---------------------------+
    |                           |Yes
    |                +----------v----------------+ No
    |                | Task host is 'localhost'? +--+
    |                +---------------------------+  |
    |                           |Yes                |
    |              No+----------v----------------+  |
    |            +---+ Task batch system is      |  |
    |            |   | 'background'?             |  |
    |            |   +---------------------------+  |
    |            |              |Yes                |
    |            |   +----------v----------------+  |
    |            |   | RETURN 'localhost'        |  |
    |            |   +---------------------------+  |
    |            |                                  |
    |    +-------v-------------+     +--------------v-------+
    |  No| batch systems match |  Yes| batch system and     |
    +<---+ and 'localhost' in  |  +--+ host both match      |
    |    | platform hosts?     |  |  +----------------------+
         +---------------------+  |                 |No
    |            |Yes             |  +--------------v-------+
    |    +-------v--------------+ |  | batch system match   |
    |    | RETURN this platform <-+--+ and regex of platform|
    |    +----------------------+ Yes| name matches host    |
    |                                +----------------------+
    |                                  |No
    +<---------------------------------+

    Args:
        job (dict):
            Suite config [runtime][TASK][job] section
        remote (dict):
            Suite config [runtime][TASK][remote] section
        platforms (dict):
            Dictionary containing platfrom definitions.

    Returns:
        platfrom (str):
            string representing a platform from the global config.

    Raises:
        PlatformLookupError:
            If no matching platform can be a found an error is raised.

    Example:
        >>> platforms = {
        ...         'desktop[0-9][0-9]|laptop[0-9][0-9]': {},
        ...         'sugar': {
        ...             'login hosts': 'localhost',
        ...             'batch system': 'slurm'
        ...         }
        ... }
        >>> job = {'batch system': 'slurm'}
        >>> remote = {'host': 'sugar'}
        >>> reverse_lookup(platforms, job, remote)
        'sugar'
        >>> remote = {}
        >>> reverse_lookup(platforms, job, remote)
        'localhost'
    """
    # These settings are removed from the incoming dictionaries for special
    # handling later - we want more than a simple match:
    #   - In the case of host we also want a regex match to the platform name
    #   - In the case of batch system we want to match the name of the system
    #     to a platform when host is localhost.
    if 'host' in remote.keys():
        task_host = remote.pop('host')
    else:
        task_host = 'localhost'
    if 'batch system' in job.keys():
        task_batch_system = job.pop('batch system')
    else:
        # Necessary? Perhaps not if batch system default is 'background'
        task_batch_system = 'background'

    # Riffle through the platforms looking for a match to our task settings.
    # reverse dict order so that user config platforms added last are examined
    # before site config platforms.
    for platform_name, platform_spec in reversed(list(platforms.items())):
        # Handle all the items requiring an exact match.
        for task_section in [job, remote]:
            shared_items = set(task_section).intersection(
                set(platform_spec.keys()))
            generic_items_match = all(
                (platform_spec[item] == task_section[item]
                 for item in shared_items))
        # All items other than batch system and host must be an exact match
        if not generic_items_match:
            continue

        # We have some special logic to identify whether task host and task
        # batch system match the platform in question.
        if (task_host == 'localhost' and task_batch_system == 'background'):
            return 'localhost'

        elif ('hosts' in platform_spec.keys()
              and task_host in platform_spec['hosts']
              and task_batch_system == platform_spec['batch system']):
            # If we have localhost with a non-background batch system we
            # use the batch system to give a sensible guess at the platform
            return platform_name

        elif (re.fullmatch(platform_name, task_host)
              and task_batch_system == platform_spec['batch system']):
            return task_host

    raise PlatformLookupError('No platform found matching your task')