Mini Shell
"""
TextFSM
=======
.. versionadded:: 2018.3.0
Execution module that processes plain text and extracts data
using TextFSM templates. The output is presented in JSON serializable
data, and can be easily re-used in other modules, or directly
inside the renderer (Jinja, Mako, Genshi, etc.).
:depends: - textfsm Python library
.. note::
Install ``textfsm`` library: ``pip install textfsm``.
"""
import logging
import os
import salt.utils.files
try:
import textfsm
HAS_TEXTFSM = True
except ImportError:
HAS_TEXTFSM = False
try:
from textfsm import clitable
HAS_CLITABLE = True
except ImportError:
HAS_CLITABLE = False
log = logging.getLogger(__name__)
__virtualname__ = "textfsm"
__proxyenabled__ = ["*"]
def __virtual__():
"""
Only load this execution module if TextFSM is installed.
"""
if HAS_TEXTFSM:
return __virtualname__
return (
False,
"The textfsm execution module failed to load: requires the textfsm library.",
)
def _clitable_to_dict(objects, fsm_handler):
"""
Converts TextFSM cli_table object to list of dictionaries.
"""
objs = []
log.debug("Cli Table: %s; FSM handler: %s", objects, fsm_handler)
for row in objects:
temp_dict = {}
for index, element in enumerate(row):
temp_dict[fsm_handler.header[index].lower()] = element
objs.append(temp_dict)
log.debug("Extraction result: %s", objs)
return objs
def extract(template_path, raw_text=None, raw_text_file=None, saltenv="base"):
r"""
Extracts the data entities from the unstructured
raw text sent as input and returns the data
mapping, processing using the TextFSM template.
template_path
The path to the TextFSM template.
This can be specified using the absolute path
to the file, or using one of the following URL schemes:
- ``salt://``, to fetch the template from the Salt fileserver.
- ``http://`` or ``https://``
- ``ftp://``
- ``s3://``
- ``swift://``
raw_text: ``None``
The unstructured text to be parsed.
raw_text_file: ``None``
Text file to read, having the raw text to be parsed using the TextFSM template.
Supports the same URL schemes as the ``template_path`` argument.
saltenv: ``base``
Salt fileserver environment from which to retrieve the file.
Ignored if ``template_path`` is not a ``salt://`` URL.
CLI Example:
.. code-block:: bash
salt '*' textfsm.extract salt://textfsm/juniper_version_template raw_text_file=s3://junos_ver.txt
salt '*' textfsm.extract http://some-server/textfsm/juniper_version_template raw_text='Hostname: router.abc ... snip ...'
Jinja template example:
.. code-block:: jinja
{%- set raw_text = 'Hostname: router.abc ... snip ...' -%}
{%- set textfsm_extract = salt.textfsm.extract('https://some-server/textfsm/juniper_version_template', raw_text) -%}
Raw text example:
.. code-block:: text
Hostname: router.abc
Model: mx960
JUNOS Base OS boot [9.1S3.5]
JUNOS Base OS Software Suite [9.1S3.5]
JUNOS Kernel Software Suite [9.1S3.5]
JUNOS Crypto Software Suite [9.1S3.5]
JUNOS Packet Forwarding Engine Support (M/T Common) [9.1S3.5]
JUNOS Packet Forwarding Engine Support (MX Common) [9.1S3.5]
JUNOS Online Documentation [9.1S3.5]
JUNOS Routing Software Suite [9.1S3.5]
TextFSM Example:
.. code-block:: text
Value Chassis (\S+)
Value Required Model (\S+)
Value Boot (.*)
Value Base (.*)
Value Kernel (.*)
Value Crypto (.*)
Value Documentation (.*)
Value Routing (.*)
Start
# Support multiple chassis systems.
^\S+:$$ -> Continue.Record
^${Chassis}:$$
^Model: ${Model}
^JUNOS Base OS boot \[${Boot}\]
^JUNOS Software Release \[${Base}\]
^JUNOS Base OS Software Suite \[${Base}\]
^JUNOS Kernel Software Suite \[${Kernel}\]
^JUNOS Crypto Software Suite \[${Crypto}\]
^JUNOS Online Documentation \[${Documentation}\]
^JUNOS Routing Software Suite \[${Routing}\]
Output example:
.. code-block:: json
{
"comment": "",
"result": true,
"out": [
{
"kernel": "9.1S3.5",
"documentation": "9.1S3.5",
"boot": "9.1S3.5",
"crypto": "9.1S3.5",
"chassis": "",
"routing": "9.1S3.5",
"base": "9.1S3.5",
"model": "mx960"
}
]
}
"""
ret = {"result": False, "comment": "", "out": None}
log.debug(
"Caching %s(saltenv: %s) using the Salt fileserver", template_path, saltenv
)
tpl_cached_path = __salt__["cp.cache_file"](template_path, saltenv=saltenv)
if tpl_cached_path is False:
ret["comment"] = "Unable to read the TextFSM template from {}".format(
template_path
)
log.error(ret["comment"])
return ret
try:
log.debug("Reading TextFSM template from cache path: %s", tpl_cached_path)
# Disabling pylint W8470 to nto complain about fopen.
# Unfortunately textFSM needs the file handle rather than the content...
# pylint: disable=W8470
with salt.utils.files.fopen(tpl_cached_path, "r") as tpl_file_handle:
# pylint: disable=W8470
tpl_file_data = tpl_file_handle.read()
log.debug(tpl_file_data)
tpl_file_handle.seek(
0
) # move the object position back at the top of the file
fsm_handler = textfsm.TextFSM(tpl_file_handle)
except textfsm.TextFSMTemplateError as tfte:
log.error("Unable to parse the TextFSM template", exc_info=True)
ret["comment"] = (
"Unable to parse the TextFSM template from {}: {}. Please check the logs.".format(
template_path, tfte
)
)
return ret
if not raw_text and raw_text_file:
log.debug("Trying to read the raw input from %s", raw_text_file)
raw_text = __salt__["cp.get_file_str"](raw_text_file, saltenv=saltenv)
if raw_text is False:
ret["comment"] = (
"Unable to read from {}. Please specify a valid input file or text.".format(
raw_text_file
)
)
log.error(ret["comment"])
return ret
if not raw_text:
ret["comment"] = "Please specify a valid input file or text."
log.error(ret["comment"])
return ret
log.debug("Processing the raw text:\n%s", raw_text)
objects = fsm_handler.ParseText(raw_text)
ret["out"] = _clitable_to_dict(objects, fsm_handler)
ret["result"] = True
return ret
def index(
command,
platform=None,
platform_grain_name=None,
platform_column_name=None,
output=None,
output_file=None,
textfsm_path=None,
index_file=None,
saltenv="base",
include_empty=False,
include_pat=None,
exclude_pat=None,
):
"""
Dynamically identify the template required to extract the
information from the unstructured raw text.
The output has the same structure as the ``extract`` execution
function, the difference being that ``index`` is capable
to identify what template to use, based on the platform
details and the ``command``.
command
The command executed on the device, to get the output.
platform
The platform name, as defined in the TextFSM index file.
.. note::
For ease of use, it is recommended to define the TextFSM
indexfile with values that can be matches using the grains.
platform_grain_name
The name of the grain used to identify the platform name
in the TextFSM index file.
.. note::
This option can be also specified in the minion configuration
file or pillar as ``textfsm_platform_grain``.
.. note::
This option is ignored when ``platform`` is specified.
platform_column_name: ``Platform``
The column name used to identify the platform,
exactly as specified in the TextFSM index file.
Default: ``Platform``.
.. note::
This is field is case sensitive, make sure
to assign the correct value to this option,
exactly as defined in the index file.
.. note::
This option can be also specified in the minion configuration
file or pillar as ``textfsm_platform_column_name``.
output
The raw output from the device, to be parsed
and extract the structured data.
output_file
The path to a file that contains the raw output from the device,
used to extract the structured data.
This option supports the usual Salt-specific schemes: ``file://``,
``salt://``, ``http://``, ``https://``, ``ftp://``, ``s3://``, ``swift://``.
textfsm_path
The path where the TextFSM templates can be found. This can be either
absolute path on the server, either specified using the following URL
schemes: ``file://``, ``salt://``, ``http://``, ``https://``, ``ftp://``,
``s3://``, ``swift://``.
.. note::
This needs to be a directory with a flat structure, having an
index file (whose name can be specified using the ``index_file`` option)
and a number of TextFSM templates.
.. note::
This option can be also specified in the minion configuration
file or pillar as ``textfsm_path``.
index_file: ``index``
The name of the TextFSM index file, under the ``textfsm_path``. Default: ``index``.
.. note::
This option can be also specified in the minion configuration
file or pillar as ``textfsm_index_file``.
saltenv: ``base``
Salt fileserver environment from which to retrieve the file.
Ignored if ``textfsm_path`` is not a ``salt://`` URL.
include_empty: ``False``
Include empty files under the ``textfsm_path``.
include_pat
Glob or regex to narrow down the files cached from the given path.
If matching with a regex, the regex must be prefixed with ``E@``,
otherwise the expression will be interpreted as a glob.
exclude_pat
Glob or regex to exclude certain files from being cached from the given path.
If matching with a regex, the regex must be prefixed with ``E@``,
otherwise the expression will be interpreted as a glob.
.. note::
If used with ``include_pat``, files matching this pattern will be
excluded from the subset of files defined by ``include_pat``.
CLI Example:
.. code-block:: bash
salt '*' textfsm.index 'sh ver' platform=Juniper output_file=salt://textfsm/juniper_version_example textfsm_path=salt://textfsm/
salt '*' textfsm.index 'sh ver' output_file=salt://textfsm/juniper_version_example textfsm_path=ftp://textfsm/ platform_column_name=Vendor
salt '*' textfsm.index 'sh ver' output_file=salt://textfsm/juniper_version_example textfsm_path=https://some-server/textfsm/ platform_column_name=Vendor platform_grain_name=vendor
TextFSM index file example:
``salt://textfsm/index``
.. code-block:: text
Template, Hostname, Vendor, Command
juniper_version_template, .*, Juniper, sh[[ow]] ve[[rsion]]
The usage can be simplified,
by defining (some of) the following options: ``textfsm_platform_grain``,
``textfsm_path``, ``textfsm_platform_column_name``, or ``textfsm_index_file``,
in the (proxy) minion configuration file or pillar.
Configuration example:
.. code-block:: yaml
textfsm_platform_grain: vendor
textfsm_path: salt://textfsm/
textfsm_platform_column_name: Vendor
And the CLI usage becomes as simple as:
.. code-block:: bash
salt '*' textfsm.index 'sh ver' output_file=salt://textfsm/juniper_version_example
Usgae inside a Jinja template:
.. code-block:: jinja
{%- set command = 'sh ver' -%}
{%- set output = salt.net.cli(command) -%}
{%- set textfsm_extract = salt.textfsm.index(command, output=output) -%}
"""
ret = {"out": None, "result": False, "comment": ""}
if not HAS_CLITABLE:
ret["comment"] = "TextFSM does not seem that has clitable embedded."
log.error(ret["comment"])
return ret
if not platform:
platform_grain_name = __opts__.get("textfsm_platform_grain") or __pillar__.get(
"textfsm_platform_grain", platform_grain_name
)
if platform_grain_name:
log.debug(
"Using the %s grain to identify the platform name", platform_grain_name
)
platform = __grains__.get(platform_grain_name)
if not platform:
ret["comment"] = (
"Unable to identify the platform name using the {} grain.".format(
platform_grain_name
)
)
return ret
log.info("Using platform: %s", platform)
else:
ret["comment"] = (
"No platform specified, no platform grain identifier configured."
)
log.error(ret["comment"])
return ret
if not textfsm_path:
log.debug(
"No TextFSM templates path specified, trying to look into the opts and"
" pillar"
)
textfsm_path = __opts__.get("textfsm_path") or __pillar__.get("textfsm_path")
if not textfsm_path:
ret["comment"] = (
"No TextFSM templates path specified. Please configure in"
" opts/pillar/function args."
)
log.error(ret["comment"])
return ret
log.debug(
"Caching %s(saltenv: %s) using the Salt fileserver", textfsm_path, saltenv
)
textfsm_cachedir_ret = __salt__["cp.cache_dir"](
textfsm_path,
saltenv=saltenv,
include_empty=include_empty,
include_pat=include_pat,
exclude_pat=exclude_pat,
)
log.debug("Cache fun return:\n%s", textfsm_cachedir_ret)
if not textfsm_cachedir_ret:
ret["comment"] = (
"Unable to fetch from {}. Is the TextFSM path correctly specified?".format(
textfsm_path
)
)
log.error(ret["comment"])
return ret
textfsm_cachedir = os.path.dirname(textfsm_cachedir_ret[0]) # first item
index_file = __opts__.get("textfsm_index_file") or __pillar__.get(
"textfsm_index_file", "index"
)
index_file_path = os.path.join(textfsm_cachedir, index_file)
log.debug("Using the cached index file: %s", index_file_path)
log.debug("TextFSM templates cached under: %s", textfsm_cachedir)
textfsm_obj = clitable.CliTable(index_file_path, textfsm_cachedir)
attrs = {"Command": command}
platform_column_name = __opts__.get(
"textfsm_platform_column_name"
) or __pillar__.get("textfsm_platform_column_name", "Platform")
log.info("Using the TextFSM platform idenfiticator: %s", platform_column_name)
attrs[platform_column_name] = platform
log.debug("Processing the TextFSM index file using the attributes: %s", attrs)
if not output and output_file:
log.debug("Processing the output from %s", output_file)
output = __salt__["cp.get_file_str"](output_file, saltenv=saltenv)
if output is False:
ret["comment"] = (
"Unable to read from {}. Please specify a valid file or text.".format(
output_file
)
)
log.error(ret["comment"])
return ret
if not output:
ret["comment"] = "Please specify a valid output text or file"
log.error(ret["comment"])
return ret
log.debug("Processing the raw text:\n%s", output)
try:
# Parse output through template
textfsm_obj.ParseCmd(output, attrs)
ret["out"] = _clitable_to_dict(textfsm_obj, textfsm_obj)
ret["result"] = True
except clitable.CliTableError as cterr:
log.error("Unable to proces the CliTable", exc_info=True)
ret["comment"] = f"Unable to process the output: {cterr}"
return ret
Zerion Mini Shell 1.0