From 16aa02186dcd3cad60622e6308a2359aebcaebbb Mon Sep 17 00:00:00 2001 From: rsuplina Date: Mon, 15 Apr 2024 12:30:06 +0100 Subject: [PATCH 1/5] Add Parcel Module Signed-off-by: rsuplina --- plugins/modules/parcel.py | 224 ++++++++++++++++++ .../plugins/modules/parcel/test_parcel.py | 66 ++++++ 2 files changed, 290 insertions(+) create mode 100644 plugins/modules/parcel.py create mode 100644 tests/unit/plugins/modules/parcel/test_parcel.py diff --git a/plugins/modules/parcel.py b/plugins/modules/parcel.py new file mode 100644 index 00000000..447ab191 --- /dev/null +++ b/plugins/modules/parcel.py @@ -0,0 +1,224 @@ +# Copyright 2024 Cloudera, Inc. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from ansible_collections.cloudera.cluster.plugins.module_utils.cm_utils import ( + ClouderaManagerModule, +) + +from cm_client import ClustersResourceApi, ParcelResourceApi +from cm_client.rest import ApiException +import time + +ANSIBLE_METADATA = { + "metadata_version": "1.1", + "status": ["preview"], + "supported_by": "community", +} + +DOCUMENTATION = r""" +--- +module: parcel +short_description: Manage the state of parcels on a Cluster +description: + - Facilitates the management of parcels on a Cluster by downloading, distributing, and activating them according to the specified state. + - Supported states include 'download', 'distribute', and 'activate', each corresponding to specific actions performed on parcels. +author: + - "Ronald Suplina (@rsuplina)" +requirements: + - cm_client +""" + +EXAMPLES = r""" +--- +- name: Download, distribute and activate a parcel on a cluster + cloudera.cluster.parcel: + host: example.cloudera.com + username: "jane_smith" + password: "S&peR4Ec*re" + cluster_name: "OneNodeECS" + product: "ECS" + parcel_version: "1.5.1-b626-ecs-1.5.1-b626.p0.42068229" + state: "activate" + +- name: Downloand and distribute a parcel on a cluster + cloudera.cluster.parcel: + host: example.cloudera.com + username: "jane_smith" + password: "S&peR4Ec*re" + cluster_name: "OneNodeECS" + product: "ECS" + parcel_version: "1.5.1-b626-ecs-1.5.1-b626.p0.42068229" + state: "distribute" + +""" + +RETURN = r""" +--- +cloudera_manager: + description: Returns details about specific parcel + type: dict + contains: + product: + product: The name of the product. + type: str + returned: optional + version: + description: The version of the product + type: str + returned: optional + stage: + description: Current stage of the parcel. + type: str + returned: optional + state: + description: The state of the parcel. This shows the progress of state transitions and if there were any errors. + type: dict + returned: optional + clusterRef: + description: A reference to the enclosing cluster. + type: dict + returned: optional + displayName: + description: Display name of the parcel. + type: str + returned: optional + description: + description: Description of the parcel. + type: str + returned: optional +""" + + +class ClouderaParcel(ClouderaManagerModule): + def __init__(self, module): + super(ClouderaParcel, self).__init__(module) + + self.cluster_name = self.get_param("cluster_name") + self.product = self.get_param("product") + self.parcel_version = self.get_param("parcel_version") + self.state = self.get_param("state") + + self.process() + + + + def download_parcel(self, parcel_api_instance, cluster_name, product, parcel_version, polling_interval): + parcel_api_instance.start_download_command(cluster_name=cluster_name, product=product, version=parcel_version) + while True: + parcel_status = parcel_api_instance.read_parcel(cluster_name=cluster_name, product=product, version=parcel_version) + if parcel_status.stage == 'DOWNLOADING': + time.sleep(polling_interval) + elif parcel_status.stage == "DOWNLOADED": + break + + def distribute_parcel(self, parcel_api_instance, cluster_name, product, parcel_version, polling_interval): + parcel_api_instance.start_distribution_command(cluster_name=cluster_name, product=product, version=parcel_version) + while True: + parcel_status = parcel_api_instance.read_parcel(cluster_name=cluster_name, product=product, version=parcel_version) + if parcel_status.stage == 'DISTRIBUTING': + time.sleep(polling_interval) + elif parcel_status.stage == "DISTRIBUTED": + break + + def activate_parcel(self, parcel_api_instance, cluster_name, product, parcel_version, polling_interval): + parcel_api_instance.activate_command(cluster_name=cluster_name, product=product, version=parcel_version) + while True: + parcel_status = parcel_api_instance.read_parcel(cluster_name=cluster_name, product=product, version=parcel_version) + if parcel_status.stage == 'ACTIVATING': + time.sleep(polling_interval) + elif parcel_status.stage == "ACTIVATED": + break + + + @ClouderaManagerModule.handle_process + def process(self): + parcel_api_instance = ParcelResourceApi(self.api_client) + cluster_api_instance = ClustersResourceApi(self.api_client) + + polling_interval = 10 + self.parcel_output = {} + self.changed = False + parcel_actions = [] + + try: + cluster_api_instance.read_cluster(cluster_name=self.cluster_name).to_dict() + except ApiException as ex: + if ex.status == 404: + self.module.fail_json(msg=f" Cluster {self.cluster_name} {ex.reason}") + + try: + existing_state = parcel_api_instance.read_parcel(cluster_name=self.cluster_name, product=self.product, version=self.parcel_version).stage + except ApiException as ex: + if ex.status == 404: + self.module.fail_json(msg=f" Parcel {self.parcel_version} {ex.reason}") + + + if self.state == "download": + if existing_state == 'AVAILABLE_REMOTELY': + parcel_actions.append(self.download_parcel) + + elif self.state == "distribute": + if existing_state == 'AVAILABLE_REMOTELY': + parcel_actions.extend([self.download_parcel, self.distribute_parcel]) + elif existing_state == 'DOWNLOADED': + parcel_actions.append(self.distribute_parcel) + + elif self.state == "activate": + if existing_state == 'AVAILABLE_REMOTELY': + parcel_actions.extend([self.download_parcel, self.distribute_parcel, self.activate_parcel]) + elif existing_state == 'DOWNLOADED': + parcel_actions.extend([self.distribute_parcel, self.activate_parcel]) + elif existing_state == 'DISTRIBUTED': + parcel_actions.append(self.activate_parcel) + + if existing_state not in ['AVAILABLE_REMOTELY','DOWNLOADED','DISTRIBUTED','ACTIVATED']: + error_msg = parcel_api_instance.read_parcel(cluster_name=self.cluster_name, product=self.product, version=self.parcel_version).state.errors[0] + self.module.fail_json(msg=error_msg) + + for action in parcel_actions: + action(parcel_api_instance, self.cluster_name, self.product, self.parcel_version, polling_interval) + self.changed = True + + self.parcel_output = parcel_api_instance.read_parcel(cluster_name=self.cluster_name, product=self.product, version=self.parcel_version).to_dict() + + +def main(): + module = ClouderaManagerModule.ansible_module( + argument_spec=dict( + cluster_name=dict(required=True, type="str"), + product=dict(required=True, type="str"), + parcel_version=dict(required=True, type="str"), + state=dict(type='str', default='activate', choices=['download', 'distribute','activate']), + ), + + supports_check_mode=True) + + result = ClouderaParcel(module) + + changed = result.changed + + output = dict( + changed=changed, + cloudera_manager=result.parcel_output, + ) + + if result.debug: + log = result.log_capture.getvalue() + output.update(debug=log, debug_lines=log.split("\n")) + + module.exit_json(**output) + + +if __name__ == "__main__": + main() diff --git a/tests/unit/plugins/modules/parcel/test_parcel.py b/tests/unit/plugins/modules/parcel/test_parcel.py new file mode 100644 index 00000000..ee835560 --- /dev/null +++ b/tests/unit/plugins/modules/parcel/test_parcel.py @@ -0,0 +1,66 @@ +# -*- coding: utf-8 -*- + +# Copyright 2024 Cloudera, Inc. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import absolute_import, division, print_function + +__metaclass__ = type +import os +import logging +import pytest + +from ansible_collections.cloudera.cluster.plugins.modules import parcel +from ansible_collections.cloudera.cluster.tests.unit import AnsibleExitJson, AnsibleFailJson + +LOG = logging.getLogger(__name__) + +def test_pytest_activate_parcel(module_args): + module_args( + { + "username": os.getenv('CM_USERNAME'), + "password": os.getenv('CM_PASSWORD'), + "host": os.getenv('CM_HOST'), + "cluster_name": "ECS_Cluster", + "product": "ECS", + "parcel_version": "1.5.1-b626-ecs-1.5.1-b626.p0.42068229", + "state": "activate" + } + ) + + with pytest.raises(AnsibleExitJson) as e: + parcel.main() + + # LOG.info(str(e.value)) + LOG.info(str(e.value.cloudera_manager)) + +def test_pytest_download_parcel(module_args): + module_args( + { + "username": os.getenv('CM_USERNAME'), + "password": os.getenv('CM_PASSWORD'), + "host": os.getenv('CM_HOST'), + "cluster_name": "ECS_Cluster", + "product": "ECS", + "parcel_version": "1.5.1-b626-ecs-1.5.1-b626.p0.42068229", + "state": "download" + } + ) + + with pytest.raises(AnsibleExitJson) as e: + parcel.main() + + # LOG.info(str(e.value)) + LOG.info(str(e.value.cloudera_manager)) + From 9b215200814c17e57c3a7882e789d4a2bb2d5e0d Mon Sep 17 00:00:00 2001 From: rsuplina Date: Wed, 24 Apr 2024 17:17:15 +0100 Subject: [PATCH 2/5] Add requested updates Signed-off-by: rsuplina --- plugins/modules/parcel.py | 146 ++++++++++++++++-- .../plugins/modules/parcel/test_parcel.py | 89 ++++++++++- 2 files changed, 215 insertions(+), 20 deletions(-) diff --git a/plugins/modules/parcel.py b/plugins/modules/parcel.py index 447ab191..abd92aa2 100644 --- a/plugins/modules/parcel.py +++ b/plugins/modules/parcel.py @@ -32,11 +32,39 @@ short_description: Manage the state of parcels on a Cluster description: - Facilitates the management of parcels on a Cluster by downloading, distributing, and activating them according to the specified state. - - Supported states include 'download', 'distribute', and 'activate', each corresponding to specific actions performed on parcels. author: - "Ronald Suplina (@rsuplina)" requirements: - cm_client +options: + cluster_name: + description: + - The name of the cluster + type: str + required: yes + product: + description: + - The name of the product, e.g. CDH, Impala + type: str + required: yes + version: + description: + - The version of the product, e.g. 1.1.0, 2.3.0. + type: str + required: yes + state: + description: + - State of the parcel. + type: str + default: 'activated' + choices: + - 'downloaded' + - 'distributed' + - 'activated' + - 'removed' + - 'undistributed' + - 'deactivated' + required: False """ EXAMPLES = r""" @@ -49,7 +77,7 @@ cluster_name: "OneNodeECS" product: "ECS" parcel_version: "1.5.1-b626-ecs-1.5.1-b626.p0.42068229" - state: "activate" + state: "activated" - name: Downloand and distribute a parcel on a cluster cloudera.cluster.parcel: @@ -59,7 +87,27 @@ cluster_name: "OneNodeECS" product: "ECS" parcel_version: "1.5.1-b626-ecs-1.5.1-b626.p0.42068229" - state: "distribute" + state: "distributed" + +- name: Remove the parcel on a specified cluster + cloudera.cluster.parcel: + host: example.cloudera.com + username: "jane_smith" + password: "S&peR4Ec*re" + cluster_name: "OneNodeECS" + product: "ECS" + parcel_version: "1.5.1-b626-ecs-1.5.1-b626.p0.42068229" + state: "removed" + +- name: Undistribute the parcel on a specified cluster + cloudera.cluster.parcel: + host: example.cloudera.com + username: "jane_smith" + password: "S&peR4Ec*re" + cluster_name: "OneNodeECS" + product: "ECS" + parcel_version: "1.5.1-b626-ecs-1.5.1-b626.p0.42068229" + state: "undistributed" """ @@ -108,23 +156,69 @@ def __init__(self, module): self.product = self.get_param("product") self.parcel_version = self.get_param("parcel_version") self.state = self.get_param("state") - + self.polling_interval = self.get_param("polling_interval") self.process() + def deactivate_parcel(self, parcel_api_instance, cluster_name, product, parcel_version, polling_interval, timeout=600): + start_time = time.time() + parcel_api_instance.deactivate_command(cluster_name=cluster_name, product=product, version=parcel_version) + while True: + elapsed_time = time.time() - start_time + if elapsed_time > timeout: + self.module.fail_json(msg="Timeout exceeded while waiting for parcel state to complete.") + parcel_status = parcel_api_instance.read_parcel(cluster_name=cluster_name, product=product, version=parcel_version) + if parcel_status.stage == 'ACTIVATED': + time.sleep(polling_interval) + elif parcel_status.stage == "DISTRIBUTED": + break + + def undistribute_parcel(self, parcel_api_instance, cluster_name, product, parcel_version, polling_interval, timeout=600): + start_time = time.time() + parcel_api_instance.start_removal_of_distribution_command(cluster_name=cluster_name, product=product, version=parcel_version) + while True: + elapsed_time = time.time() - start_time + if elapsed_time > timeout: + self.module.fail_json(msg="Timeout exceeded while waiting for parcel state to complete.") + parcel_status = parcel_api_instance.read_parcel(cluster_name=cluster_name, product=product, version=parcel_version) + if parcel_status.stage == 'UNDISTRIBUTING': + time.sleep(polling_interval) + elif parcel_status.stage == "DOWNLOADED": + break + + def remove_parcel(self, parcel_api_instance, cluster_name, product, parcel_version, polling_interval, timeout=600): + start_time = time.time() + parcel_api_instance.remove_download_command(cluster_name=cluster_name, product=product, version=parcel_version) + while True: + elapsed_time = time.time() - start_time + if elapsed_time > timeout: + self.module.fail_json(msg="Timeout exceeded while waiting for parcel state to complete.") + parcel_status = parcel_api_instance.read_parcel(cluster_name=cluster_name, product=product, version=parcel_version) + if parcel_status.stage == 'DOWNLOADED': + time.sleep(polling_interval) + elif parcel_status.stage == "AVAILABLE_REMOTELY": + break - def download_parcel(self, parcel_api_instance, cluster_name, product, parcel_version, polling_interval): + def download_parcel(self, parcel_api_instance, cluster_name, product, parcel_version, polling_interval, timeout=1200): + start_time = time.time() parcel_api_instance.start_download_command(cluster_name=cluster_name, product=product, version=parcel_version) while True: + elapsed_time = time.time() - start_time + if elapsed_time > timeout: + self.module.fail_json(msg="Timeout exceeded while waiting for parcel state to complete.") parcel_status = parcel_api_instance.read_parcel(cluster_name=cluster_name, product=product, version=parcel_version) if parcel_status.stage == 'DOWNLOADING': time.sleep(polling_interval) elif parcel_status.stage == "DOWNLOADED": break - - def distribute_parcel(self, parcel_api_instance, cluster_name, product, parcel_version, polling_interval): + + def distribute_parcel(self, parcel_api_instance, cluster_name, product, parcel_version, polling_interval, timeout=1200): + start_time = time.time() parcel_api_instance.start_distribution_command(cluster_name=cluster_name, product=product, version=parcel_version) while True: + elapsed_time = time.time() - start_time + if elapsed_time > timeout: + self.module.fail_json(msg="Timeout exceeded while waiting for parcel state to complete.") parcel_status = parcel_api_instance.read_parcel(cluster_name=cluster_name, product=product, version=parcel_version) if parcel_status.stage == 'DISTRIBUTING': time.sleep(polling_interval) @@ -146,7 +240,6 @@ def process(self): parcel_api_instance = ParcelResourceApi(self.api_client) cluster_api_instance = ClustersResourceApi(self.api_client) - polling_interval = 10 self.parcel_output = {} self.changed = False parcel_actions = [] @@ -164,17 +257,17 @@ def process(self): self.module.fail_json(msg=f" Parcel {self.parcel_version} {ex.reason}") - if self.state == "download": + if self.state == "downloaded": if existing_state == 'AVAILABLE_REMOTELY': parcel_actions.append(self.download_parcel) - elif self.state == "distribute": + elif self.state == "distributed": if existing_state == 'AVAILABLE_REMOTELY': parcel_actions.extend([self.download_parcel, self.distribute_parcel]) elif existing_state == 'DOWNLOADED': parcel_actions.append(self.distribute_parcel) - elif self.state == "activate": + elif self.state == "activated": if existing_state == 'AVAILABLE_REMOTELY': parcel_actions.extend([self.download_parcel, self.distribute_parcel, self.activate_parcel]) elif existing_state == 'DOWNLOADED': @@ -182,13 +275,35 @@ def process(self): elif existing_state == 'DISTRIBUTED': parcel_actions.append(self.activate_parcel) + + if self.state == "removed": + if existing_state == 'DOWNLOADED': + parcel_actions.append(self.remove_parcel) + if existing_state == 'DISTRIBUTED': + parcel_actions.extend([self.undistribute_parcel, self.remove_parcel]) + if existing_state == 'ACTIVATED': + parcel_actions.extend([self.deactivate_parcel, self.undistribute_parcel, self.remove_parcel]) + + if self.state == "undistributed": + if existing_state == 'DISTRIBUTED': + parcel_actions.extend([self.undistribute_parcel]) + if existing_state == 'ACTIVATED': + parcel_actions.extend([self.deactivate_parcel, self.undistribute_parcel]) + + if self.state == "deactivated": + if existing_state == 'ACTIVATED': + parcel_actions.append(self.deactivate_parcel) + + + if existing_state not in ['AVAILABLE_REMOTELY','DOWNLOADED','DISTRIBUTED','ACTIVATED']: error_msg = parcel_api_instance.read_parcel(cluster_name=self.cluster_name, product=self.product, version=self.parcel_version).state.errors[0] self.module.fail_json(msg=error_msg) - for action in parcel_actions: - action(parcel_api_instance, self.cluster_name, self.product, self.parcel_version, polling_interval) - self.changed = True + if not self.module.check_mode: + for action in parcel_actions: + action(parcel_api_instance, self.cluster_name, self.product, self.parcel_version, self.polling_interval) + self.changed = True self.parcel_output = parcel_api_instance.read_parcel(cluster_name=self.cluster_name, product=self.product, version=self.parcel_version).to_dict() @@ -198,8 +313,9 @@ def main(): argument_spec=dict( cluster_name=dict(required=True, type="str"), product=dict(required=True, type="str"), + polling_interval=dict(required=False, type="int",default=10), parcel_version=dict(required=True, type="str"), - state=dict(type='str', default='activate', choices=['download', 'distribute','activate']), + state=dict(type='str', default='activated', choices=['downloaded', 'distributed','activated','removed','undistributed','deactivated']), ), supports_check_mode=True) diff --git a/tests/unit/plugins/modules/parcel/test_parcel.py b/tests/unit/plugins/modules/parcel/test_parcel.py index ee835560..5021326b 100644 --- a/tests/unit/plugins/modules/parcel/test_parcel.py +++ b/tests/unit/plugins/modules/parcel/test_parcel.py @@ -26,16 +26,56 @@ LOG = logging.getLogger(__name__) + + +def test_pytest_download_parcel(module_args): + module_args( + { + "username": os.getenv('CM_USERNAME'), + "password": os.getenv('CM_PASSWORD'), + "host": os.getenv('CM_HOST'), + "cluster_name": "Base_Edge2AI_Node", + "product": "ECS", + "parcel_version": "1.5.1-b626-ecs-1.5.1-b626.p0.42068229", + "state": "downloaded" + } + ) + + with pytest.raises(AnsibleExitJson) as e: + parcel.main() + + # LOG.info(str(e.value)) + LOG.info(str(e.value.cloudera_manager)) + +def test_pytest_distribute_parcel(module_args): + module_args( + { + "username": os.getenv('CM_USERNAME'), + "password": os.getenv('CM_PASSWORD'), + "host": os.getenv('CM_HOST'), + "cluster_name": "Base_Edge2AI_Node", + "product": "ECS", + "parcel_version": "1.5.1-b626-ecs-1.5.1-b626.p0.42068229", + "state": "distributed" + } + ) + + with pytest.raises(AnsibleExitJson) as e: + parcel.main() + + # LOG.info(str(e.value)) + LOG.info(str(e.value.cloudera_manager)) + def test_pytest_activate_parcel(module_args): module_args( { "username": os.getenv('CM_USERNAME'), "password": os.getenv('CM_PASSWORD'), "host": os.getenv('CM_HOST'), - "cluster_name": "ECS_Cluster", + "cluster_name": "Base_Edge2AI_Node", "product": "ECS", "parcel_version": "1.5.1-b626-ecs-1.5.1-b626.p0.42068229", - "state": "activate" + "state": "activated" } ) @@ -45,16 +85,55 @@ def test_pytest_activate_parcel(module_args): # LOG.info(str(e.value)) LOG.info(str(e.value.cloudera_manager)) -def test_pytest_download_parcel(module_args): +def test_pytest_remove_parcel(module_args): + module_args( + { + "username": os.getenv('CM_USERNAME'), + "password": os.getenv('CM_PASSWORD'), + "host": os.getenv('CM_HOST'), + "cluster_name": "Base_Edge2AI_Node", + "product": "ECS", + "parcel_version": "1.5.1-b626-ecs-1.5.1-b626.p0.42068229", + "state": "removed" + } + ) + + with pytest.raises(AnsibleExitJson) as e: + parcel.main() + + # LOG.info(str(e.value)) + LOG.info(str(e.value.cloudera_manager)) + +def test_pytest_undistribute_parcel(module_args): + module_args( + { + "username": os.getenv('CM_USERNAME'), + "password": os.getenv('CM_PASSWORD'), + "host": os.getenv('CM_HOST'), + "cluster_name": "Base_Edge2AI_Node", + "product": "ECS", + "parcel_version": "1.5.1-b626-ecs-1.5.1-b626.p0.42068229", + "state": "undistributed" + } + ) + + with pytest.raises(AnsibleExitJson) as e: + parcel.main() + + # LOG.info(str(e.value)) + LOG.info(str(e.value.cloudera_manager)) + + +def test_pytest_deactivate_parcel(module_args): module_args( { "username": os.getenv('CM_USERNAME'), "password": os.getenv('CM_PASSWORD'), "host": os.getenv('CM_HOST'), - "cluster_name": "ECS_Cluster", + "cluster_name": "Base_Edge2AI_Node", "product": "ECS", "parcel_version": "1.5.1-b626-ecs-1.5.1-b626.p0.42068229", - "state": "download" + "state": "deactivated" } ) From fb73653f56dedf9f9e442007b4f0066685f72d3f Mon Sep 17 00:00:00 2001 From: rsuplina Date: Wed, 24 Apr 2024 17:53:52 +0100 Subject: [PATCH 3/5] Add Parcel Info Module Signed-off-by: rsuplina --- plugins/modules/parcel_info.py | 173 ++++++++++++++++++ .../modules/parcel_info/test_parcel_info.py | 67 +++++++ 2 files changed, 240 insertions(+) create mode 100644 plugins/modules/parcel_info.py create mode 100644 tests/unit/plugins/modules/parcel_info/test_parcel_info.py diff --git a/plugins/modules/parcel_info.py b/plugins/modules/parcel_info.py new file mode 100644 index 00000000..e25208e6 --- /dev/null +++ b/plugins/modules/parcel_info.py @@ -0,0 +1,173 @@ +# Copyright 2024 Cloudera, Inc. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from ansible_collections.cloudera.cluster.plugins.module_utils.cm_utils import ( + ClouderaManagerModule, +) + +from cm_client import ClustersResourceApi, ParcelResourceApi, ParcelsResourceApi +from cm_client.rest import ApiException + +ANSIBLE_METADATA = { + "metadata_version": "1.1", + "status": ["preview"], + "supported_by": "community", +} + +DOCUMENTATION = r""" +--- +module: parcel_info +short_description: Gather details about the parcels on the cluster +description: + - Gathers details about a single parcel or about all parcels on the cluster +author: + - "Ronald Suplina (@rsuplina)" +requirements: + - cm_client +options: + cluster_name: + description: + - The name of the cluster + type: str + required: yes + product: + description: + - The name of the product, e.g. CDH, Impala + type: str + required: no + version: + description: + - The version of the product, e.g. 1.1.0, 2.3.0. + type: str + required: no +""" + +EXAMPLES = r""" +--- +- name: Gather details about specific parcel + cloudera.cluster.parcel_info: + host: example.cloudera.com + username: "jane_smith" + password: "S&peR4Ec*re" + cluster_name: "OneNodeECS" + product: "ECS" + parcel_version: "1.5.1-b626-ecs-1.5.1-b626.p0.42068229" + +- name: Gather details about all parcels on the cluster + cloudera.cluster.parcel_info: + host: example.cloudera.com + username: "jane_smith" + password: "S&peR4Ec*re" + cluster_name: "OneNodeECS" +""" + +RETURN = r""" +--- +cloudera_manager: + description: Returns details about specific parcel or all parcels on the cluster + type: list + elements: dict + contains: + product: + product: The name of the product. + type: str + returned: always + version: + description: The version of the product + type: str + returned: always + stage: + description: Current stage of the parcel. + type: str + returned: always + state: + description: The state of the parcel. This shows the progress of state transitions and if there were any errors. + type: dict + returned: always + clusterRef: + description: A reference to the enclosing cluster. + type: dict + returned: always + displayName: + description: Display name of the parcel. + type: str + returned: always + description: + description: Description of the parcel. + type: str + returned: always +""" + + +class ClouderaParcelInfo(ClouderaManagerModule): + def __init__(self, module): + super(ClouderaParcelInfo, self).__init__(module) + self.cluster_name = self.get_param("cluster_name") + self.product = self.get_param("product") + self.parcel_version = self.get_param("parcel_version") + self.process() + + + @ClouderaManagerModule.handle_process + def process(self): + parcel_api_instance = ParcelResourceApi(self.api_client) + parcels_api_instance = ParcelsResourceApi(self.api_client) + cluster_api_instance = ClustersResourceApi(self.api_client) + + self.parcel_output = {} + self.changed = False + + try: + cluster_api_instance.read_cluster(cluster_name=self.cluster_name).to_dict() + except ApiException as ex: + if ex.status == 404: + self.module.fail_json(msg=f" Cluster {self.cluster_name} {ex.reason}") + + if self.product and self.parcel_version: + self.parcel_info = parcel_api_instance.read_parcel(cluster_name=self.cluster_name, product=self.product, version=self.parcel_version).to_dict() + self.parcel_output = {"items":[self.parcel_info]} + else: + self.parcel_output = parcels_api_instance.read_parcels(cluster_name=self.cluster_name).to_dict() + + +def main(): + module = ClouderaManagerModule.ansible_module( + argument_spec=dict( + cluster_name=dict(required=True, type="str"), + product=dict(required=False, type="str"), + parcel_version=dict(required=False, type="str")), + supports_check_mode=True, + required_together=[ + ('product', 'parcel_version'), + ], + ) + + result = ClouderaParcelInfo(module) + + changed = result.changed + + output = dict( + changed=changed, + cloudera_manager=result.parcel_output, + ) + + if result.debug: + log = result.log_capture.getvalue() + output.update(debug=log, debug_lines=log.split("\n")) + + module.exit_json(**output) + + +if __name__ == "__main__": + main() diff --git a/tests/unit/plugins/modules/parcel_info/test_parcel_info.py b/tests/unit/plugins/modules/parcel_info/test_parcel_info.py new file mode 100644 index 00000000..5ea348bb --- /dev/null +++ b/tests/unit/plugins/modules/parcel_info/test_parcel_info.py @@ -0,0 +1,67 @@ +# -*- coding: utf-8 -*- + +# Copyright 2024 Cloudera, Inc. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import absolute_import, division, print_function + +__metaclass__ = type +import os +import logging +import pytest + +from ansible_collections.cloudera.cluster.plugins.modules import parcel_info +from ansible_collections.cloudera.cluster.tests.unit import AnsibleExitJson, AnsibleFailJson + +LOG = logging.getLogger(__name__) + +def test_pytest_specific_parcel_info(module_args): + module_args( + { + "username": os.getenv('CM_USERNAME'), + "password": os.getenv('CM_PASSWORD'), + "host": os.getenv('CM_HOST'), + "port": "7180", + "verify_tls": "no", + "debug": "no", + "cluster_name": "Base_Edge2AI_Node", + "product": "ECS", + "parcel_version": "1.5.1-b626-ecs-1.5.1-b626.p0.42068229", + } + ) + + with pytest.raises(AnsibleExitJson) as e: + parcel_info.main() + + # LOG.info(str(e.value)) + LOG.info(str(e.value.cloudera_manager)) + +def test_pytest_get_all_parcels(module_args): + module_args( + { + "username": os.getenv('CM_USERNAME'), + "password": os.getenv('CM_PASSWORD'), + "host": os.getenv('CM_HOST'), + "port": "7180", + "verify_tls": "no", + "debug": "no", + "cluster_name": "Base_Edge2AI_Node", + } + ) + + with pytest.raises(AnsibleExitJson) as e: + parcel_info.main() + + # LOG.info(str(e.value)) + LOG.info(str(e.value.cloudera_manager)) From 723c157b48bc32c6361a2badfbf9608b05c03be2 Mon Sep 17 00:00:00 2001 From: rsuplina Date: Fri, 3 May 2024 14:13:15 +0100 Subject: [PATCH 4/5] Documentation parcel typo Signed-off-by: rsuplina --- plugins/modules/parcel.py | 2 +- plugins/modules/parcel_info.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/plugins/modules/parcel.py b/plugins/modules/parcel.py index abd92aa2..164d212e 100644 --- a/plugins/modules/parcel.py +++ b/plugins/modules/parcel.py @@ -121,7 +121,7 @@ product: The name of the product. type: str returned: optional - version: + parcel_version: description: The version of the product type: str returned: optional diff --git a/plugins/modules/parcel_info.py b/plugins/modules/parcel_info.py index e25208e6..557db12b 100644 --- a/plugins/modules/parcel_info.py +++ b/plugins/modules/parcel_info.py @@ -46,7 +46,7 @@ - The name of the product, e.g. CDH, Impala type: str required: no - version: + parcel_version: description: - The version of the product, e.g. 1.1.0, 2.3.0. type: str From fd0a51802cef213b4e921072d46e6a55f401ca77 Mon Sep 17 00:00:00 2001 From: rsuplina Date: Fri, 3 May 2024 14:16:34 +0100 Subject: [PATCH 5/5] Change name for Parcel Example Signed-off-by: rsuplina --- tests/unit/plugins/modules/parcel/test_parcel.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/tests/unit/plugins/modules/parcel/test_parcel.py b/tests/unit/plugins/modules/parcel/test_parcel.py index 5021326b..8f52e20c 100644 --- a/tests/unit/plugins/modules/parcel/test_parcel.py +++ b/tests/unit/plugins/modules/parcel/test_parcel.py @@ -34,7 +34,7 @@ def test_pytest_download_parcel(module_args): "username": os.getenv('CM_USERNAME'), "password": os.getenv('CM_PASSWORD'), "host": os.getenv('CM_HOST'), - "cluster_name": "Base_Edge2AI_Node", + "cluster_name": "Example_cluster", "product": "ECS", "parcel_version": "1.5.1-b626-ecs-1.5.1-b626.p0.42068229", "state": "downloaded" @@ -53,7 +53,7 @@ def test_pytest_distribute_parcel(module_args): "username": os.getenv('CM_USERNAME'), "password": os.getenv('CM_PASSWORD'), "host": os.getenv('CM_HOST'), - "cluster_name": "Base_Edge2AI_Node", + "cluster_name": "Example_cluster", "product": "ECS", "parcel_version": "1.5.1-b626-ecs-1.5.1-b626.p0.42068229", "state": "distributed" @@ -72,7 +72,7 @@ def test_pytest_activate_parcel(module_args): "username": os.getenv('CM_USERNAME'), "password": os.getenv('CM_PASSWORD'), "host": os.getenv('CM_HOST'), - "cluster_name": "Base_Edge2AI_Node", + "cluster_name": "Example_cluster", "product": "ECS", "parcel_version": "1.5.1-b626-ecs-1.5.1-b626.p0.42068229", "state": "activated" @@ -91,7 +91,7 @@ def test_pytest_remove_parcel(module_args): "username": os.getenv('CM_USERNAME'), "password": os.getenv('CM_PASSWORD'), "host": os.getenv('CM_HOST'), - "cluster_name": "Base_Edge2AI_Node", + "cluster_name": "Example_cluster", "product": "ECS", "parcel_version": "1.5.1-b626-ecs-1.5.1-b626.p0.42068229", "state": "removed" @@ -110,7 +110,7 @@ def test_pytest_undistribute_parcel(module_args): "username": os.getenv('CM_USERNAME'), "password": os.getenv('CM_PASSWORD'), "host": os.getenv('CM_HOST'), - "cluster_name": "Base_Edge2AI_Node", + "cluster_name": "Example_cluster", "product": "ECS", "parcel_version": "1.5.1-b626-ecs-1.5.1-b626.p0.42068229", "state": "undistributed" @@ -130,7 +130,7 @@ def test_pytest_deactivate_parcel(module_args): "username": os.getenv('CM_USERNAME'), "password": os.getenv('CM_PASSWORD'), "host": os.getenv('CM_HOST'), - "cluster_name": "Base_Edge2AI_Node", + "cluster_name": "Example_cluster", "product": "ECS", "parcel_version": "1.5.1-b626-ecs-1.5.1-b626.p0.42068229", "state": "deactivated"