Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add iosxr grpc plugin and grpc_nw_get module #52857

Closed
wants to merge 4 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
8 changes: 8 additions & 0 deletions lib/ansible/config/base.yml
Expand Up @@ -892,6 +892,14 @@ DEFAULT_NETCONF_PLUGIN_PATH:
ini:
- {key: netconf_plugins, section: defaults}
type: pathspec
DEFAULT_GRPC_PLUGIN_PATH:
name: gRPC Plugins Path
default: ~/.ansible/plugins/grpc:/usr/share/ansible/plugins/grpc
description: Colon separated paths in which Ansible will search for gRPC Plugins.
env: [{name: ANSIBLE_GRPC_PLUGINS}]
ini:
- {key: grpc_plugins, section: defaults}
type: pathspec
DEFAULT_NO_LOG:
name: No log
default: False
Expand Down
Empty file.
75 changes: 75 additions & 0 deletions lib/ansible/module_utils/network/grpc/grpc.py
@@ -0,0 +1,75 @@
#
# (c) 2019 Red Hat, Inc.
#
# 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 <http://www.gnu.org/licenses/>.
#

from ansible.module_utils._text import to_text
from ansible.module_utils.connection import Connection


def get_connection(module):
if hasattr(module, '_grpc_connection'):
return module._grpc_connection

capabilities = get_capabilities(module)
network_api = capabilities.get('network_api')
if network_api == 'grpc':
module._grpc_connection = Connection(module._socket_path)
else:
module.fail_json(msg='Invalid connection type %s' % network_api)

return module._grpc_connection


def get_capabilities(module):
if hasattr(module, '_grpc_capabilities'):
return module._grpc_capabilities

module._grpc_capabilities = Connection(module._socket_path).get_capabilities()
return module._grpc_capabilities


def get(module, section, data_type, check_rc=True):
conn = get_connection(module)
if data_type == 'config':
out = conn.get_config(section)
else:
out = conn.get(section)

response = out.get('response')
error = out.get('error')
if error:
if check_rc:
module.fail_json(msg=to_text(out['error'], errors='surrogate_then_replace'))
else:
module.warn(to_text(out['error'], errors='surrogate_then_replace'))

return response.strip(), error.strip()


def run_cli(module, command, display, check_rc=True):
conn = get_connection(module)
out = conn.run_cli(command, display)
response = out.get('response')
error = out.get('error')
if error:
if check_rc:
module.fail_json(msg=to_text(out['error'], errors='surrogate_then_replace'))
else:
module.warn(to_text(out['error'], errors='surrogate_then_replace'))

return response.strip(), error.strip()
Empty file.
195 changes: 195 additions & 0 deletions lib/ansible/modules/network/grpc/grpc_nw_get.py
@@ -0,0 +1,195 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-

# (c) 2019, Ansible by Red Hat, inc
# 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


ANSIBLE_METADATA = {'metadata_version': '1.1',
'status': ['preview'],
'supported_by': 'network'}


DOCUMENTATION = """
---
module: grpc_nw_get
version_added: "2.8"
author:
- "Ganesh Nalawade (@ganeshrn)"
short_description: Fetch configuration/state data from gRPC enabled target hosts.
description:
- gRPC is a high performance, open-source universal RPC framework.
- This module allows the user to fetch configuration and state data from gRPC
enabled devices.
options:
section:
description:
- This option specifies the string which acts as a filter to restrict the portions of
the data to be retrieved from the target host device. If this option is not specified the entire
configuration or state data is returned in response provided it is supported by target host.
command:
description:
- The option specifies the command to be executed on the target host and return the response
in result. This option is supported if the gRPC target host supports executing CLI command
over the gRPC connection.
display:
description:
- Encoding scheme to use when serializing output from the device. The encoding scheme
value depends on the capability of the gRPC server running on the target host.
The values can be I(json), I(text) etc.
data_type:
description:
- The type of data that should be fetched from the target host. The value depends on the
capability of the gRPC server running on target host. The values can be I(config), I(oper)
etc. based on what is supported by the gRPC server. By default it will return both configuration
and operational state data in response.
requirements:
- grpcio
- protobuf

notes:
- This module requires the gRPC system service be enabled on
the target host being managed.
- This module supports the use of connection=grpc.
- This module requires the value of 'ansible_network_os' be defined as an inventory variable.
- Tested against iosxrv 9k version 6.1.2.
"""

EXAMPLES = """
- name: Get bgp configuration data
grpc_nw_get:
section: '{"Cisco-IOS-XR-ipv4-bgp-cfg:bgp": [null]}'

- name: Get configuration JSON format over secure TLS channel
grpc_nw_get:
display: json
data: config
vars:
ansible_root_certificates_file: /home/username/ems.pem
ansible_grpc_channel_options:
'grpc.ssl_target_name_override': 'ems.cisco.com'

- name: run cli command
grpc_nw_get:
command: 'show version'
display: text
"""

