Beispiel #1
0
def get_python_input_options(resource_def: dict) -> str:
    """
    Determine how input variables from the view should be passed to this script. An optional snippet parameter
    called 'input_type' is checked to determine whether 'cli' or 'env' should be used. 'cli' indicates input
    variables will be passed along as long form arguments to the python script on the cli (for example:
    --first_arg=arg1_val --second_arg=arg2_val). 'env' indicates env variables will be set (for example:
    export first_arg=arg1_var; export second_arg=arg2_val)

    :param resource_def: the compiled .meta-cnc file
    :return: str of either 'cli' or 'env'
    """
    if 'snippet_path' not in resource_def:
        raise CCFParserError('Malformed .meta-cnc file for python3 execution')

    try:
        if 'snippets' in resource_def and len(resource_def['snippets']) > 0:
            # python type only uses first snippet from list
            snippet = resource_def['snippets'][0]
            if 'input_type' in snippet:
                if str(snippet['input_type']).lower() == 'cli':
                    return 'cli'
                elif str(snippet['input_type']).lower() == 'env':
                    return 'env'
                else:
                    return 'cli'

            return 'cli'
    except TypeError:
        raise CCFParserError(
            'Malformed .meta-cnc file for python3 execution - Malformed snippet'
        )
Beispiel #2
0
def _normalize_python_script_path(resource_def: dict) -> tuple:
    if 'snippet_path' not in resource_def:
        raise CCFParserError('Malformed .meta-cnc file for python3 execution')

    resource_dir = resource_def['snippet_path']
    if 'snippets' in resource_def and len(resource_def['snippets']) > 0:
        # python type only uses first snippet from list
        snippet = resource_def['snippets'][0]
        if 'file' in snippet and 'name' in snippet:
            script = snippet['file']

            if '/' not in script:
                script = f"./{script}"

            # ensure no funny business
            skillet_base_path = Path(resource_dir)
            print(skillet_base_path)
            script_path = skillet_base_path.joinpath(script).resolve()
            print(script_path)
            # # if skillet_base_path not in script_path.parents:
            #     raise CCFParserError('Malformed .meta-cnc file for python3 execution - Refusing to jump out of dir')

            return str(script_path.parent), script_path.name
        else:
            raise CCFParserError(
                'Malformed .meta-cnc file for python3 execution - Malformed snippet'
            )
    else:
        raise CCFParserError(
            'Malformed .meta-cnc file for python3 execution - Malformed snippet'
        )
Beispiel #3
0
def debug_meta(meta: dict, context: dict) -> dict:
    rendered_snippets = dict()

    if 'snippet_path' in meta:
        snippets_dir = meta['snippet_path']
    else:
        return rendered_snippets

    environment = Environment(loader=BaseLoader())
    for f in jinja_filters.defined_filters:
        if hasattr(jinja_filters, f):
            environment.filters[f] = getattr(jinja_filters, f)

    for snippet in meta['snippets']:
        if 'cmd' in snippet and 'cmd' != 'set':
            continue

        if 'xpath' not in snippet or 'file' not in snippet:
            print('Malformed meta-cnc error')
            raise CCFParserError

        xpath = snippet['xpath']
        xml_file_name = snippet['file']
        snippet_name = snippet['name']

        try:

            xml_full_path = os.path.abspath(os.path.join(snippets_dir, xml_file_name))
            with open(xml_full_path, 'r') as xml_file:
                xml_string = xml_file.read()

                xml_template = environment.from_string(xml_string)
                xpath_template = environment.from_string(xpath)
                xml_snippet = xml_template.render(context)
                xpath_string = xpath_template.render(context)
                rendered_snippets[snippet_name] = dict()
                rendered_snippets[snippet_name]['xpath'] = xpath_string
                rendered_snippets[snippet_name]['xml'] = xml_snippet
        except FileNotFoundError:
            err = f'Could not find file from path: {xml_full_path} from snippet: {snippet_name}'
            print(err)
            raise CCFParserError(err)
        except OSError:
            err = f'Unknown Error loading snippet: {snippet_name} from file {xml_file_name}'
            print(err)
            raise CCFParserError(err)

    return rendered_snippets
