ansible  2.9.27
About: Ansible is an IT Configuration Management, Deployment \
About: Ansible (2.x) is an IT Configuration Management, Deployment & Orchestration tool.
ansible download page.
  Fossies Dox: ansible-2.9.27.tar.gz  ("unofficial" and yet experimental doxygen-generated source code documentation)  

aci_rest.py
Go to the documentation of this file.
1#!/usr/bin/python
2# -*- coding: utf-8 -*-
3
4# Copyright: (c) 2017, Dag Wieers (@dagwieers) <dag@wieers.com>
5# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
6
7from __future__ import absolute_import, division, print_function
8__metaclass__ = type
9
10ANSIBLE_METADATA = {'metadata_version': '1.1',
11 'status': ['preview'],
12 'supported_by': 'certified'}
13
14DOCUMENTATION = r'''
15---
16module: aci_rest
17short_description: Direct access to the Cisco APIC REST API
18description:
19- Enables the management of the Cisco ACI fabric through direct access to the Cisco APIC REST API.
20- Thanks to the idempotent nature of the APIC, this module is idempotent and reports changes.
21version_added: '2.4'
22requirements:
23- lxml (when using XML payload)
24- xmljson >= 0.1.8 (when using XML payload)
25- python 2.7+ (when using xmljson)
26options:
27 method:
28 description:
29 - The HTTP method of the request.
30 - Using C(delete) is typically used for deleting objects.
31 - Using C(get) is typically used for querying objects.
32 - Using C(post) is typically used for modifying objects.
33 type: str
34 choices: [ delete, get, post ]
35 default: get
36 aliases: [ action ]
37 path:
38 description:
39 - URI being used to execute API calls.
40 - Must end in C(.xml) or C(.json).
41 type: str
42 required: yes
43 aliases: [ uri ]
44 content:
45 description:
46 - When used instead of C(src), sets the payload of the API request directly.
47 - This may be convenient to template simple requests.
48 - For anything complex use the C(template) lookup plugin (see examples)
49 or the M(template) module with parameter C(src).
50 type: raw
51 src:
52 description:
53 - Name of the absolute path of the filename that includes the body
54 of the HTTP request being sent to the ACI fabric.
55 - If you require a templated payload, use the C(content) parameter
56 together with the C(template) lookup plugin, or use M(template).
57 type: path
58 aliases: [ config_file ]
59extends_documentation_fragment: aci
60notes:
61- Certain payloads are known not to be idempotent, so be careful when constructing payloads,
62 e.g. using C(status="created") will cause idempotency issues, use C(status="modified") instead.
63 More information in :ref:`the ACI documentation <aci_guide_known_issues>`.
64- Certain payloads (and used paths) are known to report no changes happened when changes did happen.
65 This is a known APIC problem and has been reported to the vendor. A workaround for this issue exists.
66 More information in :ref:`the ACI documentation <aci_guide_known_issues>`.
67- XML payloads require the C(lxml) and C(xmljson) python libraries. For JSON payloads nothing special is needed.
68seealso:
69- module: aci_tenant
70- name: Cisco APIC REST API Configuration Guide
71 description: More information about the APIC REST API.
72 link: 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
73author:
74- Dag Wieers (@dagwieers)
75'''
76
77EXAMPLES = r'''
78- name: Add a tenant using certificate authentication
79 aci_rest:
80 host: apic
81 username: admin
82 private_key: pki/admin.key
83 method: post
84 path: /api/mo/uni.xml
85 src: /home/cisco/ansible/aci/configs/aci_config.xml
86 delegate_to: localhost
87
88- name: Add a tenant from a templated payload file from templates/
89 aci_rest:
90 host: apic
91 username: admin
92 private_key: pki/admin.key
93 method: post
94 path: /api/mo/uni.xml
95 content: "{{ lookup('template', 'aci/tenant.xml.j2') }}"
96 delegate_to: localhost
97
98- name: Add a tenant using inline YAML
99 aci_rest:
100 host: apic
101 username: admin
102 private_key: pki/admin.key
103 validate_certs: no
104 path: /api/mo/uni.json
105 method: post
106 content:
107 fvTenant:
108 attributes:
109 name: Sales
110 descr: Sales department
111 delegate_to: localhost
112
113- name: Add a tenant using a JSON string
114 aci_rest:
115 host: apic
116 username: admin
117 private_key: pki/admin.key
118 validate_certs: no
119 path: /api/mo/uni.json
120 method: post
121 content:
122 {
123 "fvTenant": {
124 "attributes": {
125 "name": "Sales",
126 "descr": "Sales department"
127 }
128 }
129 }
130 delegate_to: localhost
131
132- name: Add a tenant using an XML string
133 aci_rest:
134 host: apic
135 username: admin
136 private_key: pki/{{ aci_username }}.key
137 validate_certs: no
138 path: /api/mo/uni.xml
139 method: post
140 content: '<fvTenant name="Sales" descr="Sales departement"/>'
141 delegate_to: localhost
142
143- name: Get tenants using password authentication
144 aci_rest:
145 host: apic
146 username: admin
147 password: SomeSecretPassword
148 method: get
149 path: /api/node/class/fvTenant.json
150 delegate_to: localhost
151 register: query_result
152
153- name: Configure contracts
154 aci_rest:
155 host: apic
156 username: admin
157 private_key: pki/admin.key
158 method: post
159 path: /api/mo/uni.xml
160 src: /home/cisco/ansible/aci/configs/contract_config.xml
161 delegate_to: localhost
162
163- name: Register leaves and spines
164 aci_rest:
165 host: apic
166 username: admin
167 private_key: pki/admin.key
168 validate_certs: no
169 method: post
170 path: /api/mo/uni/controller/nodeidentpol.xml
171 content: |
172 <fabricNodeIdentPol>
173 <fabricNodeIdentP name="{{ item.name }}" nodeId="{{ item.nodeid }}" status="{{ item.status }}" serial="{{ item.serial }}"/>
174 </fabricNodeIdentPol>
175 with_items:
176 - '{{ apic_leavesspines }}'
177 delegate_to: localhost
178
179- name: Wait for all controllers to become ready
180 aci_rest:
181 host: apic
182 username: admin
183 private_key: pki/admin.key
184 validate_certs: no
185 path: /api/node/class/topSystem.json?query-target-filter=eq(topSystem.role,"controller")
186 register: apics
187 until: "'totalCount' in apics and apics.totalCount|int >= groups['apic']|count"
188 retries: 120
189 delay: 30
190 delegate_to: localhost
191 run_once: yes
192'''
193
194RETURN = r'''
195error_code:
196 description: The REST ACI return code, useful for troubleshooting on failure
197 returned: always
198 type: int
199 sample: 122
200error_text:
201 description: The REST ACI descriptive text, useful for troubleshooting on failure
202 returned: always
203 type: str
204 sample: unknown managed object class foo
205imdata:
206 description: Converted output returned by the APIC REST (register this for post-processing)
207 returned: always
208 type: str
209 sample: [{"error": {"attributes": {"code": "122", "text": "unknown managed object class foo"}}}]
210payload:
211 description: The (templated) payload send to the APIC REST API (xml or json)
212 returned: always
213 type: str
214 sample: '<foo bar="boo"/>'
215raw:
216 description: The raw output returned by the APIC REST API (xml or json)
217 returned: parse error
218 type: str
219 sample: '<?xml version="1.0" encoding="UTF-8"?><imdata totalCount="1"><error code="122" text="unknown managed object class foo"/></imdata>'
220response:
221 description: HTTP response string
222 returned: always
223 type: str
224 sample: 'HTTP Error 400: Bad Request'
225status:
226 description: HTTP status code
227 returned: always
228 type: int
229 sample: 400
230totalCount:
231 description: Number of items in the imdata array
232 returned: always
233 type: str
234 sample: '0'
235url:
236 description: URL used for APIC REST call
237 returned: success
238 type: str
239 sample: https://1.2.3.4/api/mo/uni/tn-[Dag].json?rsp-subtree=modified
240'''
241
242import json
243import os
244
245try:
246 from ansible.module_utils.six.moves.urllib.parse import parse_qsl, urlencode, urlparse, urlunparse
247 HAS_URLPARSE = True
248except Exception:
249 HAS_URLPARSE = False
250
251# Optional, only used for XML payload
252try:
253 import lxml.etree # noqa
254 HAS_LXML_ETREE = True
255except ImportError:
256 HAS_LXML_ETREE = False
257
258# Optional, only used for XML payload
259try:
260 from xmljson import cobra # noqa
261 HAS_XMLJSON_COBRA = True
262except ImportError:
263 HAS_XMLJSON_COBRA = False
264
265# Optional, only used for YAML validation
266try:
267 import yaml
268 HAS_YAML = True
269except Exception:
270 HAS_YAML = False
271
272from ansible.module_utils.basic import AnsibleModule
273from ansible.module_utils.network.aci.aci import ACIModule, aci_argument_spec
274from ansible.module_utils.urls import fetch_url
275from ansible.module_utils._text import to_text
276
277
278def update_qsl(url, params):
279 ''' Add or update a URL query string '''
280
281 if HAS_URLPARSE:
282 url_parts = list(urlparse(url))
283 query = dict(parse_qsl(url_parts[4]))
284 query.update(params)
285 url_parts[4] = urlencode(query)
286 return urlunparse(url_parts)
287 elif '?' in url:
288 return url + '&' + '&'.join(['%s=%s' % (k, v) for k, v in params.items()])
289 else:
290 return url + '?' + '&'.join(['%s=%s' % (k, v) for k, v in params.items()])
291
292
294
295 def changed(self, d):
296 ''' Check ACI response for changes '''
297
298 if isinstance(d, dict):
299 for k, v in d.items():
300 if k == 'status' and v in ('created', 'modified', 'deleted'):
301 return True
302 elif self.changedchanged(v) is True:
303 return True
304 elif isinstance(d, list):
305 for i in d:
306 if self.changedchanged(i) is True:
307 return True
308
309 return False
310
311 def response_type(self, rawoutput, rest_type='xml'):
312 ''' Handle APIC response output '''
313
314 if rest_type == 'json':
315 self.response_jsonresponse_json(rawoutput)
316 else:
317 self.response_xmlresponse_xml(rawoutput)
318
319 # Use APICs built-in idempotency
320 if HAS_URLPARSE:
321 self.resultresult['changed'] = self.changedchanged(self.imdataimdata)
322
323
324def main():
325 argument_spec = aci_argument_spec()
326 argument_spec.update(
327 path=dict(type='str', required=True, aliases=['uri']),
328 method=dict(type='str', default='get', choices=['delete', 'get', 'post'], aliases=['action']),
329 src=dict(type='path', aliases=['config_file']),
330 content=dict(type='raw'),
331 )
332
333 module = AnsibleModule(
334 argument_spec=argument_spec,
335 mutually_exclusive=[['content', 'src']],
336 )
337
338 content = module.params['content']
339 path = module.params['path']
340 src = module.params['src']
341
342 # Report missing file
343 file_exists = False
344 if src:
345 if os.path.isfile(src):
346 file_exists = True
347 else:
348 module.fail_json(msg="Cannot find/access src '%s'" % src)
349
350 # Find request type
351 if path.find('.xml') != -1:
352 rest_type = 'xml'
353 if not HAS_LXML_ETREE:
354 module.fail_json(msg='The lxml python library is missing, or lacks etree support.')
355 if not HAS_XMLJSON_COBRA:
356 module.fail_json(msg='The xmljson python library is missing, or lacks cobra support.')
357 elif path.find('.json') != -1:
358 rest_type = 'json'
359 else:
360 module.fail_json(msg='Failed to find REST API payload type (neither .xml nor .json).')
361
362 aci = ACIRESTModule(module)
363 aci.result['status'] = -1 # Ensure we always return a status
364
365 # We include the payload as it may be templated
366 payload = content
367 if file_exists:
368 with open(src, 'r') as config_object:
369 # TODO: Would be nice to template this, requires action-plugin
370 payload = config_object.read()
371
372 # Validate payload
373 if rest_type == 'json':
374 if content and isinstance(content, dict):
375 # Validate inline YAML/JSON
376 payload = json.dumps(payload)
377 elif payload and isinstance(payload, str) and HAS_YAML:
378 try:
379 # Validate YAML/JSON string
380 payload = json.dumps(yaml.safe_load(payload))
381 except Exception as e:
382 module.fail_json(msg='Failed to parse provided JSON/YAML payload: %s' % to_text(e), exception=to_text(e), payload=payload)
383 elif rest_type == 'xml' and HAS_LXML_ETREE:
384 if content and isinstance(content, dict) and HAS_XMLJSON_COBRA:
385 # Validate inline YAML/JSON
386 # FIXME: Converting from a dictionary to XML is unsupported at this time
387 # payload = etree.tostring(payload)
388 pass
389 elif payload and isinstance(payload, str):
390 try:
391 # Validate XML string
392 payload = lxml.etree.tostring(lxml.etree.fromstring(payload))
393 except Exception as e:
394 module.fail_json(msg='Failed to parse provided XML payload: %s' % to_text(e), payload=payload)
395
396 # Perform actual request using auth cookie (Same as aci.request(), but also supports XML)
397 if 'port' in aci.params and aci.params['port'] is not None:
398 aci.url = '%(protocol)s://%(host)s:%(port)s/' % aci.params + path.lstrip('/')
399 else:
400 aci.url = '%(protocol)s://%(host)s/' % aci.params + path.lstrip('/')
401 if aci.params['method'] != 'get':
402 path += '?rsp-subtree=modified'
403 aci.url = update_qsl(aci.url, {'rsp-subtree': 'modified'})
404
405 # Sign and encode request as to APIC's wishes
406 if aci.params['private_key'] is not None:
407 aci.cert_auth(path=path, payload=payload)
408
409 aci.method = aci.params['method'].upper()
410
411 # Perform request
412 resp, info = fetch_url(module, aci.url,
413 data=payload,
414 headers=aci.headers,
415 method=aci.method,
416 timeout=aci.params['timeout'],
417 use_proxy=aci.params['use_proxy'])
418
419 aci.response = info['msg']
420 aci.status = info['status']
421
422 # Report failure
423 if info['status'] != 200:
424 try:
425 # APIC error
426 aci.response_type(info['body'], rest_type)
427 aci.fail_json(msg='APIC Error %(code)s: %(text)s' % aci.error)
428 except KeyError:
429 # Connection error
430 aci.fail_json(msg='Connection failed for %(url)s. %(msg)s' % info)
431
432 aci.response_type(resp.read(), rest_type)
433
434 aci.result['imdata'] = aci.imdata
435 aci.result['totalCount'] = aci.totalCount
436
437 # Report success
438 aci.exit_json(**aci.result)
439
440
441if __name__ == '__main__':
442 main()
def response_type(self, rawoutput, rest_type='xml')
Definition: aci_rest.py:311
def to_text(obj, encoding='utf-8', errors=None, nonstring='simplerepr')
Definition: _text.py:169
def fetch_url(module, url, data=None, headers=None, method=None, use_proxy=True, force=False, last_mod_time=None, timeout=10, use_gssapi=False, unix_socket=None, ca_path=None, cookies=None)
Definition: urls.py:1426