Restconf HTTPAPI plugin and modules (#49476)
* Initial code for restconf support * Add restconf httpapi plugin * Add restonf_get module * Fix some ConnectionError usage
This commit is contained in:
parent
57349c0611
commit
d14f16e31b
6 changed files with 248 additions and 0 deletions
0
lib/ansible/module_utils/network/restconf/__init__.py
Normal file
0
lib/ansible/module_utils/network/restconf/__init__.py
Normal file
57
lib/ansible/module_utils/network/restconf/restconf.py
Normal file
57
lib/ansible/module_utils/network/restconf/restconf.py
Normal file
|
@ -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)
|
0
lib/ansible/modules/network/restconf/__init__.py
Normal file
0
lib/ansible/modules/network/restconf/__init__.py
Normal file
110
lib/ansible/modules/network/restconf/restconf_get.py
Normal file
110
lib/ansible/modules/network/restconf/restconf_get.py
Normal file
|
@ -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()
|
|
@ -288,4 +288,6 @@ class Connection(NetworkConnectionBase):
|
||||||
# Try to assign a new auth token if one is given
|
# Try to assign a new auth token if one is given
|
||||||
self._auth = self.update_auth(response, response_buffer) or self._auth
|
self._auth = self.update_auth(response, response_buffer) or self._auth
|
||||||
|
|
||||||
|
response_buffer.seek(0)
|
||||||
|
|
||||||
return response, response_buffer
|
return response, response_buffer
|
||||||
|
|
79
lib/ansible/plugins/httpapi/restconf.py
Normal file
79
lib/ansible/plugins/httpapi/restconf.py
Normal file
|
@ -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
|
Loading…
Reference in a new issue