Beispiel #4
0
def _handle_base64_outputs(snippet: dict, results: str) -> dict:
    """
    Parses results and returns a dict containing base64 encoded values
    :param snippet: snippet definition from the .meta-cnc snippets section
    :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 snippet:
        snippet_name = snippet['name']

    try:
        if 'outputs' not in snippet:
            print(f'No output defined in this snippet {snippet_name}')
            return outputs

        for output in snippet['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 CCFParserError(f'Could not base64 encode results {snippet_name}')

    return outputs
Beispiel #5
0
def _handle_xml_outputs(snippet: 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_pattern: result/system/hostname
      - name: uptime
        capture_pattern: result/system/uptime
      - name: sw_version
        capture_pattern: result/system/sw-version
        
    :param snippet: snippet definition from the .meta-cnc snippets section
    :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
    """
    outputs = dict()

    snippet_name = 'unknown'
    if 'name' in snippet:
        snippet_name = snippet['name']

    try:
        xml_doc = elementTree.fromstring(results)
        if 'outputs' not in snippet:
            print('No outputs defined in this snippet')
            return outputs

        for output in snippet['outputs']:
            if 'name' not in output or 'capture_pattern' not in output:
                continue

            var_name = output['name']
            capture_pattern = output['capture_pattern']
            outputs[var_name] = xml_doc.findtext(capture_pattern)
    except ParseError:
        print('Could not parse XML document in output_utils')
        # just return blank outputs here
        raise CCFParserError(
            f'Could not parse output as XML in {snippet_name}')

    return outputs
