Exemplo n.º 1
0
    def clone(self, name: str) -> str:
        """
        Clone a remote directory into the store.
        :param name: Name of repository
        :return: (string): Path to cloned repository
        """
        if not name:
            raise ValueError("Missing or bad name passed to Clone command.")

        self.name = name
        path = self.store + os.sep + name
        self.path = path

        if os.path.exists(path):
            self.Repo = Repo(path)

            # FIX for #56
            if self.repo_url not in self.Repo.remotes.origin.urls:
                logger.info('Found new remote URL for this named repo')

                try:
                    # only recourse is to remove the .git directory
                    if os.path.exists(os.path.join(path, '.git')):
                        shutil.rmtree(path)

                    else:
                        raise SkilletLoaderException(
                            'Refusing to remove non-git directory')

                except OSError:
                    raise SkilletLoaderException('Repo directory exists!')

                logger.debug("Cloning into {}".format(path))

                try:
                    self.Repo = Repo.clone_from(self.repo_url, path)

                except GitCommandError as gce:
                    raise SkilletLoaderException(
                        f'Could not clone repository {gce}')

            else:
                logger.debug("Updating repository...")

                try:
                    self.Repo.remotes.origin.pull()

                except GitCommandError as gce:
                    logger.error('Could not clone repository!')
                    raise SkilletLoaderException(
                        f'Error Cloning repository {gce}')

            return path

        else:
            logger.debug("Cloning into {}".format(path))
            self.Repo = Repo.clone_from(self.repo_url, path)

        self.path = path
        return path
Exemplo n.º 2
0
    def cherry_pick_element(self, element: str, cherry_pick_path: str) -> str:
        """
        Cherry picking allows the skillet builder to pull out specific bits of a larger configuration
        and load only the smaller chunks. This is especially useful when combined with 'when' conditionals

        :param element: string containing the jinja templated xml fragment
        :param cherry_pick_path: string describing the relative xpath to use to cherry pick an xml node from the
            element given as a parameter
        :return: rendered and cherry_picked element
        """

        # first, we need to render the entire element so we can parse it with xpath
        rendered_element = self.render(element, self.context).strip()
        # convert this string into an xml doc we can search for the cherry_pick path
        try:
            element_doc = elementTree.fromstring(
                f'<xml>{rendered_element}</xml>')
            cherry_picked_element = element_doc.find(cherry_pick_path)
            if cherry_picked_element is None:
                raise SkilletLoaderException(
                    'Could not locate cherry_pick path in source xml! '
                    'Check the cherry_pick xpath!')
            new_element = elementTree.tostring(cherry_picked_element).strip()
            return new_element

        except ParseError:
            raise SkilletLoaderException(
                f'Could not parse element for cherry picking for snippet: {self.name}'
            )
Exemplo n.º 3
0
    def _parse_skillet(self, path: (str, Path)) -> dict:
        if type(path) is str:
            path_str = path
            path_obj = Path(path)
        elif isinstance(path, Path):
            path_str = str(path)
            path_obj = path
        else:
            raise SkilletLoaderException(
                f'Invalid path type found in _parse_skillet!')

        if 'meta-cnc' in path_str:
            meta_cnc_file = path_obj
            if not path_obj.exists():
                raise SkilletNotFoundException(
                    f'Could not find .meta-cnc file as this location: {path}')
        else:
            # we were only passed a directory like '.' or something, try to find a .meta-cnc.yaml or .meta-cnc.yml
            directory = path_obj
            logger.debug(f'using directory {directory}')
            found_meta = False
            for filename in [
                    '.meta-cnc.yaml', '.meta-cnc.yml', 'meta-cnc.yaml',
                    'meta-cnc.yml'
            ]:
                meta_cnc_file = directory.joinpath(filename)
                logger.debug(f'checking now {meta_cnc_file}')
                if meta_cnc_file.exists():
                    found_meta = True
                    break

            if not found_meta:
                raise SkilletNotFoundException(
                    'Could not find .meta-cnc file at this location')

        snippet_path = str(meta_cnc_file.parent.absolute())
        try:
            with meta_cnc_file.open(mode='r') as sc:
                raw_service_config = oyaml.safe_load(sc.read())
                skillet = self._normalize_skillet_structure(raw_service_config)
                skillet['snippet_path'] = snippet_path
                return skillet

        except IOError as ioe:
            logger.error('Could not open metadata file in dir %s' %
                         meta_cnc_file.parent)
            raise SkilletLoaderException(
                'IOError: Could not parse metadata file in dir %s' %
                meta_cnc_file.parent)
        except YAMLError as ye:
            logger.error(ye)
            raise SkilletLoaderException(
                'YAMLError: Could not parse metadata file in dir %s' %
                meta_cnc_file.parent)
        except Exception as ex:
            logger.error(ex)
            raise SkilletLoaderException(
                'Exception: Could not parse metadata file in dir %s' %
                meta_cnc_file.parent)
Exemplo n.º 4
0
    def get_skillet_with_name(
            self,
            skillet_name: str,
            include_resolved_skillets=False) -> (Skillet, None):
        """
        Returns a single skillet from the loaded skillets list that has the matching 'name' attribute

        :param skillet_name: Name of the skillet to return
        :param include_resolved_skillets: boolean of whether to also check the resolved skillet list
        :return: Skillet or None
        """

        if not self.skillets and not self.resolved_skillets:
            raise SkilletLoaderException("No Skillets have been loaded!")

        for skillet in self.skillets:
            if skillet.name == skillet_name:
                return skillet

        # also check the resolved skillet list, which are skillets that are included from snippet includes
        if include_resolved_skillets:

            for skillet in self.resolved_skillets:
                if skillet.name == skillet_name:
                    return skillet

        return None
Exemplo n.º 5
0
    def _handle_base64_outputs(self, results: str) -> dict:
        """
        Parses results and returns a dict containing base64 encoded values

        :param results: string as returned from some action, to be encoded as base64
        :return: dict containing all outputs found from the capture pattern in each output
        """

        outputs = dict()

        snippet_name = 'unknown'
        if 'name' in self.metadata:
            snippet_name = self.metadata['name']

        try:
            if 'outputs' not in self.metadata:
                logger.info(
                    f'No output defined in this snippet {snippet_name}')
                return outputs

            for output in self.metadata['outputs']:
                if 'name' not in output:
                    continue

                results_as_bytes = bytes(results, 'utf-8')
                encoded_results = urlsafe_b64encode(results_as_bytes)
                var_name = output['name']
                outputs[var_name] = encoded_results.decode('utf-8')

        except TypeError:
            raise SkilletLoaderException(
                f'Could not base64 encode results {snippet_name}')

        return outputs