RETURN = """
stdout:
description: The raw string containing configuration or state data
received from the gRPC server.
returned: always apart from low-level errors (such as action plugin)
type: str
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: ['...', '...']
output:
description: A dictionary representing a JSON-formatted response, if the response
is a valid json string
returned: when the device response is valid JSON
type: list
sample: |
[{
"Cisco-IOS-XR-ip-static-cfg:router-static": {
"default-vrf": {
"address-family": {
"vrfipv4": {
"vrf-unicast": {
"vrf-prefixes": {
"vrf-prefix": [
{
"prefix": "0.0.0.0",
"prefix-length": 0,
"vrf-route": {
"vrf-next-hop-table": {
"vrf-next-hop-interface-name-next-hop-address": [
{
"interface-name": "MgmtEth0/RP0/CPU0/0",
"next-hop-address": "10.0.2.2"
}
]
}
}
}
]
}
}
}
}
}
}
}]
"""
from ansible.module_utils._text import to_text
from ansible.module_utils.basic import AnsibleModule
from ansible.module_utils.connection import ConnectionError
from ansible.module_utils.network.common.utils import to_list
from ansible.module_utils.network.grpc.grpc import get_capabilities, get, run_cli


def main():
"""entry point for module execution
"""
argument_spec = dict(
section=dict(),
command=dict(),
display=dict(),
data_type=dict()
)

mutually_exclusive = [['section', 'command']]

module = AnsibleModule(argument_spec=argument_spec,
mutually_exclusive=mutually_exclusive,
supports_check_mode=True)

capabilities = get_capabilities(module)

operations = capabilities['server_capabilities']

section = module.params['section']
command = module.params['command']
display = module.params['display']
data_type = module.params['data_type']

if display and display not in operations.get('display', []):
module.fail_json(msg="display option '%s' is not supported on this target host" % display)

if command and not operations.get('supports_cli_command', False):
module.fail_json(msg="command option '%s' is not supported on this target host" % command)

if data_type and data_type not in operations.get('data_type', []):
module.fail_json(msg="data_type option '%s' is not supported on this target host" % data_type)

result = {'changed': False}

try:
if command:
response, err = run_cli(module, command, display)
else:
response, err = get(module, section, data_type)

try:
output = module.from_json(response)
except ValueError:
output = None
except ConnectionError as exc:
module.fail_json(msg=to_text(exc, errors='surrogate_then_replace'), code=exc.code)

result['stdout'] = response

if output:
result['output'] = to_list(output)

module.exit_json(**result)


if __name__ == '__main__':
main()
52 changes: 52 additions & 0 deletions lib/ansible/plugins/grpc/__init__.py
@@ -0,0 +1,52 @@
# (c) 2019 Ansible Project
# 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

from ansible.plugins import AnsiblePlugin


class GrpcBase(AnsiblePlugin):
"""
A base class for implementing gRPC abstraction layer
"""

__rpc__ = ['channel', 'get_config', 'edit_config', 'get']

def __init__(self, connection):
super(GrpcBase, self).__init__()
self._connection = connection

@property
def channel(self):
return self._connection._channel

def get_config(self, section=None):
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
def get_config(self, section=None):
@abstractmethod
def get_config(self, section=None):

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The grpc protocol does not define any specific api's or RPC as in the case of netconf and restconf. The gRPC api's support for any platform is defined in the protobuf files, that's the reason these methods are not defined as abstractmethod as some of the platform may not have the need to implement these api's

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess if these methods aren't guaranteed to exist, then why present them in the base class at all? What is the benefit of having this method exist but be unimplemented on an implementation that doesn't use it?

"""
Retrieve all or part of a specified configuration
(by default entire configuration is retrieved).
:param section: This argument specifies the portion of the configuration data to retrieve
:return: Returns the response received from gRPC server from target host in string format
"""
pass

def get(self, section=None):
Qalthos marked this conversation as resolved.
Show resolved Hide resolved
"""
Retrieve device state information.
:param section: This argument specifies the portion of the state data to retrieve
(by default entire state data is retrieved)
:return: Returns the json string as a response
"""
pass

def edit_config(self, config=None, action=None):
Qalthos marked this conversation as resolved.
Show resolved Hide resolved
"""
Loads all or part of the specified *config* to the configuration datastore.
:param config: The configuration that needs to be push on target host
:param action: The action to be performed on the configuration datastore for example: 'merge',
'replace', 'delete' etc.
:return: Returns the response received from gRPC server from target host in string format
"""
pass