Beispiel #6
0
def execute_all(meta_cnc, app_dir, context):
    """
    Performs all REST operations defined in this meta-cnc file
    Each 'snippet' in the 'snippets' stanza in the .meta-cnc file will be executed in turn
    each entry in the 'snippets' stanza MUST have at least:
        'name', 'rest_path', and 'rest_operation'
    For a POST operation it must also include a 'payload' key

    * The path can include jinja2 variables as well as the payload file. Both will be interpolated before executing
    This allows things like the hostname, etc to be captured in the variables or target section

    :param meta_cnc: a parsed .meta-cnc.yaml file (self.service in class based Views)
    :param app_dir: which app_dir is this (panhandler, vistoq, etc) defined as self.app_dir on view classes
    :param context: fully populated workflow from the calling view (self.get_workflow() on the view class)
    :return: string suitable for presentation to the user
    """
    if 'snippet_path' in meta_cnc:
        snippets_dir = meta_cnc['snippet_path']
    else:
        # snippets_dir = Path(os.path.join(settings.BASE_DIR, app_dir, 'snippets', meta_cnc['name']))
        raise CCFParserError('Could not locate .meta-cnc for REST execution')

    response = dict()
    response['status'] = 'success'
    response['message'] = 'A-OK'
    response['snippets'] = dict()

    session = requests.Session()

    try:
        # execute our rest call for each item in the 'snippets' stanza of the meta-cnc file
        for snippet in meta_cnc['snippets']:
            if 'path' not in snippet:
                print('Malformed meta-cnc error')
                raise CCFParserError

            name = snippet.get('name', '')
            rest_path = snippet.get('path', '/api')
            rest_op = str(snippet.get('operation', 'get')).lower()
            payload_name = snippet.get('payload', '')
            header_dict = snippet.get('headers', dict())

            # fix for issue #42
            if type(header_dict) is not dict:
                header_dict = dict()

            # FIXME - implement this to give some control over what will be sent to rest server
            content_type = snippet.get('content_type', '')
            accepts_type = snippet.get('accepts_type', '')

            headers = dict()
            if content_type:
                headers["Content-Type"] = content_type

            if accepts_type:
                headers['Accepts-Type'] = accepts_type

            environment = Environment(loader=BaseLoader())

            for f in jinja_filters.defined_filters:
                if hasattr(jinja_filters, f):
                    environment.filters[f] = getattr(jinja_filters, f)

            path_template = environment.from_string(rest_path)
            url = path_template.render(context)

            for k, v in header_dict.items():
                v_template = environment.from_string(v)
                v_interpolated = v_template.render(context)
                print(f'adding {k} as {v_interpolated} to headers')
                headers[k] = v_interpolated

            # keep track of response text or json object
            r = ''
            if rest_op == 'post' and payload_name != '':
                payload_path = os.path.join(snippets_dir, payload_name)
                with open(payload_path, 'r') as payload_file:
                    payload_string = payload_file.read()
                    payload_template = environment.from_string(payload_string)
                    payload_interpolated = payload_template.render(context)
                    if 'Content-Type' in headers and 'form' in headers[
                            'Content-Type']:
                        print('Loading json data from payload')
                        try:
                            payload = json.loads(payload_interpolated)
                        except ValueError as ve:
                            print('Could not load payload as json data!')
                            payload = payload_interpolated
                    else:
                        payload = payload_interpolated

                    print('Using payload of')
                    print(payload)
                    print(url)
                    print(headers)
                    # FIXME - assumes JSON content_type and accepts, should take into account the values
                    # FIXME - of content-type and accepts_type from above if they were supplied
                    res = session.post(url,
                                       data=payload,
                                       verify=False,
                                       headers=headers)
                    if res.status_code != 200:
                        print('Found a non-200 response status_code!')
                        print(res.status_code)
                        response['snippets'][name] = res.text
                        break

                r = res.text

            elif rest_op == 'get':
                print('Performing REST get')
                res = session.get(url, verify=False)
                r = res.text
                if res.status_code != 200:
                    response['status'] = 'error'
                    response['snippets'][name] = r
                    break

            else:
                print('Unknown REST operation found')
                response['status'] = 'Error'
                response['message'] = 'Unkonwn REST operation found'
                return response

            # collect the response text or json and continue
            response['snippets'][name] = dict()
            response['snippets'][name]['results'] = r
            response['snippets'][name]['outputs'] = dict()

            if 'outputs' in snippet:
                outputs = output_utils.parse_outputs(meta_cnc, snippet, r)
                response['snippets'][name]['outputs'] = outputs

        # return all the collected response
        return response

    except HTTPError as he:
        response['status'] = 'error'
        response['message'] = str(he)
        return response
    except requests.exceptions.ConnectionError as ce:
        response['status'] = 'error'
        response['message'] = str(ce)
        return response
    except MissingSchema as ms:
        response['status'] = 'error'
        response['message'] = ms
        return response
    except RequestException as re:
        response['status'] = 'error'
        response['message'] = re
        return response
