New module: access Cisco ACI (network/aci/aci_rest) (#26029)

* aci_rest: New module to access Cisco ACI

This PR includes:
- Relicense as GPLv3+
- Check-mode support
- Cosmetic changes to documentation
- Examples in YAML format
- Removal of incorrect requirements (for this module)
- Do not log passwords
- Implement native fetch_url instead of requests
- Use standard hostname, username and password parameters
- Add alias src for parameter config_file
- Add mutual exclusive content option for inline data (and show some inline examples)
- Add timeout parameter
- Add validate_certs parameter
- Handling ACI result output (identical for JSON as XML input)
- Parse/expose ACI error output to user

* Lower case method, add use_ssl, Use python dicts

This commit includes:
- Use lowercase method names
- Add `use_ssl` parameter (not the `protocol` parameter)
- Use a python dict for the request data (not a JSON string)
- Documentation improvements

* Ensure one of 'content' or 'src' is provided

* Fix issue with totalCount being a string in JSON

This fixes the problem with JSON output where totalCount is a string and
not an integer.

This fixes jedelman8/aci-ansible#7

* Improve code documentation

* Improve error handling and module response

* Small typo

* Improve documentation and examples

* Keep protocol parameter, but deprecate it

* Extrude aci functions from module_utils

* aci_rest: Add unit tests
This commit is contained in:
Dag Wieers 2017-07-17 17:32:12 +02:00 committed by John R Barker
parent a3aa5d799e
commit e970237a2f
6 changed files with 753 additions and 0 deletions

1
.github/BOTMETA.yml vendored
View file

@ -429,6 +429,7 @@ files:
$modules/net_tools/omapi_host.py: nerzhul
$modules/net_tools/snmp_facts.py: ogenstad
$modules/network/a10/: ericchou1 mischapeters
$modules/network/aci/: dagwieers jedelman8
$modules/network/aos/: dgarros jeremyschulman
$modules/network/asa/asa_acl.py: gundalow ogenstad
$modules/network/asa/asa_command.py: gundalow ogenstad privateip

View file

@ -0,0 +1,395 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
# Copyright 2015 Jason Edelman <jason@networktocode.com>, Network to Code, LLC
# Copyright 2017 Dag Wieers <dag@wieers.com>
# This file is part of Ansible by Red Hat
#
# 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/>.
ANSIBLE_METADATA = {'metadata_version': '1.0',
'status': ['preview'],
'supported_by': 'community'}
DOCUMENTATION = r'''
module: aci_rest
short_description: Direct access to the Cisco APIC REST API
description:
- Enables the management of the Cisco ACI fabric through direct access to the Cisco APIC REST API.
- More information regarding the Cisco APIC REST API is available from
U(http://www.cisco.com/c/en/us/td/docs/switches/datacenter/aci/apic/sw/2-x/rest_cfg/2_1_x/b_Cisco_APIC_REST_API_Configuration_Guide.html).
author:
- Jason Edelman (@jedelman8)
- Dag Wieers (@dagwieers)
version_added: '2.4'
requirements:
- lxml (when using XML content)
- xmljson >= 0.1.8 (when using XML content)
- python 2.7+ (when using xmljson)
extends_documentation_fragment: aci
options:
method:
description:
- The HTTP method of the request.
- Using C(delete) is typically used for deleting objects.
- Using C(get) is typically used for querying objects.
- Using C(post) is typically used for modifying objects.
required: true
default: get
choices: [ delete, get, post ]
aliases: [ action ]
path:
description:
- URI being used to execute API calls.
- Must end in C(.xml) or C(.json).
required: true
aliases: [ uri ]
content:
description:
- When used instead of C(src), sets the content of the API request directly.
- This may be convenient to template simple requests, for anything complex use the M(template) module.
src:
description:
- Name of the absolute path of the filname that includes the body
of the http request being sent to the ACI fabric.
aliases: [ config_file ]
notes:
- When using inline-JSON (using C(content)), YAML requires to start with a blank line.
Otherwise the JSON statement will be parsed as a YAML mapping (dictionary) and translated into invalid JSON as a result.
- XML payloads require the C(lxml) and C(xmljson) python libraries. For JSON payloads nothing special is needed.
'''
EXAMPLES = r'''
- name: Add a tenant
aci_rest:
hostname: '{{ inventory_hostname }}'
username: '{{ aci_username }}'
password: '{{ aci_password }}'
method: post
path: /api/mo/uni.xml
src: /home/cisco/ansible/aci/configs/aci_config.xml
delegate_to: localhost
- name: Get tenants
aci_rest:
hostname: '{{ inventory_hostname }}'
username: '{{ aci_username }}'
password: '{{ aci_password }}'
method: get
path: /api/node/class/fvTenant.json
delegate_to: localhost
- name: Configure contracts
aci_rest:
hostname: '{{ inventory_hostname }}'
username: '{{ aci_username }}'
password: '{{ aci_password }}'
method: post
path: /api/mo/uni.xml
src: /home/cisco/ansible/aci/configs/contract_config.xml
delegate_to: localhost
- name: Register leaves and spines
aci_rest:
hostname: '{{ inventory_hostname }}'
username: '{{ aci_username }}'
password: '{{ aci_password }}'
validate_certs: no
method: post
path: /api/mo/uni/controller/nodeidentpol.xml
content: |
<fabricNodeIdentPol>
<fabricNodeIdentP name="{{ item.name }}" nodeId="{{ item.nodeid }}" status="{{ item.status }}" serial="{{ item.serial }}"/>
</fabricNodeIdentPol>
with_items:
- '{{ apic_leavesspines }}'
delegate_to: localhost
- name: Wait for all controllers to become ready
aci_rest:
hostname: '{{ inventory_hostname }}'
username: '{{ aci_username }}'
password: '{{ aci_password }}'
validate_certs: no
path: /api/node/class/topSystem.json?query-target-filter=eq(topSystem.role,"controller")
register: apics
until: "'totalCount' in apics and apics.totalCount|int >= groups['apic']|count"
retries: 120
delay: 30
delegate_to: localhost
run_once: yes
'''
RETURN = r'''
error_code:
description: The REST ACI return code, useful for troubleshooting on failure
returned: always
type: int
sample: 122
error_text:
description: The REST ACI descriptive text, useful for troubleshooting on failure
returned: always
type: string
sample: unknown managed object class foo
imdata:
description: Converted output returned by the APIC REST (register this for post-processing)
returned: always
type: string
sample: [{"error": {"attributes": {"code": "122", "text": "unknown managed object class foo"}}}]
payload:
description: The (templated) payload send to the APIC REST API (xml or json)
returned: always
type: string
sample: '<foo bar="boo"/>'
raw:
description: The raw output returned by the APIC REST API (xml or json)
returned: parse error
type: string
sample: '<?xml version="1.0" encoding="UTF-8"?><imdata totalCount="1"><error code="122" text="unknown managed object class foo"/></imdata>'
response:
description: HTTP response string
returned: always
type: string
sample: 'HTTP Error 400: Bad Request'
status_code:
description: HTTP status code
returned: always
type: int
sample: 400
totalCount:
description: Number of items in the imdata array
returned: always
type: string
sample: '0'
'''
import json
import os
# Optional, only used for XML payload
try:
import lxml.etree
HAS_LXML_ETREE = True
except ImportError:
HAS_LXML_ETREE = False
# Optional, only used for XML payload
try:
from xmljson import cobra
HAS_XMLJSON_COBRA = True
except ImportError:
HAS_XMLJSON_COBRA = False
# from ansible.module_utils.aci import aci_login
from ansible.module_utils.basic import AnsibleModule, get_exception
from ansible.module_utils.urls import fetch_url
from ansible.module_utils._text import to_bytes
aci_argument_spec = dict(
hostname=dict(type='str', required=True, aliases=['host']),
username=dict(type='str', default='admin', aliases=['user']),
password=dict(type='str', required=True, no_log=True),
protocol=dict(type='str'), # Deprecated in v2.8
timeout=dict(type='int', default=30),
use_ssl=dict(type='bool', default=True),
validate_certs=dict(type='bool', default=True),
)
def aci_login(module, result=dict()):
''' Log in to APIC '''
# Set protocol based on use_ssl parameter
if module.params['protocol'] is None:
module.params['protocol'] = 'https' if module.params.get('use_ssl', True) else 'http'
# Perform login request
url = '%(protocol)s://%(host)s/api/aaaLogin.json' % module.params
data = {'aaaUser': {'attributes': {'name': module.params['username'], 'pwd': module.params['password']}}}
resp, auth = fetch_url(module, url, data=json.dumps(data), method="POST", timeout=module.params['timeout'])
# Handle APIC response
if auth['status'] != 200:
try:
result.update(aci_response(auth['body'], 'json'))
result['msg'] = 'Authentication failed: %(error_code)s %(error_text)s' % result
except KeyError:
result['msg'] = '%(msg)s for %(url)s' % auth
result['response'] = auth['msg']
result['status_code'] = auth['status']
module.fail_json(**result)
return resp
def aci_response(rawoutput, rest_type='xml'):
''' Handle APIC response output '''
result = dict()
if rest_type == 'json':
# Use APIC response as module output
try:
result = json.loads(rawoutput)
except:
e = get_exception()
# Expose RAW output for troubleshooting
result['error_code'] = -1
result['error_text'] = "Unable to parse output as JSON, see 'raw' output. %s" % e
result['raw'] = rawoutput
return result
else:
# NOTE: The XML-to-JSON conversion is using the "Cobra" convention
xmldata = None
try:
xml = lxml.etree.fromstring(to_bytes(rawoutput))
xmldata = cobra.data(xml)
except:
e = get_exception()
# Expose RAW output for troubleshooting
result['error_code'] = -1
result['error_text'] = "Unable to parse output as XML, see 'raw' output. %s" % e
result['raw'] = rawoutput
return result
# Reformat as ACI does for JSON API output
if xmldata and 'imdata' in xmldata:
if 'children' in xmldata['imdata']:
result['imdata'] = xmldata['imdata']['children']
result['totalCount'] = xmldata['imdata']['attributes']['totalCount']
# Handle possible APIC error information
try:
result['error_code'] = result['imdata'][0]['error']['attributes']['code']
result['error_text'] = result['imdata'][0]['error']['attributes']['text']
except KeyError:
if 'imdata' in result and 'totalCount' in result:
result['error_code'] = 0
result['error_text'] = 'Success'
else:
result['error_code'] = -2
result['error_text'] = 'This should not happen'
return result
def main():
argument_spec = dict(
path=dict(type='str', required=True, aliases=['uri']),
method=dict(type='str', default='get', choices=['delete', 'get', 'post'], aliases=['action']),
src=dict(type='path', aliases=['config_file']),
content=dict(type='str'),
)
argument_spec.update(aci_argument_spec)
module = AnsibleModule(
argument_spec=argument_spec,
mutually_exclusive=[['content', 'src']],
supports_check_mode=True,
)
hostname = module.params['hostname']
username = module.params['username']
password = module.params['password']
path = module.params['path']
content = module.params['content']
src = module.params['src']
protocol = module.params['protocol']
use_ssl = module.params['use_ssl']
method = module.params['method']
timeout = module.params['timeout']
result = dict(
changed=False,
payload='',
)
# Report missing file
file_exists = False
if src:
if os.path.isfile(src):
file_exists = True
else:
module.fail_json(msg='Cannot find/access src:\n%s' % src)
# Find request type
if path.find('.xml') != -1:
rest_type = 'xml'
if not HAS_LXML_ETREE:
module.fail_json(msg='The lxml python library is missing, or lacks etree support.')
if not HAS_XMLJSON_COBRA:
module.fail_json(msg='The xmljson python library is missing, or lacks cobra support.')
elif path.find('.json') != -1:
rest_type = 'json'
else:
module.fail_json(msg='Failed to find REST API content type (neither .xml nor .json).')
# Set protocol for further use
if protocol is None:
protocol = 'https' if use_ssl else 'http'
else:
module.deprecate("Parameter 'protocol' is deprecated, please use 'use_ssl' instead.", 2.8)
# Perform login first
auth = aci_login(module, result)
# Prepare request data
if content:
# We include the payload as it may be templated
result['payload'] = content
elif file_exists:
with open(src, 'r') as config_object:
# TODO: Would be nice to template this, requires action-plugin
result['payload'] = config_object.read()
# Ensure changes are reported
if method in ('delete', 'post'):
# FIXME: Hardcoding changed is not idempotent
result['changed'] = True
# In check_mode we assume it works, but we don't actually perform the requested change
# TODO: Could we turn this request in a GET instead ?
if module.check_mode:
module.exit_json(response='OK (Check mode)', status=200, **result)
else:
result['changed'] = False
# Perform actual request using auth cookie
url = '%s://%s/%s' % (protocol, hostname, path.lstrip('/'))
headers = dict(Cookie=auth.headers['Set-Cookie'])
resp, info = fetch_url(module, url, data=result['payload'], method=method.upper(), timeout=timeout, headers=headers)
result['response'] = info['msg']
result['status_code'] = info['status']
# Report failure
if info['status'] != 200:
try:
result.update(aci_response(info['body'], rest_type))
result['msg'] = 'Task failed: %(error_code)s %(error_text)s' % result
except KeyError:
result['msg'] = '%(msg)s for %(url)s' % info
module.fail_json(**result)
# Report success
result.update(aci_response(resp.read(), rest_type))
module.exit_json(**result)
if __name__ == '__main__':
main()

View file

@ -0,0 +1,55 @@
# -*- coding: utf-8 -*-
# Copyright 2017 Dag Wieers <dag@wieers.com>
# This file is part of Ansible by Red Hat
#
# 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/>.
class ModuleDocFragment(object):
# Standard files documentation fragment
DOCUMENTATION = '''
options:
hostname:
description:
- IP Address or hostname of APIC resolvable by Ansible control host.
required: true
aliases: [ host ]
username:
description:
- The username to use for authentication.
required: true
default: admin
aliases: [ user ]
password:
description:
- The password to use for authentication.
required: true
timeout:
description:
- The socket level timeout in seconds.
default: 30
use_ssl:
description:
- If C(no), an HTTP connection will be used instead of the default HTTPS connection.
type: bool
default: 'yes'
validate_certs:
description:
- If C(no), SSL certificates will not be validated.
- This should only set to C(no) used on personally controlled sites using self-signed certificates.
type: bool
default: 'yes'
'''

View file

@ -25,3 +25,6 @@ deepdiff
# requirement for modules using Netconf protocol
ncclient
# requirement for aci_rest module
xmljson

View file

@ -0,0 +1,299 @@
# -*- coding: utf-8 -*-
# Copyright 2017 Dag Wieers <dag@wieers.com>
# This file is part of Ansible by Red Hat
#
# 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/>.
import sys
from ansible.compat.tests import unittest
from ansible.modules.network.aci.aci_rest import aci_response
from nose.plugins.skip import SkipTest
from lxml import etree
if sys.version_info >= (2, 7):
from xmljson import cobra
class AciRest(unittest.TestCase):
def test_invalid_aci_login(self):
self.maxDiff = None
expected_result = {
u'error_code': u'401',
u'error_text': u'Username or password is incorrect - FAILED local authentication',
u'imdata': [{
u'error': {
u'attributes': {
u'code': u'401',
u'text': u'Username or password is incorrect - FAILED local authentication',
},
},
}],
u'totalCount': '1',
}
json_response = '{"totalCount":"1","imdata":[{"error":{"attributes":{"code":"401","text":"Username or password is incorrect - FAILED local authentication"}}}]}' # NOQA
json_result = aci_response(json_response, 'json')
self.assertEqual(expected_result, json_result)
# Python 2.7+ is needed for xmljson
if sys.version_info < (2, 7):
return
xml_response = '''<?xml version="1.0" encoding="UTF-8"?><imdata totalCount="1">
<error code="401" text="Username or password is incorrect - FAILED local authentication"/>
</imdata>
'''
xml_result = aci_response(xml_response, 'xml')
self.assertEqual(json_result, xml_result)
def test_valid_aci_login(self):
self.maxDiff = None
expected_result = {
u'error_code': 0,
u'error_text': u'Success',
u'imdata': [{
u'aaaLogin': {
u'attributes': {
u'token': u'ZldYAsoO9d0FfAQM8xaEVWvQPSOYwpnqzhwpIC1r4MaToknJjlIuAt9+TvXqrZ8lWYIGPj6VnZkWiS8nJfaiaX/AyrdD35jsSxiP3zydh+849xym7ALCw/fFNsc7b5ik1HaMuSUtdrN8fmCEUy7Pq/QNpGEqkE8m7HaxAuHpmvXgtdW1bA+KKJu2zY1c/tem', # NOQA
u'siteFingerprint': u'NdxD72K/uXaUK0wn',
u'refreshTimeoutSeconds': u'600',
u'maximumLifetimeSeconds': u'86400',
u'guiIdleTimeoutSeconds': u'1200',
u'restTimeoutSeconds': u'90',
u'creationTime': u'1500134817',
u'firstLoginTime': u'1500134817',
u'userName': u'admin',
u'remoteUser': u'false',
u'unixUserId': u'15374',
u'sessionId': u'o7hObsqNTfCmDGcZI5c4ng==',
u'lastName': u'',
u'firstName': u'',
u'version': u'2.0(2f)',
u'buildTime': u'Sat Aug 20 23:07:07 PDT 2016',
u'node': u'topology/pod-1/node-1',
},
u'children': [{
u'aaaUserDomain': {
u'attributes': {
u'name': u'all',
u'rolesR': u'admin',
u'rolesW': u'admin',
},
u'children': [{
u'aaaReadRoles': {
u'attributes': {},
},
}, {
u'aaaWriteRoles': {
u'attributes': {},
u'children': [{
u'role': {
u'attributes': {
u'name': u'admin',
},
},
}],
},
}],
},
}, {
u'DnDomainMapEntry': {
u'attributes': {
u'dn': u'uni/tn-common',
u'readPrivileges': u'admin',
u'writePrivileges': u'admin',
},
},
}, {
u'DnDomainMapEntry': {
u'attributes': {
u'dn': u'uni/tn-infra',
u'readPrivileges': u'admin',
u'writePrivileges': u'admin',
},
},
}, {
u'DnDomainMapEntry': {
u'attributes': {
u'dn': u'uni/tn-mgmt',
u'readPrivileges': u'admin',
u'writePrivileges': u'admin',
},
},
}],
},
}],
u'totalCount': u'1',
}
json_response = '{"totalCount":"1","imdata":[{"aaaLogin":{"attributes":{"token":"ZldYAsoO9d0FfAQM8xaEVWvQPSOYwpnqzhwpIC1r4MaToknJjlIuAt9+TvXqrZ8lWYIGPj6VnZkWiS8nJfaiaX/AyrdD35jsSxiP3zydh+849xym7ALCw/fFNsc7b5ik1HaMuSUtdrN8fmCEUy7Pq/QNpGEqkE8m7HaxAuHpmvXgtdW1bA+KKJu2zY1c/tem","siteFingerprint":"NdxD72K/uXaUK0wn","refreshTimeoutSeconds":"600","maximumLifetimeSeconds":"86400","guiIdleTimeoutSeconds":"1200","restTimeoutSeconds":"90","creationTime":"1500134817","firstLoginTime":"1500134817","userName":"admin","remoteUser":"false","unixUserId":"15374","sessionId":"o7hObsqNTfCmDGcZI5c4ng==","lastName":"","firstName":"","version":"2.0(2f)","buildTime":"Sat Aug 20 23:07:07 PDT 2016","node":"topology/pod-1/node-1"},"children":[{"aaaUserDomain":{"attributes":{"name":"all","rolesR":"admin","rolesW":"admin"},"children":[{"aaaReadRoles":{"attributes":{}}},{"aaaWriteRoles":{"attributes":{},"children":[{"role":{"attributes":{"name":"admin"}}}]}}]}},{"DnDomainMapEntry":{"attributes":{"dn":"uni/tn-common","readPrivileges":"admin","writePrivileges":"admin"}}},{"DnDomainMapEntry":{"attributes":{"dn":"uni/tn-infra","readPrivileges":"admin","writePrivileges":"admin"}}},{"DnDomainMapEntry":{"attributes":{"dn":"uni/tn-mgmt","readPrivileges":"admin","writePrivileges":"admin"}}}]}}]}' # NOQA
json_result = aci_response(json_response, 'json')
# Python 2.7+ is needed for xmljson
if sys.version_info < (2, 7):
return
xml_response = '<?xml version="1.0" encoding="UTF-8"?><imdata totalCount="1">\n<aaaLogin token="ZldYAsoO9d0FfAQM8xaEVWvQPSOYwpnqzhwpIC1r4MaToknJjlIuAt9+TvXqrZ8lWYIGPj6VnZkWiS8nJfaiaX/AyrdD35jsSxiP3zydh+849xym7ALCw/fFNsc7b5ik1HaMuSUtdrN8fmCEUy7Pq/QNpGEqkE8m7HaxAuHpmvXgtdW1bA+KKJu2zY1c/tem" siteFingerprint="NdxD72K/uXaUK0wn" refreshTimeoutSeconds="600" maximumLifetimeSeconds="86400" guiIdleTimeoutSeconds="1200" restTimeoutSeconds="90" creationTime="1500134817" firstLoginTime="1500134817" userName="admin" remoteUser="false" unixUserId="15374" sessionId="o7hObsqNTfCmDGcZI5c4ng==" lastName="" firstName="" version="2.0(2f)" buildTime="Sat Aug 20 23:07:07 PDT 2016" node="topology/pod-1/node-1">\n<aaaUserDomain name="all" rolesR="admin" rolesW="admin">\n<aaaReadRoles/>\n<aaaWriteRoles>\n<role name="admin"/>\n</aaaWriteRoles>\n</aaaUserDomain>\n<DnDomainMapEntry dn="uni/tn-common" readPrivileges="admin" writePrivileges="admin"/>\n<DnDomainMapEntry dn="uni/tn-infra" readPrivileges="admin" writePrivileges="admin"/>\n<DnDomainMapEntry dn="uni/tn-mgmt" readPrivileges="admin" writePrivileges="admin"/>\n</aaaLogin></imdata>\n''' # NOQA
xml_result = aci_response(xml_response, 'xml')
self.assertEqual(expected_result, json_result)
self.assertEqual(json_result, xml_result)
def test_invalid_input(self):
self.maxDiff = None
expected_result = {
u'error_code': u'401',
u'error_text': u'Username or password is incorrect - FAILED local authentication',
u'imdata': [{
u'error': {
u'attributes': {
u'code': u'401',
u'text': u'Username or password is incorrect - FAILED local authentication',
},
},
}],
u'totalCount': '1',
}
json_response = '{"totalCount":"1","imdata":[{"error":{"attributes":{"code":"401","text":"Username or password is incorrect - FAILED local authentication"}}}]}' # NOQA
json_result = aci_response(json_response, 'json')
# Python 2.7+ is needed for xmljson
if sys.version_info < (2, 7):
return
xml_response = '''<?xml version="1.0" encoding="UTF-8"?><imdata totalCount="1">
<error code="401" text="Username or password is incorrect - FAILED local authentication"/>
</imdata>
'''
xml_result = aci_response(xml_response, 'xml')
self.assertEqual(expected_result, json_result)
self.assertEqual(json_result, xml_result)
def test_empty_response(self):
self.maxDiffi = None
if sys.version_info < (3, 0):
expected_json_result = {
'error_code': -1,
'error_text': "Unable to parse output as JSON, see 'raw' output. No JSON object could be decoded",
'raw': '',
}
else:
expected_json_result = {
u'error_code': -1,
u'error_text': u"Unable to parse output as JSON, see 'raw' output. Expecting value: line 1 column 1 (char 0)",
u'raw': u'',
}
json_response = ''
json_result = aci_response(json_response, 'json')
self.assertEqual(expected_json_result, json_result)
# Python 2.7+ is needed for xmljson
if sys.version_info < (2, 7):
return
elif etree.LXML_VERSION < (3, 3, 0, 0):
expected_xml_result = {
'error_code': -1,
'error_text': "Unable to parse output as XML, see 'raw' output. None",
'raw': '',
}
elif sys.version_info < (3, 0):
expected_xml_result = {
'error_code': -1,
'error_text': "Unable to parse output as XML, see 'raw' output. None (line 0)",
'raw': '',
}
else:
expected_xml_result = {
u'error_code': -1,
u'error_text': u"Unable to parse output as XML, see 'raw' output. None (line 0)",
u'raw': u'',
}
xml_response = ''
xml_result = aci_response(xml_response, 'xml')
self.assertEqual(expected_xml_result, xml_result)
def test_invalid_response(self):
self.maxDiff = None
if sys.version_info < (2, 7):
expected_json_result = {
'error_code': -1,
'error_text': "Unable to parse output as JSON, see 'raw' output. Expecting object: line 1 column 8 (char 8)",
'raw': '{ "aaa":',
}
elif sys.version_info < (3, 0):
expected_json_result = {
'error_code': -1,
'error_text': "Unable to parse output as JSON, see 'raw' output. No JSON object could be decoded",
'raw': '{ "aaa":',
}
else:
expected_json_result = {
u'error_code': -1,
u'error_text': u"Unable to parse output as JSON, see 'raw' output. Expecting value: line 1 column 9 (char 8)",
u'raw': u'{ "aaa":',
}
json_response = '{ "aaa":'
json_result = aci_response(json_response, 'json')
self.assertEqual(expected_json_result, json_result)
# Python 2.7+ is needed for xmljson
if sys.version_info < (2, 7):
return
elif etree.LXML_VERSION < (3, 3, 0, 0):
expected_xml_result = {
'error_code': -1,
'error_text': "Unable to parse output as XML, see 'raw' output. Couldn't find end of Start Tag aaa line 1, line 1, column 5", # NOQA
'raw': '<aaa ',
}
elif sys.version_info < (3, 0):
expected_xml_result = {
'error_code': -1,
'error_text': u"Unable to parse output as XML, see 'raw' output. Couldn't find end of Start Tag aaa line 1, line 1, column 6 (line 1)", # NOQA
'raw': u'<aaa ',
}
else:
expected_xml_result = {
u'error_code': -1,
u'error_text': u"Unable to parse output as XML, see 'raw' output. Couldn't find end of Start Tag aaa line 1, line 1, column 6 (<string>, line 1)", # NOQA
u'raw': u'<aaa ',
}
xml_response = '<aaa '
xml_result = aci_response(xml_response, 'xml')
self.assertEqual(expected_xml_result, xml_result)