summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorNathaniel Case <this.is@nathanielca.se>2019-02-04 09:28:26 -0500
committerGitHub <noreply@github.com>2019-02-04 09:28:26 -0500
commitd14f16e31b69f032d2a6b556d19fa21feac7fd2c (patch)
treed689ca5e3a1bed51fc85a3541e63c6e335251c97
parent57349c0611b048b426c8e3fce9f4c047fd3ff1d0 (diff)
downloadansible-d14f16e31b69f032d2a6b556d19fa21feac7fd2c.tar.gz
Restconf HTTPAPI plugin and modules (#49476)
* Initial code for restconf support * Add restconf httpapi plugin * Add restonf_get module * Fix some ConnectionError usage
-rw-r--r--lib/ansible/module_utils/network/restconf/__init__.py0
-rw-r--r--lib/ansible/module_utils/network/restconf/restconf.py57
-rw-r--r--lib/ansible/modules/network/restconf/__init__.py0
-rw-r--r--lib/ansible/modules/network/restconf/restconf_get.py110
-rw-r--r--lib/ansible/plugins/connection/httpapi.py2
-rw-r--r--lib/ansible/plugins/httpapi/restconf.py79
6 files changed, 248 insertions, 0 deletions
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 0000000000..e69de29bb2
--- /dev/null
+++ b/lib/ansible/module_utils/network/restconf/__init__.py
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 0000000000..375cd0a95b
--- /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 0000000000..e69de29bb2
--- /dev/null
+++ b/lib/ansible/modules/network/restconf/__init__.py
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 0000000000..d61767fa64
--- /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 7de944ce63..acaf0f6d27 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 0000000000..7e9b6ee266
--- /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