Beispiel #7
0
def push_meta(meta, context, force_sync=False, perform_commit=True) -> (str, None):
    """
    Push a skillet to a PanXapi connected device
    :param meta: dict containing parsed and loaded skillet
    :param context: all compiled variables from the user interaction
    :param force_sync: should we wait on a successful commit operation or return after queue
    :param perform_commit: should we actually commit or not
    :return: job_id as a str or None if no job_id could be found
    """
    xapi = panos_login()

    if xapi is None:
        raise CCFParserError('Could not login in to Palo Alto Networks Device')

    name = meta['name'] if 'name' in meta else 'unknown'

    # default to None as return value, set to job_id if possible later if a commit was requested
    return_value = None

    # _perform_backup()
    if 'snippet_path' in meta:
        snippets_dir = meta['snippet_path']
    else:
        raise CCFParserError(f'Could not locate .meta-cnc file on filesystem for Skillet: {name}')

    environment = Environment(loader=BaseLoader())

    for f in jinja_filters.defined_filters:
        if hasattr(jinja_filters, f):
            environment.filters[f] = getattr(jinja_filters, f)

    try:
        for snippet in meta['snippets']:
            if 'xpath' not in snippet or 'file' not in snippet:
                print('Malformed meta-cnc error')
                raise CCFParserError(f'Malformed snippet section in meta-cnc file for {name}')

            xpath = snippet['xpath']
            xml_file_name = snippet['file']

            # allow snippets to be skipped using the 'when' attribute
            if 'when' in snippet:
                when_template = environment.from_string(snippet.get('when', ''))
                when_result = str(when_template.render(context))
                if when_result.lower() == 'false' or when_result.lower() == 'no':
                    print(f'Skipping snippet {name} due to when condition false')
                    continue

            xml_full_path = os.path.join(snippets_dir, xml_file_name)
            with open(xml_full_path, 'r') as xml_file:
                xml_string = xml_file.read()

                xml_template = environment.from_string(xml_string)
                xpath_template = environment.from_string(xpath)
                # fix for #74, ensure multiline xpaths do not contain newlines or spaces
                xml_snippet = xml_template.render(context).strip().replace('\n', '')
                xpath_string = xpath_template.render(context).strip().replace('\n', '').replace(' ', '')
                print('Pushing xpath: %s' % xpath_string)
                try:
                    xapi.set(xpath=xpath_string, element=xml_snippet)
                    if xapi.status_code == '19' or xapi.status_code == '20':
                        print('xpath is already present')
                    elif xapi.status_code == '7':
                        raise CCFParserError(f'xpath {xpath_string} was NOT found for skillet: {name}')
                except pan.xapi.PanXapiError as pxe:
                    err_msg = str(pxe)
                    if '403' in err_msg:
                        # Auth issue, let's clear the api_key and bail out!
                        xapi = None
                        clear_credentials()

                    raise CCFParserError(f'Could not push skillet {name} / snippet {xml_file_name}! {pxe}')

        if perform_commit:

            if 'type' not in meta:
                commit_type = 'commit'
            else:
                if 'panorama' in meta['type']:
                    commit_type = 'commit-all'
                else:
                    commit_type = 'commit'

            if commit_type == 'commit-all':
                print('Performing commit-all in panorama')
                xapi.commit(cmd='<commit-all></commit-all>', sync=True)
            else:
                if force_sync:
                    xapi.commit('<commit></commit>', sync=True)
                else:
                    xapi.commit('<commit></commit>')

            results = xapi.xml_result()
            if force_sync:
                # we have the results of a job id query, the commit results are embedded therein
                doc = elementTree.XML(results)
                embedded_result = doc.find('result')
                if embedded_result is not None:
                    commit_result = embedded_result.text
                    print(f'Commit result is {commit_result}')
                    if commit_result == 'FAIL':
                        raise TargetCommitException(xapi.status_detail)
            else:
                if 'with jobid' in results:
                    result = re.match(r'.* with jobid (\d+)', results)
                    if result is not None:
                        return_value = result.group(1)

            # for gpcs baseline and svc connection network configuration do a scope push to gpcs
            # FIXME - check for 'gpcs' in meta['type'] instead of hardcoded name
            if meta['name'] == 'gpcs_baseline':
                print('push baseline and svc connection scope to gpcs')
                xapi.commit(action='all',
                            cmd='<commit-all><template-stack>'
                                '<name>Service_Conn_Template_Stack</name></template-stack></commit-all>')
                print(xapi.xml_result())

            # for gpcs remote network configuration do a scope push to gpcs
            if meta['name'] == 'gpcs_remote' or meta['name'] == 'gpcs_baseline':
                print('push remote network scope to gpcs')
                xapi.commit(action='all',
                            cmd='<commit-all><shared-policy><device-group>'
                                '<entry name="Remote_Network_Device_Group"/>'
                                '</device-group></shared-policy></commit-all>')
                print(xapi.xml_result())

        return return_value

    except UndefinedError as ue:
        raise CCFParserError(f'Undefined variable in skillet: {ue}')

    except IOError as ioe:
        raise CCFParserError(f'Could not open xml snippet file for reading! {ioe}')

    except pan.xapi.PanXapiError as pxe:
        raise CCFParserError(f'Could not push meta-cnc for skillet {name}! {pxe}')