Skip to content

Commit 050c227

Browse files
authored
Refactor service_role and service_role_info to align to current flow, utilities, and testing (#278)
* Set up two ZooKeeper roles (two SERVER roles) in zk_session * Add hostname to parse_role_result() * Update read_roles() for optional view and filter parameters * Update tests to use fixtures and discovered expected results * Add get_cluster_hosts utility function * Add family of wait_* functions for different Command types * Add API docs for get_host and get_host_ref utilities * Add filter to read_roles utility function * Add API docs to read_role and read_roles utility functions * Remove role name from create_role utility function * Add tags to create_role utility function * Add provision_service_role utility function * Add toggle_role_maintenance utility function * Add toggle_role_state utility function * Update to use wait_commands function * Update to use updated read_roles utility function * Add service_register and service_deregister test utilities for creating fixtures-as-factories * Add fixture factories for service, role, and role config group * Update to renamed service creation utility * Add register/deregister functions for use with factory fixtures * Use fixture factory utility functions and rename the fixtures * Add get_service_hosts utility * Update tests to use fixture factory utilities * Add display_name to base_cluster fixture * Remove unused import * Add autouse to zookeeper service fixture * Refactor service_role to align with updated logic, utility functions, and test fixtures * Add try-except for non-existent role * Remove erroneous imports Signed-off-by: Webster Mudge <[email protected]>
1 parent be96222 commit 050c227

File tree

15 files changed

+2426
-808
lines changed

15 files changed

+2426
-808
lines changed

plugins/module_utils/cluster_utils.py

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,12 @@
2020
normalize_output,
2121
)
2222

23-
from cm_client import ApiCluster
23+
from cm_client import (
24+
ApiClient,
25+
ApiCluster,
26+
ApiHost,
27+
ClustersResourceApi,
28+
)
2429

2530

