diff --git a/CHANGELOG.md b/CHANGELOG.md index ec9ba6a..a997398 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Ciena WaveserverAi Collection Release Notes +## v1.2.0 +### Added +- waveserverai_command module for executing CLI commands over SSH +- Support for the ``network_cli`` connection via a new cliconf plugin +- Terminal plugin to recognise Waveserver Ai prompts when using SSH transport + ## v1.1.0 ### Added - Added support for Waveserver Ai 2.5.0 diff --git a/README.md b/README.md index 43c67aa..4313df0 100644 --- a/README.md +++ b/README.md @@ -20,7 +20,7 @@ PEP440 is the schema used to describe the versions of Ansible. ### Supported connections -Supports ``netconf`` connections. +Supports ``netconf`` and ``network_cli`` connections. ## Included content @@ -30,9 +30,20 @@ Name | Description --- | --- [ciena.waveserverai.waveserverai](https://github.com/ciena/ciena.waveserverai/blob/master/docs/ciena.waveserverai.waveserverai_netconf.rst)|Use waveserverai netconf plugin to run netconf commands on Ciena waveserverai platform +### Cliconf plugins +Name | Description +--- | --- +[ciena.waveserverai.waveserverai](https://github.com/ciena/ciena.waveserverai/blob/master/plugins/cliconf/waveserverai.py)|Low level CLI transport for running commands on Waveserver Ai + +### Terminal plugins +Name | Description +--- | --- +[ciena.waveserverai.waveserverai](https://github.com/ciena/ciena.waveserverai/blob/master/plugins/terminal/waveserverai.py)|Prompt handling definitions for Waveserver Ai CLI sessions + ### Modules Name | Description --- | --- +[ciena.waveserverai.waveserverai_command](https://github.com/ciena/ciena.waveserverai/blob/master/docs/ciena.waveserverai.waveserverai_command_module.rst)|Run commands on remote devices running Ciena Waveserver Ai [ciena.waveserverai.waveserverai_facts](https://github.com/ciena/ciena.waveserverai/blob/master/docs/ciena.waveserverai.waveserverai_facts_module.rst)|Get facts about waveserverai devices. [ciena.waveserverai.waveserverai_ports](https://github.com/ciena/ciena.waveserverai/blob/master/docs/ciena.waveserverai.waveserverai_ports_module.rst)|Waveserver port configuration and operational data.Manage the ports ports configuration of a Ciena waveserverai device [ciena.waveserverai.waveserverai_ptps](https://github.com/ciena/ciena.waveserverai/blob/master/docs/ciena.waveserverai.waveserverai_ptps_module.rst)|Waveserver Physical Termination Point (PTP) configuration and operational data.Manage the ptps ptps configuration of a Ciena waveserverai device @@ -113,6 +124,3 @@ ansible-playbook -e rm_dest=$PATH_TO_ANSIBLE_COLLECTIONS_DIR \ See [LICENSE](LICENSE) to see the full text. - - - diff --git a/docs/ciena.waveserverai.waveserverai_command_module.rst b/docs/ciena.waveserverai.waveserverai_command_module.rst new file mode 100644 index 0000000..efbc1ea --- /dev/null +++ b/docs/ciena.waveserverai.waveserverai_command_module.rst @@ -0,0 +1,237 @@ +.. _ciena.waveserverai.waveserverai_command_module: + + +*************************** +ciena.waveserverai.waveserverai_command +*************************** + +**Run commands on remote devices running Ciena Waveserver Ai** + + +Version added: 1.2.0 + +.. contents:: + :local: + :depth: 1 + + +Synopsis +-------- +- Sends arbitrary commands to a Waveserver Ai node and returns the results read from the device. This module includes an argument that will cause the module to wait for a specific condition before returning or timing out if the condition is not met. + + + + +Parameters +---------- + +.. raw:: html + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
ParameterChoices/DefaultsComments
+
+ commands + +
+ list + / elements=raw + / required +
+
+ +
List of commands to send to the remote Waveserver Ai device over the configured provider. The resulting output from the command is returned. If the wait_for argument is provided, the module is not returned until the condition is satisfied or the number of retries has expired. If a command sent to the device requires answering a prompt, it is possible to pass a dict containing command, answer and prompt. Common answers are 'y' or "\r" (carriage return, must be double quotes). See examples.
+
+
+ interval + +
+ integer +
+
+ Default:
1
+
+
Configures the interval in seconds to wait between retries of the command. If the command does not pass the specified conditions, the interval indicates how long to wait before trying the command again.
+
+
+ match + +
+ string +
+
+
    Choices: +
  • any
  • +
  • all ←
  • +