Exemplo n.º 6
0
    def __get_value_from_path(self, obj: dict, config_path: str) -> Any:

        if type(obj) is not dict and type(obj) is not OrderedDict:
            logger.error("Supplied object is not an Object")
            logger.error(
                'Ensure you are passing an object here and not a string as from capture_pattern'
            )
            raise SkilletLoaderException(
                'Incorrect object format for get_value_from_path')

        if '.' in config_path or '/' in config_path:
            if '.' in config_path:
                separator = '.'
            else:
                separator = '/'
            path_elements = config_path.split(separator)
            first_path_element = path_elements[0]
            p0 = self.__check_inner_object(obj, first_path_element)
            for p in path_elements:
                if self.__has_child_node(p0, p):
                    new_p0 = p0[p]
                    p0 = new_p0
                else:
                    raise NodeNotFoundException(f'{config_path} not found!')

            return p0

        p0 = self.__check_inner_object(obj, config_path)

        if self.__has_child_node(p0, config_path):
            return p0[config_path]
        else:
            raise NodeNotFoundException(f'{config_path} not found!')
Exemplo n.º 7
0
    def get_output(self) -> Tuple[str, str]:
        if not self.detach:
            return '', 'success'

        try:
            container = self.get_container()

            if self.last_logs_time is None:
                return_data = container.logs()
            else:
                return_data = container.logs(since=self.last_logs_time)

            self.last_logs_time = int(time.time())

            return_str = self.__clean_output(return_data)

            if container.status == 'running':
                return return_str, 'running'

            else:
                logger.info(container.status)
                return return_str, self.__get_container_status()

        except APIError as ae:
            raise SkilletLoaderException(
                f'Could not get logs for {self.name}: {ae}')
Exemplo n.º 8
0
    def load_template(self, template_path: str) -> str:
        """
        Utility method to load a template file and return the contents as str

        :param template_path: relative path to the template to load
        :return: str contents
        """
        if template_path == '' or template_path is None:
            logger.error('Refusing to load empty template path')
            return ''

        skillet_path = Path(self.path)
        template_file = skillet_path.joinpath(template_path).resolve()

        if template_file.exists():
            with template_file.open(encoding='utf-8') as sf:
                return html.unescape(sf.read())

        else:
            # Add the snippet name here as well to allow for more context
            # fix for https://gitlab.com/panw-gse/as/panhandler/-/issues/19
            logger.error(
                f'Snippet: {self.name} has file attribute that does not exist')
            logger.error(f'Snippet file path is: {template_path}')
            raise SkilletLoaderException(
                f'Snippet: {self.name} - Could not resolve template path!')
Exemplo n.º 9
0
 def create_skillet(self, skillet_dict: dict) -> Skillet:
     skillet_type = skillet_dict['type']
     if skillet_type == 'panos' or skillet_type == 'panorama' or skillet_type == 'panorama-gpcs':
         from skilletlib.skillet.panos import PanosSkillet
         return PanosSkillet(skillet_dict)
     elif skillet_type == 'pan_validation':
         from skilletlib.skillet.pan_validation import PanValidationSkillet
         return PanValidationSkillet(skillet_dict)
     elif skillet_type == 'python3':
         from skilletlib.skillet.python3 import Python3Skillet
         return Python3Skillet(skillet_dict)
     elif skillet_type == 'template':
         from skilletlib.skillet.template import TemplateSkillet
         return TemplateSkillet(skillet_dict)
     elif skillet_type == 'docker':
         from skilletlib.skillet.docker import DockerSkillet
         return DockerSkillet(skillet_dict)
     elif skillet_type == 'rest':
         from skilletlib.skillet.rest import RestSkillet
         return RestSkillet(skillet_dict)
     elif skillet_type == 'workflow':
         from skilletlib.skillet.workflow import WorkflowSkillet
         return WorkflowSkillet(skillet_dict, self)
     else:
         raise SkilletLoaderException('Unknown Skillet Type!')
Exemplo n.º 10
0
    def sanitize_metadata(self, metadata: dict) -> dict:
        """
        Ensure all required keys are present in the snippet definition

        :param metadata: dict
        :return: dict
        """
        metadata = super().sanitize_metadata(metadata)

        err = f'Unknown cmd {self.cmd}'
        if self.cmd in ('set', 'edit', 'override'):
            if {'xpath', 'element'}.issubset(metadata):
                return metadata
            elif {'xpath', 'file'}.issubset(metadata):
                return metadata
            err = 'xpath and either file or element attributes are required for set, edit, or override cmds'
        elif self.cmd in ('show', 'get', 'delete'):
            if {'xpath'}.issubset(metadata):
                return metadata
            err = 'xpath attribute is required for show, get, or delete cmds'
        elif self.cmd == 'move':
            if 'where' in metadata:
                return metadata
            err = 'where attribute is required for move cmd'
        elif self.cmd in ('rename', 'clone'):
            if 'new_name' in metadata or 'newname' in metadata:
                return metadata
            err = 'new_name attribute is required for rename or move cmd'
        elif self.cmd == 'clone':
            if 'xpath_from' in metadata:
                return metadata
            err = 'xpath_from attribute is required for clone cmd'
        elif self.cmd == 'op' or self.cmd == 'cli':
            if 'cmd_str' in metadata:
                return metadata
            err = 'cmd_str attribute is required for op or cli cmd'
        elif self.cmd == 'validate':
            if {'test', 'label', 'documentation_link'}.issubset(metadata):
                # configure validation outputs manually if necessary
                # for validation we only need the output_type set to 'validation'
                metadata['output_type'] = 'validation'
                return metadata
            err = 'test, label, and documentation_link are required attributes for validate cmd'
        elif self.cmd == 'parse':
            if {'variable', 'outputs'}.issubset(metadata):
                return metadata
            err = 'variable and outputs are required attributes for parse cmd'
        elif self.cmd == 'validate_xml':
            if {'xpath'}.issubset(metadata):
                if 'file' in metadata or 'element' in metadata:
                    metadata['output_type'] = 'validation'
                    return metadata
            err = 'xpath and file or element are required attributes for validate_xml cmd'
        elif self.cmd == 'noop':
            if 'output_type' not in metadata:
                metadata['output_type'] = 'manual'
            return metadata

        raise SkilletLoaderException(f'Invalid metadata configuration: {err}')
