Why Gemfury? Push, build, and install  RubyGems npm packages Python packages Maven artifacts PHP packages Go Modules Debian packages RPM packages NuGet packages

Repository URL to install this package:

Details    
nsbl / src / nsbl / nsbl.py
Size: Mime:
# -*- coding: utf-8 -*-
from __future__ import absolute_import, division, print_function, unicode_literals

import copy
import getpass
import io
import logging
import os
import re
import shutil
import tarfile
import threading
from collections import OrderedDict
from datetime import datetime
from distutils.spawn import find_executable
from random import randint
from time import sleep

import click
from cookiecutter.main import cookiecutter
from jinja2 import Environment, FileSystemLoader
from plumbum import local
from ruamel.yaml import YAML
from ruamel.yaml.comments import CommentedMap
from six import string_types

from frkl import load_string_from_url_or_path, FrklistContext
from frkl.exceptions import FrklistConfigException
from frkl.utils import get_url_parents
from frutils import can_passwordless_sudo, dict_merge, is_url_or_abbrev
from .defaults import (
    DEFAULT_ENV_TYPE,
    ADD_TYPE_TASK_LIST,
    ADD_TYPE_ROLE,
    ADD_TYPE_ACTION,
    ADD_TYPE_CALLBACK,
    ADD_TYPE_FILTER,
    ADD_TYPE_LIBRARY,
    ANSIBLE_ROLE_CACHE_DIR,
    ADD_TYPE_FILES,
    ADD_TYPE_MODULE_UTIL,
    ADD_TYPE_TASK_LIST_FILE,
    TEMPLATES_FOLDER,
    MITOGEN_VERSION,
)
from .exceptions import NsblException
from .inventory import NsblInventory
from .nsbl_tasklist import NsblTasklist

try:
    set
except NameError:
    from sets import Set as set

log = logging.getLogger("nsbl")

cookiecutter_lock = threading.Lock()


def create_yaml():
    yaml = YAML()
    yaml.default_flow_style = False
    yaml.preserve_quotes = True
    yaml.width = 4096
    return yaml


# class NsblContext(FrklistContext):
#
#     def __init__(self, **kwargs):
#
#         self.kwargs = kwargs


def create_single_host_nsbl_env_from_tasklist(
    task_list,
    context,
    user=None,
    host="localhost",
    host_ip=None,
    connection_type=None,
    ssh_port=22,
    ssh_key=None,
    python_interpreter=None,
    global_task_vars=None,
    pre_tasks=None,
    gather_facts=True,
    additional_files=None,
    task_marker="task",
    meta_marker="task",
    **kwargs
):

    if user is None:
        user = getpass.getuser()

    config_dict = [{host: {"meta": {"type": "host"}, "tasks": task_list}}]

    if connection_type is None:
        if host in ["localhost", "127.0.0.1"]:
            connection_type = "local"
        else:
            connection_type = "ssh"

    connection_vars = {
        "ansible_port": ssh_port,
        "ansible_connection": connection_type,
        "ansible_user": user,
    }
    if python_interpreter is not None:
        connection_vars["ansible_python_interpreter"] = python_interpreter
    if ssh_key is not None:
        connection_vars["ansible_ssh_private_key_file"] = ssh_key
    if host_ip is not None:
        connection_vars["ansible_host"] = host_ip

    config_dict[0][host]["vars"] = connection_vars

    config = Nsbl(
        [config_dict],
        context=context,
        base_path=None,
        global_task_vars=global_task_vars,
        pre_tasks=pre_tasks,
        gather_facts=gather_facts,
        additional_files=additional_files,
        task_marker=task_marker,
        meta_marker=meta_marker,
    )

    return config


