#!/usr/bin/python # -*- coding: utf-8 -*- # 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 (absolute_import, division, print_function) __metaclass__ = type DOCUMENTATION = ''' module: manageiq_tenant short_description: Management of tenants in ManageIQ. extends_documentation_fragment: - community.general.manageiq author: Evert Mulder (@evertmulder) description: - The manageiq_tenant module supports adding, updating and deleting tenants in ManageIQ. requirements: - manageiq-client options: state: type: str description: - absent - tenant should not exist, present - tenant should be. choices: ['absent', 'present'] default: 'present' name: type: str description: - The tenant name. required: true default: null description: type: str description: - The tenant description. required: true default: null parent_id: type: int description: - The id of the parent tenant. If not supplied the root tenant is used. - The C(parent_id) takes president over C(parent) when supplied required: false default: null parent: type: str description: - The name of the parent tenant. If not supplied and no C(parent_id) is supplied the root tenant is used. required: false default: null quotas: type: dict description: - The tenant quotas. - All parameters case sensitive. - 'Valid attributes are:' - ' - C(cpu_allocated) (int): use null to remove the quota.' - ' - C(mem_allocated) (GB): use null to remove the quota.' - ' - C(storage_allocated) (GB): use null to remove the quota.' - ' - C(vms_allocated) (int): use null to remove the quota.' - ' - C(templates_allocated) (int): use null to remove the quota.' required: false default: null ''' EXAMPLES = ''' - 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 - 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 - 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 - 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: null manageiq_connection: url: 'http://127.0.0.1:3000' username: 'admin' password: 'smartvm' validate_certs: False - 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 ''' RETURN = ''' 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): """ 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="Parent tenant with id '%s' not found in manageiq" % str(parent_id)) 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="Parent tenant '%s' not found in manageiq" % parent) if len(parent_tenant_res) > 1: self.module.fail_json(msg="Multiple parent tenants not found in manageiq with name '%s" % 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 = '%s/tenants/%s' % (self.api_url, tenant['id']) result = self.client.post(url, action='delete') except Exception as e: self.module.fail_json(msg="failed to delete tenant %s: %s" % (tenant['name'], str(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="tenant %s is not changed." % tenant['name'], tenant=tenant['_data']) # try to update tenant try: result = self.client.post(tenant['href'], action='edit', resource=resource) except Exception as e: self.module.fail_json(msg="failed to update tenant %s: %s" % (tenant['name'], str(e))) return dict( changed=True, msg="successfully updated the tenant with id %s" % (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="missing required argument: %s" % key) url = '%s/tenants' % self.api_url 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="failed to create tenant %s: %s" % (name, str(e))) return dict( changed=True, msg="successfully created tenant '%s' with id '%s'" % (name, 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("%s/quotas?expand=resources&filter[]=name=%s" % (tenant['href'], 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("%s/quotas?expand=resources" % (tenant['href'])) 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="tenant quota '%s' does not exist" % quota_key) 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="tenant quota %s already has value %s" % (quota_key, quota_value)) else: url = '%s/quotas/%s' % (tenant['href'], 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="failed to update tenant quota %s: %s" % (quota_key, str(e))) return dict( changed=True, msg="successfully updated tenant quota %s" % quota_key) def create_tenant_quota(self, tenant, quota_key, quota_value): """ Creates the tenant quotas in manageiq. Returns: result """ url = '%s/quotas' % (tenant['href']) 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="failed to create tenant quota %s: %s" % (quota_key, str(e))) return dict( changed=True, msg="successfully created tenant quota %s" % 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="failed to delete tenant quota '%s': %s" % (quota['name'], str(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(required=False, type='int'), parent=dict(required=False, 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 = "tenant '%s' with parent_id %i does not exist in manageiq" % (name, parent_id) else: msg = "tenant '%s' with parent '%s' does not exist in manageiq" % (name, parent) 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()