Exemplo n.º 11
0
    def create_skillet(self, skillet_dict: dict) -> Skillet:
        """
        Creates a Skillet object from the given skillet definition

        :param skillet_dict: Dictionary loaded from the skillet.yaml definition file
        :return: Skillet Object
        """
        skillet_type = skillet_dict["type"]

        if skillet_type == "panos" or skillet_type == "panorama" or skillet_type == "panorama-gpcs":
            from skilletlib.skillet.panos import PanosSkillet

            return PanosSkillet(skillet_dict)

        elif skillet_type == "pan_validation":
            from skilletlib.skillet.pan_validation import PanValidationSkillet

            return PanValidationSkillet(skillet_dict)

        elif skillet_type == "python3":
            from skilletlib.skillet.python3 import Python3Skillet

            return Python3Skillet(skillet_dict)

        elif skillet_type == "template":
            from skilletlib.skillet.template import TemplateSkillet

            return TemplateSkillet(skillet_dict)

        elif skillet_type == "docker":
            from skilletlib.skillet.docker import DockerSkillet

            return DockerSkillet(skillet_dict)

        elif skillet_type == "rest":
            from skilletlib.skillet.rest import RestSkillet

            return RestSkillet(skillet_dict)

        elif skillet_type == "workflow":
            from skilletlib.skillet.workflow import WorkflowSkillet

            return WorkflowSkillet(skillet_dict, self)

        elif skillet_type == "terraform":
            from skilletlib.skillet.terraform import TerraformSkillet

            return TerraformSkillet(skillet_dict)

        elif skillet_type == "app":
            from skilletlib.skillet.app import AppSkillet

            return AppSkillet(skillet_dict)

        else:
            raise SkilletLoaderException(
                f"Unknown Skillet Type: {skillet_type}!")
Exemplo n.º 12
0
    def get_skillet_with_name(self, skillet_name: str) -> (Skillet, None):

        if not self.all_skillets:
            raise SkilletLoaderException('No Skillets have been loaded!')

        for skillet in self.all_skillets:
            if skillet.name == skillet_name:
                return skillet

        return None
Exemplo n.º 13
0
    def __difference(list1: list, list2: list) -> list:
        """
        Returns a list of items from list1 that do not exist in list2

        :param list1: list of items expected to be in list2
        :param list2: list of items list1 will be evaluated against
        """
        if not isinstance(list1, list) or not isinstance(list2, list):
            raise SkilletLoaderException(
                'difference filter takes only type list for both arguments.')

        return list(set([x for x in list1 if x not in list2]))
Exemplo n.º 14
0
    def __json_query(obj: dict, query: str) -> Any:
        """
        JMESPath query, jmespath.org for examples

        :param query: JMESPath query string
        :param obj: object to be queried
        """
        if not isinstance(query, str):
            raise SkilletLoaderException(
                'json_query requires an argument of type str')
        path = jmespath.search(query, obj)
        return path
Exemplo n.º 15
0
    def sanitize_metadata(self, metadata):
        """
        Ensure the configured metadata is valid for this snippet type
        :param metadata: dict
        :return: validated metadata dict
        """
        name = metadata.get('name', '')
        if not self.required_metadata.issubset(metadata):
            for attr_name in metadata:
                if attr_name not in self.required_metadata:
                    raise SkilletLoaderException(
                        f'Invalid snippet metadata configuration: attribute: {attr_name} '
                        f'is required for snippet: {name}')

        return metadata
Exemplo n.º 16
0
    def create_skillet(self, skillet_dict: dict) -> Skillet:
        """
        Creates a Skillet object from the given skillet definition

        :param skillet_dict: Dictionary loaded from the skillet.yaml definition file
        :return: Skillet Object
        """
        skillet_type = skillet_dict['type']

        if skillet_type == 'panos' or skillet_type == 'panorama' or skillet_type == 'panorama-gpcs':
            from skilletlib.skillet.panos import PanosSkillet
            return PanosSkillet(skillet_dict)

        elif skillet_type == 'pan_validation':
            from skilletlib.skillet.pan_validation import PanValidationSkillet
            return PanValidationSkillet(skillet_dict)

        elif skillet_type == 'python3':
            from skilletlib.skillet.python3 import Python3Skillet
            return Python3Skillet(skillet_dict)

        elif skillet_type == 'template':
            from skilletlib.skillet.template import TemplateSkillet
            return TemplateSkillet(skillet_dict)

        elif skillet_type == 'docker':
            from skilletlib.skillet.docker import DockerSkillet
            return DockerSkillet(skillet_dict)

        elif skillet_type == 'rest':
            from skilletlib.skillet.rest import RestSkillet
            return RestSkillet(skillet_dict)

        elif skillet_type == 'workflow':
            from skilletlib.skillet.workflow import WorkflowSkillet
            return WorkflowSkillet(skillet_dict, self)

        elif skillet_type == 'terraform':
            from skilletlib.skillet.terraform import TerraformSkillet
            return TerraformSkillet(skillet_dict)

        elif skillet_type == 'app':
            from skilletlib.skillet.app import AppSkillet
            return AppSkillet(skillet_dict)

        else:
            raise SkilletLoaderException(
                f'Unknown Skillet Type: {skillet_type}!')
Exemplo n.º 17
0
    def get_skillet_with_name(self, skillet_name: str) -> (Skillet, None):
        """
        Returns a single skillet from the loaded skillets list that has the matching 'name' attribute

        :param skillet_name: Name of the skillet to return
        :return: Skillet
        """

        if not self.skillets:
            raise SkilletLoaderException('No Skillets have been loaded!')

        for skillet in self.skillets:
            if skillet.name == skillet_name:
                return skillet

        return None
Exemplo n.º 18
0
    def __init__(self, repo_url, store=os.getcwd()):
        """
        Initialize a new Git repo object
        :param repo_url: URL path to repository.
        :param store: Directory to store repository in. Defaults to the current directory.
        """
        if not self.check_git_exists():
            raise SkilletLoaderException(
                'A git client must be installed to use this remote!')

        self.repo_url = repo_url
        self.store = store
        self.Repo = None
        self.name = ''
        self.path = ''
        self.update = ''