+
+
The match argument is used in conjunction with the wait_for argument to specify the match policy. Valid values are all or any. If the value is set to all then all conditionals in the wait_for must be satisfied. If the value is set to any then only one of the values must be satisfied.
+
+
+ retries + +
+ integer +
+
+ Default:
10
+
+
Specifies the number of retries a command should by tried before it is considered failed. The command is run on the target device every retry and evaluated against the wait_for conditions.
+
+
+ wait_for + +
+ list + / elements=string +
+
+ +
List of conditions to evaluate against the output of the command. The task will wait for each condition to be true before moving forward. If the conditional is not true within the configured number of retries, the task fails. See examples.
+

aliases: waitfor
+
+
+ + +Notes +----- + +.. note:: + - Requires ``ansible_connection=network_cli`` (SSH). + + + +Examples +-------- + +.. code-block:: yaml + + - name: run software show on remote devices + ciena.waveserverai.waveserverai_command: + commands: software show + + - name: run software show and check to see if output contains Installed + ciena.waveserverai.waveserverai_command: + commands: software show + wait_for: result[0] contains Installed + + - name: run multiple commands on remote nodes + ciena.waveserverai.waveserverai_command: + commands: + - software show + - xcvr show + + - name: run multiple commands and evaluate the output + ciena.waveserverai.waveserverai_command: + commands: + - software show + - xcvr show + wait_for: + - result[0] contains Installed + - result[1] contains Port + + +Return Values +------------- +Common return values are documented `here `_, the following are the fields unique to this module: + +.. raw:: html + + + + + + + + + + + + + + + + + + + + + + +
KeyReturnedDescription
+
+ failed_conditions + +
+ list +
+
failed +
The list of conditionals that have failed
+
+
Sample:
+
['...', '...']
+
+
+ stdout + +
+ list +
+
always apart from low level errors (such as action plugin) +
The set of responses from the commands
+
+
Sample:
+
['...', '...']
+
+
+ stdout_lines + +
+ list +
+
always apart from low level errors (such as action plugin) +
The value of stdout split into a list
+
+
Sample:
+
[['...', '...'], ['...'], ['...']]
+
+

