From d14f16e31b69f032d2a6b556d19fa21feac7fd2c Mon Sep 17 00:00:00 2001
From: Nathaniel Case <this.is@nathanielca.se>
Date: Mon, 4 Feb 2019 09:28:26 -0500
Subject: [PATCH] Restconf HTTPAPI plugin and modules (#49476)

* Initial code for restconf support

*  Add restconf httpapi plugin
*  Add restonf_get module

* Fix some ConnectionError usage
---
 .../module_utils/network/restconf/__init__.py |   0
 .../module_utils/network/restconf/restconf.py |  57 +++++++++
 .../modules/network/restconf/__init__.py      |   0
 .../modules/network/restconf/restconf_get.py  | 110 ++++++++++++++++++
 lib/ansible/plugins/connection/httpapi.py     |   2 +
 lib/ansible/plugins/httpapi/restconf.py       |  79 +++++++++++++
 6 files changed, 248 insertions(+)
 create mode 100644 lib/ansible/module_utils/network/restconf/__init__.py
 create mode 100644 lib/ansible/module_utils/network/restconf/restconf.py
 create mode 100644 lib/ansible/modules/network/restconf/__init__.py
 create mode 100644 lib/ansible/modules/network/restconf/restconf_get.py
 create mode 100644 lib/ansible/plugins/httpapi/restconf.py

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 <http://www.gnu.org/licenses/>.
+#
+
+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