Exemplo n.º 19
0
    def execute(self, initial_context: dict) -> dict:

        context = dict()

        try:
            context = self.initialize_context(initial_context)

            for snippet in self.get_snippets():
                # render anything that looks like a jinja template in the snippet metadata
                # mostly useful for xpaths in the panos case
                metadata = snippet.render_metadata(context)
                # check the 'when' conditional against variables currently held in the context
                if snippet.should_execute(context):
                    (output, status) = snippet.execute(context)
                    running_counter = 0
                    while status == 'running':
                        logger.info('Snippet still running...')
                        time.sleep(5)
                        (output, status) = snippet.get_output()
                        running_counter += 1
                        if running_counter > 60:
                            raise SkilletLoaderException('Snippet took too long to execute!')

                    returned_output = snippet.capture_outputs(output)
                    context.update(returned_output)

                else:
                    fail_action = metadata.get('fail_action', 'skip')
                    fail_message = metadata.get('fail_message', 'Aborted due to failed conditional!')
                    if fail_action == 'skip':
                        logger.debug(f'  Skipping Snippet: {snippet.name}')
                    else:
                        logger.debug('Conditional failed and found a fail_action')
                        logger.error(fail_message)
                        context['fail_message'] = fail_message
                        return context
        except SkilletLoaderException as sle:
            logger.error(f'Caught Exception during execution: {sle}')

        except Exception as e:
            logger.error(f'Exception caught: {e}')
        finally:
            self.cleanup()

        return context
Exemplo n.º 20
0
    def __listify(obj: Any) -> list:
        """
        Attempt to convert a string input into a list

        :param obj: raw input text
        """
        if isinstance(obj, list):
            return obj

        if isinstance(obj, str):
            if '\n' in obj:
                return [x.strip() for x in obj.split('\n') if x]
            elif ',' in obj:
                return [x.strip() for x in obj.split(',') if x]
            return [obj.strip()]

        raise SkilletLoaderException(
            'listify filter requires input of type str.')
Exemplo n.º 21
0
    def __validate_snippet_metadata(self) -> None:
        """
        Perform snippet metadata validation before we attempt to instantiate the snippet

        This will throw a SkilletLoaderException if a required attribute is not present in the metadata
        Will also set all optional metadata attributes with their default values

        :raises: SkilletLoaderException if a required field is not present
        :return: None
        """
        for s in self.snippet_stack:
            name = s.get('name', '')
            for r in self.snippet_required_metadata:
                if r not in s:
                    raise SkilletLoaderException(
                        f'Invalid snippet metadata configuration: attribute: {r} '
                        f'is required for snippet: {name}')

            for k, v in self.snippet_optional_metadata.items():
                if k not in s:
                    s[k] = v
Exemplo n.º 22
0
    def cleanup(self) -> None:
        """
        Clean up action is the docker container was started with 'async', no-op is async is False
        :return: None
        """
        if not self.detach:
            return

        try:
            container = self.get_container()

            if container:
                if container.status != 'running':
                    container.remove()
                else:
                    logger.warning(
                        f'Docker container {self.container_id} may need to be manually removed!'
                    )

        except APIError as ae:
            raise SkilletLoaderException(
                f'Could not clean up {self.name}: {ae}')
Exemplo n.º 23
0
    def execute(self, context) -> Tuple[str, str]:
        """
        Execute this cmd in the specified docker container

        :param context: context containing all the user-supplied input variables. Also contains output from previous
        steps. Raises SkilletLoaderException on error
        :return:  Tuple(dict, str) output and string representing 'success' or 'failure'
        """

        try:
            self.client = DockerClient()

        except DockerException:
            logger.error(traceback.format_exc())
            raise SkilletLoaderException(
                f'Could not contact Docker API in {self.name}')

        try:

            logger.info(f'Pulling image: {self.image} with tag: {self.tag}')
            self.client.images.pull(self.image, self.tag)

            vols = self.volumes

            image = self.image + ":" + self.tag
            logger.info('Creating container...')
            return_data = self.client.containers.run(
                image,
                self.metadata['cmd'],
                volumes=vols,
                stderr=True,
                detach=self.detach,
                working_dir=self.working_dir,
                user=self.metadata.get('user', 'root'),
                auto_remove=self.auto_remove,
                environment=context)

            if self.detach:
                # return_data will be a Container object if self.detach is True
                self.container_id = return_data.id
                output = self.container_id
                print('container id is ' + self.container_id)
                return output, 'running'

            else:
                # return_data will be the bytes returned from the command
                if type(return_data) is bytes:
                    return_str = return_data.decode('UTF-8')
                    return return_str, self.__get_container_status()
                else:
                    return return_data, self.__get_container_status()

        except ImageNotFound:
            logger.error(traceback.format_exc())
            raise SkilletLoaderException(
                f'Could not locate image {self.image} in {self.name}')
        except APIError as ae:
            logger.error(traceback.format_exc())
            raise SkilletLoaderException(
                f'Error communicating with Docker API: {ae}')
        except ContainerError as ce:
            logger.error(traceback.format_exc())
            raise SkilletLoaderException(f'Container command failed: {ce}')
        except DockerException as de:
            logger.error(traceback.format_exc())
            raise SkilletLoaderException(
                f'Could not execute docker container {self.name}: {de}')
        except ValueError as ve:
            # added or GL #77 - add diagnostics for failed docker container creation
            logger.error(traceback.format_exc())
            raise SkilletLoaderException(
                f'Could not execute docker container ValueError in {self.name}: {ve}'
            )
Exemplo n.º 24
0
    def execute(self, initial_context: dict) -> dict:
        """
        The heart of the Skillet class. This method executes the skillet by iterating over all the skillets returned
        from the 'get_skillets' method. Each one is checked if it should be executed if a 'when' conditional attribute
        is found, and if so, is executed using the snippet execute method.

        :param initial_context: context of key values pairs to use for the execution. By default this is all the
        variables defined in the skillet file with their default values. Updates from user input, the environment, etc
        will override these default values via the 'update_context' method.
        :return: a dict containing the updated context containing the output of each of the snippets
        """
        try:
            context = self.initialize_context(initial_context)
            logger.debug(f'Executing Skillet: {self.name}')

            for snippet in self.get_snippets():
                try:
                    # render anything that looks like a jinja template in the snippet metadata
                    # mostly useful for xpaths in the panos case
                    snippet.render_metadata(context)
                    # check the 'when' conditional against variables currently held in the context

                    if snippet.should_execute(context):
                        (output, status) = snippet.execute(context)
                        logger.debug(f'{snippet.name} - status: {status}')

                        if output:
                            logger.debug(f'{snippet.name} - output: {output}')

                        running_counter = 0

                        while status == 'running':
                            logger.info('Snippet still running...')
                            time.sleep(5)
                            (output, status) = snippet.get_output()
                            running_counter += 1

                            if running_counter > 60:
                                raise SkilletLoaderException(
                                    'Snippet took too long to execute!')

                        # capture all outputs
                        snippet_outputs = snippet.get_default_output(
                            output, status)
                        captured_outputs = snippet.capture_outputs(
                            output, status)

                        if captured_outputs:
                            logger.debug(
                                f'{snippet.name} - captured_outputs: {captured_outputs}'
                            )

                        self.snippet_outputs.update(snippet_outputs)
                        self.captured_outputs.update(captured_outputs)

                        context.update(snippet_outputs)
                        context.update(captured_outputs)

                except SkilletLoaderException as sle:
                    logger.error(f'Caught Exception during execution: {sle}')
                    snippet_outputs = snippet.get_default_output(
                        str(sle), 'error')
                    logger.error(snippet_outputs)
                    self.snippet_outputs.update(snippet_outputs)

                except Exception as e:
                    logger.error(f'Exception caught: {e}')
                    snippet_outputs = snippet.get_default_output(
                        str(e), 'error')
                    self.snippet_outputs.update(snippet_outputs)

        finally:
            self.cleanup()

        return self.get_results()
