Configure the cluster by running an Ansible playbook.

        The ElastiCluster configuration attribute `<kind>_groups`
        determines, for each node kind, what Ansible groups nodes of
        that kind are assigned to.

        :param cluster: cluster to configure
        :type cluster: :py:class:`elasticluster.cluster.Cluster`

        :param list extra_args:
          List of additional command-line arguments
          that are appended to each invocation of the setup program.

        :return: ``True`` on success, ``False`` otherwise. Please note, if nothing
                 has to be configured, then ``True`` is returned.

        :raises: `ConfigurationError` if the playbook can not be found
                 or is corrupt.

def setup_cluster(self, cluster, extra_args=tuple()):
        """
        Configure the cluster by running an Ansible playbook.

        The ElastiCluster configuration attribute `<kind>_groups`
        determines, for each node kind, what Ansible groups nodes of
        that kind are assigned to.

        :param cluster: cluster to configure
        :type cluster: :py:class:`elasticluster.cluster.Cluster`

        :param list extra_args:
          List of additional command-line arguments
          that are appended to each invocation of the setup program.

        :return: ``True`` on success, ``False`` otherwise. Please note, if nothing
                 has to be configured, then ``True`` is returned.

        :raises: `ConfigurationError` if the playbook can not be found
                 or is corrupt.
        """
        inventory_path = self._build_inventory(cluster)
        if inventory_path is None:
            # No inventory file has been created, maybe an
            # invalid class has been specified in config file? Or none?
            # assume it is fine.
            elasticluster.log.info("No setup required for this cluster.")
            return True
        assert os.path.exists(inventory_path), (
                "inventory file `{inventory_path}` does not exist"
                .format(inventory_path=inventory_path))

        # build list of directories to search for roles/include files
        ansible_roles_dirs = [
            # include Ansible default first ...
            '/etc/ansible/roles',
        ]
        for root_path in [
                # ... then ElastiCluster's built-in defaults
                resource_filename('elasticluster', 'share/playbooks'),
                # ... then wherever the playbook is
                os.path.dirname(self._playbook_path),
        ]:
            for path in [
                    root_path,
                    os.path.join(root_path, 'roles'),
            ]:
                if path not in ansible_roles_dirs and os.path.exists(path):
                    ansible_roles_dirs.append(path)


        # Use env vars to configure Ansible;
        # see all values in https://github.com/ansible/ansible/blob/devel/lib/ansible/constants.py
        #
        # Ansible does not merge keys in configuration files: rather
        # it uses the first configuration file found.  However,
        # environment variables can be used to selectively override
        # parts of the config; according to [1]: "they are mostly
        # considered to be a legacy system as compared to the config
        # file, but are equally valid."
        #
        # [1]: http://docs.ansible.com/ansible/intro_configuration.html#environmental-configuration
        #
        # Provide default values for important configuration variables...
        ansible_env = {
            'ANSIBLE_FORKS':             '10',
            'ANSIBLE_HOST_KEY_CHECKING': 'no',
            'ANSIBLE_PRIVATE_KEY_FILE':  cluster.user_key_private,
            'ANSIBLE_ROLES_PATH':        ':'.join(reversed(ansible_roles_dirs)),
            'ANSIBLE_SSH_PIPELINING':    'yes',
            'ANSIBLE_TIMEOUT':           '120',
        }
        # ...override them with key/values set in the config file(s)
        for k, v in self.extra_conf.items():
            if k.startswith('ansible_'):
                ansible_env[k.upper()] = str(v)
        # ...finally allow the environment have the final word
        ansible_env.update(os.environ)
        if __debug__:
            elasticluster.log.debug(
                "Calling `ansible-playbook` with the following environment:")
            for var, value in sorted(ansible_env.items()):
                elasticluster.log.debug("- %s=%r", var, value)

        elasticluster.log.debug("Using playbook file %s.", self._playbook_path)

        # build `ansible-playbook` command-line
        cmd = shlex.split(self.extra_conf.get('ansible_command', 'ansible-playbook'))
        cmd += [
            os.path.realpath(self._playbook_path),
            ('--inventory=' + inventory_path),
        ] + list(extra_args)

        if self._sudo:
            cmd.extend([
                # force all plays to use `sudo` (even if not marked as such)
                '--become',
                # desired sudo-to user
                ('--become-user=' + self._sudo_user),
            ])

        # determine Ansible verbosity as a function of ElastiCluster's
        # log level (we cannot read `ElastiCluster().params.verbose`
        # here, still we can access the log configuration since it's
        # global).
        verbosity = (logging.WARNING - elasticluster.log.getEffectiveLevel()) / 10
        if verbosity > 0:
            cmd.append('-' + ('v' * verbosity))  # e.g., `-vv`

        # append any additional arguments provided by users
        ansible_extra_args = self.extra_conf.get('ansible_extra_args', None)
        if ansible_extra_args:
            cmd += shlex.split(ansible_extra_args)

        cmdline = ' '.join(cmd)
        elasticluster.log.debug("Running Ansible command `%s` ...", cmdline)

        rc = call(cmd, env=ansible_env, bufsize=1, close_fds=True)
        if rc == 0:
            elasticluster.log.info("Cluster correctly configured.")
            return True
        else:
            elasticluster.log.error(
                "Command `%s` failed with exit code %d.", cmdline, rc)
            elasticluster.log.error(
                "Check the output lines above for additional information on this error.")
            elasticluster.log.error(
                "The cluster has likely *not* been configured correctly."
                " You may need to re-run `elasticluster setup` or fix the playbooks.")
            return False