def _run_playbook_impl(playbook, hosts_fn=None, cwd=None, ignore_errors=False):
    # Unfortunately, there's no good way to get the options instance
    # with proper defaults since it's generated by argparse inside
    # PlaybookCLI. Due to the fact that the options can't be empty
    # and must contain proper values we have not choice but extract
    # them from PlaybookCLI instance.
    playbook_cli = PlaybookCLI(['to-be-stripped', playbook])
    playbook_cli.parse()
    options = playbook_cli.options

    # Get others required options.
    loader = DataLoader()
    variable_manager = VariableManager()
    inventory = Inventory(loader, variable_manager)
    variable_manager.set_inventory(inventory)
    variable_manager.extra_vars = _get_user_settings(loader)

    # Limit playbook execution to hosts returned by 'hosts_fn'.
    if hosts_fn is not None:
        inventory.subset([
            host.get_vars()['inventory_hostname']
            for host in hosts_fn(inventory)
        ])

    # Finally, we can create a playbook executor and run the playbook.
    executor = PlaybookExecutor(
        playbooks=[playbook],
        inventory=inventory,
        variable_manager=variable_manager,
        loader=loader,
        options=options,
        passwords={}
    )

    # Some playbooks may rely on current working directory, so better allow
    # to change it before execution.
    with _setcwd(cwd):
        exitcode = executor.run()

    # Celery treats exceptions from task as way to mark it failed. So let's
    # throw one to do so in case return code is not zero.
    if all([not ignore_errors, exitcode is not None, exitcode != 0]):
        raise Exception('Playbook "%s" has been finished with errors. '
                        'Exit code is "%d".' % (playbook, exitcode))

    return exitcode