def create_nsbl_env(
    urls,
    base_path=None,
    context=None,
    default_env_type=DEFAULT_ENV_TYPE,
    additional_files=None,
    global_task_vars=None,
    task_marker="task",
    meta_marker="task",
):

    if isinstance(urls, string_types):
        urls = [urls]  # we always want a list of lists as input for the Nsbl object

    if is_url_or_abbrev(urls[0]):
        click.echo("Downloading configuration: {}".format(urls[0]))
    config_dicts = load_string_from_url_or_path(
        urls, create_python_object=True, safe_load=False
    )

    if not base_path:
        parent = get_url_parents(urls, return_list=True)
        if len(parent) > 2:
            raise NsblException(
                "Multiple base paths calculated ({}). This is not allowed as it introduces unpredictable behaviour. Please specify the base path manually.".format(
                    parent
                )
            )
        base_path = list(parent)[0]

    config = Nsbl(
        config_dicts,
        base_path=base_path,
        context=context,
        default_env_type=default_env_type,
        additional_files=additional_files,
        global_task_vars=global_task_vars,
        task_marker=task_marker,
        meta_marker=meta_marker,
    )

    return config


# def expand_nsbl_config(configs):
#     """Expands the nsbl configuration.
#
#     Args:
#         configs (list): a list of configuration items
#     """
#
#     chain = [FrklProcessor(NSBL_INVENTORY_BOOTSTRAP_FORMAT)]
#     f = Frkl(configs, chain)
#     result = f.process()
#
#     return result

DEFAULT_ELEVATED_PERMISSIONS_REQUIRED = True
DEFAULT_PASSWORDLESS_SUDO_POSSIBLE = False


