#!/usr/bin/python # Copyright (c) 2018, Evert Mulder (base on manageiq_user.py by Daniel Korn ) # 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 annotations DOCUMENTATION = r""" module: manageiq_tenant short_description: Management of tenants in ManageIQ extends_documentation_fragment: - community.general.manageiq - community.general.attributes author: Evert Mulder (@evertmulder) description: - The manageiq_tenant module supports adding, updating and deleting tenants in ManageIQ. requirements: - manageiq-client attributes: check_mode: support: none diff_mode: support: none options: state: type: str description: - V(absent) - tenant should not exist, - V(present) - tenant should be. choices: ['absent', 'present'] default: 'present' name: type: str description: - The tenant name. required: true default: description: type: str description: - The tenant description. required: true default: parent_id: type: int description: - The ID of the parent tenant. If not supplied the root tenant is used. - The O(parent_id) takes president over O(parent) when supplied. required: false default: parent: type: str description: - The name of the parent tenant. If not supplied and no O(parent_id) is supplied the root tenant is used. required: false default: quotas: type: dict description: - The tenant quotas. - All parameters case sensitive. - 'Valid attributes are:' - '- V(cpu_allocated) (int): use null to remove the quota.' - '- V(mem_allocated) (GB): use null to remove the quota.' - '- V(storage_allocated) (GB): use null to remove the quota.' - '- V(vms_allocated) (int): use null to remove the quota.' - '- V(templates_allocated) (int): use null to remove the quota.' required: false default: {} """ EXAMPLES = r""" - name: Update the root tenant in ManageIQ community.general.manageiq_tenant: name: 'My Company' description: 'My company name' manageiq_connection: url: 'http://127.0.0.1:3000' username: 'admin' password: 'smartvm' validate_certs: false # only do this when you trust the network! - name: Create a tenant in ManageIQ community.general.manageiq_tenant: name: 'Dep1' description: 'Manufacturing department' parent_id: 1 manageiq_connection: url: 'http://127.0.0.1:3000' username: 'admin' password: 'smartvm' validate_certs: false # only do this when you trust the network! - name: Delete a tenant in ManageIQ community.general.manageiq_tenant: state: 'absent' name: 'Dep1' parent_id: 1 manageiq_connection: url: 'http://127.0.0.1:3000' username: 'admin' password: 'smartvm' validate_certs: false # only do this when you trust the network! - name: Set tenant quota for cpu_allocated, mem_allocated, remove quota for vms_allocated community.general.manageiq_tenant: name: 'Dep1' parent_id: 1 quotas: - cpu_allocated: 100 - mem_allocated: 50 - vms_allocated: manageiq_connection: url: 'http://127.0.0.1:3000' username: 'admin' password: 'smartvm' validate_certs: false # only do this when you trust the network! - name: Delete a tenant in ManageIQ using a token community.general.manageiq_tenant: state: 'absent' name: 'Dep1' parent_id: 1 manageiq_connection: url: 'http://127.0.0.1:3000' token: 'sometoken' validate_certs: false # only do this when you trust the network! """ RETURN = r""" tenant: description: The tenant. returned: success type: complex contains: id: description: The tenant ID. returned: success type: int name: description: The tenant name. returned: success type: str description: description: The tenant description. returned: success type: str parent_id: description: The ID of the parent tenant. returned: success type: int quotas: description: List of tenant quotas. returned: success type: list sample: cpu_allocated: 100 mem_allocated: 50 """ from ansible.module_utils.basic import AnsibleModule from ansible_collections.community.general.plugins.module_utils.manageiq import ManageIQ, manageiq_argument_spec class ManageIQTenant: """ Object to execute tenant management operations in manageiq. """ def __init__(self, manageiq): self.manageiq = manageiq self.module = self.manageiq.module self.api_url = self.manageiq.api_url self.client = self.manageiq.client def tenant(self, name, parent_id, parent): """Search for tenant object by name and parent_id or parent or the root tenant if no parent or parent_id is supplied. Returns: the parent tenant, None for the root tenant the tenant or None if tenant was not found. """ if parent_id: parent_tenant_res = self.client.collections.tenants.find_by(id=parent_id) if not parent_tenant_res: self.module.fail_json(msg=f"Parent tenant with id '{parent_id}' not found in manageiq") parent_tenant = parent_tenant_res[0] tenants = self.client.collections.tenants.find_by(name=name) for tenant in tenants: try: ancestry = tenant["ancestry"] except AttributeError: ancestry = None if ancestry: tenant_parent_id = int(ancestry.split("/")[-1]) if int(tenant_parent_id) == parent_id: return parent_tenant, tenant return parent_tenant, None else: if parent: parent_tenant_res = self.client.collections.tenants.find_by(name=parent) if not parent_tenant_res: self.module.fail_json(msg=f"Parent tenant '{parent}' not found in manageiq") if len(parent_tenant_res) > 1: self.module.fail_json(msg=f"Multiple parent tenants not found in manageiq with name '{parent}'") parent_tenant = parent_tenant_res[0] parent_id = int(parent_tenant["id"]) tenants = self.client.collections.tenants.find_by(name=name) for tenant in tenants: try: ancestry = tenant["ancestry"] except AttributeError: ancestry = None if ancestry: tenant_parent_id = int(ancestry.split("/")[-1]) if tenant_parent_id == parent_id: return parent_tenant, tenant return parent_tenant, None else: # No parent or parent id supplied we select the root tenant return None, self.client.collections.tenants.find_by(ancestry=None)[0] def compare_tenant(self, tenant, name, description): """Compare tenant fields with new field values. Returns: false if tenant fields have some difference from new fields, true o/w. """ found_difference = (name and tenant["name"] != name) or (description and tenant["description"] != description) return not found_difference def delete_tenant(self, tenant): """Deletes a tenant from manageiq. Returns: dict with `msg` and `changed` """ try: url = f"{self.api_url}/tenants/{tenant['id']}" result = self.client.post(url, action="delete") except Exception as e: self.module.fail_json(msg=f"failed to delete tenant {tenant['name']}: {e}") if result["success"] is False: self.module.fail_json(msg=result["message"]) return dict(changed=True, msg=result["message"]) def edit_tenant(self, tenant, name, description): """Edit a manageiq tenant. Returns: dict with `msg` and `changed` """ resource = dict(name=name, description=description, use_config_for_attributes=False) # check if we need to update ( compare_tenant is true is no difference found ) if self.compare_tenant(tenant, name, description): return dict(changed=False, msg=f"tenant {tenant['name']} is not changed.", tenant=tenant["_data"]) # try to update tenant try: self.client.post(tenant["href"], action="edit", resource=resource) except Exception as e: self.module.fail_json(msg=f"failed to update tenant {tenant['name']}: {e}") return dict(changed=True, msg=f"successfully updated the tenant with id {tenant['id']}") def create_tenant(self, name, description, parent_tenant): """Creates the tenant in manageiq. Returns: dict with `msg`, `changed` and `tenant_id` """ parent_id = parent_tenant["id"] # check for required arguments for key, value in dict(name=name, description=description, parent_id=parent_id).items(): if value in (None, ""): self.module.fail_json(msg=f"missing required argument: {key}") url = f"{self.api_url}/tenants" resource = {"name": name, "description": description, "parent": {"id": parent_id}} try: result = self.client.post(url, action="create", resource=resource) tenant_id = result["results"][0]["id"] except Exception as e: self.module.fail_json(msg=f"failed to create tenant {name}: {e}") return dict( changed=True, msg=f"successfully created tenant '{name}' with id '{tenant_id}'", tenant_id=tenant_id ) def tenant_quota(self, tenant, quota_key): """Search for tenant quota object by tenant and quota_key. Returns: the quota for the tenant, or None if the tenant quota was not found. """ tenant_quotas = self.client.get(f"{tenant['href']}/quotas?expand=resources&filter[]=name={quota_key}") return tenant_quotas["resources"] def tenant_quotas(self, tenant): """Search for tenant quotas object by tenant. Returns: the quotas for the tenant, or None if no tenant quotas were not found. """ tenant_quotas = self.client.get(f"{tenant['href']}/quotas?expand=resources") return tenant_quotas["resources"] def update_tenant_quotas(self, tenant, quotas): """Creates the tenant quotas in manageiq. Returns: dict with `msg` and `changed` """ changed = False messages = [] for quota_key, quota_value in quotas.items(): current_quota_filtered = self.tenant_quota(tenant, quota_key) if current_quota_filtered: current_quota = current_quota_filtered[0] else: current_quota = None if quota_value: # Change the byte values to GB if quota_key in ["storage_allocated", "mem_allocated"]: quota_value_int = int(quota_value) * 1024 * 1024 * 1024 else: quota_value_int = int(quota_value) if current_quota: res = self.edit_tenant_quota(tenant, current_quota, quota_key, quota_value_int) else: res = self.create_tenant_quota(tenant, quota_key, quota_value_int) else: if current_quota: res = self.delete_tenant_quota(tenant, current_quota) else: res = dict(changed=False, msg=f"tenant quota '{quota_key}' does not exist") if res["changed"]: changed = True messages.append(res["msg"]) return dict(changed=changed, msg=", ".join(messages)) def edit_tenant_quota(self, tenant, current_quota, quota_key, quota_value): """Update the tenant quotas in manageiq. Returns: result """ if current_quota["value"] == quota_value: return dict(changed=False, msg=f"tenant quota {quota_key} already has value {quota_value}") else: url = f"{tenant['href']}/quotas/{current_quota['id']}" resource = {"value": quota_value} try: self.client.post(url, action="edit", resource=resource) except Exception as e: self.module.fail_json(msg=f"failed to update tenant quota {quota_key}: {e}") return dict(changed=True, msg=f"successfully updated tenant quota {quota_key}") def create_tenant_quota(self, tenant, quota_key, quota_value): """Creates the tenant quotas in manageiq. Returns: result """ url = f"{tenant['href']}/quotas" resource = {"name": quota_key, "value": quota_value} try: self.client.post(url, action="create", resource=resource) except Exception as e: self.module.fail_json(msg=f"failed to create tenant quota {quota_key}: {e}") return dict(changed=True, msg=f"successfully created tenant quota {quota_key}") def delete_tenant_quota(self, tenant, quota): """deletes the tenant quotas in manageiq. Returns: result """ try: result = self.client.post(quota["href"], action="delete") except Exception as e: self.module.fail_json(msg=f"failed to delete tenant quota '{quota['name']}': {e}") return dict(changed=True, msg=result["message"]) def create_tenant_response(self, tenant, parent_tenant): """Creates the ansible result object from a manageiq tenant entity Returns: a dict with the tenant id, name, description, parent id, quota's """ tenant_quotas = self.create_tenant_quotas_response(tenant["tenant_quotas"]) try: ancestry = tenant["ancestry"] tenant_parent_id = ancestry.split("/")[-1] except AttributeError: # The root tenant does not return the ancestry attribute tenant_parent_id = None return dict( id=tenant["id"], name=tenant["name"], description=tenant["description"], parent_id=tenant_parent_id, quotas=tenant_quotas, ) @staticmethod def create_tenant_quotas_response(tenant_quotas): """Creates the ansible result object from a manageiq tenant_quotas entity Returns: a dict with the applied quotas, name and value """ if not tenant_quotas: return {} result = {} for quota in tenant_quotas: if quota["unit"] == "bytes": value = float(quota["value"]) / (1024 * 1024 * 1024) else: value = quota["value"] result[quota["name"]] = value return result def main(): argument_spec = dict( name=dict(required=True, type="str"), description=dict(required=True, type="str"), parent_id=dict(type="int"), parent=dict(type="str"), state=dict(choices=["absent", "present"], default="present"), quotas=dict(type="dict", default={}), ) # add the manageiq connection arguments to the arguments argument_spec.update(manageiq_argument_spec()) module = AnsibleModule(argument_spec=argument_spec) name = module.params["name"] description = module.params["description"] parent_id = module.params["parent_id"] parent = module.params["parent"] state = module.params["state"] quotas = module.params["quotas"] manageiq = ManageIQ(module) manageiq_tenant = ManageIQTenant(manageiq) parent_tenant, tenant = manageiq_tenant.tenant(name, parent_id, parent) # tenant should not exist if state == "absent": # if we have a tenant, delete it if tenant: res_args = manageiq_tenant.delete_tenant(tenant) # if we do not have a tenant, nothing to do else: if parent_id: msg = f"tenant '{name}' with parent_id {int(parent_id)} does not exist in manageiq" else: msg = f"tenant '{name}' with parent '{parent}' does not exist in manageiq" res_args = dict(changed=False, msg=msg) # tenant should exist if state == "present": # if we have a tenant, edit it if tenant: res_args = manageiq_tenant.edit_tenant(tenant, name, description) # if we do not have a tenant, create it else: res_args = manageiq_tenant.create_tenant(name, description, parent_tenant) tenant = manageiq.client.get_entity("tenants", res_args["tenant_id"]) # quotas as supplied and we have a tenant if quotas: tenant_quotas_res = manageiq_tenant.update_tenant_quotas(tenant, quotas) if tenant_quotas_res["changed"]: res_args["changed"] = True res_args["tenant_quotas_msg"] = tenant_quotas_res["msg"] tenant.reload(expand="resources", attributes=["tenant_quotas"]) res_args["tenant"] = manageiq_tenant.create_tenant_response(tenant, parent_tenant) module.exit_json(**res_args) if __name__ == "__main__": main()