Exemplo n.º 25
0
    def compile_skillet_dict(self, skillet: dict) -> dict:
        """
        Compile the skillet dictionary including any included snippets from other skillets. Included snippets and
        variables will be inserted into the skillet dictionary and any replacements / updates to those snippets /
        variables will be made before hand.

        :param skillet: skillet definition dictionary
        :return: full compiled skillet definition dictionary
        """
        snippets = list()
        variables: list = skillet['variables']

        for snippet in skillet.get('snippets', []):

            if 'include' not in snippet:
                snippets.append(snippet)
                continue

            included_skillet: Skillet = self.get_skillet_with_name(
                snippet['include'], include_resolved_skillets=True)
            if included_skillet is None:
                raise SkilletLoaderException(
                    f'Could not find included Skillet with name: {snippet["include"]}'
                )

            if 'include_snippets' not in snippet and 'include_variables' not in snippet:
                # include all snippets by default
                for included_snippet in included_skillet.snippet_stack:
                    propagated_snippet = self.__propagate_snippet_metadata(
                        snippet, included_snippet)
                    snippets.append(propagated_snippet)

                for v in included_skillet.variables:
                    found_variable = False
                    for tv in skillet['variables']:
                        if tv['name'] == v['name']:
                            # do not add variable if one with the same name already exists
                            found_variable = True

                    if not found_variable:
                        # this variable does not exist in the skillet_dict variables, so add it here
                        variables.append(v)

            elif 'include_snippets' not in snippet:
                # include all snippets by default
                for included_snippet in included_skillet.snippet_stack:
                    included_snippet = self.__propagate_snippet_metadata(
                        snippet, included_snippet)
                    snippets.append(included_snippet)

            else:
                for include_snippet in snippet['include_snippets']:
                    include_snippet_name = include_snippet['name']
                    include_snippet_object = included_skillet.get_snippet_by_name(
                        include_snippet_name)
                    include_meta = include_snippet_object.metadata
                    # the meta attribute in the metadata is a dict that we do not want to completely overwrite
                    if 'meta' in include_snippet:
                        include_snippet_object_meta = include_meta.get(
                            'meta', {})
                        if isinstance(include_snippet_object_meta, dict) and \
                                isinstance(include_snippet.get('meta', {}), dict):
                            new_meta = include_snippet_object_meta.copy()
                            new_meta.update(include_snippet.get('meta', {}))
                            include_snippet['meta'] = new_meta

                    # propagate everything form the parent if it's there
                    include_meta = self.__propagate_snippet_metadata(
                        snippet, include_meta)

                    # update with locally defined options as well, if anyu
                    include_meta.update(include_snippet)

                    # ensure the name is set properly
                    include_meta[
                        'name'] = f'{included_skillet.name}.{include_snippet_name}'

                    snippets.append(include_meta)

            if 'include_variables' in snippet:
                if isinstance(snippet['include_variables'],
                              str) and snippet['include_variables'] == 'all':
                    for v in included_skillet.variables:
                        found_variable = False
                        for tv in skillet['variables']:
                            if tv['name'] == v['name']:
                                # do not add variable if one with the same name already exists
                                found_variable = True

                        if not found_variable:
                            # this variable does not exist in the skillet_dict variables, so add it here
                            variables.append(v)
                elif isinstance(snippet['include_variables'], list):
                    for v in snippet['include_variables']:
                        # we need to include only the variables listed here and possibly update them with any
                        # new / modified attributes
                        included_variable_orig = included_skillet.get_variable_by_name(
                            v['name'])

                        # #163 - always uses deepcopy when using includes / overrides
                        included_variable = copy.deepcopy(
                            included_variable_orig)
                        # update this variable definition accordingly if necessary
                        included_variable.update(v)

                        # now check to see if this skillet has this variable already defined
                        found_variable = False
                        for ev in variables:
                            if ev['name'] == v['name']:
                                found_variable = True
                                # it is nonsensical to update the variable definition here from the included skillet
                                # just use what is defined locally, otherwise the builder should not have defined it
                                # here!
                                logger.info(
                                    'not updating existing variable definition from '
                                    'the resolved skillet definition')

                        if not found_variable:
                            # this included variable was not defined locally, so go ahead and append the updated version
                            variables.append(included_variable)

        skillet['snippets'] = snippets
        skillet['variables'] = variables

        return skillet
