Source code for freckles.frecklet.tasks

# -*- coding: utf-8 -*-
import copy
import logging
import os
from collections import Mapping, Sequence

from colorama import Style
from six import string_types
from treelib import Tree

from freckles.defaults import (
    TASK_KEY_NAME,
    FRECKLET_KEY_NAME,
    ARGS_KEY,
    VARS_KEY,
    DEFAULT_FRECKLES_JINJA_ENV,
    FRECKLETS_KEY,
    FRECKLES_DEFAULT_ARG_SCHEMA,
    CONST_KEY_NAME,
)
from freckles.exceptions import FrecklesConfigException, FreckletBuildException
from frkl import FrklProcessor, Frkl
from frkl.defaults import (
    CHILD_MARKER_NAME,
    DEFAULT_LEAF_NAME,
    DEFAULT_LEAFKEY_NAME,
    OTHER_KEYS_NAME,
    KEY_MOVE_MAP_NAME,
)
from frkl.processors import ConfigProcessor
from frutils import add_key_to_dict, get_template_keys, special_dict_to_dict
from ting.ting_attributes import ValueAttribute, TingAttribute, MultiCacheResult

log = logging.getLogger("freckles")


[docs]class FreckletsAttribute(ValueAttribute): def __init__(self): super(FreckletsAttribute, self).__init__( target_attr_name="frecklets", source_attr_name="_metadata" )
[docs] def get_attribute(self, ting, attribute_name=None): result = ValueAttribute.get_attribute(self, ting, attribute_name=attribute_name) return result
[docs]def fill_defaults(task_item): if CONST_KEY_NAME not in task_item.keys(): task_item[CONST_KEY_NAME] = {} if FRECKLET_KEY_NAME not in task_item.keys(): task_item[FRECKLET_KEY_NAME] = {} if TASK_KEY_NAME not in task_item.keys(): task_item[TASK_KEY_NAME] = {} if ( "name" not in task_item[FRECKLET_KEY_NAME].keys() and "command" not in task_item[TASK_KEY_NAME].keys() ): ti = {} ti[TASK_KEY_NAME] = task_item[TASK_KEY_NAME] ti[FRECKLET_KEY_NAME] = task_item[FRECKLET_KEY_NAME] raise FrecklesConfigException( "Neither 'task/command' nor 'frecklet/name' key in task config: {}".format( ti ) ) if "name" not in task_item[FRECKLET_KEY_NAME].keys(): task_item[FRECKLET_KEY_NAME]["name"] = task_item[TASK_KEY_NAME]["command"] elif "command" not in task_item[TASK_KEY_NAME].keys(): task_item[TASK_KEY_NAME]["command"] = task_item[FRECKLET_KEY_NAME]["name"] if "type" not in task_item[FRECKLET_KEY_NAME].keys(): task_item[FRECKLET_KEY_NAME]["type"] = "frecklet"
[docs]class ExplodedArgsProcessor(ConfigProcessor): """Sets the exploded args property.""" def __init__(self, **init_params): self.args = init_params["args"]
[docs] def process_current_config(self): new_config = self.current_input_config new_config["args"] = self.args new_config.pop(CONST_KEY_NAME, None) return new_config
[docs]class SpecialCaseProcessor(ConfigProcessor): """Makes sure that no keywords are in vars."""
[docs] def process_current_config(self): new_config = self.current_input_config frecklets = new_config.pop("frecklets", []) new_config["frecklets"] = [] for task in frecklets: if ( isinstance(task, Mapping) and "target" in task.keys() and FRECKLET_KEY_NAME in task.keys() ): # this is a frecklecutable, let's wrap it in one so users don't have to do it task = {"frecklecute": task} elif ( isinstance(task, Mapping) and FRECKLET_KEY_NAME in task.keys() and isinstance(task[FRECKLET_KEY_NAME], string_types) ): frecklet_name = task.pop(FRECKLET_KEY_NAME) task[FRECKLET_KEY_NAME] = {"name": frecklet_name} if isinstance(task, Mapping) and len(task) == 1: task_name = list(task.keys())[0] value = task[task_name] if ( FRECKLET_KEY_NAME in value.keys() or TASK_KEY_NAME in value.keys() or VARS_KEY in value.keys() ): new_task = { FRECKLET_KEY_NAME: {"name": task_name}, VARS_KEY: task[task_name], } task = new_task new_config["frecklets"].append(task) # if task.get(FRECKLET_KEY_NAME, {}).get("name", None) == "frecklecute": # print("HIT") # import sys # sys.exit() # task[FRECKLET_KEY_NAME]["type"] = "frecklecutable" return new_config
[docs]class CommandNameProcessor(ConfigProcessor): """Adds potential missing command/name keys."""
[docs] def process_current_config(self): new_config = self.current_input_config fill_defaults(new_config) return new_config
[docs]class DirectParentArgsProcessor(ConfigProcessor): """Adds arguments for 'direct' parents. For example if a frecklet has something like: - user-exists: name: "{{:: name ::}}" Where the variable name stays the same, and no filter is involved, the args will be inherited directly. """ def __init__(self, **init_params): self.index = init_params["index"]
[docs] def process_current_config(self): new_config = self.current_input_config args = new_config.setdefault("args", {}) vars = new_config.get("vars", {}) for k, v in vars.items(): if k in args.keys(): continue if "{{{{:: {} ::}}}}".format(k) == v: parent_name = new_config[FRECKLET_KEY_NAME]["name"] parent = self.index.get(parent_name) if parent is None: raise FreckletBuildException( "Can't assemble frecklet.", reason="Parent frecklet '{}' not available.".format( parent_name ), ) p_args = parent.args a = p_args.get(k, None) if a is not None: args[k] = a else: args[k] = copy.deepcopy(FRECKLES_DEFAULT_ARG_SCHEMA) return new_config
[docs]class TaskTypePrefixProcessor(ConfigProcessor): """Adds a 'become': True key/value pair if the task name is all uppercase."""
[docs] def process_current_config(self): new_config = self.current_input_config command_name = new_config[TASK_KEY_NAME].get("command", None) task_type = new_config[FRECKLET_KEY_NAME].get("type", None) if "::" in command_name: if task_type is not None and task_type != "frecklet": raise FrecklesConfigException( "Invalid task item '{}': command name contains '::', but type is already specified.".format( new_config ) ) task_type, command_name = command_name.split("::", 1) new_config[TASK_KEY_NAME]["command"] = command_name new_config[FRECKLET_KEY_NAME]["type"] = task_type become = new_config[TASK_KEY_NAME].get("become", None) if task_type and task_type.isupper() and become is None: new_config[TASK_KEY_NAME]["become"] = True new_config[FRECKLET_KEY_NAME]["type"] = task_type.lower() if command_name.isupper() and become is None: new_config[TASK_KEY_NAME]["become"] = True new_config[TASK_KEY_NAME]["command"] = command_name.lower() name = new_config[FRECKLET_KEY_NAME].get("name") if name.isupper(): name = name.lower() new_config[FRECKLET_KEY_NAME]["name"] = name return new_config
[docs]class MoveEmbeddedTaskKeysProcessor(ConfigProcessor): """Moves keys that start with __task__ from the vars to the task sub-dict."""
[docs] def process_current_config(self): new_config = self.current_input_config vars = new_config.get("vars", None) if vars is None: vars = {} new_config["vars"] = vars vars_to_move = {} for k in list(vars.keys()): if "::" in k: vars_to_move[k] = vars.pop(k) for k, v in vars_to_move.items(): add_key_to_dict(new_config, k, v, split_token="::") return new_config
[docs]class AddRootFreckletProcessor(ConfigProcessor): def __init__(self, **init_params): self.ting = init_params["ting"]
[docs] def process_current_config(self): new_config = self.current_input_config return {"root_frecklet": self.ting, "task": new_config}
# return {"task": new_config}
[docs]class CleanupFreckletProcessor(ConfigProcessor): def __init__(self, **init_params): pass
[docs] def process_current_config(self): new_config = self.current_input_config new_config["task"].pop("doc", None) return new_config
[docs]class NodeCounter(object): def __init__(self): self._current = 0
[docs] def up(self): self._current = self._current + 1
@property def current_count(self): return self._current
[docs]class TaskTreeAttribute(TingAttribute):
[docs] def provides(self): return ["task_tree", "task_leaves", "resources"]
[docs] def requires(self): return ["tasklist_detailed"]
[docs] def get_attribute(self, ting, attribute_name=None): counter = NodeCounter() task_tree = Tree() task_tree.create_node(identifier=counter.current_count, tag=ting.id, data=ting) self._build_tree(task_tree=task_tree, ting=ting, counter=counter) temp_leaves = task_tree.leaves() task_leaves = [] requirements = {} for t in temp_leaves: leaf = t.data["task"] task_leaves.append(leaf) req = leaf[FRECKLET_KEY_NAME].get("resources", {}) for k, v in req.items(): if isinstance(v, string_types) or not isinstance(v, Sequence): v = [v] requirements.setdefault(k, []).extend(v) result = { "task_tree": task_tree, "task_leaves": task_leaves, "resources": requirements, } return MultiCacheResult(**result)
def _build_tree(self, task_tree, ting, counter): parent_id = counter.current_count for task in ting.tasklist_detailed: task_name = task["task"][FRECKLET_KEY_NAME]["name"] task_type = task["task"][FRECKLET_KEY_NAME].get("type", "frecklet") if task_type != "frecklet": counter.up() task_tree.create_node( identifier=counter.current_count, tag="{}:{}".format(task_type, task_name), data=task, parent=parent_id, ) continue task_ting = ting._meta_parent_repo.get(task_name) if task_ting is None: if os.path.exists(ting.id): f_name = os.path.basename(ting.id) else: f_name = ting.id raise FreckletBuildException( frecklet=ting.id, msg="No child-frecklet named '{}' in context.".format(task_name), solution="""Make sure the frecklet '{}' is contained in one of the repos of this context. If the frecklet is in the current path, you can add the current path to the context via the '--repo' argument: {}frecklecute {}--repo .{} {}{} <arg> ...{} """.format( task_name, Style.DIM, Style.BRIGHT, Style.RESET_ALL, Style.DIM, f_name, Style.RESET_ALL, ), references={ "Context documentation": "https://freckles.io/doc/configuration#context" }, ) counter.up() task_tree.create_node( identifier=counter.current_count, tag="{}:{}".format(task_type, task_name), data=task, parent=parent_id, ) self._build_tree(task_tree=task_tree, ting=task_ting, counter=counter)
[docs]class TaskListDetailedAttribute(TingAttribute): FRECKLET_FORMAT = { CHILD_MARKER_NAME: FRECKLETS_KEY, DEFAULT_LEAF_NAME: FRECKLET_KEY_NAME, DEFAULT_LEAFKEY_NAME: "name", OTHER_KEYS_NAME: [ "args", "doc", # "frecklet_meta", "meta", # "const", FRECKLET_KEY_NAME, TASK_KEY_NAME, ], KEY_MOVE_MAP_NAME: {"*": ("vars", "default")}, "use_context": False, }
[docs] def provides(self): return ["tasklist_detailed"]
[docs] def requires(self): return ["args", "_meta_parent_repo", "_metadata"]
[docs] def get_attribute(self, ting, attribute_name=None): log.debug("Processing tasklist for frecklet: {}".format(ting.id)) chain = [ ExplodedArgsProcessor(args=ting.args), SpecialCaseProcessor(), FrklProcessor(**TaskListDetailedAttribute.FRECKLET_FORMAT), # DirectParentArgsProcessor(index=ting._meta_parent_repo), CommandNameProcessor(), TaskTypePrefixProcessor(), MoveEmbeddedTaskKeysProcessor(), # TaskPathProcessor(index=self.index), AddRootFreckletProcessor(ting=ting), CleanupFreckletProcessor(), ] f = Frkl([ting._metadata], chain) tasklist_detailed = f.process() return tasklist_detailed
[docs]def prettyfiy_task(task_detailed, arg_map): t = {} t[FRECKLET_KEY_NAME] = task_detailed[FRECKLET_KEY_NAME] if TASK_KEY_NAME in task_detailed.keys(): t[TASK_KEY_NAME] = task_detailed[TASK_KEY_NAME] if VARS_KEY in task_detailed.keys(): t[VARS_KEY] = task_detailed[VARS_KEY] # if CONST_KEY_NAME in task_detailed.keys(): # t[CONST_KEY_NAME] = task_detailed[CONST_KEY_NAME] tks = get_template_keys(t, jinja_env=DEFAULT_FRECKLES_JINJA_ENV) args = {} for tk in tks: temp = arg_map.get(tk, None) if temp is None: continue # temp = task_detailed[ARGS_KEY][tk] args[tk] = special_dict_to_dict(temp) t[ARGS_KEY] = args return t
[docs]class TaskListAttribute(TingAttribute):
[docs] def provides(self): return ["tasklist"]
[docs] def requires(self): return ["tasklist_detailed"]
[docs] def get_attribute(self, ting, attribute_name=None): tasklist = [] for td_node in ting.tasklist_detailed: td = td_node["task"] t = prettyfiy_task(td, ting.vars_frecklet) tasklist.append(t) return tasklist
[docs]class TaskListResolvedAttribute(TingAttribute):
[docs] def provides(self): return ["tasklist_resolved"]
[docs] def requires(self): return ["task_tree"]
[docs] def get_attribute(self, ting, attribute_name=None): tasklist = [] for td_node in ting.task_tree.leaves(): td = td_node.data["task"] t = prettyfiy_task(td, ting.vars_frecklet) tasklist.append(t) return tasklist