Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

restconf_config module #51971

Merged
merged 13 commits into from
Mar 4, 2019
4 changes: 3 additions & 1 deletion lib/ansible/module_utils/network/common/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -264,7 +264,9 @@ def dict_diff(base, comparable):
if isinstance(value, dict):
item = comparable.get(key)
if item is not None:
updates[key] = dict_diff(value, comparable[key])
sub_diff = dict_diff(value, comparable[key])
if sub_diff:
updates[key] = sub_diff
else:
comparable_value = comparable.get(key)
if comparable_value is not None:
Expand Down
163 changes: 163 additions & 0 deletions lib/ansible/modules/network/restconf/restconf_config.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
#!/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_config
version_added: "2.8"
author: "Ganesh Nalawade (@ganeshrn)"
short_description: Handles create, update, read and delete of configuration data on RESTCONF enabled devices.
description:
- RESTCONF is a standard mechanisms to allow web applications to configure and manage
data. RESTCONF is a IETF standard and documented on RFC 8040.
- This module allows the user to configure data on RESTCONF enabled devices.
options:
path:
description:
- URI being used to execute API calls.
required: true
content:
description:
- The configuration data in format as specififed in C(format) option. Required unless C(method) is
I(delete).
method:
description:
- The RESTCONF method to manage the configuration change on device. The value I(post) is used to
create a data resource or invoke an operation resource, I(put) is used to replace the target
data resource, I(patch) is used to modify the target resource, and I(delete) is used to delete
the target resource.
required: false
default: post
choices: ['post', 'put', 'patch', 'delete']
format:
description:
- The format of the configuration provided as value of C(content). Accepted values are I(xml) and I(json) and
the given configuration format should be supported by remote RESTCONF server.
default: json
choices: ['json', 'xml']
'''

EXAMPLES = '''
- name: create l3vpn services
restconf_config:
path: /config/ietf-l3vpn-svc:l3vpn-svc/vpn-services
content: |
{
"vpn-service":[
{
"vpn-id": "red_vpn2",
"customer-name": "blue",
"vpn-service-topology": "ietf-l3vpn-svc:any-to-any"
},
{
"vpn-id": "blue_vpn1",
"customer-name": "red",
"vpn-service-topology": "ietf-l3vpn-svc:any-to-any"
}
]
}
'''

RETURN = '''
'''

import json

from ansible.module_utils.basic import AnsibleModule
from ansible.module_utils._text import to_text
from ansible.module_utils.connection import ConnectionError
from ansible.module_utils.network.common.utils import dict_diff
from ansible.module_utils.network.restconf import restconf
from ansible.module_utils.six import string_types


def main():
"""entry point for module execution
"""
argument_spec = dict(
path=dict(required=True),
content=dict(),
method=dict(choices=['post', 'put', 'patch', 'delete'], default='post'),
format=dict(choices=['json', 'xml'], default='json'),
)
required_if = [
['method', 'post', ['content']],
['method', 'put', ['content']],
['method', 'patch', ['content']],
]

module = AnsibleModule(
argument_spec=argument_spec,
required_if=required_if,
supports_check_mode=True
)

path = module.params['path']
candidate = module.params['content']
method = module.params['method']
format = module.params['format']

if isinstance(candidate, string_types):
candidate = json.loads(candidate)

warnings = list()
result = {'changed': False, 'warnings': warnings}

running = None
response = None
commit = not module.check_mode
try:
running = restconf.get(module, path, output=format)
except ConnectionError as exc:
if exc.code == 404:
running = None
else:
module.fail_json(msg=to_text(exc), code=exc.code)

try:
if method == 'delete':
if running:
if commit:
response = restconf.edit_config(module, path=path, method='DELETE')
result['changed'] = True
else:
warnings.append("delete not executed as resource '%s' does not exist" % path)
else:
if running:
if method == 'post':
module.fail_json(msg="resource '%s' already exist" % path, code=409)
diff = dict_diff(running, candidate)
result['candidate'] = candidate
result['running'] = running
else:
method = 'POST'
diff = candidate

if diff:
if module._diff:
result['diff'] = {'prepared': diff, 'before': candidate, 'after': running}

if commit:
response = restconf.edit_config(module, path=path, content=diff, method=method.upper(), format=format)
result['changed'] = True

except ConnectionError as exc:
module.fail_json(msg=str(exc), code=exc.code)

result['response'] = response

module.exit_json(**result)


if __name__ == '__main__':
main()
2 changes: 2 additions & 0 deletions lib/ansible/plugins/connection/httpapi.py
Original file line number Diff line number Diff line change
Expand Up @@ -232,6 +232,8 @@ def _connect(self):
port = self.get_option('port') or (443 if protocol == 'https' else 80)
self._url = '%s://%s:%s' % (protocol, host, port)

self.queue_message('vvv', "ESTABLISH HTTP(S) CONNECTFOR USER: %s TO %s" %
(self._play_context.remote_user, self._url))
self.httpapi.set_become(self._play_context)
self.httpapi.login(self.get_option('remote_user'), self.get_option('password'))

Expand Down
34 changes: 23 additions & 11 deletions lib/ansible/plugins/httpapi/restconf.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,8 +41,10 @@

import json

from ansible.module_utils._text import to_text
from ansible.module_utils.network.common.utils import to_list
from ansible.module_utils.connection import ConnectionError
from ansible.module_utils.six.moves.urllib.error import HTTPError
from ansible.plugins.httpapi import HttpApiBase


Expand All @@ -54,26 +56,36 @@ def send_request(self, data, **message_kwargs):
if data:
data = json.dumps(data)

path = self.get_option('root_path') + message_kwargs.get('path', '')
path = '/'.join([self.get_option('root_path').rstrip('/'), message_kwargs.get('path', '').lstrip('/')])

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'))
try:
response, response_data = self.connection.send(path, data, headers=headers, method=message_kwargs.get('method'))
except HTTPError as exc:
response_data = exc

return handle_response(response_data.read())
return handle_response(response_data)

def handle_httperror(self, exc):
return None


def handle_response(response):
if 'error' in response and 'jsonrpc' not in response:
error = response['error']
try:
response_json = json.loads(response.read())
except ValueError:
if isinstance(response, HTTPError):
raise ConnectionError(to_text(response), code=response.code)
return response.read()

if 'errors' in response_json and 'jsonrpc' not in response_json:
errors = response_json['errors']['error']

error_text = []
for data in error['data']:
error_text.extend(data.get('errors', []))
error_text = '\n'.join(error_text) or error['message']
error_text = '\n'.join((error['error-message'] for error in errors))

raise ConnectionError(error_text, code=error['code'])
raise ConnectionError(error_text, code=response.code)

return response
return response_json
5 changes: 5 additions & 0 deletions lib/ansible/utils/jsonrpc.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import traceback

from ansible.module_utils._text import to_text
from ansible.module_utils.connection import ConnectionError
from ansible.module_utils.six import binary_type
from ansible.utils.display import Display

Expand Down Expand Up @@ -42,6 +43,10 @@ def handle_request(self, request):
else:
try:
result = rpc_method(*args, **kwargs)
except ConnectionError as exc:
display.vvv(traceback.format_exc())
error = self.error(code=exc.code, message=to_text(exc))
response = json.dumps(error)
except Exception as exc:
display.vvv(traceback.format_exc())
error = self.internal_error(data=to_text(exc, errors='surrogate_then_replace'))
Expand Down