Exemplo n.º 26
0
    def __handle_xml_outputs(self, output_definition: dict,
                             results: str) -> dict:
        """
        Parse the results string as an XML document
        Example skillet.yaml snippets section:
        snippets:

          - name: system_info
            path: /api/?type=op&cmd=<show><system><info></info></system></show>&key={{ api_key }}
            output_type: xml
            outputs:
              - name: hostname
                capture_value: result/system/hostname
              - name: uptime
                capture_value: result/system/uptime
              - name: sw_version
                capture_value: result/system/sw-version


        :param results: string as returned from some action, to be parsed as XML document
        :return: dict containing all outputs found from the capture pattern in each output
        """

        captured_output = dict()

        def unique_tag_list(elements: list) -> bool:
            tag_list = list()
            for el in elements:
                # some xpath queries can return a list of str
                if isinstance(el, str):
                    return False

                if el.tag not in tag_list:
                    tag_list.append(el.tag)

            if len(tag_list) == 1:
                # all tags in this list are the same
                return False
            else:
                # there are unique tags in this list
                return True

        def convert_entry(el: elementTree.Element):
            # force_lists always returns a list even though in most cases, we really only want a single item
            # due to an exact xpath match. However, we might still want force_list to apply further down in the
            # the document.
            tag_name = el.tag

            res = xmltodict.parse(elementTree.tostring(el),
                                  force_list=self.xml_force_list_keys)

            if tag_name not in self.xml_force_list_keys:
                return res

            if tag_name in res and \
                    isinstance(res[tag_name], list) and \
                    len(res[tag_name]) == 1:
                # unwind unnecessary list at the top level here
                return {tag_name: res[tag_name][0]}

            return res

        try:
            xml_doc = etree.XML(results)

            # xml_doc = elementTree.fromstring(results)
            # allow jinja syntax in capture_pattern, capture_value, capture_object etc

            local_context = self.context.copy()
            output = self.__render_output_metadata(output_definition,
                                                   local_context)

            var_name = output['name']
            if 'capture_pattern' in output or 'capture_value' in output:

                if 'capture_value' in output:
                    capture_pattern = output['capture_value']
                else:
                    capture_pattern = output['capture_pattern']

                # by default we will attempt to return the text of the found element
                return_type = 'text'
                entries = xml_doc.xpath(capture_pattern)
                logger.debug(f'found entries: {entries}')
                if len(entries) == 0:
                    captured_output[var_name] = ''
                elif len(entries) == 1:
                    entry = entries.pop()
                    if isinstance(entry, str):
                        captured_output[var_name] = str(entry)
                    else:
                        if len(entry) == 0:
                            # this tag has no children, so try to grab the text
                            if return_type == 'text':
                                captured_output[var_name] = str(
                                    entry.text).strip()
                            else:
                                captured_output[var_name] = entry.tag
                        else:
                            # we have 1 Element returned, so the user has a fairly specific xpath
                            # however, this element has children itself, so we can't return a text value
                            # just return the tag name of this element only
                            captured_output[var_name] = entry.tag
                else:
                    # we have a list of elements returned from the users xpath query
                    capture_list = list()
                    # are there unique tags in this list? or is this a list of the same tag names?
                    if unique_tag_list(entries):
                        return_type = 'tag'
                    for entry in entries:
                        if isinstance(entry, str):
                            capture_list.append(entry)
                        else:
                            if len(entry) == 0:
                                if return_type == 'text':
                                    if entry.text is not None:
                                        capture_list.append(entry.text.strip())
                                    else:
                                        # If there is no text, then try to grab a sensible attribute
                                        # if you need more control than this, then you should first
                                        # capture_object to convert to a python object then use a jinja filter
                                        # to get what you need
                                        if 'value' in entry.attrib:
                                            capture_list.append(
                                                entry.attrib.get('value', ''))
                                        elif 'name' in entry.attrib:
                                            capture_list.append(
                                                entry.attrib.get('name', ''))
                                        else:
                                            capture_list.append(
                                                json.dumps(dict(entry.attrib)))
                                else:
                                    capture_list.append(entry.tag)
                            else:
                                capture_list.append(entry.tag)

                    captured_output[var_name] = capture_list

            elif 'capture_object' in output:
                capture_pattern = output['capture_object']
                entries = xml_doc.xpath(capture_pattern)

                if len(entries) == 0:
                    captured_output[var_name] = None
                elif len(entries) == 1:
                    captured_output[var_name] = convert_entry(entries.pop())

                else:
                    capture_list = list()
                    for entry in entries:
                        capture_list.append(convert_entry(entry))

                    # FIXME - isn't this duplicated below?
                    captured_output[var_name] = self.__filter_outputs(
                        output, capture_list, self.context)

            elif 'capture_list' in output:
                capture_pattern = output['capture_list']
                entries = xml_doc.xpath(capture_pattern)

                capture_list = list()
                for entry in entries:
                    if isinstance(entry, str):
                        capture_list.append(entry)
                    else:
                        capture_list.append(convert_entry(entry))

                captured_output[var_name] = capture_list

            elif 'capture_xml' in output:
                capture_pattern = output['capture_xml']
                entries = xml_doc.xpath(capture_pattern)
                if len(entries) == 0:
                    captured_output[var_name] = None
                elif len(entries) == 1:
                    captured_output[var_name] = etree.tostring(
                        entries.pop(), encoding='unicode')
                else:
                    outer_tag = etree.fromstring('<xml/>')
                    for e in entries:
                        outer_tag.append(e)
                    found_entries_str = etree.tostring(outer_tag,
                                                       encoding='unicode')
                    captured_output[var_name] = found_entries_str

                # short circuit return here as it makes no sense to do the filtering on a plain string object
                return captured_output
            # filter selected items here
            captured_output[var_name] = self.__filter_outputs(
                output, captured_output[var_name], local_context)

        except ParseError:
            logger.error('Could not parse XML document in output_utils')
            # just return blank captured_outputs here
            raise SkilletLoaderException(
                f'Could not parse output as XML in {self.name}')

        return captured_output
Exemplo n.º 27
0
    def execute(self, initial_context: dict) -> dict:
        """
        The heart of the Skillet class. This method executes the skillet by iterating over all the skillets returned
        from the 'get_skillets' method. Each one is checked if it should be executed if a 'when' conditional attribute
        is found, and if so, is executed using the snippet execute method.

        :param initial_context: context of key values pairs to use for the execution. By default this is all the
        variables defined in the skillet file with their default values. Updates from user input, the environment, etc
        will override these default values via the 'update_context' method.
        :return: a dict containing the updated context containing the output of each of the snippets
        """
        try:
            # reset success on execution
            self.success = True
            context = self.initialize_context(initial_context)
            logger.debug(f'Executing Skillet: {self.name}')

            for snippet in self.get_snippets():
                try:
                    # allow subclasses to override this
                    snippet.update_context(context)

                    loop_vars = snippet.get_loop_parameter()
                    index = 0
                    for item in loop_vars:
                        context['loop'] = item
                        context['loop_index'] = index

                        # check the 'when' conditional against variables currently held in the context
                        if snippet.should_execute(context):

                            # fix for #136
                            snippet.render_metadata(context)

                            (output, status) = snippet.execute(context)
                            logger.debug(f'{snippet.name} - status: {status}')

                            if status != 'success':
                                self.success = False

                            if output:
                                logger.debug(
                                    f'{snippet.name} - output: {output}')

                            running_counter = 0

                            while status == 'running':
                                logger.info('Snippet still running...')
                                time.sleep(5)
                                (output, status) = snippet.get_output()
                                running_counter += 1

                                if running_counter > 60:
                                    raise SkilletLoaderException(
                                        'Snippet took too long to execute!')

                            # capture all outputs
                            snippet_outputs = snippet.get_default_output(
                                output, status)
                            captured_outputs = snippet.capture_outputs(
                                output, status)

                            if snippet.name in self.snippet_outputs:
                                self.snippet_outputs[snippet.name].append(
                                    snippet_outputs)
                            else:
                                # create a list of track progress here
                                self.snippet_outputs[snippet.name] = [
                                    snippet_outputs
                                ]

                            if captured_outputs:
                                logger.debug(
                                    f'{snippet.name} - captured_outputs: {captured_outputs}'
                                )
                                # fixme - how does this interact with looping?
                                self.captured_outputs.update(captured_outputs)

                            # simple context addition here, does not count on iteration ?
                            # context.update(snippet_outputs)
                            context.update(captured_outputs)

                        index = index + 1
                        snippet.reset_metadata()

                except SkilletLoaderException as sle:
                    self.success = False
                    logger.error(f'Caught Exception during execution: {sle}')
                    snippet_outputs = snippet.get_default_output(
                        str(sle), 'error')
                    logger.error(snippet_outputs)
                    if snippet.name in self.snippet_outputs:
                        self.snippet_outputs[snippet.name].append(
                            snippet_outputs)
                    else:
                        self.snippet_outputs[snippet.name] = [snippet_outputs]

                except Exception as e:
                    self.success = False
                    logger.error(
                        f'Exception caught in snippet: {snippet.name}: {e}')
                    snippet_outputs = snippet.get_default_output(
                        str(e), 'error')
                    if snippet.name in self.snippet_outputs:
                        self.snippet_outputs[snippet.name].append(
                            snippet_outputs)
                    else:
                        self.snippet_outputs[snippet.name] = [snippet_outputs]

        finally:
            self.cleanup()

        return self.get_results()