class Nsbl(object):
    """Holds and parses configuration to generate Ansible task lists and inventories.

    Args:
        config (list): a list of configuration items
        base_path (str): the base path to use for relative task list imports etc.
        context (NsblContext): the context for this environment
        default_env_type (str): the type a environment is if it is not explicitely specified, either ENV_TYPE_HOST or ENV_TYPE_GROUP
        additional_files (dict): a dict of additional files to copy into the Ansible environment
        # allow_external_roles (bool): whether to allow the downloading of external roles
        global_task_vars (dict): global variables for all tasks (will still be overwritten by task-list specific task vars if present)
        pre_tasks (list): a list of tasks that will be inserted into every playbook
        gather_facts (boolean): whether the gather_facts step should be enabled/disabled
        task_marker (string): the marker string that indicates a task metadata key
        meta_marker (string): the maker string that indicates a 'meta' metadata key (for getting the description of a task)
    """

    def __init__(
        self,
        config,
        base_path=None,
        context=None,
        default_env_type=DEFAULT_ENV_TYPE,
        additional_files=None,
        global_task_vars=None,
        pre_tasks=None,
        gather_facts=True,
        task_marker="task",
        meta_marker="task",
    ):

        self.base_path = base_path
        self.plays = CommentedMap()
        self.internal_roles = set()
        self.external_roles = set()

        self.task_marker = task_marker
        self.meta_marker = meta_marker

        self.pre_tasks = pre_tasks
        self.gather_facts = gather_facts

        if global_task_vars is None:
            global_task_vars = {}
        self.global_task_vars = global_task_vars

        self.config = config
        self._lock = threading.Lock()
        if context is None:
            context = FrklistContext()
        elif isinstance(context, (dict, CommentedMap, OrderedDict)):
            context = FrklistContext(**context)
        elif not isinstance(context, FrklistContext):
            raise FrklistConfigException(
                "Invalid type for context: {}".format(type(context))
            )
        self.context = context

        self.default_env_type = default_env_type

        if additional_files is None:
            additional_files = {}
        self.additional_files = copy.deepcopy(additional_files)

        self.inventory = NsblInventory.create(
            self.config, default_env_type=self.default_env_type, pre_chain=[]
        )

        tl_id = 0
        for tasks in self.inventory.tasks:

            task_list_meta = tasks["meta"]
            env_name = task_list_meta["name"]
            env_id = task_list_meta["_env_id"]

            # task_list_vars = tasks.get("vars", {})
            # print(task_list_vars)
            # dict_merge(task_list_vars, self.global_task_vars, copy_dct=False)
            task_list_vars = self.global_task_vars

            task_list = tasks["tasks"]

            meta = {}
            meta["tasklist_id"] = tl_id
            meta["env_id"] = env_id
            meta["env_name"] = env_name
            # meta["tasklist_parent"] = self.base_path

            tl = NsblTasklist(
                task_list,
                context=self.context,
                meta=meta,
                vars=task_list_vars,
                task_marker=self.task_marker,
                meta_marker=self.meta_marker,
            )
            self.plays["{}_{}".format(env_name, env_id)] = {
                "task_list": tl,
                "meta": task_list_meta,
                "env_id": env_id,
                "env_name": env_name,
            }

            tl_id = tl_id + 1
            self.internal_roles.update(tl.internal_roles)
            self.external_roles.update(tl.external_roles)

    def get_play(self, env_name, env_id):

        return self.plays.get("{}_{}".format(env_name, env_id), None)

    def get_tasklist(self, env_name, env_id):

        return self.get_play(env_name, env_id)["task_list"]

    def render(
        self,
        env_dir,
        global_vars=None,
        extract_vars=True,
        force=False,
        elevated_permissions_required=None,
        passwordless_sudo_possible=None,
        secure_vars=None,
        ansible_args="",
        callback="default",
        force_update_roles=False,
        add_timestamp_to_env=False,
        add_symlink_to_env=False,
        extra_paths=None,
        show_tasks_with_password_in_log=False,
        use_mitogen=False,
        use_ara=False,
        parent_task=None,
    ):
        """Creates the ansible environment in the folder provided.

        Args:
          env_dir (str): the folder where the environment should be created
          global_vars (dict): vars to be rendered as global on top of a playbook
          extract_vars (bool): whether to extract a hostvars and groupvars directory for the inventory (True), or render a dynamic inventory script for the environment (default, True) -- Not supported at the moment
          force (bool): overwrite environment if already present at the specified location, use with caution because this might delete an important folder if you get the 'target' dir wrong
          elevated_permissions_required (bool): whether to include the '--ask-become-pass' arg to the ansible-playbook call
          passwordless_sudo_possible (bool): whether the user executing this run does have passwordless sudo permissions
          secure_vars (dict): vars to keep in a vault (not implemented yet)
          ansible_args (str): parameters to give to ansible-playbook (like: "-vvv")
          callback (str): name of the callback to use, default: nsbl_internal
          force_update_roles (bool): whether to overwrite external roles that were already downloaded
          add_timestamp_to_env (bool): whether to add a timestamp to the env_dir -- useful for when this is called from other programs (e.g. freckles)
          add_symlink_to_env (str): whether to add a symlink to the current env from a fixed location (useful to archive all runs/logs)
          extra_paths (str): a list of of extra paths to be exported before the ansible playbook run
          show_tasks_with_password_in_log (bool): whether to show tasks that contain a password variable in the log (only enable for debugging!)
          use_mitogen (bool): use mitogen as a strategy plugin to speed up execution
          use_ara (bool): use ara (https://ara.readthedocs.io)
          parent_task (TaskDetail): parent task (for task callbacks, optional)
        """
        if elevated_permissions_required is None:
            elevated_permissions_required = DEFAULT_ELEVATED_PERMISSIONS_REQUIRED

        if not isinstance(elevated_permissions_required, bool):
            raise Exception("'ask_become_pass' needs to be boolean value")

        if extra_paths is None:
            extra_paths = []

        if passwordless_sudo_possible is None:
            if self.inventory.localhost_only:
                passwordless_sudo_possible = can_passwordless_sudo()
            else:
                passwordless_sudo_possible = DEFAULT_PASSWORDLESS_SUDO_POSSIBLE

        env_dir = os.path.expanduser(env_dir)
        if add_timestamp_to_env:
            start_date = datetime.now()
            date_string = start_date.strftime("%y%m%d_%H_%M_%S")
            dirname, basename = os.path.split(env_dir)
            env_dir = os.path.join(dirname, "{}_{}".format(basename, date_string))

        if global_vars is None:
            global_vars = {}

        result = {}
        result["env_dir"] = env_dir
        if os.path.exists(env_dir) and force:
            shutil.rmtree(env_dir)

        inventory_dir = os.path.join(env_dir, "inventory")
        result["inventory_dir"] = inventory_dir

        if extract_vars:
            inv_target = "../inventory/hosts"
        else:
            inv_target = "../inventory/inventory"

        result["extract_vars"] = extract_vars

        playbook_dir = os.path.join(env_dir, "plays")
        result["playbook_dir"] = playbook_dir
        # roles_base_dir = os.path.join(env_dir, "roles")
        result["roles_base_dir"] = playbook_dir

        # if password is None:
        #     if elevated_permissions_required:
        #         if passwordless_sudo_possible:
        #             ask_sudo = False
        #         else:
        #             ask_sudo = True
        #     else:
        #         ask_sudo = False
        # else:
        #     ask_sudo = True

        all_plays_name = "all_plays.yml"
        result["default_playbook_name"] = all_plays_name

        ansible_playbook_args = ansible_args
        result["ansible_playbook_cli_args"] = ansible_playbook_args
        result["run_playbooks_script"] = os.path.join(env_dir, "run_all_plays.sh")

        ara_base = None
        if use_ara:

            ara_success = False

            python_exe = find_executable("python", path=extra_paths)

            if python_exe:
                try:
                    ara_base = local[python_exe](["-m", "ara.setup.path"])
                    if ara_base:
                        ara_base = ara_base.strip()
                        ara_success = True
                except (Exception):
                    pass

            if not ara_success:
                ara_base = None
                log.warning("Did not find 'ara' in python env, not using it...")

        if ara_base:
            callback_plugins_list = "callback_plugins:{}/plugins/callback".format(
                ara_base
            )
            action_plugins_list = "action_plugins:{}/plugins/action".format(ara_base)
            # library_plugins_list = "library:{}/plugins/modules".format(ara_base)
            # module_utils_list = "module_utils:{}/plugins/module_utils".format(ara_base)
        else:
            callback_plugins_list = "callback_plugins"
            action_plugins_list = "action_plugins"

        library_plugins_list = "library"
        module_utils_list = "module_utils"

        if not use_mitogen:
            mitogen = ""
        else:
            mitogen_path = "../opt/mitogen-{}".format(MITOGEN_VERSION)
            mitogen = os.path.join(
                mitogen_path, "ansible_mitogen", "plugins", "strategy"
            )

        cookiecutter_details = {
            "inventory": inv_target,
            "env_dir": env_dir,
            "extra_paths": extra_paths,
            "playbook_dir": playbook_dir,
            "ansible_playbook_args": ansible_playbook_args,
            "library_path": library_plugins_list,
            "action_plugins_path": action_plugins_list,
            "module_utils_path": module_utils_list,
            "extra_script_commands": "",
            "ask_sudo": "",
            "playbook": all_plays_name,
            "callback_plugins": callback_plugins_list,
            "callback_plugin_name": callback,
            "callback_whitelist": "default_to_file",
            "mitogen": mitogen,
            "use_ara": "true" if use_ara else "false",
        }

        log.debug("Creating build environment from template...")
        log.debug("Using cookiecutter details: {}".format(cookiecutter_details))

        template_path = os.path.join(
            os.path.dirname(__file__), "external", "nsbl-environment-template"
        )

        create_cookiecutter_env(
            template_path=template_path, cookiecutter_details=cookiecutter_details
        )

        if add_symlink_to_env:
            link_path = os.path.expanduser(add_symlink_to_env)
            if os.path.exists(link_path) or os.path.islink(link_path):
                os.unlink(link_path)
            link_parent = os.path.abspath(os.path.join(link_path, os.pardir))
            try:
                os.makedirs(link_parent)
            except (Exception):
                pass

            os.symlink(env_dir, link_path)

            result["env_dir_link"] = link_path

        # write inventory
        if extract_vars:
            self.inventory.extract_vars(inventory_dir)

        self.inventory.write_inventory_file_or_script(
            inventory_dir, extract_vars=extract_vars
        )

        # extract mitogen
        if use_mitogen:
            mitogen_archive_path = os.path.join(
                os.path.dirname(__file__),
                "external",
                "mitogen",
                "mitogen-{}.tar.gz".format(MITOGEN_VERSION),
            )
            opt_path = os.path.join(env_dir, "opt")
            if not os.path.exists(opt_path):
                os.makedirs(opt_path)
            mitogen_archive = tarfile.open(mitogen_archive_path)
            mitogen_archive.extractall(path=opt_path)
            mitogen_archive.close()

        # write roles
        all_playbooks = []
        all_playbook_names = []
        ext_roles = []
        task_details = []

        for play, task_list_details in self.plays.items():

            # td = copy.deepcopy(task_list_details)
            tasks = task_list_details["task_list"]
            id = task_list_details["env_id"]
            name = task_list_details["env_name"]
            playbook_name = "play_{}_{}.yml".format(name, id)
            playbook_file = os.path.join(playbook_dir, playbook_name)

            playbook_vars = {}
            dict_merge(playbook_vars, global_vars, copy_dct=False)
            dict_merge(playbook_vars, tasks.vars, copy_dct=False)
            playbook_vars["_env_id"] = id
            playbook_vars["_env_name"] = name

            task_list = tasks.render_tasklist(
                secure_vars=secure_vars,
                show_tasks_with_password_in_log=show_tasks_with_password_in_log,
            )

            if self.pre_tasks:
                # TODO: clean that up, tasks could now go directly into the playbook
                playbook_pre_file = os.path.join(
                    playbook_dir, "pre_{}".format(playbook_name)
                )
                tasklist_name = "pre_tasks_{}_{}".format(name, id)
                tasklist_name = re.sub(r"\W+", "", tasklist_name)
                tl_pre = {
                    "type": ADD_TYPE_TASK_LIST,
                    "target_name": "{}.yml".format(tasklist_name),
                    "tasklist": self.pre_tasks,
                    "var_name": tasklist_name,
                }
                self.additional_files[tasklist_name] = tl_pre
                import_pre_tasks = {
                    "name": "[importing host preparation tasks]",
                    "import_tasks": "{{{{ {} }}}}".format(tasklist_name),
                }
                playbook_dict_pre = CommentedMap()
                playbook_dict_pre["hosts"] = name
                playbook_dict_pre["strategy"] = "linear"
                playbook_dict_pre["gather_facts"] = False
                playbook_dict_pre["vars"] = playbook_vars
                playbook_dict_pre["tasks"] = [import_pre_tasks]
                all_playbooks.append(
                    {
                        "name": "pre_{}".format(playbook_name),
                        "dict": playbook_dict_pre,
                        "file": playbook_pre_file,
                    }
                )
                all_playbook_names.append("pre_{}".format(playbook_name))

            playbook_dict = CommentedMap()
            playbook_dict["hosts"] = name

            playbook_dict["gather_facts"] = self.gather_facts

            playbook_dict["vars"] = playbook_vars
            playbook_dict["tasks"] = task_list

            # td["playbook"] = playbook_dict
            all_playbooks.append(
                {"name": playbook_name, "dict": playbook_dict, "file": playbook_file}
            )
            all_playbook_names.append(playbook_name)

            dict_merge(self.additional_files, tasks.additional_files, copy_dct=False)

            if tasks.external_roles:
                for n in tasks.external_roles:
                    if n not in ext_roles:
                        ext_roles.append(n)

        # copy external files
        external_files_vars = {}

        action_plugins_target = os.path.join("plays", "action_plugins")
        callback_plugins_target = os.path.join("plays", "callback_plugins")
        filter_plugins_target = os.path.join("plays", "filter_plugins")
        library_plugins_target = os.path.join("plays", "library")
        module_utils_target = os.path.join("plays", "module_utils")
        roles_target = os.path.join("roles", "internal")
        task_lists_target = "task_lists"
        files_target = "files"

        additional_files = copy.deepcopy(self.additional_files)

        for path, details in additional_files.items():

            file_type = details["type"]

            # if not os.path.exists(path):
            #     import pp
            #     print(path)
            #     pp(details)
            #     raise NsblException(
            #         "Resource '{}' not available: {}".format(file_type, path)
            #     )

            playbook_var_name = details.get("var_name", None)
            file_name = details["target_name"]

            skip_copy = False
            if file_type == ADD_TYPE_TASK_LIST:
                # content = details["tasklist"]
                target = os.path.join(env_dir, task_lists_target, file_name)
                log.debug("Render 'ansible'-type tasklist: {}".format(path))

                target_parent = os.path.dirname(target)
                if not os.path.exists(target_parent):
                    os.makedirs(target_parent)

                with io.open(target, "w", encoding="utf-8") as tlf:
                    yaml = create_yaml()
                    yaml.dump(details["tasklist"], tlf)

                skip_copy = True
                playbook_var_value = os.path.join(
                    "{{ playbook_dir }}", "..", task_lists_target, file_name
                )

            elif file_type == ADD_TYPE_ROLE:
                target = os.path.join(env_dir, roles_target, file_name)
                playbook_var_value = None
                copy_source_type = "dir"
            elif file_type == ADD_TYPE_ACTION:
                target = os.path.join(env_dir, action_plugins_target, file_name)
                playbook_var_value = None
                copy_source_type = "file"
            elif file_type == ADD_TYPE_CALLBACK:
                target = os.path.join(env_dir, callback_plugins_target, file_name)
                playbook_var_value = None
                copy_source_type = "file"
            elif file_type == ADD_TYPE_FILTER:
                target = os.path.join(env_dir, filter_plugins_target, file_name)
                playbook_var_value = None
                copy_source_type = "file"
            elif file_type == ADD_TYPE_LIBRARY:
                target = os.path.join(env_dir, library_plugins_target, file_name)
                playbook_var_value = None
                copy_source_type = "file"
            elif file_type == ADD_TYPE_MODULE_UTIL:
                target = os.path.join(env_dir, module_utils_target, file_name)
                copy_source_type = "file"
                playbook_var_value = None
            elif file_type == ADD_TYPE_FILES:
                target = os.path.join(env_dir, files_target, file_name)
                copy_source_type = "file"
                playbook_var_value = None
            elif file_type == ADD_TYPE_TASK_LIST_FILE:
                target = os.path.join(env_dir, task_lists_target, file_name)
                copy_source_type = "file"
                playbook_var_value = None
            else:
                raise NsblException("Invalid external file type: {}".format(file_type))

            if not skip_copy:
                log.debug("Copying {} '{}': {}".format(file_type, file_name, target))
                target_parent = os.path.dirname(target)
                if not os.path.exists(target_parent):
                    os.makedirs(target_parent)
                if not os.path.isdir(os.path.realpath(target_parent)):
                    raise NsblException(
                        "Can't copy files to '{}': not a directory".format(
                            target_parent
                        )
                    )

                if copy_source_type == "file":
                    shutil.copyfile(path, target)
                else:
                    shutil.copytree(path, target)

            if playbook_var_value:
                if playbook_var_name in external_files_vars.keys():
                    raise NsblException(
                        "Duplicate key for external files: {}".format(playbook_var_name)
                    )
                log.debug(
                    "Setting variable '{}' to: {}".format(
                        playbook_var_name, playbook_var_value
                    )
                )
                external_files_vars[playbook_var_name] = playbook_var_value

        # render all playbooks
        for playbook in all_playbooks:

            playbook_file = playbook["file"]
            playbook_dict = playbook["dict"]

            # adding external files vars
            dict_merge(
                playbook_dict.setdefault("vars", {}),
                external_files_vars,
                copy_dct=False,
            )

            with io.open(playbook_file, "w", encoding="utf-8") as pf:
                yaml = create_yaml()
                yaml.dump([playbook_dict], pf)

        result["task_details"] = task_details
        result["additional_files"] = self.additional_files
        jinja_env = Environment(loader=FileSystemLoader(TEMPLATES_FOLDER))
        template = jinja_env.get_template("play.yml")
        output_text = template.render(playbooks=all_playbook_names)

        all_plays_file = os.path.join(env_dir, "plays", all_plays_name)
        result["all_plays_file"] = all_plays_file
        with io.open(all_plays_file, "w", encoding="utf-8") as text_file:
            text_file.write(output_text)

        if ext_roles:

            ext_roles_target = os.path.join(env_dir, "roles", "external")
            # render roles_requirements.yml
            jinja_env = Environment(loader=FileSystemLoader(TEMPLATES_FOLDER))
            roles_requirements_file = os.path.join(
                ext_roles_target, "roles_requirements.yml"
            )

            if not os.path.exists(ext_roles_target):
                os.makedirs(ext_roles_target)

            roles_to_copy = {}
            for role in ext_roles:
                role_src = os.path.join(ANSIBLE_ROLE_CACHE_DIR, role)
                target = os.path.join(ext_roles_target, role)
                roles_to_copy[role_src] = target

                template = jinja_env.get_template("external_role.yml")
                output_text = template.render(role={"src": role, "name": role})
                with io.open(roles_requirements_file, "a", encoding="utf-8") as myfile:
                    myfile.write(output_text)

            # download external roles
            role_requirement_file = os.path.join(
                env_dir, "roles", "external", "roles_requirements.yml"
            )

            if not os.path.exists(ANSIBLE_ROLE_CACHE_DIR):
                os.makedirs(ANSIBLE_ROLE_CACHE_DIR)

            command = [
                "install",
                "-r",
                role_requirement_file,
                "-p",
                ANSIBLE_ROLE_CACHE_DIR,
            ]
            if force_update_roles:
                command.append("--force")
            log.debug("Downloading and installing external roles...")

            if parent_task:
                download_task = parent_task.add_subtask(
                    task_name="download external roles",
                    msg="downloading role(s): {}".format(" ".join(ext_roles)),
                )
            else:
                download_task = None
                click.echo("Managing external roles...")

            # path = os.environ["PATH"] + ":" + extra_paths
            path = extra_paths
            galaxy_exe = find_executable("ansible-galaxy", path=path)
            if not galaxy_exe:
                raise Exception("Could not find 'ansible-galaxy' executable")
            ansible_galaxy = local[galaxy_exe]

            try:
                rc, stdout, stderr = ansible_galaxy.run(command)

                if download_task is None:
                    for line in stdout.split("\n"):
                        if (
                            "already installed" not in line
                            and "--force to change" not in line
                            and "unspecified" not in line
                        ):
                            # log.debug("Installing role: {}".format(line.encode('utf8')))

                            click.echo("  {}".format(line.encode("utf8")), nl=False)
                else:
                    if rc == 0:
                        success = True
                    else:
                        success = False

                    download_task.finish(
                        success=success,
                        msg=stdout,
                        error_msg=stderr,
                        skipped=False,
                        changed=True,
                    )

            except (Exception) as e:

                if download_task is not None:
                    download_task.finish(
                        success=False, error_msg=str(e), skipped=False, changed=True
                    )
                raise e

            if roles_to_copy:
                if parent_task is not None:
                    copy_task = parent_task.add_subtask(
                        task_name="copy roles",
                        msg="copying role(s) into Ansible environment",
                    )
                else:
                    copy_task = None
                    if len(ext_roles) == 1:

                        click.echo(
                            "Copying role from Ansible cache: {}".format(ext_roles[0])
                        )
                    else:
                        click.echo("Copying roles from Ansible cache:")
                        for r in ext_roles:
                            click.echo("  - {}".format(r))

                for src, target in roles_to_copy.items():
                    log.debug("Coping external role: {} -> {}".format(src, target))
                    shutil.copytree(src, target)

                if copy_task is not None:
                    copy_task.finish(success=True, skipped=False, changed=True)

        return result


def create_cookiecutter_env(
    template_path, cookiecutter_details, overwrite_if_exists=False
):

    with cookiecutter_lock:
        try:
            cookiecutter(
                template_path,
                extra_context=cookiecutter_details,
                no_input=True,
                overwrite_if_exists=overwrite_if_exists,
            )
        except (Exception) as e:
            # print("XXXXXXXXXXXXXXXXXXXXXXXXXX: {}".format(e))
            # this is weird, sometimes when trying to run parallel freckles job, this fails

            i = 0
            success = False
            last_exception = e
            while i < 6:
                i = i + 1
                sl = randint(500, 1500) / 1000
                log.debug(
                    "Error when trying to create Ansible env dir, retrying after sleeping {} milliseconds: {}".format(
                        sl, e
                    )
                )
                sleep(sl)
                try:
                    cookiecutter(
                        template_path,
                        extra_context=cookiecutter_details,
                        no_input=True,
                        overwrite_if_exists=True,
                    )
                    success = True
                    break
                except (Exception) as le:
                    # print("yyyyyyyyyyyyyyy {}".format(le))
                    last_exception = le
            if not success:
                raise last_exception