def build_inventory(setup):
    '''Builds an inventory for use as part of an
    `dynamic Ansible inventory <http://docs.ansible.com/ansible/intro_dynamic_inventory.html>`_
    according to the
    `script conventions <http://docs.ansible.com/ansible/developing_inventory.html#script-conventions>`_::

        {
            "_meta" : {
                "hostvars" : {
                    "host1": {},
                    "host2": {},
                    "host3": {},
                    ...
            },
            "all": {
                "vars": {}
            },
            "group1": {
                "hosts": ["host1", "host2"],
                "vars": {}
            },
            "group2": {
                "hosts": ["host3"],
                "vars": {}
            },
            ...
        }


    Parameters
    ----------
    setup: tmdeploy.config.Setup
        setup configuration

    Returns
    -------
    dict
        mapping of groups to hosts
    '''
    inventory = dict()
    inventory['all'] = dict()
    inventory['all']['vars'] = {
        'provider': setup.cloud.provider,
        'region': setup.cloud.region,
        'key_name': setup.cloud.key_name,
        'key_file': os.path.expandvars(
            os.path.expanduser(
                setup.cloud.key_file_public
            )
        ),
        'network': setup.cloud.network,
        'subnetwork': setup.cloud.subnetwork,
        'ip_range': setup.cloud.ip_range,
        'proxy_env': {
            'http_proxy': setup.cloud.proxy,
            'https_proxy': setup.cloud.proxy
        }
    }
    inventory['_meta'] = dict()
    inventory['_meta']['hostvars'] = dict()

    if not isinstance(setup, Setup):
        raise TypeError(
            'Argument "setup" must have type {0}.'.format(
                '.'.join([Setup.__module__, Setup.__name__])
            )
        )
    for cluster in setup.architecture.clusters:
        logger.info('configure cluster "%s"', cluster.name)
        for node_type in cluster.node_types:
            logger.info('configure node type "%s"', node_type.name)
            for i in range(node_type.count):
                host_name = HOSTNAME_FORMAT.format(
                    name=setup.architecture.name, cluster=cluster.name,
                    node_type=node_type.name, index=i+1
                )
                host_vars = dict()
                for k, v in node_type.instance.to_dict().items():
                    if k == 'tags':
                        # Every server is part of the "compute-storage"
                        # security group, which is important for servers to be
                        # able to connect to each other when part of a cluster.
                        security_groups = 'compute-storage'
                        if 'web' in v:
                            host_vars['assign_public_ip'] = 'yes'
                            security_groups = ','.join([
                                security_groups, 'web'
                            ])
                        else:
                            host_vars['assign_public_ip'] = 'no'
                        host_vars['security_groups'] = security_groups
                    if isinstance(v, list):
                        v = ','.join(v)
                    host_vars[k] = v
                inventory['_meta']['hostvars'][host_name] = host_vars
                for group in node_type.groups:
                    logger.info('add group "%s"', group.name)
                    if group.name not in inventory:
                        inventory[group.name] = {'hosts': list()}
                    inventory[group.name]['hosts'].append(host_name)
                    inventory[group.name]['vars'] = dict()
                    if group.vars is not None:
                        inventory[group.name]['vars'].update(group.vars)
                    if node_type.vars is not None:
                        inventory[group.name]['vars'].update(node_type.vars)

    return inventory