def _run_single_op(state, op_hash):
    '''
    Run a single operation for all servers. Can be configured to run in serial.
    '''
    op_meta = state.op_meta[op_hash]
    op_types = []
    if op_meta['serial']:
        op_types.append('serial')
    if op_meta['run_once']:
        op_types.append('run once')
    logger.info('{0} {1} {2}'.format(
        click.style('--> Starting{0}operation:'.format(
            ' {0} '.format(', '.join(op_types)) if op_types else ' ',
        ), 'blue'),
        click.style(', '.join(op_meta['names']), bold=True),
        tuple(op_meta['args']) if op_meta['args'] else '',
    ))
    failed_hosts = set()
    if op_meta['serial']:
        with progress_spinner(state.inventory) as progress:
            for host in state.inventory:
                result = _run_server_op(state, host, op_hash)
                progress(host)
                if not result:
                    failed_hosts.add(host)
    else:
        batches = [state.inventory]
        if op_meta['parallel']:
            parallel = op_meta['parallel']
            hosts = list(state.inventory)
            batches = [
                hosts[i:i + parallel]
                for i in range(0, len(hosts), parallel)
            ]
        for batch in batches:
            with progress_spinner(batch) as progress:
                greenlet_to_host = {
                    state.pool.spawn(_run_server_op, state, host, op_hash): host
                    for host in batch
                }
                for greenlet in gevent.iwait(greenlet_to_host.keys()):
                    host = greenlet_to_host[greenlet]
                    progress(host)
                for greenlet, host in six.iteritems(greenlet_to_host):
                    if not greenlet.get():
                        failed_hosts.add(host)
    if not op_meta['ignore_errors']:
        state.fail_hosts(failed_hosts)
    if pyinfra.is_cli:
        print()