Skip to content

Commit

Permalink
Add Cloudera Manager config modules (#211)
Browse files Browse the repository at this point in the history
* Add get_cm_config function
* Add cm_config_info module
* Add cm_config module
* Refactor tests for cm_endpoint_info
* Add basic integration tests for cm_config_info
* Update pytest.ini for AnsibleCollectionFinder warning
* Fix alias for version parameter
* Strip trailing spaces and path separators from provided endpoint parameter
* Add ClouderaManagerMutableModule class to add 'message' parameter
* Update cm_config to handle setting and unsetting parameters, including diff mode
* Add initial tests for cm_config

Signed-off-by: Webster Mudge <wmudge@cloudera.com>
  • Loading branch information
wmudge authored Mar 21, 2024
1 parent 67148e3 commit 9064af6
Show file tree
Hide file tree
Showing 8 changed files with 599 additions and 41 deletions.
4 changes: 3 additions & 1 deletion plugins/doc_fragments/cm_options.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ class ModuleDocFragment(object):
required: False
default: True
aliases:
- tls
- api_version
force_tls:
description:
- Flag to force TLS during CM API endpoint discovery.
Expand All @@ -69,6 +69,8 @@ class ModuleDocFragment(object):
- Username for access to the CM API endpoint.
type: str
required: True
aliases:
- user
password:
description:
- Password for access to the CM API endpoint.
Expand Down
34 changes: 31 additions & 3 deletions plugins/module_utils/cm_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@
from ansible.module_utils.basic import AnsibleModule
from ansible.module_utils.common.text.converters import to_text
from time import sleep
from cm_client import ApiClient, Configuration
from cm_client import ApiClient, ApiConfigList, Configuration
from cm_client.rest import ApiException, RESTClientObject
from cm_client.apis.cloudera_manager_resource_api import ClouderaManagerResourceApi
from cm_client.apis.commands_resource_api import CommandsResourceApi
Expand Down Expand Up @@ -246,7 +246,7 @@ def initialize_client(self):

# If provided a CML endpoint URL, use it directly
if self.url:
config.host = self.url
config.host = str(self.url).rstrip(" /")
# Otherwise, run discovery on missing parts
else:
config.host = self.discover_endpoint(config)
Expand Down Expand Up @@ -340,6 +340,9 @@ def call_api(self, path, method, query=None, field="items", body=None):
data = data[field]
return data if type(data) is list else [data]

def get_cm_config(self, scope: str = "summary") -> ApiConfigList:
return ClouderaManagerResourceApi(self.api_client).get_config(view=scope).items

@staticmethod
def ansible_module_internal(argument_spec={}, required_together=[], **kwargs):
"""
Expand All @@ -358,7 +361,7 @@ def ansible_module_internal(argument_spec={}, required_together=[], **kwargs):
required=False, type="bool", default=True, aliases=["tls"]
),
ssl_ca_cert=dict(type="path", aliases=["tls_cert", "ssl_cert"]),
username=dict(required=True, type="str"),
username=dict(required=True, type="str", aliases=["user"]),
password=dict(required=True, type="str", no_log=True),
debug=dict(
required=False,
Expand Down Expand Up @@ -396,3 +399,28 @@ def ansible_module(
required_together=required_together,
**kwargs,
)


class ClouderaManagerMutableModule(ClouderaManagerModule):
def __init__(self, module):
super(ClouderaManagerMutableModule, self).__init__(module)
self.message = self.get_param("message")

@staticmethod
def ansible_module(
argument_spec={},
mutually_exclusive=[],
required_one_of=[],
required_together=[],
**kwargs,
):
return ClouderaManagerModule.ansible_module(
dict(
**argument_spec,
message=dict(default="Managed by Ansible", aliases=["msg"])
),
mutually_exclusive,
required_one_of,
required_together,
**kwargs
)
231 changes: 231 additions & 0 deletions plugins/modules/cm_config.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,231 @@
# Copyright 2023 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.

import cm_client

from ansible.module_utils.common.dict_transformations import recursive_diff

from ansible_collections.cloudera.cluster.plugins.module_utils.cm_utils import (
ClouderaManagerMutableModule,
)

ANSIBLE_METADATA = {
"metadata_version": "1.1",
"status": ["preview"],
"supported_by": "community",
}

DOCUMENTATION = r"""
---
module: cm_config
short_description: Manage the configuration of Cloudera Manager
description:
- Manage Cloudera Manager configuration settings.
author:
- "Webster Mudge (@wmudge)"
requirements:
- cm_client
options:
parameters:
description:
- The Cloudera Manager configuration to set.
- To unset a parameter, use C(None) as the value.
type: dict
required: yes
aliases:
- params
extends_documentation_fragment:
- cloudera.cluster.cm_options
- cloudera.cluster.cm_endpoint
attributes:
check_mode:
support: full
diff_mode:
support: full
"""

EXAMPLES = r"""
---
- name: Update several Cloudera Manager parameters
cloudera.cluster.cm_config:
host: example.cloudera.com
username: "jane_smith"
password: "S&peR4Ec*re"
parameters:
frontend_url: "schema://host:port"
custom_header_color: "PURPLE"
- name: Reset or remove a Cloudera Manager parameter
cloudera.cluster.cm_config:
host: example.cloudera.com
username: "jane_smith"
password: "S&peR4Ec*re"
parameters:
custom_header_color: None
"""

RETURN = r"""
---
config:
description:
- List of Cloudera Manager configurations.
- Returns the C(summary) view of the resulting configuration.
type: list
elements: dict
returned: always
contains:
name:
description:
- The canonical name that identifies this configuration parameter.
type: str
returned: when supported
value:
description:
- The user-defined value.
- When absent, the default value (if any) will be used.
- Can also be absent, when enumerating allowed configs.
type: str
returned: when supported
required:
description:
- Whether this configuration is required for the object.
- If any required configuration is not set, operations on the object may not work.
- Requires I(full) view.
type: bool
returned: when supported
default:
description:
- The default value.
- Requires I(full) view.
type: str
returned: when supported
display_name:
description:
- A user-friendly name of the parameters, as would have been shown in the web UI.
- Requires I(full) view.
type: str
returned: when supported
description:
description:
- A textual description of the parameter.
- Requires I(full) view.
type: str
returned: when supported
related_name:
description:
- If applicable, contains the related configuration variable used by the source project.
- Requires I(full) view.
type: str
returned: when supported
sensitive:
description:
- Whether this configuration is sensitive, i.e. contains information such as passwords, which might affect how the value of this configuration might be shared by the caller.
type: bool
returned: when supported
validate_state:
description:
- State of the configuration parameter after validation.
- Requires I(full) view.
type: str
returned: when supported
validation_message:
description:
- A message explaining the parameter's validation state.
- Requires I(full) view.
type: str
returned: when supported
validation_warnings_suppressed:
description:
- Whether validation warnings associated with this parameter are suppressed.
- In general, suppressed validation warnings are hidden in the Cloudera Manager UI.
- Configurations that do not produce warnings will not contain this field.
- Requires I(full) view.
type: bool
returned: when supported
"""


class ClouderaManagerConfig(ClouderaManagerMutableModule):
def __init__(self, module):
super(ClouderaManagerConfig, self).__init__(module)

# Set the parameters
self.params = self.get_param("parameters")

# Initialize the return value
self.changed = False
self.diff = {}
self.config = []

# Execute the logic
self.process()

@ClouderaManagerMutableModule.handle_process
def process(self):
existing = self.get_cm_config("full")

current = {r.name: r.value for r in existing}
incoming = {k.upper(): v for k, v in self.params.items()}

(_, add) = recursive_diff(current, incoming)

if add:
self.changed = True

if self.module._diff:
self.diff = dict(before={k: current[k] for k in add.keys()}, after=add)

if not self.module.check_mode:
body = cm_client.ApiConfigList(
items=[cm_client.ApiConfig(name=k, value=v) for k, v in add.items()]
)
# Return 'summary'
self.config = [
p.to_dict()
for p in cm_client.ClouderaManagerResourceApi(self.api_client)
.update_config(message=self.message, body=body)
.items
]
else:
# Return 'summary'
self.config = [p.to_dict() for p in self.get_cm_config()]


def main():
module = ClouderaManagerMutableModule.ansible_module(
argument_spec=dict(
parameters=dict(type=dict, required=True, aliases=["params"]),
),
supports_check_mode=True,
)

result = ClouderaManagerConfig(module)

output = dict(
changed=result.changed,
config=result.config,
)

if module._diff:
output.update(diff=result.diff)

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()
Loading

0 comments on commit 9064af6

Please sign in to comment.