Exemplo n.º 28
0
    def compile_skillet_dict(self, skillet: dict) -> dict:
        """
        Compile the skillet dictionary including any included snippets from other skillets. Included snippets and
        variables will be inserted into the skillet dictionary and any replacements / updates to those snippets /
        variables will be made before hand.

        :param skillet: skillet definition dictionary
        :return: full compiled skillet definition dictionary
        """
        snippets = list()
        variables: list = skillet["variables"]
        parent_only_variables: list = variables.copy()

        for snippet in skillet.get("snippets", []):

            if "include" not in snippet:
                snippets.append(snippet)
                continue

            included_skillet: Skillet = self.get_skillet_with_name(
                snippet["include"], include_resolved_skillets=True)
            if included_skillet is None:
                raise SkilletLoaderException(
                    f'Could not find included Skillet with name: {snippet["include"]}'
                )

            if "include_snippets" not in snippet:
                # include all snippets by default
                for included_snippet in included_skillet.snippet_stack:
                    include_snippet_name = included_snippet["name"]
                    included_meta = self.__propagate_snippet_metadata(
                        snippet, included_snippet)
                    included_meta[
                        "name"] = f"{included_skillet.name}.{include_snippet_name}"
                    snippets.append(included_meta)

            else:
                for include_snippet in snippet["include_snippets"]:
                    include_snippet_name = include_snippet["name"]
                    include_snippet_object = included_skillet.get_snippet_by_name(
                        include_snippet_name)
                    include_meta = include_snippet_object.metadata
                    # the meta attribute in the metadata is a dict that we do not want to completely overwrite
                    if "meta" in include_snippet:
                        include_snippet_object_meta = include_meta.get(
                            "meta", {})
                        if isinstance(include_snippet_object_meta,
                                      dict) and isinstance(
                                          include_snippet.get("meta", {}),
                                          dict):
                            new_meta = include_snippet_object_meta.copy()
                            new_meta.update(include_snippet.get("meta", {}))
                            include_snippet["meta"] = new_meta

                    # propagate everything form the parent if it's there
                    include_meta = self.__propagate_snippet_metadata(
                        snippet, include_meta)

                    # update with locally defined options as well, if anyu
                    include_meta.update(include_snippet)

                    # ensure the name is set properly
                    include_meta[
                        "name"] = f"{included_skillet.name}.{include_snippet_name}"

                    snippets.append(include_meta)

            if "include_variables" not in snippet:
                for v in included_skillet.variables:
                    variables = self.__update_variable_list(
                        variables, parent_only_variables, v, False)
            else:
                if isinstance(snippet["include_variables"],
                              str) and snippet["include_variables"] == "all":
                    # incorporate every variable into the compiled skillet's variable list
                    for v in included_skillet.variables:
                        variables = self.__update_variable_list(
                            variables, parent_only_variables, v, False)

                elif isinstance(snippet["include_variables"], list):

                    # handle case where we have a single dict item with name == 'all' to override all snippet attributes
                    # see issue #182 for details
                    if len(snippet["include_variables"]) == 1 and snippet[
                            "include_variables"][0]["name"] == "all":
                        override_attribute = snippet["include_variables"][0]

                        for v in included_skillet.variables:
                            original_name = v["name"]
                            # #163 - always uses deepcopy when using includes / overrides
                            overridden_variable = copy.deepcopy(v)
                            # update this variable definition accordingly if necessary
                            overridden_variable.update(override_attribute)
                            # reformat name from 'all' back to original name
                            overridden_variable["name"] = original_name
                            # incorporate variable into compiled list but merge if var exists
                            variables = self.__update_variable_list(
                                variables, parent_only_variables,
                                overridden_variable, True)

                    # handle case where there's a list of variable to include and override
                    else:
                        for v in snippet["include_variables"]:
                            # we need to include only the variables listed here and possibly update them with any
                            # new / modified attributes
                            included_variable_orig = included_skillet.get_variable_by_name(
                                v["name"])

                            # #163 - always uses deepcopy when using includes / overrides
                            included_variable = copy.deepcopy(
                                included_variable_orig)
                            # update this variable definition accordingly if necessary
                            included_variable.update(v)

                            # incorporate variable into compiled list and no merge since in this case the
                            # child variable overrides anything that exists
                            variables = self.__update_variable_list(
                                variables, parent_only_variables,
                                included_variable, False)

        skillet["snippets"] = snippets
        skillet["variables"] = variables

        return skillet
