From 32f9d78fa38255f4007b2aa8b3c3a9597a6803ca Mon Sep 17 00:00:00 2001 From: Alexei Znamensky <103110+russoz@users.noreply.github.com> Date: Wed, 12 Oct 2022 21:27:21 +1300 Subject: [PATCH] manageiq_policies_info: new module (#5321) * manageiq_provider_info: new module * fix reference to manageiq.module * add missing alias in suboption * fix filename in botmeta * Update plugins/modules/remote_management/manageiq/manageiq_policies_info.py Co-authored-by: Felix Fontein * Update plugins/modules/remote_management/manageiq/manageiq_policies_info.py Co-authored-by: Felix Fontein * fix description of parameters * Update plugins/modules/remote_management/manageiq/manageiq_policies_info.py Co-authored-by: Felix Fontein * Update plugins/modules/remote_management/manageiq/manageiq_policies_info.py Co-authored-by: Felix Fontein * remove change applied on the wrong branch * fix the module name in metadata files * Update plugins/modules/remote_management/manageiq/manageiq_policies_info.py Co-authored-by: Felix Fontein * adjust RETURN documentation * adjust RETURN documentation indentation * Update plugins/modules/remote_management/manageiq/manageiq_policies_info.py Co-authored-by: Felix Fontein * Update plugins/modules/remote_management/manageiq/manageiq_policies_info.py Co-authored-by: Felix Fontein * Update plugins/modules/remote_management/manageiq/manageiq_policies_info.py Co-authored-by: Felix Fontein Co-authored-by: Felix Fontein --- .github/BOTMETA.yml | 2 + meta/runtime.yml | 2 + plugins/module_utils/manageiq.py | 176 ++++++++++++++++ .../manageiq/manageiq_policies.py | 188 +----------------- .../manageiq/manageiq_policies_info.py | 119 +++++++++++ 5 files changed, 307 insertions(+), 180 deletions(-) create mode 100644 plugins/modules/remote_management/manageiq/manageiq_policies_info.py diff --git a/.github/BOTMETA.yml b/.github/BOTMETA.yml index 408ec38e03..45b86fdcbe 100644 --- a/.github/BOTMETA.yml +++ b/.github/BOTMETA.yml @@ -980,6 +980,8 @@ files: maintainers: elad661 $modules/remote_management/manageiq/manageiq_group.py: maintainers: evertmulder + $modules/remote_management/manageiq/manageiq_policies_info.py: + maintainers: russoz $team_manageiq $modules/remote_management/manageiq/manageiq_tenant.py: maintainers: evertmulder $modules/remote_management/oneview/: diff --git a/meta/runtime.yml b/meta/runtime.yml index eb6d226fec..85c6a5454a 100644 --- a/meta/runtime.yml +++ b/meta/runtime.yml @@ -710,6 +710,8 @@ plugin_routing: redirect: community.general.remote_management.manageiq.manageiq_group manageiq_policies: redirect: community.general.remote_management.manageiq.manageiq_policies + manageiq_policies_info: + redirect: community.general.remote_management.manageiq.manageiq_policies_info manageiq_provider: redirect: community.general.remote_management.manageiq.manageiq_provider manageiq_tags: diff --git a/plugins/module_utils/manageiq.py b/plugins/module_utils/manageiq.py index fd2652e3b4..83d44c1110 100644 --- a/plugins/module_utils/manageiq.py +++ b/plugins/module_utils/manageiq.py @@ -156,3 +156,179 @@ class ManageIQ(object): msg = "{collection_name} where {params} does not exist in manageiq".format( collection_name=collection_name, params=str(params)) self.module.fail_json(msg=msg) + + def policies(self, resource_id, resource_type, resource_name): + manageiq = ManageIQ(self.module) + + # query resource id, fail if resource does not exist + if resource_id is None: + resource_id = manageiq.find_collection_resource_or_fail(resource_type, name=resource_name)['id'] + + return ManageIQPolicies(manageiq, resource_type, resource_id) + + +class ManageIQPolicies(object): + """ + Object to execute policies management operations of manageiq resources. + """ + + def __init__(self, manageiq, resource_type, resource_id): + self.manageiq = manageiq + + self.module = self.manageiq.module + self.api_url = self.manageiq.api_url + self.client = self.manageiq.client + + self.resource_type = resource_type + self.resource_id = resource_id + self.resource_url = '{api_url}/{resource_type}/{resource_id}'.format( + api_url=self.api_url, + resource_type=resource_type, + resource_id=resource_id) + + def query_profile_href(self, profile): + """ Add or Update the policy_profile href field + + Example: + {name: STR, ...} => {name: STR, href: STR} + """ + resource = self.manageiq.find_collection_resource_or_fail( + "policy_profiles", **profile) + return dict(name=profile['name'], href=resource['href']) + + def query_resource_profiles(self): + """ Returns a set of the profile objects objects assigned to the resource + """ + url = '{resource_url}/policy_profiles?expand=resources' + try: + response = self.client.get(url.format(resource_url=self.resource_url)) + except Exception as e: + msg = "Failed to query {resource_type} policies: {error}".format( + resource_type=self.resource_type, + error=e) + self.module.fail_json(msg=msg) + + resources = response.get('resources', []) + + # clean the returned rest api profile object to look like: + # {profile_name: STR, profile_description: STR, policies: ARR} + profiles = [self.clean_profile_object(profile) for profile in resources] + + return profiles + + def query_profile_policies(self, profile_id): + """ Returns a set of the policy objects assigned to the resource + """ + url = '{api_url}/policy_profiles/{profile_id}?expand=policies' + try: + response = self.client.get(url.format(api_url=self.api_url, profile_id=profile_id)) + except Exception as e: + msg = "Failed to query {resource_type} policies: {error}".format( + resource_type=self.resource_type, + error=e) + self.module.fail_json(msg=msg) + + resources = response.get('policies', []) + + # clean the returned rest api policy object to look like: + # {name: STR, description: STR, active: BOOL} + policies = [self.clean_policy_object(policy) for policy in resources] + + return policies + + def clean_policy_object(self, policy): + """ Clean a policy object to have human readable form of: + { + name: STR, + description: STR, + active: BOOL + } + """ + name = policy.get('name') + description = policy.get('description') + active = policy.get('active') + + return dict( + name=name, + description=description, + active=active) + + def clean_profile_object(self, profile): + """ Clean a profile object to have human readable form of: + { + profile_name: STR, + profile_description: STR, + policies: ARR + } + """ + profile_id = profile['id'] + name = profile.get('name') + description = profile.get('description') + policies = self.query_profile_policies(profile_id) + + return dict( + profile_name=name, + profile_description=description, + policies=policies) + + def profiles_to_update(self, profiles, action): + """ Create a list of policies we need to update in ManageIQ. + + Returns: + Whether or not a change took place and a message describing the + operation executed. + """ + profiles_to_post = [] + assigned_profiles = self.query_resource_profiles() + + # make a list of assigned full profile names strings + # e.g. ['openscap profile', ...] + assigned_profiles_set = set([profile['profile_name'] for profile in assigned_profiles]) + + for profile in profiles: + assigned = profile.get('name') in assigned_profiles_set + + if (action == 'unassign' and assigned) or (action == 'assign' and not assigned): + # add/update the policy profile href field + # {name: STR, ...} => {name: STR, href: STR} + profile = self.query_profile_href(profile) + profiles_to_post.append(profile) + + return profiles_to_post + + def assign_or_unassign_profiles(self, profiles, action): + """ Perform assign/unassign action + """ + # get a list of profiles needed to be changed + profiles_to_post = self.profiles_to_update(profiles, action) + if not profiles_to_post: + return dict( + changed=False, + msg="Profiles {profiles} already {action}ed, nothing to do".format( + action=action, + profiles=profiles)) + + # try to assign or unassign profiles to resource + url = '{resource_url}/policy_profiles'.format(resource_url=self.resource_url) + try: + response = self.client.post(url, action=action, resources=profiles_to_post) + except Exception as e: + msg = "Failed to {action} profile: {error}".format( + action=action, + error=e) + self.module.fail_json(msg=msg) + + # check all entities in result to be successful + for result in response['results']: + if not result['success']: + msg = "Failed to {action}: {message}".format( + action=action, + message=result['message']) + self.module.fail_json(msg=msg) + + # successfully changed all needed profiles + return dict( + changed=True, + msg="Successfully {action}ed profiles: {profiles}".format( + action=action, + profiles=profiles)) diff --git a/plugins/modules/remote_management/manageiq/manageiq_policies.py b/plugins/modules/remote_management/manageiq/manageiq_policies.py index 3d54a7b5aa..ae36094768 100644 --- a/plugins/modules/remote_management/manageiq/manageiq_policies.py +++ b/plugins/modules/remote_management/manageiq/manageiq_policies.py @@ -25,17 +25,17 @@ options: state: type: str description: - - absent - policy_profiles should not exist, - - present - policy_profiles should exist, - - list - list current policy_profiles and policies. + - C(absent) - policy_profiles should not exist, + - C(present) - policy_profiles should exist, + - C(list) - list current policy_profiles and policies. choices: ['absent', 'present', 'list'] default: 'present' policy_profiles: type: list elements: dict description: - - list of dictionaries, each includes the policy_profile 'name' key. - - required if state is present or absent. + - List of dictionaries, each includes the policy_profile C(name) key. + - Required if I(state) is C(present) or C(absent). resource_type: type: str description: @@ -133,179 +133,12 @@ from ansible.module_utils.basic import AnsibleModule from ansible_collections.community.general.plugins.module_utils.manageiq import ManageIQ, manageiq_argument_spec, manageiq_entities -class ManageIQPolicies(object): - """ - Object to execute policies management operations of manageiq resources. - """ - - def __init__(self, manageiq, resource_type, resource_id): - self.manageiq = manageiq - - self.module = self.manageiq.module - self.api_url = self.manageiq.api_url - self.client = self.manageiq.client - - self.resource_type = resource_type - self.resource_id = resource_id - self.resource_url = '{api_url}/{resource_type}/{resource_id}'.format( - api_url=self.api_url, - resource_type=resource_type, - resource_id=resource_id) - - def query_profile_href(self, profile): - """ Add or Update the policy_profile href field - - Example: - {name: STR, ...} => {name: STR, href: STR} - """ - resource = self.manageiq.find_collection_resource_or_fail( - "policy_profiles", **profile) - return dict(name=profile['name'], href=resource['href']) - - def query_resource_profiles(self): - """ Returns a set of the profile objects objects assigned to the resource - """ - url = '{resource_url}/policy_profiles?expand=resources' - try: - response = self.client.get(url.format(resource_url=self.resource_url)) - except Exception as e: - msg = "Failed to query {resource_type} policies: {error}".format( - resource_type=self.resource_type, - error=e) - self.module.fail_json(msg=msg) - - resources = response.get('resources', []) - - # clean the returned rest api profile object to look like: - # {profile_name: STR, profile_description: STR, policies: ARR} - profiles = [self.clean_profile_object(profile) for profile in resources] - - return profiles - - def query_profile_policies(self, profile_id): - """ Returns a set of the policy objects assigned to the resource - """ - url = '{api_url}/policy_profiles/{profile_id}?expand=policies' - try: - response = self.client.get(url.format(api_url=self.api_url, profile_id=profile_id)) - except Exception as e: - msg = "Failed to query {resource_type} policies: {error}".format( - resource_type=self.resource_type, - error=e) - self.module.fail_json(msg=msg) - - resources = response.get('policies', []) - - # clean the returned rest api policy object to look like: - # {name: STR, description: STR, active: BOOL} - policies = [self.clean_policy_object(policy) for policy in resources] - - return policies - - def clean_policy_object(self, policy): - """ Clean a policy object to have human readable form of: - { - name: STR, - description: STR, - active: BOOL - } - """ - name = policy.get('name') - description = policy.get('description') - active = policy.get('active') - - return dict( - name=name, - description=description, - active=active) - - def clean_profile_object(self, profile): - """ Clean a profile object to have human readable form of: - { - profile_name: STR, - profile_description: STR, - policies: ARR - } - """ - profile_id = profile['id'] - name = profile.get('name') - description = profile.get('description') - policies = self.query_profile_policies(profile_id) - - return dict( - profile_name=name, - profile_description=description, - policies=policies) - - def profiles_to_update(self, profiles, action): - """ Create a list of policies we need to update in ManageIQ. - - Returns: - Whether or not a change took place and a message describing the - operation executed. - """ - profiles_to_post = [] - assigned_profiles = self.query_resource_profiles() - - # make a list of assigned full profile names strings - # e.g. ['openscap profile', ...] - assigned_profiles_set = set([profile['profile_name'] for profile in assigned_profiles]) - - for profile in profiles: - assigned = profile.get('name') in assigned_profiles_set - - if (action == 'unassign' and assigned) or (action == 'assign' and not assigned): - # add/update the policy profile href field - # {name: STR, ...} => {name: STR, href: STR} - profile = self.query_profile_href(profile) - profiles_to_post.append(profile) - - return profiles_to_post - - def assign_or_unassign_profiles(self, profiles, action): - """ Perform assign/unassign action - """ - # get a list of profiles needed to be changed - profiles_to_post = self.profiles_to_update(profiles, action) - if not profiles_to_post: - return dict( - changed=False, - msg="Profiles {profiles} already {action}ed, nothing to do".format( - action=action, - profiles=profiles)) - - # try to assign or unassign profiles to resource - url = '{resource_url}/policy_profiles'.format(resource_url=self.resource_url) - try: - response = self.client.post(url, action=action, resources=profiles_to_post) - except Exception as e: - msg = "Failed to {action} profile: {error}".format( - action=action, - error=e) - self.module.fail_json(msg=msg) - - # check all entities in result to be successful - for result in response['results']: - if not result['success']: - msg = "Failed to {action}: {message}".format( - action=action, - message=result['message']) - self.module.fail_json(msg=msg) - - # successfully changed all needed profiles - return dict( - changed=True, - msg="Successfully {action}ed profiles: {profiles}".format( - action=action, - profiles=profiles)) - - def main(): actions = {'present': 'assign', 'absent': 'unassign', 'list': 'list'} argument_spec = dict( policy_profiles=dict(type='list', elements='dict'), - resource_id=dict(required=False, type='int'), - resource_name=dict(required=False, type='str'), + resource_id=dict(type='int'), + resource_name=dict(type='str'), resource_type=dict(required=True, type='str', choices=list(manageiq_entities().keys())), state=dict(required=False, type='str', @@ -335,12 +168,7 @@ def main(): resource_type = manageiq_entities()[resource_type_key] manageiq = ManageIQ(module) - - # query resource id, fail if resource does not exist - if resource_id is None: - resource_id = manageiq.find_collection_resource_or_fail(resource_type, name=resource_name)['id'] - - manageiq_policies = ManageIQPolicies(manageiq, resource_type, resource_id) + manageiq_policies = manageiq.policies(resource_id, resource_type, resource_name) if action == 'list': # return a list of current profiles for this object diff --git a/plugins/modules/remote_management/manageiq/manageiq_policies_info.py b/plugins/modules/remote_management/manageiq/manageiq_policies_info.py new file mode 100644 index 0000000000..cdf4cafdaf --- /dev/null +++ b/plugins/modules/remote_management/manageiq/manageiq_policies_info.py @@ -0,0 +1,119 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# Copyright (c) 2022, Alexei Znamensky +# Copyright (c) 2017, Daniel Korn +# Copyright (c) 2017, Yaacov Zamir +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + + +DOCUMENTATION = ''' + +module: manageiq_policies_info +version_added: 5.8.0 + +short_description: Listing of resource policy_profiles in ManageIQ +extends_documentation_fragment: + - community.general.manageiq + +author: Alexei Znamensky (@russoz) +description: + - The manageiq_policies module supports listing policy_profiles in ManageIQ. + +options: + resource_type: + type: str + description: + - The type of the resource to obtain the profile for. + required: true + choices: ['provider', 'host', 'vm', 'blueprint', 'category', 'cluster', + 'data store', 'group', 'resource pool', 'service', 'service template', + 'template', 'tenant', 'user'] + resource_name: + type: str + description: + - The name of the resource to obtain the profile for. + - Must be specified if I(resource_id) is not set. Both options are mutually exclusive. + resource_id: + type: int + description: + - The ID of the resource to obtain the profile for. + - Must be specified if I(resource_name) is not set. Both options are mutually exclusive. +''' + +EXAMPLES = ''' +- name: List current policy_profile and policies for a provider in ManageIQ + community.general.manageiq_policies_info: + resource_name: 'EngLab' + resource_type: 'provider' + manageiq_connection: + url: 'http://127.0.0.1:3000' + username: 'admin' + password: 'smartvm' + register: result +''' + +RETURN = ''' +profiles: + description: + - List current policy_profile and policies for a provider in ManageIQ. + returned: always + type: list + elements: dict + sample: + - policies: + - active: true + description: OpenSCAP + name: openscap policy + - active: true, + description: Analyse incoming container images + name: analyse incoming container images + - active: true + description: Schedule compliance after smart state analysis + name: schedule compliance after smart state analysis + profile_description: OpenSCAP profile + profile_name: openscap profile +''' + +from ansible.module_utils.basic import AnsibleModule +from ansible_collections.community.general.plugins.module_utils.manageiq import ManageIQ, ManageIQPolicies, manageiq_argument_spec, manageiq_entities + + +def main(): + argument_spec = dict( + resource_id=dict(required=False, type='int'), + resource_name=dict(required=False, type='str'), + resource_type=dict(required=True, type='str', + choices=list(manageiq_entities().keys())), + ) + # add the manageiq connection arguments to the arguments + argument_spec.update(manageiq_argument_spec()) + + module = AnsibleModule( + argument_spec=argument_spec, + mutually_exclusive=[["resource_id", "resource_name"]], + required_one_of=[["resource_id", "resource_name"]], + supports_check_mode=True, + ) + + resource_id = module.params['resource_id'] + resource_type_key = module.params['resource_type'] + resource_name = module.params['resource_name'] + + # get the resource type + resource_type = manageiq_entities()[resource_type_key] + + manageiq_policies = ManageIQ(module).policies(resource_id, resource_type, resource_name) + + # return a list of current profiles for this object + current_profiles = manageiq_policies.query_resource_profiles() + res_args = dict(changed=False, profiles=current_profiles) + + module.exit_json(**res_args) + + +if __name__ == "__main__": + main()