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:
Nathaniel Case 2019-02-04 09:28:26 -05:00 committed by GitHub
parent 57349c0611
commit d14f16e31b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 248 additions and 0 deletions

View 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)

View 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()

View file

@ -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

View 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