Exemplo n.º 29
0
    def _parse_skillet(self, path: (str, Path)) -> dict:
        """
        Parse the skillet metadata file from the Path and return a valid skillet definition dictionary

        :param path: relative PosixPath of a file to load and validate
        :return: skillet definition dictionary
        """

        if type(path) is str:
            path_str = path
            path_obj = Path(path)

        elif isinstance(path, Path):
            path_str = str(path)
            path_obj = path

        else:
            raise SkilletLoaderException(
                "Invalid path type found in _parse_skillet!")

        if "meta-cnc" in path_str or "skillet.y" in path_str:
            meta_cnc_file = path_obj

            if not path_obj.exists():
                raise SkilletNotFoundException(
                    f"Could not find skillet.yaml file as this location: {path}"
                )

        else:
            # we were only passed a directory like '.' or something, try to find a skillet.yaml or .meta-cnc.yml
            directory = path_obj
            logger.debug(f"using directory {directory}")

            found_files = list()
            found_files.extend(directory.glob(".meta-cnc.y*"))
            found_files.extend(directory.glob("*skillet.y*"))

            if not found_files:
                raise SkilletNotFoundException(
                    "Could not find skillet definition file at this location")

            if len(found_files) > 1:
                logger.warning(
                    "Found more than 1 skillet file at this location! Using first file found!"
                )

            meta_cnc_file = found_files[0]

        if meta_cnc_file is None:
            raise SkilletNotFoundException(
                "Could not find skillet definition file at this location")

        snippet_path = str(meta_cnc_file.parent.absolute())
        skillet_file = str(meta_cnc_file.name)

        try:

            with meta_cnc_file.open(mode="r", encoding="utf-8") as sc:
                raw_service_config = oyaml.safe_load(sc.read())
                skillet = self.normalize_skillet_dict(raw_service_config)
                skillet["snippet_path"] = snippet_path
                skillet["skillet_path"] = snippet_path
                skillet["skillet_filename"] = skillet_file
                return skillet

        except IOError:
            logger.error("Could not open metadata file in dir %s" %
                         meta_cnc_file.parent)
            raise SkilletLoaderException(
                "IOError: Could not parse metadata file in dir %s" %
                meta_cnc_file.parent)

        except YAMLError as ye:
            logger.error(ye)
            raise SkilletLoaderException(
                "YAMLError: Could not parse metadata file in dir %s" %
                meta_cnc_file.parent)

        except Exception as ex:
            logger.error(ex)
            raise SkilletLoaderException(
                "Exception: Could not parse metadata file in dir %s" %
                meta_cnc_file.parent)
Exemplo n.º 30
0
    def __handle_xml_outputs(self, output_definition: dict,
                             results: str) -> dict:
        """
        Parse the results string as an XML document
        Example .meta-cnc snippets section:
        snippets:

          - name: system_info
            path: /api/?type=op&cmd=<show><system><info></info></system></show>&key={{ api_key }}
            output_type: xml
            outputs:
              - name: hostname
                capture_value: result/system/hostname
              - name: uptime
                capture_value: result/system/uptime
              - name: sw_version
                capture_value: result/system/sw-version


        :param results: string as returned from some action, to be parsed as XML document
        :return: dict containing all outputs found from the capture pattern in each output
        """

        captured_output = dict()

        def unique_tag_list(elements: list) -> bool:
            tag_list = list()
            for el in elements:
                # some xpath queries can return a list of str
                if isinstance(el, str):
                    return False

                if el.tag not in tag_list:
                    tag_list.append(el.tag)

            if len(tag_list) == 1:
                # all tags in this list are the same
                return False
            else:
                # there are unique tags in this list
                return True

        try:
            xml_doc = etree.XML(results)

            # xml_doc = elementTree.fromstring(results)
            # allow jinja syntax in capture_pattern, capture_value, capture_object etc

            local_context = self.context.copy()
            output = self.__render_output_metadata(output_definition,
                                                   local_context)

            var_name = output['name']
            if 'capture_pattern' in output or 'capture_value' in output:

                if 'capture_value' in output:
                    capture_pattern = output['capture_value']
                else:
                    capture_pattern = output['capture_pattern']

                # by default we will attempt to return the text of the found element
                return_type = 'text'
                entries = xml_doc.xpath(capture_pattern)
                logger.debug(f'found entries: {entries}')
                if len(entries) == 0:
                    captured_output[var_name] = ''
                elif len(entries) == 1:
                    entry = entries.pop()
                    if isinstance(entry, str):
                        captured_output[var_name] = str(entry)
                    else:
                        if len(entry) == 0:
                            # this tag has no children, so try to grab the text
                            if return_type == 'text':
                                captured_output[var_name] = str(
                                    entry.text).strip()
                            else:
                                captured_output[var_name] = entry.tag
                        else:
                            # we have 1 Element returned, so the user has a fairly specific xpath
                            # however, this element has children itself, so we can't return a text value
                            # just return the tag name of this element only
                            captured_output[var_name] = entry.tag
                else:
                    # we have a list of elements returned from the users xpath query
                    capture_list = list()
                    # are there unique tags in this list? or is this a list of the same tag names?
                    if unique_tag_list(entries):
                        return_type = 'tag'
                    for entry in entries:
                        if isinstance(entry, str):
                            capture_list.append(entry)
                        else:
                            if len(entry) == 0:
                                if return_type == 'text':
                                    if entry.text is not None:
                                        capture_list.append(entry.text.strip())
                                    else:
                                        # If there is no text, then try to grab a sensible attribute
                                        # if you need more control than this, then you should first
                                        # capture_object to convert to a python object then use a jinja filter
                                        # to get what you need
                                        if 'value' in entry.attrib:
                                            capture_list.append(
                                                entry.attrib.get('value', ''))
                                        elif 'name' in entry.attrib:
                                            capture_list.append(
                                                entry.attrib.get('name', ''))
                                        else:
                                            capture_list.append(
                                                json.dumps(dict(entry.attrib)))
                                else:
                                    capture_list.append(entry.tag)
                            else:
                                capture_list.append(entry.tag)

                    captured_output[var_name] = capture_list

            elif 'capture_object' in output:
                capture_pattern = output['capture_object']
                entries = xml_doc.xpath(capture_pattern)

                if len(entries) == 0:
                    captured_output[var_name] = None
                elif len(entries) == 1:
                    captured_output[var_name] = xmltodict.parse(
                        elementTree.tostring(entries.pop()))
                else:
                    capture_list = list()
                    for entry in entries:
                        capture_list.append(
                            xmltodict.parse(elementTree.tostring(entry)))
                    captured_output[var_name] = capture_list

            elif 'capture_list' in output:
                capture_pattern = output['capture_list']
                entries = xml_doc.xpath(capture_pattern)

                capture_list = list()
                for entry in entries:
                    if isinstance(entry, str):
                        capture_list.append(entry)
                    else:
                        capture_list.append(
                            xmltodict.parse(elementTree.tostring(entry)))

                captured_output[var_name] = capture_list

            # filter selected items here
            captured_output[var_name] = self.__filter_outputs(
                output, captured_output[var_name], local_context)

        except ParseError:
            logger.error('Could not parse XML document in output_utils')
            # just return blank captured_outputs here
            raise SkilletLoaderException(
                f'Could not parse output as XML in {self.name}')

        return captured_output