1#!/usr/bin/python 2# 3# (c) 2018, Evert Mulder <evertmulder@gmail.com> (base on manageiq_user.py by Daniel Korn <korndaniel1@gmail.com>) 4# 5# This file is part of Ansible 6# 7# Ansible is free software: you can redistribute it and/or modify 8# it under the terms of the GNU General Public License as published by 9# the Free Software Foundation, either version 3 of the License, or 10# (at your option) any later version. 11# 12# Ansible is distributed in the hope that it will be useful, 13# but WITHOUT ANY WARRANTY; without even the implied warranty of 14# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15# GNU General Public License for more details. 16# 17# You should have received a copy of the GNU General Public License 18# along with Ansible. If not, see <http://www.gnu.org/licenses/>. 19 20from __future__ import (absolute_import, division, print_function) 21 22__metaclass__ = type 23 24ANSIBLE_METADATA = {'metadata_version': '1.1', 25 'status': ['preview'], 26 'supported_by': 'community'} 27 28DOCUMENTATION = ''' 29 30module: manageiq_group 31 32short_description: Management of groups in ManageIQ. 33extends_documentation_fragment: manageiq 34version_added: '2.8' 35author: Evert Mulder (@evertmulder) 36description: 37 - The manageiq_group module supports adding, updating and deleting groups in ManageIQ. 38requirements: 39- manageiq-client 40 41options: 42 state: 43 description: 44 - absent - group should not exist, present - group should be. 45 choices: ['absent', 'present'] 46 default: 'present' 47 description: 48 description: 49 - The group description. 50 required: true 51 default: null 52 role_id: 53 description: 54 - The the group role id 55 required: false 56 default: null 57 role: 58 description: 59 - The the group role name 60 - The C(role_id) has precedence over the C(role) when supplied. 61 required: false 62 default: null 63 tenant_id: 64 description: 65 - The tenant for the group identified by the tenant id. 66 required: false 67 default: null 68 tenant: 69 description: 70 - The tenant for the group identified by the tenant name. 71 - The C(tenant_id) has precedence over the C(tenant) when supplied. 72 - Tenant names are case sensitive. 73 required: false 74 default: null 75 managed_filters: 76 description: The tag values per category 77 type: dict 78 required: false 79 default: null 80 managed_filters_merge_mode: 81 description: 82 - In merge mode existing categories are kept or updated, new categories are added. 83 - In replace mode all categories will be replaced with the supplied C(managed_filters). 84 choices: [ merge, replace ] 85 default: replace 86 belongsto_filters: 87 description: A list of strings with a reference to the allowed host, cluster or folder 88 type: list 89 required: false 90 default: null 91 belongsto_filters_merge_mode: 92 description: 93 - In merge mode existing settings are merged with the supplied C(belongsto_filters). 94 - In replace mode current values are replaced with the supplied C(belongsto_filters). 95 choices: [ merge, replace ] 96 default: replace 97''' 98 99EXAMPLES = ''' 100- name: Create a group in ManageIQ with the role EvmRole-user and tenant 'my_tenant' 101 manageiq_group: 102 description: 'MyGroup-user' 103 role: 'EvmRole-user' 104 tenant: 'my_tenant' 105 manageiq_connection: 106 url: 'https://manageiq_server' 107 username: 'admin' 108 password: 'smartvm' 109 validate_certs: False 110 111- name: Create a group in ManageIQ with the role EvmRole-user and tenant with tenant_id 4 112 manageiq_group: 113 description: 'MyGroup-user' 114 role: 'EvmRole-user' 115 tenant_id: 4 116 manageiq_connection: 117 url: 'https://manageiq_server' 118 username: 'admin' 119 password: 'smartvm' 120 validate_certs: False 121 122- name: 123 - Create or update a group in ManageIQ with the role EvmRole-user and tenant my_tenant. 124 - Apply 3 prov_max_cpu and 2 department tags to the group. 125 - Limit access to a cluster for the group. 126 manageiq_group: 127 description: 'MyGroup-user' 128 role: 'EvmRole-user' 129 tenant: my_tenant 130 managed_filters: 131 prov_max_cpu: 132 - '1' 133 - '2' 134 - '4' 135 department: 136 - defense 137 - engineering 138 managed_filters_merge_mode: replace 139 belongsto_filters: 140 - "/belongsto/ExtManagementSystem|ProviderName/EmsFolder|Datacenters/EmsFolder|dc_name/EmsFolder|host/EmsCluster|Cluster name" 141 belongsto_filters_merge_mode: merge 142 manageiq_connection: 143 url: 'https://manageiq_server' 144 username: 'admin' 145 password: 'smartvm' 146 validate_certs: False 147 148- name: Delete a group in ManageIQ 149 manageiq_group: 150 state: 'absent' 151 description: 'MyGroup-user' 152 manageiq_connection: 153 url: 'http://127.0.0.1:3000' 154 username: 'admin' 155 password: 'smartvm' 156 157- name: Delete a group in ManageIQ using a token 158 manageiq_group: 159 state: 'absent' 160 description: 'MyGroup-user' 161 manageiq_connection: 162 url: 'http://127.0.0.1:3000' 163 token: 'sometoken' 164''' 165 166RETURN = ''' 167group: 168 description: The group. 169 returned: success 170 type: complex 171 contains: 172 description: 173 description: The group description 174 returned: success 175 type: str 176 id: 177 description: The group id 178 returned: success 179 type: int 180 group_type: 181 description: The group type, system or user 182 returned: success 183 type: str 184 role: 185 description: The group role name 186 returned: success 187 type: str 188 tenant: 189 description: The group tenant name 190 returned: success 191 type: str 192 managed_filters: 193 description: The tag values per category 194 returned: success 195 type: dict 196 belongsto_filters: 197 description: A list of strings with a reference to the allowed host, cluster or folder 198 returned: success 199 type: list 200 created_on: 201 description: Group creation date 202 returned: success 203 type: str 204 sample: "2018-08-12T08:37:55+00:00" 205 updated_on: 206 description: Group update date 207 returned: success 208 type: int 209 sample: "2018-08-12T08:37:55+00:00" 210''' 211 212from ansible.module_utils.basic import AnsibleModule 213from ansible.module_utils.manageiq import ManageIQ, manageiq_argument_spec 214 215 216class ManageIQgroup(object): 217 """ 218 Object to execute group management operations in manageiq. 219 """ 220 221 def __init__(self, manageiq): 222 self.manageiq = manageiq 223 224 self.module = self.manageiq.module 225 self.api_url = self.manageiq.api_url 226 self.client = self.manageiq.client 227 228 def group(self, description): 229 """ Search for group object by description. 230 Returns: 231 the group, or None if group was not found. 232 """ 233 groups = self.client.collections.groups.find_by(description=description) 234 if len(groups) == 0: 235 return None 236 else: 237 return groups[0] 238 239 def tenant(self, tenant_id, tenant_name): 240 """ Search for tenant entity by name or id 241 Returns: 242 the tenant entity, None if no id or name was supplied 243 """ 244 245 if tenant_id: 246 tenant = self.client.get_entity('tenants', tenant_id) 247 if not tenant: 248 self.module.fail_json(msg="Tenant with id '%s' not found in manageiq" % str(tenant_id)) 249 return tenant 250 else: 251 if tenant_name: 252 tenant_res = self.client.collections.tenants.find_by(name=tenant_name) 253 if not tenant_res: 254 self.module.fail_json(msg="Tenant '%s' not found in manageiq" % tenant_name) 255 if len(tenant_res) > 1: 256 self.module.fail_json(msg="Multiple tenants found in manageiq with name '%s" % tenant_name) 257 tenant = tenant_res[0] 258 return tenant 259 else: 260 # No tenant name or tenant id supplied 261 return None 262 263 def role(self, role_id, role_name): 264 """ Search for a role object by name or id. 265 Returns: 266 the role entity, None no id or name was supplied 267 268 the role, or send a module Fail signal if role not found. 269 """ 270 if role_id: 271 role = self.client.get_entity('roles', role_id) 272 if not role: 273 self.module.fail_json(msg="Role with id '%s' not found in manageiq" % str(role_id)) 274 return role 275 else: 276 if role_name: 277 role_res = self.client.collections.roles.find_by(name=role_name) 278 if not role_res: 279 self.module.fail_json(msg="Role '%s' not found in manageiq" % role_name) 280 if len(role_res) > 1: 281 self.module.fail_json(msg="Multiple roles found in manageiq with name '%s" % role_name) 282 return role_res[0] 283 else: 284 # No role name or role id supplied 285 return None 286 287 @staticmethod 288 def merge_dict_values(norm_current_values, norm_updated_values): 289 """ Create an merged update object for manageiq group filters. 290 291 The input dict contain the tag values per category. 292 If the new values contain the category, all tags for that category are replaced 293 If the new values do not contain the category, the existing tags are kept 294 295 Returns: 296 the nested array with the merged values, used in the update post body 297 """ 298 299 # If no updated values are supplied, in merge mode, the original values must be returned 300 # otherwise the existing tag filters will be removed. 301 if norm_current_values and (not norm_updated_values): 302 return norm_current_values 303 304 # If no existing tag filters exist, use the user supplied values 305 if (not norm_current_values) and norm_updated_values: 306 return norm_updated_values 307 308 # start with norm_current_values's keys and values 309 res = norm_current_values.copy() 310 # replace res with norm_updated_values's keys and values 311 res.update(norm_updated_values) 312 return res 313 314 def delete_group(self, group): 315 """ Deletes a group from manageiq. 316 317 Returns: 318 a dict of: 319 changed: boolean indicating if the entity was updated. 320 msg: a short message describing the operation executed. 321 """ 322 try: 323 url = '%s/groups/%s' % (self.api_url, group['id']) 324 result = self.client.post(url, action='delete') 325 except Exception as e: 326 self.module.fail_json(msg="failed to delete group %s: %s" % (group['description'], str(e))) 327 328 if result['success'] is False: 329 self.module.fail_json(msg=result['message']) 330 331 return dict( 332 changed=True, 333 msg="deleted group %s with id %i" % (group['description'], group['id'])) 334 335 def edit_group(self, group, description, role, tenant, norm_managed_filters, managed_filters_merge_mode, 336 belongsto_filters, belongsto_filters_merge_mode): 337 """ Edit a manageiq group. 338 339 Returns: 340 a dict of: 341 changed: boolean indicating if the entity was updated. 342 msg: a short message describing the operation executed. 343 """ 344 345 if role or norm_managed_filters or belongsto_filters: 346 group.reload(attributes=['miq_user_role_name', 'entitlement']) 347 348 try: 349 current_role = group['miq_user_role_name'] 350 except AttributeError: 351 current_role = None 352 353 changed = False 354 resource = {} 355 356 if description and group['description'] != description: 357 resource['description'] = description 358 changed = True 359 360 if tenant and group['tenant_id'] != tenant['id']: 361 resource['tenant'] = dict(id=tenant['id']) 362 changed = True 363 364 if role and current_role != role['name']: 365 resource['role'] = dict(id=role['id']) 366 changed = True 367 368 if norm_managed_filters or belongsto_filters: 369 370 # Only compare if filters are supplied 371 entitlement = group['entitlement'] 372 373 if 'filters' not in entitlement: 374 # No existing filters exist, use supplied filters 375 managed_tag_filters_post_body = self.normalized_managed_tag_filters_to_miq(norm_managed_filters) 376 resource['filters'] = {'managed': managed_tag_filters_post_body, "belongsto": belongsto_filters} 377 changed = True 378 else: 379 current_filters = entitlement['filters'] 380 new_filters = self.edit_group_edit_filters(current_filters, 381 norm_managed_filters, managed_filters_merge_mode, 382 belongsto_filters, belongsto_filters_merge_mode) 383 if new_filters: 384 resource['filters'] = new_filters 385 changed = True 386 387 if not changed: 388 return dict( 389 changed=False, 390 msg="group %s is not changed." % group['description']) 391 392 # try to update group 393 try: 394 self.client.post(group['href'], action='edit', resource=resource) 395 changed = True 396 except Exception as e: 397 self.module.fail_json(msg="failed to update group %s: %s" % (group['name'], str(e))) 398 399 return dict( 400 changed=changed, 401 msg="successfully updated the group %s with id %s" % (group['description'], group['id'])) 402 403 def edit_group_edit_filters(self, current_filters, norm_managed_filters, managed_filters_merge_mode, 404 belongsto_filters, belongsto_filters_merge_mode): 405 """ Edit a manageiq group filters. 406 407 Returns: 408 None if no the group was not updated 409 If the group was updated the post body part for updating the group 410 """ 411 filters_updated = False 412 new_filters_resource = {} 413 414 # Process belongsto filters 415 if 'belongsto' in current_filters: 416 current_belongsto_set = set(current_filters['belongsto']) 417 else: 418 current_belongsto_set = set() 419 420 if belongsto_filters: 421 new_belongsto_set = set(belongsto_filters) 422 else: 423 new_belongsto_set = set() 424 425 if current_belongsto_set == new_belongsto_set: 426 new_filters_resource['belongsto'] = current_filters['belongsto'] 427 else: 428 if belongsto_filters_merge_mode == 'merge': 429 current_belongsto_set.update(new_belongsto_set) 430 new_filters_resource['belongsto'] = list(current_belongsto_set) 431 else: 432 new_filters_resource['belongsto'] = list(new_belongsto_set) 433 filters_updated = True 434 435 # Process belongsto managed filter tags 436 # The input is in the form dict with keys are the categories and the tags are supplied string array 437 # ManageIQ, the current_managed, uses an array of arrays. One array of categories. 438 # We normalize the user input from a dict with arrays to a dict of sorted arrays 439 # We normalize the current manageiq array of arrays also to a dict of sorted arrays so we can compare 440 norm_current_filters = self.manageiq_filters_to_sorted_dict(current_filters) 441 442 if norm_current_filters == norm_managed_filters: 443 if 'managed' in current_filters: 444 new_filters_resource['managed'] = current_filters['managed'] 445 else: 446 if managed_filters_merge_mode == 'merge': 447 merged_dict = self.merge_dict_values(norm_current_filters, norm_managed_filters) 448 new_filters_resource['managed'] = self.normalized_managed_tag_filters_to_miq(merged_dict) 449 else: 450 new_filters_resource['managed'] = self.normalized_managed_tag_filters_to_miq(norm_managed_filters) 451 filters_updated = True 452 453 if not filters_updated: 454 return None 455 456 return new_filters_resource 457 458 def create_group(self, description, role, tenant, norm_managed_filters, belongsto_filters): 459 """ Creates the group in manageiq. 460 461 Returns: 462 the created group id, name, created_on timestamp, 463 updated_on timestamp. 464 """ 465 # check for required arguments 466 for key, value in dict(description=description).items(): 467 if value in (None, ''): 468 self.module.fail_json(msg="missing required argument: %s" % key) 469 470 url = '%s/groups' % self.api_url 471 472 resource = {'description': description} 473 474 if role is not None: 475 resource['role'] = dict(id=role['id']) 476 477 if tenant is not None: 478 resource['tenant'] = dict(id=tenant['id']) 479 480 if norm_managed_filters or belongsto_filters: 481 managed_tag_filters_post_body = self.normalized_managed_tag_filters_to_miq(norm_managed_filters) 482 resource['filters'] = {'managed': managed_tag_filters_post_body, "belongsto": belongsto_filters} 483 484 try: 485 result = self.client.post(url, action='create', resource=resource) 486 except Exception as e: 487 self.module.fail_json(msg="failed to create group %s: %s" % (description, str(e))) 488 489 return dict( 490 changed=True, 491 msg="successfully created group %s" % description, 492 group_id=result['results'][0]['id'] 493 ) 494 495 @staticmethod 496 def normalized_managed_tag_filters_to_miq(norm_managed_filters): 497 if not norm_managed_filters: 498 return None 499 500 return list(norm_managed_filters.values()) 501 502 @staticmethod 503 def manageiq_filters_to_sorted_dict(current_filters): 504 if 'managed' not in current_filters: 505 return None 506 507 res = {} 508 for tag_list in current_filters['managed']: 509 tag_list.sort() 510 key = tag_list[0].split('/')[2] 511 res[key] = tag_list 512 513 return res 514 515 @staticmethod 516 def normalize_user_managed_filters_to_sorted_dict(managed_filters, module): 517 if not managed_filters: 518 return None 519 520 res = {} 521 for cat_key in managed_filters: 522 cat_array = [] 523 if not isinstance(managed_filters[cat_key], list): 524 module.fail_json(msg='Entry "{0}" of managed_filters must be a list!'.format(cat_key)) 525 for tags in managed_filters[cat_key]: 526 miq_managed_tag = "/managed/" + cat_key + "/" + tags 527 cat_array.append(miq_managed_tag) 528 # Do not add empty categories. ManageIQ will remove all categories that are not supplied 529 if cat_array: 530 cat_array.sort() 531 res[cat_key] = cat_array 532 return res 533 534 @staticmethod 535 def create_result_group(group): 536 """ Creates the ansible result object from a manageiq group entity 537 538 Returns: 539 a dict with the group id, description, role, tenant, filters, group_type, created_on, updated_on 540 """ 541 try: 542 role_name = group['miq_user_role_name'] 543 except AttributeError: 544 role_name = None 545 546 managed_filters = None 547 belongsto_filters = None 548 if 'filters' in group['entitlement']: 549 filters = group['entitlement']['filters'] 550 if 'belongsto' in filters: 551 belongsto_filters = filters['belongsto'] 552 if 'managed' in filters: 553 managed_filters = {} 554 for tag_list in filters['managed']: 555 key = tag_list[0].split('/')[2] 556 tags = [] 557 for t in tag_list: 558 tags.append(t.split('/')[3]) 559 managed_filters[key] = tags 560 561 return dict( 562 id=group['id'], 563 description=group['description'], 564 role=role_name, 565 tenant=group['tenant']['name'], 566 managed_filters=managed_filters, 567 belongsto_filters=belongsto_filters, 568 group_type=group['group_type'], 569 created_on=group['created_on'], 570 updated_on=group['updated_on'], 571 ) 572 573 574def main(): 575 argument_spec = dict( 576 description=dict(required=True, type='str'), 577 state=dict(choices=['absent', 'present'], default='present'), 578 role_id=dict(required=False, type='int'), 579 role=dict(required=False, type='str'), 580 tenant_id=dict(required=False, type='int'), 581 tenant=dict(required=False, type='str'), 582 managed_filters=dict(required=False, type='dict'), 583 managed_filters_merge_mode=dict(required=False, choices=['merge', 'replace'], default='replace'), 584 belongsto_filters=dict(required=False, type='list', elements='str'), 585 belongsto_filters_merge_mode=dict(required=False, choices=['merge', 'replace'], default='replace'), 586 ) 587 # add the manageiq connection arguments to the arguments 588 argument_spec.update(manageiq_argument_spec()) 589 590 module = AnsibleModule( 591 argument_spec=argument_spec 592 ) 593 594 description = module.params['description'] 595 state = module.params['state'] 596 role_id = module.params['role_id'] 597 role_name = module.params['role'] 598 tenant_id = module.params['tenant_id'] 599 tenant_name = module.params['tenant'] 600 managed_filters = module.params['managed_filters'] 601 managed_filters_merge_mode = module.params['managed_filters_merge_mode'] 602 belongsto_filters = module.params['belongsto_filters'] 603 belongsto_filters_merge_mode = module.params['belongsto_filters_merge_mode'] 604 605 manageiq = ManageIQ(module) 606 manageiq_group = ManageIQgroup(manageiq) 607 608 group = manageiq_group.group(description) 609 610 # group should not exist 611 if state == "absent": 612 # if we have a group, delete it 613 if group: 614 res_args = manageiq_group.delete_group(group) 615 # if we do not have a group, nothing to do 616 else: 617 res_args = dict( 618 changed=False, 619 msg="group '%s' does not exist in manageiq" % description) 620 621 # group should exist 622 if state == "present": 623 624 tenant = manageiq_group.tenant(tenant_id, tenant_name) 625 role = manageiq_group.role(role_id, role_name) 626 norm_managed_filters = manageiq_group.normalize_user_managed_filters_to_sorted_dict(managed_filters, module) 627 # if we have a group, edit it 628 if group: 629 res_args = manageiq_group.edit_group(group, description, role, tenant, 630 norm_managed_filters, managed_filters_merge_mode, 631 belongsto_filters, belongsto_filters_merge_mode) 632 633 # if we do not have a group, create it 634 else: 635 res_args = manageiq_group.create_group(description, role, tenant, norm_managed_filters, belongsto_filters) 636 group = manageiq.client.get_entity('groups', res_args['group_id']) 637 638 group.reload(expand='resources', attributes=['miq_user_role_name', 'tenant', 'entitlement']) 639 res_args['group'] = manageiq_group.create_result_group(group) 640 641 module.exit_json(**res_args) 642 643 644if __name__ == "__main__": 645 main() 646