diff --git a/lib/ansible/module_utils/network/restconf/__init__.py b/lib/ansible/module_utils/network/restconf/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/lib/ansible/module_utils/network/restconf/restconf.py b/lib/ansible/module_utils/network/restconf/restconf.py new file mode 100644 index 00000000000..375cd0a95be --- /dev/null +++ b/lib/ansible/module_utils/network/restconf/restconf.py @@ -0,0 +1,57 @@ +# This code is part of Ansible, but is an independent component. +# This particular file snippet, and this file snippet only, is BSD licensed. +# Modules you write using this snippet, which is embedded dynamically by Ansible +# still belong to the author of the module, and may assign their own license +# to the complete work. +# +# (c) 2018 Red Hat Inc. +# +# Redistribution and use in source and binary forms, with or without modification, +# are permitted provided that the following conditions are met: +# +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions and the following disclaimer in the documentation +# and/or other materials provided with the distribution. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. +# IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, +# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT +# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE +# USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +# + +from ansible.module_utils.connection import Connection + + +def get(module, path=None, content=None, fields=None, output='json'): + if path is None: + raise ValueError('path value must be provided') + if content: + path += '?' + 'content=%s' % content + if fields: + path += '?' + 'field=%s' % fields + + accept = None + if output == 'xml': + accept = 'application/yang.data+xml' + + connection = Connection(module._socket_path) + return connection.send_request(None, path=path, method='GET', accept=accept) + + +def edit_config(module, path=None, content=None, method='GET', format='json'): + if path is None: + raise ValueError('path value must be provided') + + content_type = None + if format == 'xml': + content_type = 'application/yang.data+xml' + + connection = Connection(module._socket_path) + return connection.send_request(content, path=path, method=method, content_type=content_type) diff --git a/lib/ansible/modules/network/restconf/__init__.py b/lib/ansible/modules/network/restconf/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/lib/ansible/modules/network/restconf/restconf_get.py b/lib/ansible/modules/network/restconf/restconf_get.py new file mode 100644 index 00000000000..d61767fa64d --- /dev/null +++ b/lib/ansible/modules/network/restconf/restconf_get.py @@ -0,0 +1,110 @@ +#!/usr/bin/python +# Copyright: 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 + + +ANSIBLE_METADATA = {'metadata_version': '1.1', + 'status': ['preview'], + 'supported_by': 'network'} + + +DOCUMENTATION = """ +--- +module: restconf_get +version_added: "2.8" +author: "Ganesh Nalawade (@ganeshrn)" +short_description: Fetch configuration/state data from RESTCONF enabled devices. +description: + - RESTCONF is a standard mechanisms to allow web applications to access the + configuration data and state data developed and standardized by + the IETF. It is documented in RFC 8040. + - This module allows the user to fetch configuration and state data from RESTCONF + enabled devices. +options: + path: + description: + - URI being used to execute API calls. + required: true + content: + description: + - The C(content) is a query parameter that controls how descendant nodes of the + requested data nodes in C(path) will be processed in the reply. If value is + I(config) return only configuration descendant data nodes of value in C(path). + If value is I(nonconfig) return only non-configuration descendant data nodes + of value in C(path). If value is I(all) return all descendant data nodes of + value in C(path) + required: false + choices: ['config', 'nonconfig', 'all'] + output: + description: + - The output of response received. + required: false + default: json + choices: ['json', 'xml'] +""" + +EXAMPLES = """ +- name: get l3vpn services + restconf_get: + path: /config/ietf-l3vpn-svc:l3vpn-svc/vpn-services +""" + +RETURN = """ +response: + description: A dictionary representing a JSON-formatted response + returned: when the device response is valid JSON + type: dict + sample: | + { + "vpn-services": { + "vpn-service": [ + { + "customer-name": "red", + "vpn-id": "blue_vpn1", + "vpn-service-topology": "ietf-l3vpn-svc:any-to-any" + } + ] + } + } + +""" + +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.restconf import restconf + + +def main(): + """entry point for module execution + """ + argument_spec = dict( + path=dict(required=True), + content=dict(choices=['config', 'nonconfig', 'all']), + output=dict(choices=['json', 'xml'], default='json'), + ) + + module = AnsibleModule( + argument_spec=argument_spec, + supports_check_mode=True + ) + + result = {'changed': False} + + try: + response = restconf.get(module, **module.params) + except ConnectionError as exc: + module.fail_json(msg=to_text(exc), code=exc.code) + + result.update({ + 'response': response, + }) + + module.exit_json(**result) + + +if __name__ == '__main__': + main() diff --git a/lib/ansible/plugins/connection/httpapi.py b/lib/ansible/plugins/connection/httpapi.py index 7de944ce63d..acaf0f6d27d 100644 --- a/lib/ansible/plugins/connection/httpapi.py +++ b/lib/ansible/plugins/connection/httpapi.py @@ -288,4 +288,6 @@ class Connection(NetworkConnectionBase): # Try to assign a new auth token if one is given self._auth = self.update_auth(response, response_buffer) or self._auth + response_buffer.seek(0) + return response, response_buffer diff --git a/lib/ansible/plugins/httpapi/restconf.py b/lib/ansible/plugins/httpapi/restconf.py new file mode 100644 index 00000000000..7e9b6ee2664 --- /dev/null +++ b/lib/ansible/plugins/httpapi/restconf.py @@ -0,0 +1,79 @@ +# Copyright (c) 2018 Cisco and/or its affiliates. +# +# 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 = """ +--- +author: Ansible Networking Team +httpapi: restconf +short_description: HttpApi Plugin for devices supporting Restconf API +description: + - This HttpApi plugin provides methods to connect to Restconf API + endpoints. +version_added: "2.8" +options: + root_path: + type: str + description: + - Specifies the location of the Restconf root. + default: '/restconf' + vars: + - name: ansible_httpapi_restconf_root +""" + +import json + +from ansible.module_utils.network.common.utils import to_list +from ansible.module_utils.connection import ConnectionError +from ansible.plugins.httpapi import HttpApiBase + + +CONTENT_TYPE = 'application/yang.data+json' + + +class HttpApi(HttpApiBase): + def send_request(self, data, **message_kwargs): + if data: + data = json.dumps(data) + + path = self.get_option('root_path') + message_kwargs.get('path', '') + + headers = { + 'Content-Type': message_kwargs.get('content_type') or CONTENT_TYPE, + 'Accept': message_kwargs.get('accept') or CONTENT_TYPE, + } + response, response_data = self.connection.send(path, data, headers=headers, method=message_kwargs.get('method')) + + return handle_response(response_data.read()) + + +def handle_response(response): + if 'error' in response and 'jsonrpc' not in response: + error = response['error'] + + error_text = [] + for data in error['data']: + error_text.extend(data.get('errors', [])) + error_text = '\n'.join(error_text) or error['message'] + + raise ConnectionError(error_text, code=error['code']) + + return response