Skip to content

Commit 5fe7636

Browse files
authored
Update configuration reconciliation logic (#286)
* Add reconcile_service_config() function with check/retry * Update to use reconcile_service_config utility * Allow test connections to skip TLS verification * Update resolve_parameter_updates() to filter out redacted values * Update reconcile_service_config() to allow for redacted value filtering * Remove parcel activation from host module * Add redacted_skipped flag to service module * Update role config group creation logic to handle multiple base groups * Rename resolve_parameter_updates() to resolve_parameter_changeset() * Update skip_redacted parameter * Add reconcile_config_list_updates() function to replace ConfigListUpdates class * Switch from ConfigListUpdates class to reconcile_config_list_updates() function * Update function name change for resolve_parameter_changeset() * Skip tests for deprecated modules * Update service_type_info tests to use fixture factory * Add skip_redacted and message parameters throughout logic * Convert to use reconcile_config_list_updates() function Signed-off-by: Webster Mudge <[email protected]>
1 parent 123da77 commit 5fe7636

File tree

29 files changed

+376
-149
lines changed

29 files changed

+376
-149
lines changed

plugins/module_utils/cm_utils.py

Lines changed: 44 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -130,8 +130,11 @@ def _normalize(value):
130130
return {k: _normalize(v) for k, v in add.items()}
131131

132132

133-
def resolve_parameter_updates(
134-
current: dict, incoming: dict, purge: bool = False
133+
def resolve_parameter_changeset(
134+
current: dict,
135+
incoming: dict,
136+
purge: bool = False,
137+
skip_redacted: bool = False,
135138
) -> dict:
136139
"""Produce a change set between two parameter dictionaries.
137140
@@ -142,6 +145,7 @@ def resolve_parameter_updates(
142145
current (dict): Existing parameters
143146
incoming (dict): Declared parameters
144147
purge (bool, optional): Flag to reset any current parameters not found in the declared set. Defaults to False.
148+
skip_redacted (bool, optional): Flag to not include parameters with REDACTED values in the changeset. Defaults to False.
145149
146150
Returns:
147151
dict: A change set of the updates
@@ -159,7 +163,9 @@ def resolve_parameter_updates(
159163
updates = {
160164
k: v
161165
for k, v in diff[1].items()
162-
if k in current or (k not in current and v is not None)
166+
if (k in current and not skip_redacted)
167+
or (k in current and (skip_redacted and current[k] != "REDACTED"))
168+
or (k not in current and v is not None)
163169
}
164170

165171
if purge:
@@ -172,6 +178,40 @@ def resolve_parameter_updates(
172178
return updates
173179

174180

181+
def reconcile_config_list_updates(
182+
existing: ApiConfigList,
183+
config: dict,
184+
purge: bool = False,
185+
skip_redacted: bool = False,
186+
) -> tuple[ApiConfigList, dict, dict]:
187+
"""Return a reconciled configuration list and the change deltas.
188+
189+
The function will normalize parameter values to remove whitespace from strings and
190+
convert integers and Booleans to their string representations. Optionally, the
191+
function will reset undeclared parameters or skip parameters that it is unable to
192+
resolve, i.e. REDACTED parameter values.
193+
194+
Args:
195+
existing (ApiConfigList): Existing parameters
196+
config (dict): Declared parameters
197+
purge (bool, optional): Flag to reset any current parameters not found in the declared set. Defaults to False.
198+
skip_redacted (bool, optional): Flag to not include parameters with REDACTED values in the changeset. Defaults to False.
199+
Returns:
200+
tuple[ApiConfigList, dict, dict]: Updated configuration list and the before and after deltas
201+
"""
202+
current = {r.name: r.value for r in existing.items}
203+
changeset = resolve_parameter_changeset(current, config, purge, skip_redacted)
204+
205+
before = {k: current[k] if k in current else None for k in changeset.keys()}
206+
after = changeset
207+
208+
reconciled_config = ApiConfigList(
209+
items=[ApiConfig(name=k, value=v) for k, v in changeset.items()]
210+
)
211+
212+
return (reconciled_config, before, after)
213+
214+
175215
def resolve_tag_updates(
176216
current: dict, incoming: dict, purge: bool = False
177217
) -> tuple[dict, dict]:
@@ -224,7 +264,7 @@ def changed(self) -> bool:
224264
class ConfigListUpdates(object):
225265
def __init__(self, existing: ApiConfigList, updates: dict, purge: bool) -> None:
226266
current = {r.name: r.value for r in existing.items}
227-
changeset = resolve_parameter_updates(current, updates, purge)
267+
changeset = resolve_parameter_changeset(current, updates, purge)
228268

229269
self.diff = dict(
230270
before={k: current[k] if k in current else None for k in changeset.keys()},

plugins/module_utils/host_utils.py

Lines changed: 18 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -44,9 +44,9 @@
4444

4545
from ansible_collections.cloudera.cluster.plugins.module_utils.cm_utils import (
4646
normalize_output,
47+
reconcile_config_list_updates,
4748
wait_command,
4849
wait_bulk_commands,
49-
ConfigListUpdates,
5050
)
5151
from ansible_collections.cloudera.cluster.plugins.module_utils.host_template_utils import (
5252
HostTemplateException,
@@ -243,6 +243,7 @@ def reconcile_host_role_configs(
243243
role_configs: list[dict], # service, type, and config (optional)
244244
purge: bool,
245245
check_mode: bool,
246+
skip_redacted: bool,
246247
message: str = None,
247248
) -> tuple[list[dict], list[dict]]:
248249

@@ -275,15 +276,22 @@ def reconcile_host_role_configs(
275276
if incoming_role_config["config"] or purge:
276277
incoming_config = incoming_role_config.get("config", dict())
277278

278-
updates = ConfigListUpdates(current_role.config, incoming_config, purge)
279+
(
280+
updated_config,
281+
config_before,
282+
config_after,
283+
) = reconcile_config_list_updates(
284+
current_role.config,
285+
incoming_config,
286+
purge,
287+
skip_redacted,
288+
)
279289

280-
if updates.changed:
281-
diff_before.append(
282-
dict(name=current_role.name, config=current_role.config)
283-
)
284-
diff_after.append(dict(name=current_role.name, config=updates.config))
290+
if config_before or config_after:
291+
diff_before.append(dict(name=current_role.name, config=config_before))
292+
diff_after.append(dict(name=current_role.name, config=config_after))
285293

286-
current_role.config = updates.config
294+
current_role.config = updated_config
287295

288296
if not check_mode:
289297
role_api.update_role_config(
@@ -303,6 +311,7 @@ def reconcile_host_role_config_groups(
303311
host: ApiHost,
304312
role_config_groups: list[dict], # service, type (optional), name (optional)
305313
purge: bool,
314+
skip_redacted: bool,
306315
check_mode: bool,
307316
) -> tuple[list[dict], list[dict]]:
308317

@@ -350,6 +359,7 @@ def reconcile_host_role_config_groups(
350359
cluster_rcgs=cluster_rcgs,
351360
)
352361

362+
# TODO Validate if the parcel staging check is still needed
353363
# Read the parcel states for the cluster until all are at a stable stage
354364
wait_parcel_staging(
355365
api_client=api_client,

plugins/module_utils/role_config_group_utils.py

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414

1515
from ansible_collections.cloudera.cluster.plugins.module_utils.cm_utils import (
1616
normalize_output,
17-
ConfigListUpdates,
17+
reconcile_config_list_updates,
1818
)
1919
from ansible_collections.cloudera.cluster.plugins.module_utils.role_utils import (
2020
InvalidRoleTypeException,
@@ -120,6 +120,7 @@ def update_role_config_group(
120120
display_name: str = None,
121121
config: dict = None,
122122
purge: bool = False,
123+
skip_redacted: bool = False,
123124
) -> tuple[ApiRoleConfigGroup, dict, dict]:
124125
before, after = dict(), dict()
125126

@@ -134,12 +135,14 @@ def update_role_config_group(
134135
if config is None:
135136
config = dict()
136137

137-
updates = ConfigListUpdates(role_config_group.config, config, purge)
138+
(updated_config, config_before, config_after) = reconcile_config_list_updates(
139+
role_config_group.config, config, purge, skip_redacted
140+
)
138141

139-
if updates.changed:
140-
before.update(config=updates.diff["before"])
141-
after.update(config=updates.diff["after"])
142-
role_config_group.config = updates.config
142+
if config_before or config_after:
143+
before.update(config=config_before)
144+
after.update(config=config_after)
145+
role_config_group.config = updated_config
143146

144147
return (role_config_group, before, after)
145148

plugins/module_utils/service_utils.py

Lines changed: 98 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -18,10 +18,10 @@
1818

1919
from ansible_collections.cloudera.cluster.plugins.module_utils.cm_utils import (
2020
normalize_output,
21-
resolve_parameter_updates,
21+
reconcile_config_list_updates,
22+
resolve_parameter_changeset,
2223
wait_command,
2324
wait_commands,
24-
ConfigListUpdates,
2525
TagUpdates,
2626
)
2727
from ansible_collections.cloudera.cluster.plugins.module_utils.role_config_group_utils import (
@@ -388,10 +388,80 @@ def read_cm_service(api_client: ApiClient) -> ApiService:
388388
return service
389389

390390

391+
def reconcile_service_config(
392+
api_client: ApiClient,
393+
service: ApiService,
394+
config: dict,
395+
purge: bool,
396+
check_mode: bool,
397+
skip_redacted: bool,
398+
message: str,
399+
) -> tuple[dict, dict]:
400+
service_api = ServicesResourceApi(api_client)
401+
402+
def _handle_config(
403+
existing: ApiServiceConfig,
404+
) -> tuple[ApiServiceConfig, dict, dict]:
405+
current = {r.name: r.value for r in existing.items}
406+
changeset = resolve_parameter_changeset(current, config, purge, skip_redacted)
407+
408+
before = {k: current[k] if k in current else None for k in changeset.keys()}
409+
after = changeset
410+
411+
reconciled_config = ApiServiceConfig(
412+
items=[ApiConfig(name=k, value=v) for k, v in changeset.items()]
413+
)
414+
415+
return (reconciled_config, before, after)
416+
417+
initial_before = dict()
418+
initial_after = dict()
419+
retry = 0
420+
421+
while retry < 3:
422+
existing_config = service_api.read_service_config(
423+
cluster_name=service.cluster_ref.cluster_name,
424+
service_name=service.name,
425+
)
426+
427+
(updated_config, before, after) = _handle_config(existing_config)
428+
429+
if (before or after) and not check_mode:
430+
if retry == 0:
431+
initial_before, initial_after = before, after
432+
433+
service_api.update_service_config(
434+
cluster_name=service.cluster_ref.cluster_name,
435+
service_name=service.name,
436+
message=message,
437+
body=updated_config,
438+
)
439+
440+
config_check = service_api.read_service_config(
441+
cluster_name=service.cluster_ref.cluster_name,
442+
service_name=service.name,
443+
)
444+
445+
(_, checked_before, checked_after) = _handle_config(config_check)
446+
447+
if not checked_before or not checked_after:
448+
return (initial_before, initial_after)
449+
else:
450+
retry += 1
451+
else:
452+
return (before, after)
453+
454+
raise ServiceException(
455+
f"Unable to reconcile service-wide configuration for '{service.name}' in cluster '{service.cluster_ref.cluster_name}",
456+
before,
457+
after,
458+
)
459+
460+
391461
class ServiceConfigUpdates(object):
392462
def __init__(self, existing: ApiServiceConfig, updates: dict, purge: bool) -> None:
393463
current = {r.name: r.value for r in existing.items}
394-
changeset = resolve_parameter_updates(current, updates, purge)
464+
changeset = resolve_parameter_changeset(current, updates, purge)
395465

396466
self.before = {
397467
k: current[k] if k in current else None for k in changeset.keys()
@@ -436,7 +506,10 @@ def reconcile_service_role_config_groups(
436506
role_config_groups: list[dict],
437507
purge: bool,
438508
check_mode: bool,
509+
skip_redacted: bool,
510+
message: str,
439511
) -> tuple[dict, dict]:
512+
440513
# Map the current role config groups by name and by base role type
441514
base_rcg_map, rcg_map = dict(), dict()
442515
for rcg in service.role_config_groups:
@@ -463,6 +536,7 @@ def reconcile_service_role_config_groups(
463536
display_name=incoming_rcg["display_name"],
464537
config=incoming_rcg["config"],
465538
purge=purge,
539+
skip_redacted=skip_redacted,
466540
)
467541

468542
if before or after:
@@ -475,6 +549,7 @@ def reconcile_service_role_config_groups(
475549
service_name=service.name,
476550
role_config_group_name=current_rcg.name,
477551
body=updated_rcg,
552+
message=message,
478553
)
479554

480555
# Else create the new custom role config group
@@ -499,6 +574,7 @@ def reconcile_service_role_config_groups(
499574
display_name=incoming_rcg["display_name"],
500575
config=incoming_rcg["config"],
501576
purge=purge,
577+
skip_redacted=skip_redacted,
502578
)
503579

504580
if before or after:
@@ -511,6 +587,7 @@ def reconcile_service_role_config_groups(
511587
service_name=service.name,
512588
role_config_group_name=current_rcg.name,
513589
body=updated_rcg,
590+
message=message,
514591
)
515592

516593
# Process role config group additions
@@ -529,6 +606,7 @@ def reconcile_service_role_config_groups(
529606
(updated_rcg, before, after) = update_role_config_group(
530607
role_config_group=current_rcg,
531608
purge=purge,
609+
skip_redacted=skip_redacted,
532610
)
533611

534612
if before or after:
@@ -541,6 +619,7 @@ def reconcile_service_role_config_groups(
541619
service_name=service.name,
542620
role_config_group_name=current_rcg.name,
543621
body=updated_rcg,
622+
message=message,
544623
)
545624

546625
# Reset to base and remove any remaining custom role config groups
@@ -578,6 +657,8 @@ def reconcile_service_roles(
578657
roles: list[dict],
579658
purge: bool,
580659
check_mode: bool,
660+
skip_redacted: bool,
661+
message: str,
581662
# maintenance: bool,
582663
# state: str,
583664
) -> tuple[dict, dict]:
@@ -656,22 +737,30 @@ def reconcile_service_roles(
656737
if incoming_config is None:
657738
incoming_config = dict()
658739

659-
updates = ConfigListUpdates(
660-
current_role.config, incoming_config, purge
740+
(
741+
updated_config,
742+
config_before,
743+
config_after,
744+
) = reconcile_config_list_updates(
745+
current_role.config,
746+
incoming_config,
747+
purge,
748+
skip_redacted,
661749
)
662750

663-
if updates.changed:
664-
instance_role_before.update(config=current_role.config)
665-
instance_role_after.update(config=updates.config)
751+
if config_before or config_after:
752+
instance_role_before.update(config=config_before)
753+
instance_role_after.update(config=config_after)
666754

667-
current_role.config = updates.config
755+
current_role.config = updated_config
668756

669757
if not check_mode:
670758
role_api.update_role_config(
671759
cluster_name=service.cluster_ref.cluster_name,
672760
service_name=service.name,
673761
role_name=current_role.name,
674762
body=current_role.config,
763+
message=message,
675764
)
676765

677766
# Reconcile role tags

0 commit comments

Comments
 (0)