#!/usr/bin/env python

import os
import ast
import sys
import json
import time
import errno
import hashlib

from uuid import getnode as get_mac
from collections import OrderedDict

from linchpin.ansible_runner import ansible_runner

from linchpin.hooks.state import State
from linchpin.hooks import LinchpinHooks

from linchpin.rundb.basedb import BaseDB
from linchpin.rundb.drivers import DB_DRIVERS

from linchpin.exceptions import ActionError
from linchpin.exceptions import LinchpinError
from linchpin.exceptions import SchemaError
from linchpin.exceptions import TopologyError
from linchpin.exceptions import ValidationError
from linchpin.exceptions import ValidationErrorHandler

from linchpin.utils.validator import AnyofValidator

from linchpin.InventoryFilters import GenericInventory


class LinchpinAPI(object):

    def __init__(self, ctx):
        """
        LinchpinAPI constructor

        :param ctx: context object from context.py

        """

        self.__meta__ = "API"

        self.ctx = ctx

        self.hook_state = None
        self._hook_observers = []
        self.playbook_pre_states = self.get_cfg('playbook_pre_states',
                                                {'up': 'preup',
                                                 'destroy': 'predestroy'})
        self.playbook_post_states = self.get_cfg('playbook_post_states',
                                                 {'up': 'postup',
                                                  'destroy': 'postdestroy',
                                                  'postinv': 'inventory'})
        self.hooks = LinchpinHooks(self)

        self.target_data = {}

        base_path = '/'.join(os.path.dirname(__file__).split('/')[0:-1])
        pkg = self.get_cfg(section='lp', key='pkg', default='linchpin')
        lp_path = '{0}/{1}'.format(base_path, pkg)
        self.pb_ext = self.get_cfg('extensions', 'playbooks', default='.yml')

        # get external_provider_path
        xp_path = self.get_cfg('lp',
                               'external_providers_path',
                               default='').split(':')

        pb_path = '{0}/{1}'.format(lp_path,
                                   self.get_evar('playbooks_folder',
                                                 default='provision'))
        self.pb_path = [pb_path]

        for path in xp_path:
            self.pb_path.append(os.path.expanduser(path))

        self.set_evar('lp_path', lp_path)
        self.set_evar('pb_path', self.pb_path)
        self.set_evar('from_api', True)
        self.workspace = self.get_evar('workspace')


    def setup_rundb(self):
        """
        Configures the run database parameters, sets them into extra_vars
        """

        rundb_conn = self.get_cfg(section='lp',
                                  key='rundb_conn')
        rundb_type = self.get_cfg(section='lp',
                                  key='rundb_type',
                                  default='TinyRunDB')
        rundb_conn_type = self.get_cfg(section='lp',
                                       key='rundb_conn_type',
                                       default='file')
        self.rundb_hash = self.get_cfg(section='lp',
                                       key='rundb_hash',
                                       default='sha256')

        if rundb_conn_type == 'file':

            rundb_conn_f = rundb_conn.replace('::mac::', str(get_mac()))
            rundb_conn_f = rundb_conn_f.replace('{{ workspace }}',
                                                self.workspace)
            rundb_conn_f = os.path.realpath(os.path.expanduser(rundb_conn_f))
            rundb_conn_dir = os.path.dirname(rundb_conn_f)

            if not os.path.exists(rundb_conn_dir):
                try:
                    os.makedirs(rundb_conn_dir)
                except OSError as exc:
                    if (exc.errno == errno.EEXIST and
                            os.path.isdir(rundb_conn_dir)):
                        pass
                    else:
                        raise

        self.set_evar('rundb_type', rundb_type)
        self.set_evar('rundb_conn', rundb_conn_f)
        self.set_evar('rundb_hash', self.rundb_hash)

        return BaseDB(DB_DRIVERS[rundb_type], rundb_conn_f)


    def get_cfg(self, section=None, key=None, default=None):
        """
        Get cfgs value(s) by section and/or key, or the whole cfgs object

        :param section: section from ini-style config file

        :param key: key to get from config file, within section

        :param default: default value to return if nothing is found.
        """

        return self.ctx.get_cfg(section=section, key=key, default=default)


    def set_cfg(self, section, key, value):
        """
        Set a value in cfgs. Does not persist into a file,
        only during the current execution.

        :param section: section within ini-style config file

        :param key: key to use

        :param value: value to set into section within config file
        """

        self.ctx.set_cfg(section, key, value)


    def get_evar(self, key=None, default=None):
        """
        Get the current evars (extra_vars)

        :param key: key to use

        :param default: default value to return if nothing is found
        (default: None)
        """

        return self.ctx.get_evar(key=key, default=default)


    def set_evar(self, key, value):
        """
        Set a value into evars (extra_vars). Does not persist into a file,
        only during the current execution.

        :param key: key to use

        :param value: value to set into evars
        """

        self.ctx.set_evar(key, value)


    @property
    def hook_state(self):
        """
        getter function for hook_state property of the API object
        """

        return self.hook_state


    @hook_state.setter
    def hook_state(self, hook_state):
        """
        hook_state property setter , splits the hook_state string in
        subhook_state and sets linchpin.hook_state object

        :param hook_state: valid hook_state string mentioned in linchpin.conf
        """

        # call run_hooks after hook_state is being set
        if hook_state is None:
            return
        else:
            self.ctx.log_debug('hook {0} initiated'.format(hook_state))
            self._hook_state = State(hook_state, None, self.ctx)

            for callback in self._hook_observers:
                callback(self._hook_state)


    def bind_to_hook_state(self, callback):
        """
        Function used by LinchpinHooksclass to add callbacks

        :param callback: callback function
        """

        self._hook_observers.append(callback)


    def lp_journal(self, view='target', targets=[],
                   fields=None, count=1, tx_ids=None):

        rundb = self.setup_rundb()

        journal = {}

        if not len(targets):
            targets = rundb.get_tables()

        if view == 'target':
            # initialize rundb table
            dateformat = self.get_cfg('logger',
                                      'dateformat',
                                      default='%m/%d/%Y %I:%M:%S %p')
            for target in targets:
                tgt_records = rundb.get_records(table=target, count=count)

                if fields and 'start' in fields:
                    for run_id, record in tgt_records.iteritems():
                        st = record.get('start')
                        strt = float(st) if len(st) else float(1000000000.0)
                        start = time.localtime(strt)
                        record['start'] = time.strftime(dateformat, start)

                if fields and 'end' in fields:
                    for run_id, record in tgt_records.iteritems():
                        en = record.get('end')
                        endt = float(st) if len(en) else float(1000000000.0)
                        end = time.localtime(endt)
                        record['end'] = time.strftime(dateformat, end)


                journal[target] = tgt_records
        if view == 'tx':
            if len(tx_ids):
                journal = rundb.get_tx_records(tx_ids)
            else:
                journal = rundb.get_records('linchpin', count=count)

        return journal


    def _find_playbook_path(self, playbook):

        for path in self.pb_path:
            p = '{0}/{1}{2}'.format(path, playbook, self.pb_ext)

            if os.path.exists(os.path.expanduser(p)):
                return path

        raise LinchpinError("playbook '{0}' not found in"
                            " path: {1}".format(playbook, self.pb_path))


    def _fix_broken_topologies(self, res_grp, res_grp_type):
        """
        Because the new tooling requires resource_definitions, both beaker
        and openshift didn't previously suport this section in topologies.
        This function is called if resource_definitions are not included
        in the topology. The topology will be given a resource_definitions
        section.

        :param res_grp: The resource group to update

        :param res_grp_type: Which type to convert (this may not matter)
        """

        res_defs = {}
        # with beaker, there will only be one
        # resource_definition upon conversion
        if res_grp_type == 'beaker':
            res_defs['role'] = 'bkr_server'
            res_defs['whiteboard'] = res_grp.pop('whiteboard',
                                                 'Provisioned with LinchPin')
            res_defs['job_group'] = res_grp.pop('job_group')
            res_defs['recipesets'] = res_grp.pop('recipesets')
            res_defs['cancel_message'] = res_grp.pop('cancel_message',
                                                     'Canceled by LinchPin')
            res_defs['max_attempts'] = res_grp.pop('max_attempts', 60)
            res_defs['attempt_wait_time'] = res_grp.pop('attempt_wait_time',
                                                        60)
            res_grp['resource_definitions'] = [res_defs]

            return res_grp

        if res_grp_type == 'openshift':
            res_defs = res_grp.pop('resources')

            count = 0
            for res in res_defs:
                res['name'] = 'openshift-res_{0}'.format(str(count))
                count += 1
                if res.get('inline_data'):
                    res['role'] = 'openshift_inline'
                    res['data'] = res.pop('inline_data')
                if res.get('file_reference'):
                    res['role'] = 'openshift_external'
                    res['filiename'] = res.pop('file_reference')

            res_grp['resource_definitions'] = res_defs

            # the old openshift role put credentials in the
            # resource group definition. Moving to credentials
            creds = {}
            creds['api_endpoint'] = res_grp.pop('api_endpoint')
            creds['api_token'] = res_grp.pop('api_token')
            res_grp['credentials'] = creds

            return res_grp


    def _convert_topology(self, topology):
        """
        For backward compatiblity, convert the old topology format
        into the new format. Should be pretty straightforward and simple.

        ;param topology: topology dictionary
        """
        try:
            res_grps = topology.get('resource_groups')
            if res_grps:
                for res_grp in res_grps:
                    if 'res_group_type' in res_grp.keys():
                        res_grp['resource_group_type'] = (
                            res_grp.pop('res_group_type'))

                    if 'res_defs' in res_grp.keys():
                        res_grp['resource_definitions'] = (
                            res_grp.pop('res_defs'))

                    res_defs = res_grp.get('resource_definitions')
                    if not res_defs:
                        # this means it's either a beaker or openshift topology
                        res_grp_type = res_grp.get('resource_group_type')

                        res_group = self._fix_broken_topologies(res_grp,
                                                                res_grp_type)
                        res_defs = res_group.get('resource_definitions')
                        res_grp['resource_definitions'] = res_defs

                    if res_defs:
                        for res_def in res_defs:
                            if 'res_name' in res_def.keys():
                                res_def['name'] = res_def.pop('res_name')
                            if 'type' in res_def.keys():
                                res_def['role'] = res_def.pop('type')
                            if 'res_type' in res_def.keys():
                                res_def['role'] = res_def.pop('res_type')
                            if 'count' in res_def.keys():
                                res_def['count'] = int(res_def.pop('count'))
                    else:
                        raise TopologyError("'resource_definitions' do not"
                                            " validate in topology"
                                            " ({0})".format(topology))
            else:
                raise TopologyError("'resource_groups' do not validate"
                                    " in topology ({0})".format(topology))

        except Exception:
            raise LinchpinError("Unknown error converting schema. Check"
                                " template data")

    def _convert_layout(self, layout_data):
        """
        Convert the layout to retain order of the layout hosts
        ;param layout_data: layout_data
        """
        layout_json = layout_data
        layout_hosts = []
        ihosts = layout_data["inventory_layout"]["hosts"]
        for k in ihosts:
            layout_host = {}
            layout_host["name"] = k
            for key in ihosts[k]:
                layout_host[key] = ihosts[k][key]
            layout_hosts.append(layout_host)
        layout_json["inventory_layout"]["hosts"] = layout_hosts
        return layout_json


    def validate_topology(self, topology):
        """
        Validate the provided topology against the schema

        ;param topology: topology dictionary
        """

        res_grps = topology.get('resource_groups')
        resources = []

        for group in res_grps:
            res_grp_type = (group.get('resource_group_type') or
                            group.get('res_group_type'))

            pb_path = self._find_playbook_path(res_grp_type)

            try:
                sp = "{0}/roles/{1}/files/schema.json".format(pb_path,
                                                              res_grp_type)
                schema = json.load(open(sp))
            except Exception as e:
                raise LinchpinError("Error with schema: '{0}'"
                                    " {1}".format(sp, e))

            res_defs = group.get('resource_definitions')


            # preload this so it will validate against the schema
            document = {'res_defs': res_defs}
            v = AnyofValidator(schema,
                               error_handler=ValidationErrorHandler)

            if not v.validate(document):
                try:
                    err = self._gen_error_msg("", "", v.errors)
                    raise SchemaError(err)
                except NotImplementedError as e:
                    # we shouldn't have this issue using cerberus >= 1.2, but
                    # this is here just in case an older version has to be used
                    print("There was an error validating your schema, but we\
                          can't seem to format it for you")
                    print("Here's the raw error data in case you want to go\
                          through it by hand:")
                    print(v._errors)

            resources.append(group)

        return resources


    def _gen_error_msg(self, prefix, section, error):
        # set the prefix for this subtree
        if section != "":
            if prefix != "":
                prefix += "[" + str(section) + "]"
            else:
                prefix = str(section)

        if isinstance(error, str):
            if prefix == "":
                return error
            return prefix + ": " + error + os.linesep
        elif isinstance(error, list):
            msg = ""
            for i, e in enumerate(error):
                # we don't need to change the Vprefix here
                msg += self._gen_error_msg(prefix, "", e)
            return msg
        else:  # in this case, error is a dict
            msg = ""
            for key, val in error.iteritems():
                msg += self._gen_error_msg(prefix, key, val)
            return msg


    def validate_layout(self, layout):
        """
        Validate the provided layout against the schema

        :param layout: layout dictionary
        """

        pb_path = self._find_playbook_path("layout")
        try:
            sp = "{0}/roles/common/files/schema.json".format(pb_path)

            schema = json.load(open(sp))
        except Exception as e:
            raise LinchpinError("Error with schema: '{0}' {1}".format(sp, e))

        v = AnyofValidator(schema)

        if not v.validate(layout):
            raise SchemaError('Schema validation failed: {0}'.format(v.errors))


    def generate_inventory(self, resource_data, layout, inv_format="cfg",
                           topology_data={}, config_data={}):
        inv = GenericInventory.GenericInventory(inv_format=inv_format)
        inventory = inv.get_inventory(resource_data, layout, topology_data,
                                      config_data)
        return inventory

    def get_pf_data_from_rundb(self, targets, run_id=None, tx_id=None):
        """
        This function takes the action and provision_data, returns the
        pinfile data

        :param targets: A list of targets for which to get the data

        :param targets: Tuple of target(s) for which to gather data.

        :param run_id: run_id associated with target (Default: None)

        :param tx_id: tx_id for which to gather data (Default: None)
        """

        rundb = self.setup_rundb()

        if run_id and tx_id:
            raise ActionError("'run_id' and 'tx_id' are mutually exclusive")

        pf_data = {}
        pinfile = OrderedDict()

        if run_id:
            for target in targets:
                pf_data[target] = rundb.get_record(target,
                                                   action='up',
                                                   run_id=run_id)
        if tx_id:
            record = rundb.get_tx_record(tx_id)

            if not record or not len(record):
                return None

            if len(targets):
                for tgts in record['targets']:
                        for tgt, data in tgts.iteritems():
                            run_id = int(data.keys()[0])
                            if tgt in targets:
                                tgt_data = (rundb.get_record(tgt,
                                            action=record['action'],
                                            run_id=run_id))
                                pf_data[tgt] = tgt_data
            else:
                for tgts in record['targets']:
                    for tgt, data in tgts.iteritems():
                        run_id = int(data.keys()[0])
                        tgt_data = (rundb.get_record(tgt,
                                    action=record['action'],
                                    run_id=run_id))
                        pf_data[tgt] = tgt_data


        for t, data in pf_data.iteritems():
            topo_data = data[0]['inputs'][0].get('topology_data')
            layout_data = data[0]['inputs'][0].get('layout_data')
            hooks_data = data[0]['inputs'][0].get('hooks_data')

            pinfile[t] = {}
            pinfile[t]['topology'] = topo_data
            pinfile[t]['run_id'] = data[1]
            if layout_data:
                pinfile[t]['layout'] = layout_data
            if hooks_data:
                pinfile[t]['hooks'] = hooks_data

        return pinfile


    def do_action(self, provision_data, action='up', run_id=None, tx_id=None):
        """
        This function takes provision_data, and executes the given
        action for each target within the provision_data disctionary.

        :param provision_data: PinFile data as a dictionary, with
        target information

        :param action: Action taken (up, destroy, etc). (Default: up)

        :param run_id: Provided run_id to duplicate/destroy (Default: None)

        :param tx_id: Provided tx_id to duplicate/destroy (Default: None)

        .. .note:: The `run_id` value differs from the `rundb_id`, in that
                   the `run_id` is an existing value in the database.
                   The `rundb_id` value is created to store the new record.
                   If the `run_id` is passed, it is used to collect an existing
                   `uhash` value from the given `run_id`, which is in turn used
                   to perform an idempotent reprovision, or destroy provisioned
                   resources.
        """
        ansible_console = False
        if self.ctx.cfgs.get('ansible'):
            ansible_console = (
                ast.literal_eval(self.get_cfg('ansible',
                                              'console',
                                              default='False')))

        if not ansible_console:
            ansible_console = bool(self.ctx.verbosity)

        results = {}


        return_code = 99
        vault_pass = self.get_evar('vault_pass')
        if vault_pass == '':
            self.set_evar('vault_pass',
                          self.ctx.get_cfg('evars',
                                           'vault_pass',
                                           default=''))

        for target in provision_data.keys():
            if not isinstance(provision_data[target], dict):
                raise LinchpinError("Target '{0}' does not"
                                    " exist.".format(target))

        targets = [x.lower() for x in provision_data.keys()]
        if 'linchpin' in targets:
            raise LinchpinError("Target 'linchpin' is not allowed.")

        for target in provision_data.keys():
            if target == 'cfgs':
                continue

            self.ctx.log_debug("Processing target: {0}".format(target))

            results[target] = {}
            self.set_evar('target', target)

            rundb = self.setup_rundb()

            if tx_id:
                record = rundb.get_tx_record(tx_id)
                run_id = (record['targets'][0][target].keys()[0])


            rundb_schema = json.loads(self.get_cfg(section='lp',
                                      key='rundb_schema'))
            rundb.schema = rundb_schema
            self.set_evar('rundb_schema', rundb_schema)

            start = time.time()
            st_uhash = int(start * 1000)
            uhash = None

            # generate a new rundb_id
            # (don't confuse it with an already existing run_id)
            rundb_id = rundb.init_table(target)
            orig_run_id = rundb_id

            uhash_length = self.get_cfg('lp', 'rundb_uhash_length')
            uhash_len = int(uhash_length)
            if not run_id:
                uh = hashlib.new(self.rundb_hash,
                                 ':'.join([target, str(tx_id),
                                          str(rundb_id), str(st_uhash)]))
                uhash = uh.hexdigest()[:uhash_len]

            if action == 'destroy' or run_id:
                # look for the action='up' records to destroy
                data, orig_run_id = rundb.get_record(target,
                                                     action='up',
                                                     run_id=run_id)

                if data:
                    uhash = data.get('uhash')
                    self.ctx.log_debug("using data from"
                                       " run_id: {0}".format(run_id))

            elif action not in ['up', 'destroy']:
                # it doesn't appear this code will will execute,
                # but if it does...
                raise LinchpinError("Attempting '{0}' action on"
                                    " target: '{1}' failed. Not an"
                                    " action.".format(action, target))


            self.ctx.log_debug('rundb_id: {0}'.format(rundb_id))
            self.ctx.log_debug('uhash: {0}'.format(uhash))

            rundb.update_record(target, rundb_id, 'uhash', uhash)
            rundb.update_record(target, rundb_id, 'start', str(start))
            rundb.update_record(target, rundb_id, 'action', action)

            self.set_evar('orig_run_id', orig_run_id)
            self.set_evar('rundb_id', rundb_id)
            self.set_evar('uhash', uhash)

            topology_data = provision_data[target].get('topology')

            # if validation fails the first time, convert topo from old -> new
            try:
                resources = self.validate_topology(topology_data)
            except (SchemaError, KeyError):
                # if topology fails, try converting from old to new style
                try:
                    self._convert_topology(topology_data)
                    resources = self.validate_topology(topology_data)
                except SchemaError:
                    raise ValidationError("Topology '{0}' does not validate.  "
                                          "For more information run `linchpin "
                                          "validate`".format(topology_data))


            self.set_evar('topo_data', topology_data)

            rundb.update_record(target,
                                rundb_id,
                                'inputs',
                                [
                                    {'topology_data':
                                     provision_data[target]['topology']}
                                ])

            if provision_data[target].get('layout', None):
                l_data = provision_data[target]['layout']
                try:
                    self.validate_layout(l_data)
                except SchemaError:
                    raise ValidationError("Layout '{0}' does not"
                                          " validate".format(l_data))

                provision_data[target]['layout'] = self._convert_layout(l_data)
                self.set_evar('layout_data', provision_data[target]['layout'])

                rundb.update_record(target,
                                    rundb_id,
                                    'inputs',
                                    [
                                        {'layout_data':
                                         provision_data[target]['layout']}
                                    ])

            if provision_data[target].get('hooks', None):
                hooks_data = provision_data[target].get('hooks')
                self.set_evar('hooks_data', hooks_data)
                rundb.update_record(target,
                                    rundb_id,
                                    'inputs',
                                    [
                                        {'hooks_data':
                                         provision_data[target]['hooks']}
                                    ])

            if provision_data.get('cfgs', None):
                vars_data = provision_data[target].get('cfgs')
                self.set_evar('cfgs_data', vars_data)
                rundb.update_record(target,
                                    rundb_id,
                                    'cfgs',
                                    [
                                        {'user':
                                         provision_data['cfgs']}
                                    ])

            # note : changing the state triggers the hooks
            self.hooks.rundb = (rundb, rundb_id)
            self.pb_hooks = self.get_cfg('hookstates', action)
            self.ctx.log_debug('calling: {0}{1}'.format('pre', action))

            if (('pre' in self.pb_hooks) and not self.get_cfg('hook_flags',
                                                              'no_hooks')):
                self.hook_state = '{0}{1}'.format('pre', action)

            # FIXME need to add rundb data for hooks results

            # invoke the appropriate action
            return_code, results[target]['task_results'] = (
                self._invoke_playbooks(resources, action=action,
                                       console=ansible_console)
            )

            # logs the return_code back to console and quits
            # quits if return_code > 0
            self.ctx.log("Playbook Return code {0}".format(str(return_code)))
            if return_code > 0:
                sys.exit(return_code)

            if not return_code:
                self.ctx.log_state("Action '{0}' on Target '{1}' is "
                                   "complete".format(action, target))

            # FIXME Check the result[target] value here, and fail if applicable.
            # It's possible that a flag might allow more targets to run, then
            # return an error code at the end.

            # add post provision hook for inventory generation
            if (('inv' in self.pb_hooks) and not self.get_cfg('hook_flags',
                                                              'no_hooks')):
                self.hook_state = 'postinv'

            if (('post' in self.pb_hooks) and
                    (self.__meta__ == "API") and
                    not self.get_cfg('hook_flags', 'no_hooks')):
                self.hook_state = '{0}{1}'.format('post', action)

            end = time.time()

            rundb.update_record(target, rundb_id, 'end', str(end))
            rundb.update_record(target, rundb_id, 'rc', return_code)

            run_data = rundb.get_record(target,
                                        action=action,
                                        run_id=rundb_id)

            results[target]['rundb_data'] = {rundb_id: run_data[0]}


        # generate the linchpin_id and structure
        lp_schema = ('{"action": "", "targets": []}')

        rundb = self.setup_rundb()
        rundb.schema = json.loads(lp_schema)
        lp_id = rundb.init_table('linchpin')

        summary = {}

        for target, data in results.iteritems():
            for k, v in data['rundb_data'].iteritems():
                summary[target] = {k: {'rc': v['rc'], 'uhash': v['uhash']}}

        rundb.update_record('linchpin', lp_id, 'action', action)
        rundb.update_record('linchpin', lp_id, 'targets', [summary])

        lp_data = {lp_id: {'action': action,
                           'summary_data': summary,
                           'results_data': results}}

        return (return_code, lp_data)


    def do_validation(self, provision_data, old_schema=False):
        """
        This function takes provision_data, and attempts to validate the
        topologies for that data

        :param provision_data: PinFile data as a dictionary, with
        target information
        """

        results = {}


        return_code = 0

        for target in provision_data.keys():
            if not isinstance(provision_data[target], dict):
                raise LinchpinError("Target '{0}' does not"
                                    " exist.".format(target))

        targets = [x.lower() for x in provision_data.keys()]
        if 'linchpin' in targets:
            raise LinchpinError("Target 'linchpin' is not allowed.")

        for target in provision_data.keys():
            self.ctx.log_debug("Processing target: {0}".format(target))

            results[target] = {}
            self.set_evar('target', target)

            # validate topology
            topology_data = provision_data[target].get('topology')

            try:
                self.validate_topology(topology_data)
            except (SchemaError, KeyError) as e:
                # try to validate against old schema
                try:
                    self._convert_topology(topology_data)
                    self.validate_topology(topology_data)
                except SchemaError as s:
                    error = "errors:\n".format(target, topology_data)
                    if old_schema:
                        # there's an inline way of doing this with join() but
                        # this looks cleaner
                        for line in iter(str(s).splitlines(True)):
                            error += "\t" + line
                    else:
                        if type(e) == KeyError:
                            error += "\tfield res_defs['type'] is no longer"\
                                     "Please use 'role' instead"
                        else:
                            for line in iter(str(e).splitlines(True)):
                                error += "\t" + line

                    results[target]['topology'] = error
                    return_code += 1
                else:
                    results[target]['topology'] = "topology valid under old "\
                                                  "schema"


            else:
                results[target]['topology'] = "valid"

            # validate layout
            if provision_data[target].get('layout', None):
                l_data = provision_data[target]['layout']

                try:
                    self.validate_layout(l_data)
                except SchemaError as e:
                    error = "errors:".format(target, l_data)
                    for line in iter(str(e).splitlines(True)):
                        error += "\t" + line

                    results[target]['layout'] = error + "\n"

                else:
                    results[target]['layout'] = "valid"


        return return_code, results


    def get_run_data(self, tx_id, fields, targets=()):
        """
        Returns the RunDB for data from a specified field given a tx_id.
        The fields consist of the major sections in the RunDB (target
        view only). Those fields are action, start, end, inputs, outputs,
        uhash, and rc.

        :param tx_id: tx_id to search
        :param fields: Tuple of fields to retrieve for each record requested.
        :param targets: Tuple of targets to search from within the tx_ids
        """

        rundb = self.setup_rundb()

        tgt_run_ids = {}
        target_data = {}

        record = rundb.get_tx_record(tx_id)

        if not record or not len(record):
            return None


        # get run_ids to query
        if len(targets):
            for tgts in record['targets']:
                    for tgt, data in tgts.iteritems():
                        if tgt in targets:
                            tgt_run_ids[tgt] = int(data.keys()[0])
        else:
            for tgts in record['targets']:
                for tgt, data in tgts.iteritems():
                    tgt_run_ids[tgt] = int(data.keys()[0])

        for target, run_id in tgt_run_ids.iteritems():
            record = rundb.get_record(target, run_id=run_id, action='up')
            field_data = {}
            single_value_fields = ('action',
                                   'start',
                                   'end',
                                   'rc',
                                   'uhash')

            for field in fields:
                f = record[0].get(field)
                if f:
                    if field in single_value_fields:
                        field_data[field] = f
                    else:
                        data_array = {}
                        for fld in f:
                            for k, v in fld.iteritems():
                                if field == 'outputs':
                                    if isinstance(v, dict):
                                        values = v
                                    else:
                                        values = []
                                        for value in v:
                                            values.append(value)
                                    data_array[k] = values
                                else:
                                    data_array[k] = v

                            field_data[field] = data_array

            target_data[target] = field_data

        return target_data

    def _get_run_data_by_txid(self, tx_id=None):
        rundb = self.setup_rundb()
        latest_run_data = {}
        run_data = {}
        if tx_id is None:
            latest_run_data = rundb.get_records('linchpin', count=1)
            run_data = self.get_run_data(latest_run_data.keys()[0],
                                         ('outputs',
                                          'inputs',
                                          'cfgs'))
        else:
            latest_run_data = rundb.get_records('linchpin',
                                                count='all')
            latest_run_data = {tx_id: latest_run_data.get(tx_id)}
            run_data = self.get_run_data(tx_id,
                                         ('outputs',
                                          'inputs',
                                          'cfgs'))
        for k in latest_run_data:
            v = latest_run_data[k]
            target_group = v.get("targets", [])
            # Note:
            # target_group always returns a dict inside a list due to
            # rundb implemenatation. This code works for one PinFile run
            # might be subjected to change in future releases
            if len(target_group) == 0:
                continue
            else:
                target_group = target_group[0]
            for key in target_group.keys():
                target_group[key].update(run_data.get(key, {}))
            latest_run_data[k]["targets"] = [target_group]
        return latest_run_data

    def _get_module_path(self):
        module_paths = []
        module_folder = self.get_cfg('lp',
                                     'module_folder',
                                     default='library')
        for path in reversed(self.pb_path):
            module_paths.append('{0}/{1}/'.format(path, module_folder))
        return module_paths

    def _find_n_run_pb(self, pb_name, inv_src, console=True):
        pb_path = self._find_playbook_path(pb_name)
        playbook_path = '{0}/{1}{2}'.format(pb_path, pb_name, self.pb_ext)
        extra_vars = self.get_evar()
        return_code, res = ansible_runner(playbook_path,
                                          self._get_module_path(),
                                          extra_vars,
                                          inventory_src=inv_src,
                                          verbosity=self.ctx.verbosity,
                                          console=console)
        return return_code, res

    def _invoke_playbooks(self, resources={}, action='up', console=True,
                          providers=[]):
        """
        Uses the Ansible API code to invoke the specified linchpin playbook

        :param resources: dict of resources to provision
        :param action: Which ansible action to run (default: 'up')
        :param console: Whether to display the ansible console (default: True)
        """

        return_code = 0
        results = []

        self.set_evar('_action', action)
        self.set_evar('state', 'present')

        if action == 'setup' or action == 'ask_sudo_setup':
            self.set_evar('setup_providers', providers)
            return_code, res = self._find_n_run_pb(action,
                                                   "localhost",
                                                   console=console)
            if res:
                results.append(res)
            if not len(results):
                results = None
            return (return_code, results)

        if action == 'destroy':
            self.set_evar('state', 'absent')

        inventory_src = '{0}/localhost'.format(self.workspace)

        for resource in resources:
            self.set_evar('resources', resource)
            playbook = resource.get('resource_group_type')
            return_code, res = self._find_n_run_pb(playbook,
                                                   inventory_src,
                                                   console=console)
            if res:
                results.append(res)

        if not len(results):
            results = None

        return (return_code, results)
