2121import logging
2222
2323from functools import wraps
24+ from typing import Union
2425from urllib3 import disable_warnings
2526from urllib3 .exceptions import InsecureRequestWarning , MaxRetryError , HTTPError
2627from urllib3 .util import Url
2728from urllib .parse import urljoin
2829from time import sleep
2930from ansible .module_utils .basic import AnsibleModule
3031from ansible .module_utils .common .text .converters import to_text
31-
32+ from time import sleep
3233from cm_client import ApiClient , Configuration
3334from cm_client .rest import ApiException , RESTClientObject
3435from cm_client .apis .cloudera_manager_resource_api import ClouderaManagerResourceApi
3940__maintainer__ = [
"[email protected] " ]
4041
4142
43+ class ClusterTemplate (object ):
44+ IDEMPOTENT_IDS = frozenset (
45+ ["refName" , "name" , "clusterName" , "hostName" , "product" ]
46+ )
47+ UNIQUE_IDS = frozenset (["repositories" ])
48+
49+ def __init__ (self , warn_fn , error_fn ) -> None :
50+ self ._warn = warn_fn
51+ self ._error = error_fn
52+
53+ def merge (self , base : Union [dict , list ], fragment : Union [dict , list ]) -> bool :
54+ if isinstance (base , dict ) and isinstance (fragment , dict ):
55+ self ._update_dict (base , fragment )
56+ elif isinstance (base , list ) and isinstance (fragment , list ):
57+ self ._update_list (base , fragment )
58+ else :
59+ raise TypeError (
60+ f"Base and fragment arguments must be the same type: base[{ type (base )} ], fragment[{ type (fragment )} ]"
61+ )
62+
63+ def _update_dict (self , base , fragment , breadcrumbs = "" ) -> None :
64+ for key , value in fragment .items ():
65+ crumb = breadcrumbs + "/" + key
66+
67+ # If the key is idempotent, error that the values are different
68+ if key in self .IDEMPOTENT_IDS :
69+ if base [key ] != value :
70+ self ._error (f"Unable to override value for distinct key [{ crumb } ]" )
71+ continue
72+
73+ # If it's a new key, add to the bae
74+ if key not in base :
75+ base [key ] = value
76+ # If the value is a dictionary, merge
77+ elif isinstance (value , dict ):
78+ self ._update_dict (base [key ], value , crumb )
79+ # If the value is a list, merge
80+ elif isinstance (value , list ):
81+ self ._update_list (base [key ], value , crumb )
82+ # Else the value is a scalar
83+ else :
84+ # If the value is different, override
85+ if base [key ] != value :
86+ self ._warn (
87+ f"Overriding value for key [{ crumb } ]], Old: [{ base [key ]} ], New: [{ value } ]"
88+ )
89+ base [key ] = value
90+
91+ if key in self .UNIQUE_IDS :
92+ base [key ] = list (set (base [key ]))
93+ base [key ].sort (key = lambda x : json .dumps (x , sort_keys = True ))
94+
95+ def _update_list (self , base , fragment , breadcrumbs = "" ) -> None :
96+ for entry in fragment :
97+ if isinstance (entry , dict ):
98+ # Discover if the incoming dict has an idempotent key
99+ idempotent_key = next (
100+ iter (
101+ [
102+ id
103+ for id in set (entry .keys ()).intersection (
104+ self .IDEMPOTENT_IDS
105+ )
106+ ]
107+ ),
108+ None ,
109+ )
110+
111+ # Merge the idemponent key's dictionary rather than appending as a new entry
112+ if idempotent_key :
113+ existing_entry = next (
114+ iter (
115+ [
116+ i
117+ for i in base
118+ if isinstance (i , dict )
119+ and idempotent_key in i
120+ and i [idempotent_key ] == entry [idempotent_key ]
121+ ]
122+ ),
123+ None ,
124+ )
125+ if existing_entry :
126+ self ._update_dict (
127+ existing_entry ,
128+ entry ,
129+ f"{ breadcrumbs } /[{ idempotent_key } ={ entry [idempotent_key ]} ]" ,
130+ )
131+ continue
132+ # Else, drop to appending the entry as net new
133+ base .append (entry )
134+
135+ base .sort (key = lambda x : json .dumps (x , sort_keys = True ))
136+
137+
42138class ClouderaManagerModule (object ):
43139 """Base Ansible Module for API access to Cloudera Manager."""
44140
@@ -61,13 +157,12 @@ def _add_log(err):
61157 err = dict (
62158 msg = "API error: " + to_text (ae .reason ),
63159 status_code = ae .status ,
64- body = ae .body .decode ("utf-8" ),
65160 )
66- if err [ " body" ] != "" :
161+ if ae . body :
67162 try :
68- err .update (body = json .loads (err [ " body" ] ))
69- except Exception as te :
70- pass
163+ err .update (body = json .loads (ae . body ))
164+ except Exception :
165+ err . update ( body = ae . body . decode ( "utf-8" )),
71166
72167 self .module .fail_json (** _add_log (err ))
73168 except MaxRetryError as maxe :
@@ -285,7 +380,7 @@ def ansible_module(
285380 mutually_exclusive = [],
286381 required_one_of = [],
287382 required_together = [],
288- ** kwargs
383+ ** kwargs ,
289384 ):
290385 """
291386 Creates the base Ansible module argument spec and dependencies,
0 commit comments