2631
CLUSTER_OUTPUT = [
@@ -44,3 +49,13 @@ def parse_cluster_result(cluster: ApiCluster) -> dict:
4449
output = dict(version=cluster.full_version)
4550
output.update(normalize_output(cluster.to_dict(), CLUSTER_OUTPUT))
4651
return output
52+
53+
54+
def get_cluster_hosts(api_client: ApiClient, cluster: ApiCluster) -> list[ApiHost]:
55+
return (
56+
ClustersResourceApi(api_client)
57+
.list_hosts(
58+
cluster_name=cluster.name,
59+
)
60+
.items
61+
)

plugins/module_utils/cm_utils.py

Lines changed: 92 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -32,10 +32,13 @@
3232
from ansible.module_utils.common.text.converters import to_native, to_text
3333
from time import sleep
3434
from cm_client import (
35+
ApiBulkCommandList,
3536
ApiClient,
3637
ApiCommand,
38+
ApiCommandList,
3739
ApiConfig,
3840
ApiConfigList,
41+
ApiEntityTag,
3942
Configuration,
4043
)
4144
from cm_client.rest import ApiException, RESTClientObject
@@ -47,6 +50,43 @@
4750
__maintainer__ = ["[email protected]"]
4851

4952

53+
def wait_bulk_commands(
54+
api_client: ApiClient,
55+
commands: ApiBulkCommandList,
56+
polling: int = 120,
57+
delay: int = 10,
58+
):
59+
if commands.errors:
60+
error_msg = "\n".join(commands.errors)
61+
raise Exception(error_msg)
62+
63+
for cmd in commands.items:
64+
# Serial monitoring
65+
wait_command(api_client, cmd, polling, delay)
66+
67+
68+
def wait_commands(
69+
api_client: ApiClient, commands: ApiCommandList, polling: int = 120, delay: int = 10
70+
):
71+
for cmd in commands.items:
72+
# Serial monitoring
73+
wait_command(api_client, cmd, polling, delay)
74+
75+
76+
def wait_command(
77+
api_client: ApiClient, command: ApiCommand, polling: int = 120, delay: int = 10
78+
):
79+
poll_count = 0
80+
while command.active:
81+
if poll_count > polling:
82+
raise Exception("Command timeout: " + str(command.id))
83+
sleep(delay)
84+
poll_count += 1
85+
command = CommandsResourceApi(api_client).read_command(command.id)
86+
if not command.success:
87+
raise Exception(command.result_message)
88+
89+
5090
def normalize_output(entity: dict, filter: list) -> dict:
5191
output = {}
5292
for k in filter:
@@ -131,8 +171,8 @@ def resolve_tag_updates(
131171
incoming_tags = {
132172
k: str(v)
133173
for k, v in incoming.items()
134-
if (isinstance(v, str) and v.strip() != "")
135-
or (not isinstance(v, str) and v is not None)
174+
if (type(v) is str and v.strip() != "")
175+
or (type(v) is not str and v is not None)
136176
}
137177

138178
delta_add = {}
@@ -151,6 +191,29 @@ def resolve_tag_updates(
151191
return (delta_add, delta_del)
152192

153193

194+
class TagUpdates(object):
195+
def __init__(
196+
self, existing: list[ApiEntityTag], updates: dict, purge: bool
197+
) -> None:
198+
(_additions, _deletions) = resolve_tag_updates(
199+
current={t.name: t.value for t in existing},
200+
incoming=updates,
201+
purge=purge,
202+
)
203+
204+
self.diff = dict(
205+
before=_deletions,
206+
after=_additions,
207+
)
208+
209+
self.additions = [ApiEntityTag(k, v) for k, v in _additions.items()]
210+
self.deletions = [ApiEntityTag(k, v) for k, v in _deletions.items()]
211+
212+
@property
213+
def changed(self) -> bool:
214+
return bool(self.additions) or bool(self.deletions)
215+
216+
154217
class ConfigListUpdates(object):
155218
def __init__(self, existing: ApiConfigList, updates: dict, purge: bool) -> None:
156219
current = {r.name: r.value for r in existing.items}
@@ -507,15 +570,15 @@ def wait_command(self, command: ApiCommand, polling: int = 10, delay: int = 5):
507570
If the command exceeds the polling limit, it fails with a timeout error.
508571
If the command completes unsuccessfully, it fails with the command's result message.
509572
510-
Inputs:
573+
Args:
511574
command (ApiCommand): The command object to monitor.
512575
polling (int, optional): The maximum number of polling attempts before timing out. Default is 10.
513576
delay (int, optional): The time (in seconds) to wait between polling attempts. Default is 5.
514577
515578
Raises:
516579
module.fail_json: If the command times out or fails.
517580
518-
Return:
581+
Returns:
519582
None: The function returns successfully if the command completes and is marked as successful.
520583
"""
521584
poll_count = 0
@@ -530,6 +593,31 @@ def wait_command(self, command: ApiCommand, polling: int = 10, delay: int = 5):
530593
msg=to_text(command.result_message), command_id=to_text(command.id)
531594
)
532595

596+
def wait_commands(
597+
self, commands: ApiBulkCommandList, polling: int = 10, delay: int = 5
598+
):
599+
"""
600+
Waits for a list of commands to complete, polling each status at regular intervals.
601+
If a command exceeds the polling limit, it fails with a timeout error.
602+
If a command completes unsuccessfully, it fails with the command's result message.
603+
604+
Args:
605+
commands (ApiBulkCommandList): Cloudera Manager bulk commands
606+
607+
Raises:
608+
module.fail_json: If the command times out or fails.
609+
610+
Returns:
611+
none: The function returns successfully if the commands complete and are marked as successful.
612+
"""
613+
if commands.errors:
614+
error_msg = "\n".join(commands.errors)
615+
self.module.fail_json(msg=error_msg)
616+
617+
for c in commands.items:
618+
# Not in parallel...
619+
self.wait_command(c, polling, delay)
620+
533621
@staticmethod
534622
def ansible_module_internal(argument_spec={}, required_together=[], **kwargs):
535623
"""

plugins/module_utils/host_utils.py

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,19 @@
2828
def get_host(
2929
api_client: ApiClient, hostname: str = None, host_id: str = None
3030
) -> ApiHost:
31+
"""Retrieve a Host by either hostname or host ID.
32+
33+
Args:
34+
api_client (ApiClient): Cloudera Manager API client.
35+
hostname (str, optional): The cluster hostname. Defaults to None.
36+
host_id (str, optional): The cluster host ID. Defaults to None.
37+
38+
Raises:
39+
ex: ApiException for all non-404 errors.
40+
41+
Returns:
42+
ApiHost: Host object. If not found, returns None.
43+
"""
3144
if hostname:
3245
return next(
3346
(
@@ -50,7 +63,21 @@ def get_host(
5063
def get_host_ref(
5164
api_client: ApiClient, hostname: str = None, host_id: str = None
5265
) -> ApiHostRef:
66+
"""Retrieve a Host Reference by either hostname or host ID.
67+
68+
Args:
69+
api_client (ApiClient): Cloudera Manager API client.
70+
hostname (str, optional): The cluster hostname. Defaults to None.
71+
host_id (str, optional): The cluster host ID. Defaults to None.
72+
73+
Raises:
74+
ex: ApiException for all non-404 errors.
75+
76+
Returns:
77+
ApiHostRef: Host reference object. If not found, returns None.
78+
"""
5379
host = get_host(api_client, hostname, host_id)
80+
5481
if host is not None:
5582
return ApiHostRef(host.host_id, host.hostname)
5683
else:

0 commit comments

Comments
 (0)