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.py
Go to the documentation of this file.
1# -*- coding: utf-8 -*-
2
3# This code is part of Ansible, but is an independent component
4
5# This particular file snippet, and this file snippet only, is BSD licensed.
6# Modules you write using this snippet, which is embedded dynamically by Ansible
7# still belong to the author of the module, and may assign their own license
8# to the complete work.
9
10# Copyright: (c) 2017, Dag Wieers <dag@wieers.com>
11# Copyright: (c) 2017, Jacob McGill (@jmcgill298)
12# Copyright: (c) 2017, Swetha Chunduri (@schunduri)
13# Copyright: (c) 2019, Rob Huelga (@RobW3LGA)
14# All rights reserved.
15
16# Redistribution and use in source and binary forms, with or without modification,
17# are permitted provided that the following conditions are met:
18#
19# * Redistributions of source code must retain the above copyright
20# notice, this list of conditions and the following disclaimer.
21# * Redistributions in binary form must reproduce the above copyright notice,
22# this list of conditions and the following disclaimer in the documentation
23# and/or other materials provided with the distribution.
24#
25# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
26# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
27# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
28# IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
29# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
30# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
31# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
32# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE
33# USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
34
35import base64
36import json
37import os
38from copy import deepcopy
39
41from ansible.module_utils.urls import fetch_url
42from ansible.module_utils._text import to_bytes, to_native
43
44# Optional, only used for APIC signature-based authentication
45try:
46 from OpenSSL.crypto import FILETYPE_PEM, load_privatekey, sign
47 HAS_OPENSSL = True
48except ImportError:
49 HAS_OPENSSL = False
50
51# Optional, only used for XML payload
52try:
53 import lxml.etree
54 HAS_LXML_ETREE = True
55except ImportError:
56 HAS_LXML_ETREE = False
57
58# Optional, only used for XML payload
59try:
60 from xmljson import cobra
61 HAS_XMLJSON_COBRA = True
62except ImportError:
63 HAS_XMLJSON_COBRA = False
64
65
67 return dict(
68 host=dict(type='str', required=True, aliases=['hostname']),
69 port=dict(type='int', required=False),
70 username=dict(type='str', default='admin', aliases=['user']),
71 password=dict(type='str', no_log=True),
72 private_key=dict(type='str', aliases=['cert_key'], no_log=True), # Beware, this is not the same as client_key !
73 certificate_name=dict(type='str', aliases=['cert_name']), # Beware, this is not the same as client_cert !
74 output_level=dict(type='str', default='normal', choices=['debug', 'info', 'normal']),
75 timeout=dict(type='int', default=30),
76 use_proxy=dict(type='bool', default=True),
77 use_ssl=dict(type='bool', default=True),
78 validate_certs=dict(type='bool', default=True),
79 )
80
81
82class ACIModule(object):
83
84 def __init__(self, module):
85 self.modulemodule = module
86 self.paramsparams = module.params
87 self.resultresult = dict(changed=False)
88 self.headersheaders = dict()
89 self.child_classeschild_classes = set()
90
91 # error output
92 self.errorerror = dict(code=None, text=None)
93
94 # normal output
95 self.existingexisting = None
96
97 # info output
98 self.configconfig = dict()
99 self.originaloriginal = None
100 self.proposedproposed = dict()
101
102 # debug output
103 self.filter_stringfilter_string = ''
104 self.methodmethod = None
105 self.pathpath = None
106 self.responseresponse = None
107 self.statusstatus = None
108 self.urlurl = None
109
110 # aci_rest output
111 self.imdataimdata = None
112 self.totalCounttotalCount = None
113
114 # Ensure protocol is set
115 self.define_protocoldefine_protocol()
116
117 if self.modulemodule._debug:
118 self.modulemodule.warn('Enable debug output because ANSIBLE_DEBUG was set.')
119 self.paramsparams['output_level'] = 'debug'
120
121 if self.paramsparams['private_key']:
122 # Perform signature-based authentication, no need to log on separately
123 if not HAS_OPENSSL:
124 self.modulemodule.fail_json(msg='Cannot use signature-based authentication because pyopenssl is not available')
125 elif self.paramsparams['password'] is not None:
126 self.modulemodule.warn("When doing ACI signatured-based authentication, providing parameter 'password' is not required")
127 elif self.paramsparams['password']:
128 # Perform password-based authentication, log on using password
129 self.loginlogin()
130 else:
131 self.modulemodule.fail_json(msg="Either parameter 'password' or 'private_key' is required for authentication")
132
133 def boolean(self, value, true='yes', false='no'):
134 ''' Return an acceptable value back '''
135
136 # When we expect value is of type=bool
137 if value is None:
138 return None
139 elif value is True:
140 return true
141 elif value is False:
142 return false
143
144 # If all else fails, escalate back to user
145 self.modulemodule.fail_json(msg="Boolean value '%s' is an invalid ACI boolean value.")
146
147 def iso8601_format(self, dt):
148 ''' Return an ACI-compatible ISO8601 formatted time: 2123-12-12T00:00:00.000+00:00 '''
149 try:
150 return dt.isoformat(timespec='milliseconds')
151 except Exception:
152 tz = dt.strftime('%z')
153 return '%s.%03d%s:%s' % (dt.strftime('%Y-%m-%dT%H:%M:%S'), dt.microsecond / 1000, tz[:3], tz[3:])
154
156 ''' Set protocol based on use_ssl parameter '''
157
158 # Set protocol for further use
159 self.paramsparams['protocol'] = 'https' if self.paramsparams.get('use_ssl', True) else 'http'
160
161 def define_method(self):
162 ''' Set method based on state parameter '''
163
164 # Set method for further use
165 state_map = dict(absent='delete', present='post', query='get')
166 self.paramsparams['method'] = state_map[self.paramsparams['state']]
167
168 def login(self):
169 ''' Log in to APIC '''
170
171 # Perform login request
172 if 'port' in self.paramsparams and self.paramsparams['port'] is not None:
173 url = '%(protocol)s://%(host)s:%(port)s/api/aaaLogin.json' % self.paramsparams
174 else:
175 url = '%(protocol)s://%(host)s/api/aaaLogin.json' % self.paramsparams
176 payload = {'aaaUser': {'attributes': {'name': self.paramsparams['username'], 'pwd': self.paramsparams['password']}}}
177 resp, auth = fetch_url(self.modulemodule, url,
178 data=json.dumps(payload),
179 method='POST',
180 timeout=self.paramsparams['timeout'],
181 use_proxy=self.paramsparams['use_proxy'])
182
183 # Handle APIC response
184 if auth['status'] != 200:
185 self.responseresponse = auth['msg']
186 self.statusstatus = auth['status']
187 try:
188 # APIC error
189 self.response_jsonresponse_json(auth['body'])
190 self.fail_jsonfail_json(msg='Authentication failed: %(code)s %(text)s' % self.errorerror)
191 except KeyError:
192 # Connection error
193 self.fail_jsonfail_json(msg='Connection failed for %(url)s. %(msg)s' % auth)
194
195 # Retain cookie for later use
196 self.headersheaders['Cookie'] = resp.headers['Set-Cookie']
197
198 def cert_auth(self, path=None, payload='', method=None):
199 ''' Perform APIC signature-based authentication, not the expected SSL client certificate authentication. '''
200
201 if method is None:
202 method = self.paramsparams['method'].upper()
203
204 # NOTE: ACI documentation incorrectly uses complete URL
205 if path is None:
206 path = self.pathpath
207 path = '/' + path.lstrip('/')
208
209 if payload is None:
210 payload = ''
211
212 # Check if we got a private key. This allows the use of vaulting the private key.
213 if self.paramsparams['private_key'].startswith('-----BEGIN PRIVATE KEY-----'):
214 try:
215 sig_key = load_privatekey(FILETYPE_PEM, self.paramsparams['private_key'])
216 except Exception:
217 self.modulemodule.fail_json(msg="Cannot load provided 'private_key' parameter.")
218 # Use the username as the certificate_name value
219 if self.paramsparams['certificate_name'] is None:
220 self.paramsparams['certificate_name'] = self.paramsparams['username']
221 elif self.paramsparams['private_key'].startswith('-----BEGIN CERTIFICATE-----'):
222 self.modulemodule.fail_json(msg="Provided 'private_key' parameter value appears to be a certificate. Please correct.")
223 else:
224 # If we got a private key file, read from this file.
225 # NOTE: Avoid exposing any other credential as a filename in output...
226 if not os.path.exists(self.paramsparams['private_key']):
227 self.modulemodule.fail_json(msg="The provided private key file does not appear to exist. Is it a filename?")
228 try:
229 with open(self.paramsparams['private_key'], 'r') as fh:
230 private_key_content = fh.read()
231 except Exception:
232 self.modulemodule.fail_json(msg="Cannot open private key file '%s'." % self.paramsparams['private_key'])
233 if private_key_content.startswith('-----BEGIN PRIVATE KEY-----'):
234 try:
235 sig_key = load_privatekey(FILETYPE_PEM, private_key_content)
236 except Exception:
237 self.modulemodule.fail_json(msg="Cannot load private key file '%s'." % self.paramsparams['private_key'])
238 # Use the private key basename (without extension) as certificate_name
239 if self.paramsparams['certificate_name'] is None:
240 self.paramsparams['certificate_name'] = os.path.basename(os.path.splitext(self.paramsparams['private_key'])[0])
241 elif private_key_content.startswith('-----BEGIN CERTIFICATE-----'):
242 self.modulemodule.fail_json(msg="Provided private key file %s appears to be a certificate. Please correct." % self.paramsparams['private_key'])
243 else:
244 self.modulemodule.fail_json(msg="Provided private key file '%s' does not appear to be a private key. Please correct." % self.paramsparams['private_key'])
245
246 # NOTE: ACI documentation incorrectly adds a space between method and path
247 sig_request = method + path + payload
248 sig_signature = base64.b64encode(sign(sig_key, sig_request, 'sha256'))
249 sig_dn = 'uni/userext/user-%s/usercert-%s' % (self.paramsparams['username'], self.paramsparams['certificate_name'])
250 self.headersheaders['Cookie'] = 'APIC-Certificate-Algorithm=v1.0; ' +\
251 'APIC-Certificate-DN=%s; ' % sig_dn +\
252 'APIC-Certificate-Fingerprint=fingerprint; ' +\
253 'APIC-Request-Signature=%s' % to_native(sig_signature)
254
255 def response_json(self, rawoutput):
256 ''' Handle APIC JSON response output '''
257 try:
258 jsondata = json.loads(rawoutput)
259 except Exception as e:
260 # Expose RAW output for troubleshooting
261 self.errorerror = dict(code=-1, text="Unable to parse output as JSON, see 'raw' output. %s" % e)
262 self.resultresult['raw'] = rawoutput
263 return
264
265 # Extract JSON API output
266 try:
267 self.imdataimdata = jsondata['imdata']
268 except KeyError:
269 self.imdataimdata = dict()
270 self.totalCounttotalCount = int(jsondata['totalCount'])
271
272 # Handle possible APIC error information
273 self.response_errorresponse_error()
274
275 def response_xml(self, rawoutput):
276 ''' Handle APIC XML response output '''
277
278 # NOTE: The XML-to-JSON conversion is using the "Cobra" convention
279 try:
280 xml = lxml.etree.fromstring(to_bytes(rawoutput))
281 xmldata = cobra.data(xml)
282 except Exception as e:
283 # Expose RAW output for troubleshooting
284 self.errorerror = dict(code=-1, text="Unable to parse output as XML, see 'raw' output. %s" % e)
285 self.resultresult['raw'] = rawoutput
286 return
287
288 # Reformat as ACI does for JSON API output
289 try:
290 self.imdataimdata = xmldata['imdata']['children']
291 except KeyError:
292 self.imdataimdata = dict()
293 self.totalCounttotalCount = int(xmldata['imdata']['attributes']['totalCount'])
294
295 # Handle possible APIC error information
296 self.response_errorresponse_error()
297
298 def response_error(self):
299 ''' Set error information when found '''
300
301 # Handle possible APIC error information
302 if self.totalCounttotalCount != '0':
303 try:
304 self.errorerror = self.imdataimdata[0]['error']['attributes']
305 except (KeyError, IndexError):
306 pass
307
308 def request(self, path, payload=None):
309 ''' Perform a REST request '''
310
311 # Ensure method is set (only do this once)
312 self.define_methoddefine_method()
313 self.pathpath = path
314
315 if 'port' in self.paramsparams and self.paramsparams['port'] is not None:
316 self.urlurl = '%(protocol)s://%(host)s:%(port)s/' % self.paramsparams + path.lstrip('/')
317 else:
318 self.urlurl = '%(protocol)s://%(host)s/' % self.paramsparams + path.lstrip('/')
319
320 # Sign and encode request as to APIC's wishes
321 if self.paramsparams['private_key']:
322 self.cert_authcert_auth(path=path, payload=payload)
323
324 # Perform request
325 resp, info = fetch_url(self.modulemodule, self.urlurl,
326 data=payload,
327 headers=self.headersheaders,
328 method=self.paramsparams['method'].upper(),
329 timeout=self.paramsparams['timeout'],
330 use_proxy=self.paramsparams['use_proxy'])
331
332 self.responseresponse = info['msg']
333 self.statusstatus = info['status']
334
335 # Handle APIC response
336 if info['status'] != 200:
337 try:
338 # APIC error
339 self.response_jsonresponse_json(info['body'])
340 self.fail_jsonfail_json(msg='APIC Error %(code)s: %(text)s' % self.errorerror)
341 except KeyError:
342 # Connection error
343 self.fail_jsonfail_json(msg='Connection failed for %(url)s. %(msg)s' % info)
344
345 self.response_jsonresponse_json(resp.read())
346
347 def query(self, path):
348 ''' Perform a query with no payload '''
349
350 self.pathpath = path
351
352 if 'port' in self.paramsparams and self.paramsparams['port'] is not None:
353 self.urlurl = '%(protocol)s://%(host)s:%(port)s/' % self.paramsparams + path.lstrip('/')
354 else:
355 self.urlurl = '%(protocol)s://%(host)s/' % self.paramsparams + path.lstrip('/')
356
357 # Sign and encode request as to APIC's wishes
358 if self.paramsparams['private_key']:
359 self.cert_authcert_auth(path=path, method='GET')
360
361 # Perform request
362 resp, query = fetch_url(self.modulemodule, self.urlurl,
363 data=None,
364 headers=self.headersheaders,
365 method='GET',
366 timeout=self.paramsparams['timeout'],
367 use_proxy=self.paramsparams['use_proxy'])
368
369 # Handle APIC response
370 if query['status'] != 200:
371 self.responseresponse = query['msg']
372 self.statusstatus = query['status']
373 try:
374 # APIC error
375 self.response_jsonresponse_json(query['body'])
376 self.fail_jsonfail_json(msg='APIC Error %(code)s: %(text)s' % self.errorerror)
377 except KeyError:
378 # Connection error
379 self.fail_jsonfail_json(msg='Connection failed for %(url)s. %(msg)s' % query)
380
381 query = json.loads(resp.read())
382
383 return json.dumps(query['imdata'], sort_keys=True, indent=2) + '\n'
384
385 def request_diff(self, path, payload=None):
386 ''' Perform a request, including a proper diff output '''
387 self.resultresult['diff'] = dict()
388 self.resultresult['diff']['before'] = self.queryquery(path)
389 self.requestrequest(path, payload=payload)
390 # TODO: Check if we can use the request output for the 'after' diff
391 self.resultresult['diff']['after'] = self.queryquery(path)
392
393 if self.resultresult['diff']['before'] != self.resultresult['diff']['after']:
394 self.resultresult['changed'] = True
395
396 # TODO: This could be designed to update existing keys
397 def update_qs(self, params):
398 ''' Append key-value pairs to self.filter_string '''
399 accepted_params = dict((k, v) for (k, v) in params.items() if v is not None)
400 if accepted_params:
401 if self.filter_stringfilter_string:
402 self.filter_stringfilter_string += '&'
403 else:
404 self.filter_stringfilter_string = '?'
405 self.filter_stringfilter_string += '&'.join(['%s=%s' % (k, v) for (k, v) in accepted_params.items()])
406
407 # TODO: This could be designed to accept multiple obj_classes and keys
408 def build_filter(self, obj_class, params):
409 ''' Build an APIC filter based on obj_class and key-value pairs '''
410 accepted_params = dict((k, v) for (k, v) in params.items() if v is not None)
411 if len(accepted_params) == 1:
412 return ','.join('eq({0}.{1},"{2}")'.format(obj_class, k, v) for (k, v) in accepted_params.items())
413 elif len(accepted_params) > 1:
414 return 'and(' + ','.join(['eq({0}.{1},"{2}")'.format(obj_class, k, v) for (k, v) in accepted_params.items()]) + ')'
415
417 target_class = obj['target_class']
418 target_filter = obj['target_filter']
419 subtree_class = obj['subtree_class']
420 subtree_filter = obj['subtree_filter']
421 object_rn = obj['object_rn']
422 mo = obj['module_object']
423 add_subtree_filter = obj['add_subtree_filter']
424 add_target_filter = obj['add_target_filter']
425
426 if self.modulemodule.params['state'] in ('absent', 'present') and mo is not None:
427 self.pathpath = 'api/mo/uni/{0}.json'.format(object_rn)
428 self.update_qsupdate_qs({'rsp-prop-include': 'config-only'})
429
430 else:
431 # State is 'query'
432 if object_rn is not None:
433 # Query for a specific object in the module's class
434 self.pathpath = 'api/mo/uni/{0}.json'.format(object_rn)
435 else:
436 self.pathpath = 'api/class/{0}.json'.format(target_class)
437
438 if add_target_filter:
439 self.update_qsupdate_qs(
440 {'query-target-filter': self.build_filterbuild_filter(target_class, target_filter)})
441
442 if add_subtree_filter:
443 self.update_qsupdate_qs(
444 {'rsp-subtree-filter': self.build_filterbuild_filter(subtree_class, subtree_filter)})
445
446 if 'port' in self.paramsparams and self.paramsparams['port'] is not None:
447 self.urlurl = '{protocol}://{host}:{port}/{path}'.format(
448 path=self.pathpath, **self.modulemodule.params)
449
450 else:
451 self.urlurl = '{protocol}://{host}/{path}'.format(
452 path=self.pathpath, **self.modulemodule.params)
453
454 if self.child_classeschild_classes:
455 self.update_qsupdate_qs(
456 {'rsp-subtree': 'full', 'rsp-subtree-class': ','.join(sorted(self.child_classeschild_classes))})
457
458 def _deep_url_parent_object(self, parent_objects, parent_class):
459
460 for parent_object in parent_objects:
461 if parent_object['aci_class'] is parent_class:
462 return parent_object
463
464 return None
465
466 def construct_deep_url(self, target_object, parent_objects=None, child_classes=None):
467 """
468 This method is used to retrieve the appropriate URL path and filter_string to make the request to the APIC.
469
470 :param target_object: The target class dictionary containing parent_class, aci_class, aci_rn, target_filter, and module_object keys.
471 :param parent_objects: The parent class list of dictionaries containing parent_class, aci_class, aci_rn, target_filter, and module_object keys.
472 :param child_classes: The list of child classes that the module supports along with the object.
473 :type target_object: dict
474 :type parent_objects: list[dict]
475 :type child_classes: list[string]
476 :return: The path and filter_string needed to build the full URL.
477 """
478
479 self.filter_stringfilter_string = ''
480 rn_builder = None
481 subtree_classes = None
482 add_subtree_filter = False
483 add_target_filter = False
484 has_target_query = False
485 has_target_query_compare = False
486 has_target_query_difference = False
487 has_target_query_called = False
488
489 if child_classes is None:
490 self.child_classeschild_classes = set()
491 else:
492 self.child_classeschild_classes = set(child_classes)
493
494 target_parent_class = target_object['parent_class']
495 target_class = target_object['aci_class']
496 target_rn = target_object['aci_rn']
497 target_filter = target_object['target_filter']
498 target_module_object = target_object['module_object']
499
500 url_path_object = dict(
501 target_class=target_class,
502 target_filter=target_filter,
503 subtree_class=target_class,
504 subtree_filter=target_filter,
505 module_object=target_module_object
506 )
507
508 if target_module_object is not None:
509 rn_builder = target_rn
510 else:
511 has_target_query = True
512 has_target_query_compare = True
513
514 if parent_objects is not None:
515 current_parent_class = target_parent_class
516 has_parent_query_compare = False
517 has_parent_query_difference = False
518 is_first_parent = True
519 is_single_parent = None
520 search_classes = set()
521
522 while current_parent_class != 'uni':
523 parent_object = self._deep_url_parent_object_deep_url_parent_object(
524 parent_objects=parent_objects, parent_class=current_parent_class)
525
526 if parent_object is not None:
527 parent_parent_class = parent_object['parent_class']
528 parent_class = parent_object['aci_class']
529 parent_rn = parent_object['aci_rn']
530 parent_filter = parent_object['target_filter']
531 parent_module_object = parent_object['module_object']
532
533 if is_first_parent:
534 is_single_parent = True
535 else:
536 is_single_parent = False
537 is_first_parent = False
538
539 if parent_parent_class != 'uni':
540 search_classes.add(parent_class)
541
542 if parent_module_object is not None:
543 if rn_builder is not None:
544 rn_builder = '{0}/{1}'.format(parent_rn,
545 rn_builder)
546 else:
547 rn_builder = parent_rn
548
549 url_path_object['target_class'] = parent_class
550 url_path_object['target_filter'] = parent_filter
551
552 has_target_query = False
553 else:
554 rn_builder = None
555 subtree_classes = search_classes
556
557 has_target_query = True
558 if is_single_parent:
559 has_parent_query_compare = True
560
561 current_parent_class = parent_parent_class
562 else:
563 raise ValueError("Reference error for parent_class '{0}'. Each parent_class must reference a valid object".format(current_parent_class))
564
565 if not has_target_query_difference and not has_target_query_called:
566 if has_target_query is not has_target_query_compare:
567 has_target_query_difference = True
568 else:
569 if not has_parent_query_difference and has_target_query is not has_parent_query_compare:
570 has_parent_query_difference = True
571 has_target_query_called = True
572
573 if not has_parent_query_difference and has_parent_query_compare and target_module_object is not None:
574 add_target_filter = True
575
576 elif has_parent_query_difference and target_module_object is not None:
577 add_subtree_filter = True
578 self.child_classeschild_classes.add(target_class)
579
580 if has_target_query:
581 add_target_filter = True
582
583 elif has_parent_query_difference and not has_target_query and target_module_object is None:
584 self.child_classeschild_classes.add(target_class)
585 self.child_classeschild_classes.update(subtree_classes)
586
587 elif not has_parent_query_difference and not has_target_query and target_module_object is None:
588 self.child_classeschild_classes.add(target_class)
589
590 elif not has_target_query and is_single_parent and target_module_object is None:
591 self.child_classeschild_classes.add(target_class)
592
593 url_path_object['object_rn'] = rn_builder
594 url_path_object['add_subtree_filter'] = add_subtree_filter
595 url_path_object['add_target_filter'] = add_target_filter
596
597 self._deep_url_path_builder_deep_url_path_builder(url_path_object)
598
599 def construct_url(self, root_class, subclass_1=None, subclass_2=None, subclass_3=None, child_classes=None):
600 """
601 This method is used to retrieve the appropriate URL path and filter_string to make the request to the APIC.
602
603 :param root_class: The top-level class dictionary containing aci_class, aci_rn, target_filter, and module_object keys.
604 :param sublass_1: The second-level class dictionary containing aci_class, aci_rn, target_filter, and module_object keys.
605 :param sublass_2: The third-level class dictionary containing aci_class, aci_rn, target_filter, and module_object keys.
606 :param sublass_3: The fourth-level class dictionary containing aci_class, aci_rn, target_filter, and module_object keys.
607 :param child_classes: The list of child classes that the module supports along with the object.
608 :type root_class: dict
609 :type subclass_1: dict
610 :type subclass_2: dict
611 :type subclass_3: dict
612 :type child_classes: list
613 :return: The path and filter_string needed to build the full URL.
614 """
615 self.filter_stringfilter_string = ''
616
617 if child_classes is None:
618 self.child_classeschild_classes = set()
619 else:
620 self.child_classeschild_classes = set(child_classes)
621
622 if subclass_3 is not None:
623 self._construct_url_4_construct_url_4(root_class, subclass_1, subclass_2, subclass_3)
624 elif subclass_2 is not None:
625 self._construct_url_3_construct_url_3(root_class, subclass_1, subclass_2)
626 elif subclass_1 is not None:
627 self._construct_url_2_construct_url_2(root_class, subclass_1)
628 else:
629 self._construct_url_1_construct_url_1(root_class)
630
631 if 'port' in self.paramsparams and self.paramsparams['port'] is not None:
632 self.urlurl = '{protocol}://{host}:{port}/{path}'.format(path=self.pathpath, **self.modulemodule.params)
633 else:
634 self.urlurl = '{protocol}://{host}/{path}'.format(path=self.pathpath, **self.modulemodule.params)
635
636 if self.child_classeschild_classes:
637 # Append child_classes to filter_string if filter string is empty
638 self.update_qsupdate_qs({'rsp-subtree': 'full', 'rsp-subtree-class': ','.join(sorted(self.child_classeschild_classes))})
639
640 def _construct_url_1(self, obj):
641 """
642 This method is used by construct_url when the object is the top-level class.
643 """
644 obj_class = obj['aci_class']
645 obj_rn = obj['aci_rn']
646 obj_filter = obj['target_filter']
647 mo = obj['module_object']
648
649 if self.modulemodule.params['state'] in ('absent', 'present'):
650 # State is absent or present
651 self.pathpath = 'api/mo/uni/{0}.json'.format(obj_rn)
652 self.update_qsupdate_qs({'rsp-prop-include': 'config-only'})
653 elif mo is None:
654 # Query for all objects of the module's class (filter by properties)
655 self.pathpath = 'api/class/{0}.json'.format(obj_class)
656 self.update_qsupdate_qs({'query-target-filter': self.build_filterbuild_filter(obj_class, obj_filter)})
657 else:
658 # Query for a specific object in the module's class
659 self.pathpath = 'api/mo/uni/{0}.json'.format(obj_rn)
660
661 def _construct_url_2(self, parent, obj):
662 """
663 This method is used by construct_url when the object is the second-level class.
664 """
665 parent_class = parent['aci_class']
666 parent_rn = parent['aci_rn']
667 parent_filter = parent['target_filter']
668 parent_obj = parent['module_object']
669 obj_class = obj['aci_class']
670 obj_rn = obj['aci_rn']
671 obj_filter = obj['target_filter']
672 mo = obj['module_object']
673
674 if self.modulemodule.params['state'] in ('absent', 'present'):
675 # State is absent or present
676 self.pathpath = 'api/mo/uni/{0}/{1}.json'.format(parent_rn, obj_rn)
677 self.update_qsupdate_qs({'rsp-prop-include': 'config-only'})
678 elif parent_obj is None and mo is None:
679 # Query for all objects of the module's class
680 self.pathpath = 'api/class/{0}.json'.format(obj_class)
681 self.update_qsupdate_qs({'query-target-filter': self.build_filterbuild_filter(obj_class, obj_filter)})
682 elif parent_obj is None: # mo is known
683 # Query for all objects of the module's class that match the provided ID value
684 self.pathpath = 'api/class/{0}.json'.format(obj_class)
685 self.update_qsupdate_qs({'query-target-filter': self.build_filterbuild_filter(obj_class, obj_filter)})
686 elif mo is None: # parent_obj is known
687 # Query for all object's of the module's class that belong to a specific parent object
688 self.child_classeschild_classes.add(obj_class)
689 self.pathpath = 'api/mo/uni/{0}.json'.format(parent_rn)
690 else:
691 # Query for specific object in the module's class
692 self.pathpath = 'api/mo/uni/{0}/{1}.json'.format(parent_rn, obj_rn)
693
694 def _construct_url_3(self, root, parent, obj):
695 """
696 This method is used by construct_url when the object is the third-level class.
697 """
698 root_class = root['aci_class']
699 root_rn = root['aci_rn']
700 root_filter = root['target_filter']
701 root_obj = root['module_object']
702 parent_class = parent['aci_class']
703 parent_rn = parent['aci_rn']
704 parent_filter = parent['target_filter']
705 parent_obj = parent['module_object']
706 obj_class = obj['aci_class']
707 obj_rn = obj['aci_rn']
708 obj_filter = obj['target_filter']
709 mo = obj['module_object']
710
711 if self.modulemodule.params['state'] in ('absent', 'present'):
712 # State is absent or present
713 self.pathpath = 'api/mo/uni/{0}/{1}/{2}.json'.format(root_rn, parent_rn, obj_rn)
714 self.update_qsupdate_qs({'rsp-prop-include': 'config-only'})
715 elif root_obj is None and parent_obj is None and mo is None:
716 # Query for all objects of the module's class
717 self.pathpath = 'api/class/{0}.json'.format(obj_class)
718 self.update_qsupdate_qs({'query-target-filter': self.build_filterbuild_filter(obj_class, obj_filter)})
719 elif root_obj is None and parent_obj is None: # mo is known
720 # Query for all objects of the module's class matching the provided ID value of the object
721 self.pathpath = 'api/class/{0}.json'.format(obj_class)
722 self.update_qsupdate_qs({'query-target-filter': self.build_filterbuild_filter(obj_class, obj_filter)})
723 elif root_obj is None and mo is None: # parent_obj is known
724 # Query for all objects of the module's class that belong to any parent class
725 # matching the provided ID value for the parent object
726 self.child_classeschild_classes.add(obj_class)
727 self.pathpath = 'api/class/{0}.json'.format(parent_class)
728 self.update_qsupdate_qs({'query-target-filter': self.build_filterbuild_filter(parent_class, parent_filter)})
729 elif parent_obj is None and mo is None: # root_obj is known
730 # Query for all objects of the module's class that belong to a specific root object
731 self.child_classeschild_classes.update([parent_class, obj_class])
732 self.pathpath = 'api/mo/uni/{0}.json'.format(root_rn)
733 # NOTE: No need to select by root_filter
734 # self.update_qs({'query-target-filter': self.build_filter(root_class, root_filter)})
735 elif root_obj is None: # mo and parent_obj are known
736 # Query for all objects of the module's class that belong to any parent class
737 # matching the provided ID values for both object and parent object
738 self.child_classeschild_classes.add(obj_class)
739 self.pathpath = 'api/class/{0}.json'.format(parent_class)
740 self.update_qsupdate_qs({'query-target-filter': self.build_filterbuild_filter(parent_class, parent_filter)})
741 self.update_qsupdate_qs({'rsp-subtree-filter': self.build_filterbuild_filter(obj_class, obj_filter)})
742 elif parent_obj is None: # mo and root_obj are known
743 # Query for all objects of the module's class that match the provided ID value and belong to a specific root object
744 self.child_classeschild_classes.add(obj_class)
745 self.pathpath = 'api/mo/uni/{0}.json'.format(root_rn)
746 # NOTE: No need to select by root_filter
747 # self.update_qs({'query-target-filter': self.build_filter(root_class, root_filter)})
748 # TODO: Filter by parent_filter and obj_filter
749 self.update_qsupdate_qs({'rsp-subtree-filter': self.build_filterbuild_filter(obj_class, obj_filter)})
750 elif mo is None: # root_obj and parent_obj are known
751 # Query for all objects of the module's class that belong to a specific parent object
752 self.child_classeschild_classes.add(obj_class)
753 self.pathpath = 'api/mo/uni/{0}/{1}.json'.format(root_rn, parent_rn)
754 # NOTE: No need to select by parent_filter
755 # self.update_qs({'query-target-filter': self.build_filter(parent_class, parent_filter)})
756 else:
757 # Query for a specific object of the module's class
758 self.pathpath = 'api/mo/uni/{0}/{1}/{2}.json'.format(root_rn, parent_rn, obj_rn)
759
760 def _construct_url_4(self, root, sec, parent, obj):
761 """
762 This method is used by construct_url when the object is the fourth-level class.
763 """
764 root_class = root['aci_class']
765 root_rn = root['aci_rn']
766 root_filter = root['target_filter']
767 root_obj = root['module_object']
768 sec_class = sec['aci_class']
769 sec_rn = sec['aci_rn']
770 sec_filter = sec['target_filter']
771 sec_obj = sec['module_object']
772 parent_class = parent['aci_class']
773 parent_rn = parent['aci_rn']
774 parent_filter = parent['target_filter']
775 parent_obj = parent['module_object']
776 obj_class = obj['aci_class']
777 obj_rn = obj['aci_rn']
778 obj_filter = obj['target_filter']
779 mo = obj['module_object']
780
781 if self.child_classeschild_classes is None:
782 self.child_classeschild_classes = [obj_class]
783
784 if self.modulemodule.params['state'] in ('absent', 'present'):
785 # State is absent or present
786 self.pathpath = 'api/mo/uni/{0}/{1}/{2}/{3}.json'.format(root_rn, sec_rn, parent_rn, obj_rn)
787 self.update_qsupdate_qs({'rsp-prop-include': 'config-only'})
788 # TODO: Add all missing cases
789 elif root_obj is None:
790 self.child_classeschild_classes.add(obj_class)
791 self.pathpath = 'api/class/{0}.json'.format(obj_class)
792 self.update_qsupdate_qs({'query-target-filter': self.build_filterbuild_filter(obj_class, obj_filter)})
793 elif sec_obj is None:
794 self.child_classeschild_classes.add(obj_class)
795 self.pathpath = 'api/mo/uni/{0}.json'.format(root_rn)
796 # NOTE: No need to select by root_filter
797 # self.update_qs({'query-target-filter': self.build_filter(root_class, root_filter)})
798 # TODO: Filter by sec_filter, parent and obj_filter
799 self.update_qsupdate_qs({'rsp-subtree-filter': self.build_filterbuild_filter(obj_class, obj_filter)})
800 elif parent_obj is None:
801 self.child_classeschild_classes.add(obj_class)
802 self.pathpath = 'api/mo/uni/{0}/{1}.json'.format(root_rn, sec_rn)
803 # NOTE: No need to select by sec_filter
804 # self.update_qs({'query-target-filter': self.build_filter(sec_class, sec_filter)})
805 # TODO: Filter by parent_filter and obj_filter
806 self.update_qsupdate_qs({'rsp-subtree-filter': self.build_filterbuild_filter(obj_class, obj_filter)})
807 elif mo is None:
808 self.child_classeschild_classes.add(obj_class)
809 self.pathpath = 'api/mo/uni/{0}/{1}/{2}.json'.format(root_rn, sec_rn, parent_rn)
810 # NOTE: No need to select by parent_filter
811 # self.update_qs({'query-target-filter': self.build_filter(parent_class, parent_filter)})
812 else:
813 # Query for a specific object of the module's class
814 self.pathpath = 'api/mo/uni/{0}/{1}/{2}/{3}.json'.format(root_rn, sec_rn, parent_rn, obj_rn)
815
816 def delete_config(self):
817 """
818 This method is used to handle the logic when the modules state is equal to absent. The method only pushes a change if
819 the object exists, and if check_mode is False. A successful change will mark the module as changed.
820 """
821 self.proposedproposed = dict()
822
823 if not self.existingexisting:
824 return
825
826 elif not self.modulemodule.check_mode:
827 # Sign and encode request as to APIC's wishes
828 if self.paramsparams['private_key']:
829 self.cert_authcert_auth(method='DELETE')
830
831 resp, info = fetch_url(self.modulemodule, self.urlurl,
832 headers=self.headersheaders,
833 method='DELETE',
834 timeout=self.paramsparams['timeout'],
835 use_proxy=self.paramsparams['use_proxy'])
836
837 self.responseresponse = info['msg']
838 self.statusstatus = info['status']
839 self.methodmethod = 'DELETE'
840
841 # Handle APIC response
842 if info['status'] == 200:
843 self.resultresult['changed'] = True
844 self.response_jsonresponse_json(resp.read())
845 else:
846 try:
847 # APIC error
848 self.response_jsonresponse_json(info['body'])
849 self.fail_jsonfail_json(msg='APIC Error %(code)s: %(text)s' % self.errorerror)
850 except KeyError:
851 # Connection error
852 self.fail_jsonfail_json(msg='Connection failed for %(url)s. %(msg)s' % info)
853 else:
854 self.resultresult['changed'] = True
855 self.methodmethod = 'DELETE'
856
857 def get_diff(self, aci_class):
858 """
859 This method is used to get the difference between the proposed and existing configurations. Each module
860 should call the get_existing method before this method, and add the proposed config to the module results
861 using the module's config parameters. The new config will added to the self.result dictionary.
862
863 :param aci_class: Type str.
864 This is the root dictionary key for the MO's configuration body, or the ACI class of the MO.
865 """
866 proposed_config = self.proposedproposed[aci_class]['attributes']
867 if self.existingexisting:
868 existing_config = self.existingexisting[0][aci_class]['attributes']
869 config = {}
870
871 # values are strings, so any diff between proposed and existing can be a straight replace
872 for key, value in proposed_config.items():
873 existing_field = existing_config.get(key)
874 if value != existing_field:
875 config[key] = value
876
877 # add name back to config only if the configs do not match
878 if config:
879 # TODO: If URLs are built with the object's name, then we should be able to leave off adding the name back
880 # config["name"] = proposed_config["name"]
881 config = {aci_class: {'attributes': config}}
882
883 # check for updates to child configs and update new config dictionary
884 children = self.get_diff_childrenget_diff_children(aci_class)
885 if children and config:
886 config[aci_class].update({'children': children})
887 elif children:
888 config = {aci_class: {'attributes': {}, 'children': children}}
889
890 else:
891 config = self.proposedproposed
892
893 self.configconfig = config
894
895 @staticmethod
896 def get_diff_child(child_class, proposed_child, existing_child):
897 """
898 This method is used to get the difference between a proposed and existing child configs. The get_nested_config()
899 method should be used to return the proposed and existing config portions of child.
900
901 :param child_class: Type str.
902 The root class (dict key) for the child dictionary.
903 :param proposed_child: Type dict.
904 The config portion of the proposed child dictionary.
905 :param existing_child: Type dict.
906 The config portion of the existing child dictionary.
907 :return: The child config with only values that are updated. If the proposed dictionary has no updates to make
908 to what exists on the APIC, then None is returned.
909 """
910 update_config = {child_class: {'attributes': {}}}
911 for key, value in proposed_child.items():
912 existing_field = existing_child.get(key)
913 if value != existing_field:
914 update_config[child_class]['attributes'][key] = value
915
916 if not update_config[child_class]['attributes']:
917 return None
918
919 return update_config
920
921 def get_diff_children(self, aci_class):
922 """
923 This method is used to retrieve the updated child configs by comparing the proposed children configs
924 agains the objects existing children configs.
925
926 :param aci_class: Type str.
927 This is the root dictionary key for the MO's configuration body, or the ACI class of the MO.
928 :return: The list of updated child config dictionaries. None is returned if there are no changes to the child
929 configurations.
930 """
931 proposed_children = self.proposedproposed[aci_class].get('children')
932 if proposed_children:
933 child_updates = []
934 existing_children = self.existingexisting[0][aci_class].get('children', [])
935
936 # Loop through proposed child configs and compare against existing child configuration
937 for child in proposed_children:
938 child_class, proposed_child, existing_child = self.get_nested_configget_nested_config(child, existing_children)
939
940 if existing_child is None:
941 child_update = child
942 else:
943 child_update = self.get_diff_childget_diff_child(child_class, proposed_child, existing_child)
944
945 # Update list of updated child configs only if the child config is different than what exists
946 if child_update:
947 child_updates.append(child_update)
948 else:
949 return None
950
951 return child_updates
952
953 def get_existing(self):
954 """
955 This method is used to get the existing object(s) based on the path specified in the module. Each module should
956 build the URL so that if the object's name is supplied, then it will retrieve the configuration for that particular
957 object, but if no name is supplied, then it will retrieve all MOs for the class. Following this method will ensure
958 that this method can be used to supply the existing configuration when using the get_diff method. The response, status,
959 and existing configuration will be added to the self.resultresult dictionary.
960 """
961 uri = self.urlurl + self.filter_stringfilter_string
962
963 # Sign and encode request as to APIC's wishes
964 if self.paramsparams['private_key']:
965 self.cert_authcert_auth(path=self.pathpath + self.filter_stringfilter_string, method='GET')
966
967 resp, info = fetch_url(self.modulemodule, uri,
968 headers=self.headersheaders,
969 method='GET',
970 timeout=self.paramsparams['timeout'],
971 use_proxy=self.paramsparams['use_proxy'])
972 self.responseresponse = info['msg']
973 self.statusstatus = info['status']
974 self.methodmethod = 'GET'
975
976 # Handle APIC response
977 if info['status'] == 200:
978 self.existingexisting = json.loads(resp.read())['imdata']
979 else:
980 try:
981 # APIC error
982 self.response_jsonresponse_json(info['body'])
983 self.fail_jsonfail_json(msg='APIC Error %(code)s: %(text)s' % self.errorerror)
984 except KeyError:
985 # Connection error
986 self.fail_jsonfail_json(msg='Connection failed for %(url)s. %(msg)s' % info)
987
988 @staticmethod
989 def get_nested_config(proposed_child, existing_children):
990 """
991 This method is used for stiping off the outer layers of the child dictionaries so only the configuration
992 key, value pairs are returned.
993
994 :param proposed_child: Type dict.
995 The dictionary that represents the child config.
996 :param existing_children: Type list.
997 The list of existing child config dictionaries.
998 :return: The child's class as str (root config dict key), the child's proposed config dict, and the child's
999 existing configuration dict.
1000 """
1001 for key in proposed_child.keys():
1002 child_class = key
1003 proposed_config = proposed_child[key]['attributes']
1004 existing_config = None
1005
1006 # FIXME: Design causes issues for repeated child_classes
1007 # get existing dictionary from the list of existing to use for comparison
1008 for child in existing_children:
1009 if child.get(child_class):
1010 existing_config = child[key]['attributes']
1011 # NOTE: This is an ugly fix
1012 # Return the one that is a subset match
1013 if set(proposed_config.items()).issubset(set(existing_config.items())):
1014 break
1015
1016 return child_class, proposed_config, existing_config
1017
1018 def payload(self, aci_class, class_config, child_configs=None):
1019 """
1020 This method is used to dynamically build the proposed configuration dictionary from the config related parameters
1021 passed into the module. All values that were not passed values from the playbook task will be removed so as to not
1022 inadvertently change configurations.
1023
1024 :param aci_class: Type str
1025 This is the root dictionary key for the MO's configuration body, or the ACI class of the MO.
1026 :param class_config: Type dict
1027 This is the configuration of the MO using the dictionary keys expected by the API
1028 :param child_configs: Type list
1029 This is a list of child dictionaries associated with the MOs config. The list should only
1030 include child objects that are used to associate two MOs together. Children that represent
1031 MOs should have their own module.
1032 """
1033 proposed = dict((k, str(v)) for k, v in class_config.items() if v is not None)
1034 self.proposedproposed = {aci_class: {'attributes': proposed}}
1035
1036 # add child objects to proposed
1037 if child_configs:
1038 children = []
1039 for child in child_configs:
1040 child_copy = deepcopy(child)
1041 has_value = False
1042 for root_key in child_copy.keys():
1043 for final_keys, values in child_copy[root_key]['attributes'].items():
1044 if values is None:
1045 child[root_key]['attributes'].pop(final_keys)
1046 else:
1047 child[root_key]['attributes'][final_keys] = str(values)
1048 has_value = True
1049 if has_value:
1050 children.append(child)
1051
1052 if children:
1053 self.proposedproposed[aci_class].update(dict(children=children))
1054
1055 def post_config(self):
1056 """
1057 This method is used to handle the logic when the modules state is equal to present. The method only pushes a change if
1058 the object has differences than what exists on the APIC, and if check_mode is False. A successful change will mark the
1059 module as changed.
1060 """
1061 if not self.configconfig:
1062 return
1063 elif not self.modulemodule.check_mode:
1064 # Sign and encode request as to APIC's wishes
1065 if self.paramsparams['private_key']:
1066 self.cert_authcert_auth(method='POST', payload=json.dumps(self.configconfig))
1067
1068 resp, info = fetch_url(self.modulemodule, self.urlurl,
1069 data=json.dumps(self.configconfig),
1070 headers=self.headersheaders,
1071 method='POST',
1072 timeout=self.paramsparams['timeout'],
1073 use_proxy=self.paramsparams['use_proxy'])
1074
1075 self.responseresponse = info['msg']
1076 self.statusstatus = info['status']
1077 self.methodmethod = 'POST'
1078
1079 # Handle APIC response
1080 if info['status'] == 200:
1081 self.resultresult['changed'] = True
1082 self.response_jsonresponse_json(resp.read())
1083 else:
1084 try:
1085 # APIC error
1086 self.response_jsonresponse_json(info['body'])
1087 self.fail_jsonfail_json(msg='APIC Error %(code)s: %(text)s' % self.errorerror)
1088 except KeyError:
1089 # Connection error
1090 self.fail_jsonfail_json(msg='Connection failed for %(url)s. %(msg)s' % info)
1091 else:
1092 self.resultresult['changed'] = True
1093 self.methodmethod = 'POST'
1094
1095 def exit_json(self, **kwargs):
1096
1097 if 'state' in self.paramsparams:
1098 if self.paramsparams['state'] in ('absent', 'present'):
1099 if self.paramsparams['output_level'] in ('debug', 'info'):
1100 self.resultresult['previous'] = self.existingexisting
1101
1102 # Return the gory details when we need it
1103 if self.paramsparams['output_level'] == 'debug':
1104 if 'state' in self.paramsparams:
1105 self.resultresult['filter_string'] = self.filter_stringfilter_string
1106 self.resultresult['method'] = self.methodmethod
1107 # self.result['path'] = self.path # Adding 'path' in result causes state: absent in output
1108 self.resultresult['response'] = self.responseresponse
1109 self.resultresult['status'] = self.statusstatus
1110 self.resultresult['url'] = self.urlurl
1111
1112 if 'state' in self.paramsparams:
1113 self.originaloriginal = self.existingexisting
1114 if self.paramsparams['state'] in ('absent', 'present'):
1115 self.get_existingget_existing()
1116
1117 # if self.module._diff and self.original != self.existing:
1118 # self.result['diff'] = dict(
1119 # before=json.dumps(self.original, sort_keys=True, indent=4),
1120 # after=json.dumps(self.existing, sort_keys=True, indent=4),
1121 # )
1122 self.resultresult['current'] = self.existingexisting
1123
1124 if self.paramsparams['output_level'] in ('debug', 'info'):
1125 self.resultresult['sent'] = self.configconfig
1126 self.resultresult['proposed'] = self.proposedproposed
1127
1128 self.resultresult.update(**kwargs)
1129 self.modulemodule.exit_json(**self.resultresult)
1130
1131 def fail_json(self, msg, **kwargs):
1132
1133 # Return error information, if we have it
1134 if self.errorerror['code'] is not None and self.errorerror['text'] is not None:
1135 self.resultresult['error'] = self.errorerror
1136
1137 if 'state' in self.paramsparams:
1138 if self.paramsparams['state'] in ('absent', 'present'):
1139 if self.paramsparams['output_level'] in ('debug', 'info'):
1140 self.resultresult['previous'] = self.existingexisting
1141
1142 # Return the gory details when we need it
1143 if self.paramsparams['output_level'] == 'debug':
1144 if self.imdataimdata is not None:
1145 self.resultresult['imdata'] = self.imdataimdata
1146 self.resultresult['totalCount'] = self.totalCounttotalCount
1147
1148 if self.paramsparams['output_level'] == 'debug':
1149 if self.urlurl is not None:
1150 if 'state' in self.paramsparams:
1151 self.resultresult['filter_string'] = self.filter_stringfilter_string
1152 self.resultresult['method'] = self.methodmethod
1153 # self.result['path'] = self.path # Adding 'path' in result causes state: absent in output
1154 self.resultresult['response'] = self.responseresponse
1155 self.resultresult['status'] = self.statusstatus
1156 self.resultresult['url'] = self.urlurl
1157
1158 if 'state' in self.paramsparams:
1159 if self.paramsparams['output_level'] in ('debug', 'info'):
1160 self.resultresult['sent'] = self.configconfig
1161 self.resultresult['proposed'] = self.proposedproposed
1162
1163 self.resultresult.update(**kwargs)
1164 self.modulemodule.fail_json(msg=msg, **self.resultresult)
def _deep_url_parent_object(self, parent_objects, parent_class)
Definition: aci.py:458
def get_diff_child(child_class, proposed_child, existing_child)
Definition: aci.py:896
def get_nested_config(proposed_child, existing_children)
Definition: aci.py:989
def _construct_url_4(self, root, sec, parent, obj)
Definition: aci.py:760
def build_filter(self, obj_class, params)
Definition: aci.py:408
def get_diff_children(self, aci_class)
Definition: aci.py:921
def payload(self, aci_class, class_config, child_configs=None)
Definition: aci.py:1018
def fail_json(self, msg, **kwargs)
Definition: aci.py:1131
def construct_url(self, root_class, subclass_1=None, subclass_2=None, subclass_3=None, child_classes=None)
Definition: aci.py:599
def boolean(self, value, true='yes', false='no')
Definition: aci.py:133
def _construct_url_3(self, root, parent, obj)
Definition: aci.py:694
def cert_auth(self, path=None, payload='', method=None)
Definition: aci.py:198
def request(self, path, payload=None)
Definition: aci.py:308
def request_diff(self, path, payload=None)
Definition: aci.py:385
def construct_deep_url(self, target_object, parent_objects=None, child_classes=None)
Definition: aci.py:466
def _construct_url_2(self, parent, obj)
Definition: aci.py:661
def to_bytes(obj, encoding='utf-8', errors=None, nonstring='simplerepr')
Definition: _text.py:52
def sign(key, msg)
Definition: urls.py:43
def load_privatekey(path, passphrase=None, check_passphrase=True, content=None, backend='pyopenssl')
Definition: crypto.py:202
def issubset(subset, superset)
Definition: mso.py:19
def get(network_os, module_name, connection_type)
Definition: providers.py:36
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
def update(client, current_stream, stream_name, number_of_shards=1, retention_period=None, tags=None, wait=False, wait_timeout=300, check_mode=False)