+ + +Status +------ + + +Authors +~~~~~~~ + +- Ciena diff --git a/galaxy.yml b/galaxy.yml index 8e8948e..379c1aa 100644 --- a/galaxy.yml +++ b/galaxy.yml @@ -1,5 +1,5 @@ --- -version: 1.1.0 +version: 1.2.0 authors: - Santiago Echevarria (sechevar@ciena.com) - Jeff Groom (jgroom@ciena.com) diff --git a/meta/runtime.yml b/meta/runtime.yml index d168bfd..05a8504 100644 --- a/meta/runtime.yml +++ b/meta/runtime.yml @@ -4,6 +4,8 @@ plugin_routing: action: facts: redirect: ciena.waveserverai.waveserverai_facts + command: + redirect: ciena.waveserverai.waveserverai_command ports: redirect: ciena.waveserverai.waveserverai_ports ptps: diff --git a/plugins/cliconf/__init__.py b/plugins/cliconf/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/plugins/cliconf/waveserverai.py b/plugins/cliconf/waveserverai.py new file mode 100644 index 0000000..95b3a8c --- /dev/null +++ b/plugins/cliconf/waveserverai.py @@ -0,0 +1,109 @@ +# (c) 2025 Ciena Corp. +# +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . +# +from __future__ import absolute_import, division, print_function + +__metaclass__ = type + +DOCUMENTATION = """ +--- +name: waveserverai +author: + - Ciena +short_description: Use waveserverai cliconf to run commands on Ciena Waveserver Ai platform +description: + - This waveserverai plugin provides low level abstraction APIs for sending and receiving + CLI commands from Ciena Waveserver Ai network devices. +""" + +import json +import re + +from ansible.errors import AnsibleConnectionFailure, AnsibleError +from ansible.module_utils._text import to_text +from ansible.module_utils.common._collections_compat import Mapping +from ansible_collections.ansible.netcommon.plugins.module_utils.network.common.utils import to_list +from ansible.plugins.cliconf import CliconfBase + + +class Cliconf(CliconfBase): + def get_device_info(self): + return {"network_os": "ciena.waveserverai.waveserverai"} + + def get_config(self, source="running", flags=None, format="text"): + command = "configuration show" + try: + return self.send_command(command) + except AnsibleConnectionFailure: + # Some platforms may not support a running-config view over CLI. + # Re-raise so callers get a clear failure rather than silent None. + raise + + def edit_config(self, candidate=None, commit=False, comment=None): + raise AnsibleError("Configuration editing is not supported over CLI for Waveserver Ai") + + def get( + self, + command=None, + prompt=None, + answer=None, + sendonly=False, + newline=True, + output=None, + check_all=False, + ): + if not command: + raise ValueError("must provide value of command to execute") + if output: + raise ValueError("'output' value %s is not supported for get" % output) + + return self.send_command( + command=command, + prompt=prompt, + answer=answer, + sendonly=sendonly, + newline=newline, + check_all=check_all, + ) + + def run_commands(self, commands=None, check_rc=True): + if commands is None: + raise ValueError("'commands' value is required") + + responses = [] + for cmd in to_list(commands): + if not isinstance(cmd, Mapping): + cmd = {"command": cmd} + + output = cmd.pop("output", None) + if output: + raise ValueError("'output' value %s is not supported for run_commands" % output) + + try: + out = self.send_command(**cmd) + except AnsibleConnectionFailure as exc: + if check_rc: + raise + out = getattr(exc, "err", exc) + + responses.append(out) + + return responses + + def get_capabilities(self): + result = super(Cliconf, self).get_capabilities() + return json.dumps(result) diff --git a/plugins/module_utils/network/waveserverai/waveserverai.py b/plugins/module_utils/network/waveserverai/waveserverai.py index d18eddb..fe232f2 100644 --- a/plugins/module_utils/network/waveserverai/waveserverai.py +++ b/plugins/module_utils/network/waveserverai/waveserverai.py @@ -119,3 +119,24 @@ def get_configuration(module, source="running", format="xml", filter=None): except ConnectionError as exc: module.fail_json(msg=to_text(exc, errors="surrogate_then_replace")) return reply + + +def run_commands(module, commands, check_rc=True): + """Send a list of commands to the device over the active connection.""" + + capabilities = get_capabilities(module) + network_api = capabilities.get("network_api") + if network_api != "cliconf": + module.fail_json( + msg=( + "waveserverai_command requires ansible_connection=network_cli. " + "Current connection type is '%s'." % (network_api or "unknown") + ) + ) + + connection = get_connection(module) + try: + response = connection.run_commands(commands=commands, check_rc=check_rc) + except ConnectionError as exc: + module.fail_json(msg=to_text(exc, errors="surrogate_then_replace")) + return response diff --git a/plugins/modules/waveserverai_command.py b/plugins/modules/waveserverai_command.py new file mode 100644 index 0000000..15a9b54 --- /dev/null +++ b/plugins/modules/waveserverai_command.py @@ -0,0 +1,181 @@ +#!/usr/bin/python +# +# Copyright: (c) 2025 Ciena Corp +# Copyright 2019 Red Hat +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) +from __future__ import absolute_import, division, print_function + +__metaclass__ = type + +DOCUMENTATION = """ +module: waveserverai_command +author: + - Ciena +short_description: Run commands on remote devices running Ciena Waveserver Ai +description: +- Sends arbitrary commands to a Waveserver Ai node and returns the results read from the device. + This module includes an argument that will cause the module to wait for a specific condition + before returning or timing out if the condition is not met. +options: + commands: + description: + - List of commands to send to the remote Waveserver Ai device over the configured provider. + The resulting output from the command is returned. If the I(wait_for) argument is provided, + the module is not returned until the condition is satisfied or the number of retries has + expired. If a command sent to the device requires answering a prompt, it is possible to pass + a dict containing I(command), I(answer) and I(prompt). Common answers are 'y' or "\\r" + (carriage return, must be double quotes). See examples. + required: true + type: list + elements: raw + wait_for: + description: + - List of conditions to evaluate against the output of the command. The task will wait for + each condition to be true before moving forward. If the conditional is not true within the + configured number of retries, the task fails. See examples. + aliases: + - waitfor + type: list + elements: str + match: + description: + - The I(match) argument is used in conjunction with the I(wait_for) argument to specify the + match policy. Valid values are C(all) or C(any). If the value is set to C(all) then all + conditionals in the wait_for must be satisfied. If the value is set to C(any) then only one + of the values must be satisfied. + default: all + type: str + choices: + - any + - all + retries: + description: + - Specifies the number of retries a command should be tried before it is considered failed. + The command is run on the target device every retry and evaluated against the I(wait_for) + conditions. + default: 10 + type: int + interval: + description: + - Configures the interval in seconds to wait between retries of the command. If the command + does not pass the specified conditions, the interval indicates how long to wait before trying + the command again. + default: 1 + type: int +notes: +- Requires an SSH-based connection type, such as C(connection=network_cli). +""" + +EXAMPLES = """ +- name: run software show on remote devices + ciena.waveserverai.waveserverai_command: + commands: software show + +- name: run software show and check to see if output contains Normal + ciena.waveserverai.waveserverai_command: + commands: software show + wait_for: result[0] contains Normal + +- name: run multiple commands on remote nodes + ciena.waveserverai.waveserverai_command: + commands: + - software show + - xcvr show + +- name: run multiple commands and evaluate the output + ciena.waveserverai.waveserverai_command: + commands: + - software show + - xcvr show + wait_for: + - result[0] contains Installed + - result[1] contains Port + +""" + +RETURN = """ +stdout: + description: The set of responses from the commands + returned: always apart from low level errors (such as action plugin) + type: list + sample: ['...', '...'] +stdout_lines: + description: The value of stdout split into a list + returned: always apart from low level errors (such as action plugin) + type: list + sample: [['...', '...'], ['...'], ['...']] +failed_conditions: + description: The list of conditionals that have failed + returned: failed + type: list + sample: ['...', '...'] +""" + +import time + +from ansible.module_utils._text import to_text +from ansible.module_utils.basic import AnsibleModule +from ansible_collections.ansible.netcommon.plugins.module_utils.network.common.parsing import Conditional +from ansible_collections.ansible.netcommon.plugins.module_utils.network.common.utils import ( + transform_commands, + to_lines, +) +from ansible_collections.ciena.waveserverai.plugins.module_utils.network.waveserverai.waveserverai import ( + run_commands, +) + + +def parse_commands(module, warnings): + return transform_commands(module) + + +def main(): + argument_spec = dict( + commands=dict(type="list", required=True, elements="raw"), + wait_for=dict(type="list", aliases=["waitfor"], elements="str"), + match=dict(default="all", choices=["all", "any"], type="str"), + retries=dict(default=10, type="int"), + interval=dict(default=1, type="int"), + ) + + module = AnsibleModule(argument_spec=argument_spec, supports_check_mode=True) + + warnings = [] + result = {"changed": False, "warnings": warnings} + + commands = parse_commands(module, warnings) + + wait_for = module.params["wait_for"] or [] + try: + conditionals = [Conditional(condition) for condition in wait_for] + except AttributeError as exc: + module.fail_json(msg=to_text(exc)) + + retries = module.params["retries"] + interval = module.params["interval"] + match = module.params["match"] + + while retries > 0: + responses = run_commands(module, commands) + for conditional in list(conditionals): + if conditional(responses): + if match == "any": + conditionals = [] + break + conditionals.remove(conditional) + if not conditionals: + break + time.sleep(interval) + retries -= 1 + + if conditionals: + failed_conditions = [conditional.raw for conditional in conditionals] + msg = "One or more conditional statements have not been satisfied" + module.fail_json(msg=msg, failed_conditions=failed_conditions) + + result.update({"stdout": responses, "stdout_lines": list(to_lines(responses))}) + module.exit_json(**result) + + +if __name__ == "__main__": + main() diff --git a/plugins/terminal/__init__.py b/plugins/terminal/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/plugins/terminal/waveserverai.py b/plugins/terminal/waveserverai.py new file mode 100644 index 0000000..ab805f2 --- /dev/null +++ b/plugins/terminal/waveserverai.py @@ -0,0 +1,41 @@ +# (c) 2025 Ciena Corp. +# +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . +# +from __future__ import absolute_import, division, print_function + +__metaclass__ = type + +import re + +from ansible.plugins.terminal import TerminalBase + + +class TerminalModule(TerminalBase): + # mirror SAOS10 prompt handling with relaxed trailing whitespace + terminal_stdout_re = [ + re.compile(rb"[\w+\-.:@/\[\]]+(?:\([^\)]+\)){0,3}[*]?>\s"), + re.compile(rb"[\w+\-.:@/\[\]]+(?:\([^\)]+\)){0,3}[*]?#\s"), + re.compile(rb"diag@\S+\$\s"), + ] + + terminal_stderr_re = [ + re.compile(rb"SHELL PARSER FAILURE"), + re.compile(rb"ERROR\:"), + re.compile(rb"Error\:"), + ] + + terminal_initial_prompt_newline = False diff --git a/tests/static/playbook.yml b/tests/static/playbook.yml index b2c900b..97d5158 100644 --- a/tests/static/playbook.yml +++ b/tests/static/playbook.yml @@ -1,8 +1,5 @@ --- - hosts: all - connection: netconf - collections: - - ciena.waveserverai gather_facts: false name: Gather facts for ciena device Waveserver Ai tasks: @@ -12,17 +9,27 @@ - min gather_network_resources: - all + connection: netconf - name: Disable interfaces - waveserverai_xcvrs: + ciena.waveserverai.waveserverai_xcvrs: config: - xcvr_id: 1-1 state: admin_state: disabled + connection: netconf - name: Enable interfaces - waveserverai_xcvrs: + ciena.waveserverai.waveserverai_xcvrs: config: - xcvr_id: 1-1 state: admin_state: enabled + connection: netconf + + - name: Commands + ciena.waveserverai.waveserverai_command: + commands: + - xcvr show + - xcvr enable xcvr 1/1 + connection: network_cli