From 48922660fe637f6b8ae3a70b247f7a02d15ed9a0 Mon Sep 17 00:00:00 2001 From: Yaacov Zamir Date: Thu, 3 Aug 2017 17:49:47 +0300 Subject: [PATCH] New model manageiq manageiq user (#26641) * ManageIQ: manageiq_user module, module utils and doc_fragment ManageIQ is an open source management platform for Hybrid IT. This change is adding: - manageiq_user module, responsible for user management in ManageIQ - manageiq utils - manageiq doc_fragment * Handle import error * Use formatting options * group parameter is required * changed doesn't need to be an attribute * resource dictionary should contain values which isn't None * move from monitoring to remote-management * Use ManageIQ nameing convention * Do not set defauts in arguments * Use idempotent state parameter instead of action * Check import error in the manageiq util class * Update the miq documentation * rename the connection configuration from miq to manageiq_connection * All messeges start with non cap, fix typos, add examples, rename vars * more typos fixes * Make sure we insert only strings to logs by using % formating * use suboptions keyword for the manageiq connection * do not log the managiq connection struct (it include sensitive information like username and password) * add missing from __future__ * ahh, wrong no-log line * Use sub options --- .../dev_guide/developing_module_utilities.rst | 1 + lib/ansible/module_utils/manageiq.py | 119 +++++++ .../remote_management/manageiq/__init__.py | 0 .../manageiq/manageiq_user.py | 293 ++++++++++++++++++ .../utils/module_docs_fragments/manageiq.py | 55 ++++ 5 files changed, 468 insertions(+) create mode 100755 lib/ansible/module_utils/manageiq.py create mode 100644 lib/ansible/modules/remote_management/manageiq/__init__.py create mode 100755 lib/ansible/modules/remote_management/manageiq/manageiq_user.py create mode 100644 lib/ansible/utils/module_docs_fragments/manageiq.py diff --git a/docs/docsite/rst/dev_guide/developing_module_utilities.rst b/docs/docsite/rst/dev_guide/developing_module_utilities.rst index fb712b71906..21bc9ede363 100644 --- a/docs/docsite/rst/dev_guide/developing_module_utilities.rst +++ b/docs/docsite/rst/dev_guide/developing_module_utilities.rst @@ -29,6 +29,7 @@ The following is a list of module_utils files and a general description. The mod - ismount.py - Contains single helper function that fixes os.path.ismount - junos.py - Definitions and helper functions for modules that manage Junos networking devices - known_hosts.py - utilities for working with known_hosts file +- manageiq.py - Functions and utilities for modules that work with ManageIQ platform and its resources. - mysql.py - Allows modules to connect to a MySQL instance - netapp.py - Functions and utilities for modules that work with the NetApp storage platforms. - netcfg.py - Configuration utility functions for use by networking modules diff --git a/lib/ansible/module_utils/manageiq.py b/lib/ansible/module_utils/manageiq.py new file mode 100755 index 00000000000..9c2cbf5b929 --- /dev/null +++ b/lib/ansible/module_utils/manageiq.py @@ -0,0 +1,119 @@ +# +# Copyright (c) 2017, Daniel Korn +# +# This code is part of Ansible, but is an independent component. +# This particular file snippet, and this file snippet only, is BSD licensed. +# Modules you write using this snippet, which is embedded dynamically by Ansible +# still belong to the author of the module, and may assign their own license +# to the complete work. +# +# Redistribution and use in source and binary forms, with or without modification, +# are permitted provided that the following conditions are met: +# +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions and the following disclaimer in the documentation +# and/or other materials provided with the distribution. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. +# IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, +# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT +# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE +# USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +import os + +try: + from manageiq_client.api import ManageIQClient + HAS_CLIENT = True +except ImportError: + HAS_CLIENT = False + + +def manageiq_argument_spec(): + return dict( + url=dict(default=os.environ.get('MIQ_URL', None)), + username=dict(default=os.environ.get('MIQ_USERNAME', None)), + password=dict(default=os.environ.get('MIQ_PASSWORD', None), no_log=True), + verify_ssl=dict(default=True, type='bool'), + ca_bundle_path=dict(required=False, default=None), + ) + + +def check_client(module): + if not HAS_CLIENT: + module.fail_json(msg='manageiq_client.api is required for this module') + + +class ManageIQ(object): + """ + class encapsulating ManageIQ API client. + """ + + def __init__(self, module): + # handle import errors + check_client(module) + + params = module.params['manageiq_connection'] + + # check for required arguments + for arg in ['url', 'username', 'password']: + if params[arg] in (None, ''): + module.fail_json(msg="missing required argument: manageiq_connection[{}]".format(arg)) + + url = params['url'] + username = params['username'] + password = params['password'] + verify_ssl = params['verify_ssl'] + ca_bundle_path = params['ca_bundle_path'] + + self._module = module + self._api_url = url + '/api' + self._client = ManageIQClient(self._api_url, (username, password), verify_ssl=verify_ssl, ca_bundle_path=ca_bundle_path) + + @property + def module(self): + """ Ansible module module + + Returns: + the ansible module + """ + return self._module + + @property + def api_url(self): + """ Base ManageIQ API + + Returns: + the base ManageIQ API + """ + return self._api_url + + @property + def client(self): + """ ManageIQ client + + Returns: + the ManageIQ client + """ + return self._client + + def find_collection_resource_by(self, collection_name, **params): + """ Searches the collection resource by the collection name and the param passed. + + Returns: + the resource as an object if it exists in manageiq, None otherwise. + """ + try: + entity = self.client.collections.__getattribute__(collection_name).get(**params) + except ValueError: + return None + except Exception as e: + self.module.fail_json(msg="failed to find resource {error}".format(error=e)) + return vars(entity) diff --git a/lib/ansible/modules/remote_management/manageiq/__init__.py b/lib/ansible/modules/remote_management/manageiq/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/lib/ansible/modules/remote_management/manageiq/manageiq_user.py b/lib/ansible/modules/remote_management/manageiq/manageiq_user.py new file mode 100755 index 00000000000..07a30b8d465 --- /dev/null +++ b/lib/ansible/modules/remote_management/manageiq/manageiq_user.py @@ -0,0 +1,293 @@ +#!/usr/bin/python +# +# (c) 2017, Daniel Korn +# +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . + +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + + +ANSIBLE_METADATA = {'metadata_version': '1.0', + 'status': ['preview'], + 'supported_by': 'community'} + + +DOCUMENTATION = ''' + +module: manageiq_user + +short_description: Management of users in ManageIQ. +extends_documentation_fragment: manageiq +version_added: '2.4' +author: Daniel Korn (@dkorn) +description: + - The manageiq_user module supports adding, updating and deleting users in ManageIQ. + +options: + state: + description: + - absent - user should not exist, present - user should be. + required: False + choices: ['absent', 'present'] + default: 'present' + userid: + description: + - The unique userid in manageiq, often mentioned as username. + required: true + name: + description: + - The users' full name. + required: false + default: null + password: + description: + - The users' password. + required: false + default: null + group: + description: + - The name of the group to which the user belongs. + required: false + default: null + email: + description: + - The users' E-mail address. + required: false + default: null +''' + +EXAMPLES = ''' +- name: Create a new user in ManageIQ + manageiq_user: + userid: 'jdoe' + name: 'Jane Doe' + password: 'VerySecret' + group: 'EvmGroup-user' + email: 'jdoe@example.com' + manageiq_connection: + url: 'http://127.0.0.1:3000' + username: 'admin' + password: 'smartvm' + verify_ssl: False + +- name: Delete a user in ManageIQ + manageiq_user: + state: 'absent' + userid: 'jdoe' + manageiq_connection: + url: 'http://127.0.0.1:3000' + username: 'admin' + password: 'smartvm' + verify_ssl: False + +- name: Update email of user in ManageIQ + manageiq_user: + userid: 'jdoe' + email: 'jaustine@example.com' + manageiq_connection: + url: 'http://127.0.0.1:3000' + username: 'admin' + password: 'smartvm' + verify_ssl: False +''' + +RETURN = ''' +''' + +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.manageiq import ManageIQ, manageiq_argument_spec + + +class ManageIQUser(object): + """ + Object to execute user 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 group_id(self, description): + """ Search for group id by group description. + + Returns: + the group id, or send a module Fail signal if group not found. + """ + group = self.manageiq.find_collection_resource_by('groups', description=description) + if not group: # group doesn't exist + self.module.fail_json( + msg="group %s does not exist in manageiq" % (description)) + + return group['id'] + + def user(self, userid): + """ Search for user object by userid. + + Returns: + the user, or None if user not found. + """ + return self.manageiq.find_collection_resource_by('users', userid=userid) + + def compare_user(self, user, name, group_id, password, email): + """ Compare user fields with new field values. + + Returns: + false if user fields have some difference from new fields, true o/w. + """ + found_difference = ( + (name and user['name'] != name) or + (password is not None) or + (email and user['email'] != email) or + (group_id and user['group']['id'] != group_id) + ) + + return not found_difference + + def delete_user(self, user): + """ Deletes a user from manageiq. + + Returns: + a short message describing the operation executed. + """ + try: + url = '%s/users/%s' % (self.api_url, user['id']) + result = self.client.post(url, action='delete') + except Exception as e: + self.module.fail_json(msg="failed to delete user %s: %s" % (user['userid'], str(e))) + + return dict(changed=True, msg=result['message']) + + def edit_user(self, user, name, group, password, email): + """ Edit a user from manageiq. + + Returns: + a short message describing the operation executed. + """ + group_id = None + url = '%s/users/%s' % (self.api_url, user['id']) + + resource = dict(userid=user['userid']) + if group is not None: + group_id = self.group_id(group) + resource['group'] = dict(id=group_id) + if name is not None: + resource['name'] = name + if password is not None: + resource['password'] = password + if email is not None: + resource['email'] = email + + # check if we need to update ( compare_user is true is no difference found ) + if self.compare_user(user, name, group_id, password, email): + return dict( + changed=False, + msg="user %s is not changed." % (user['userid'])) + + # try to update user + try: + result = self.client.post(url, action='edit', resource=resource) + except Exception as e: + self.module.fail_json(msg="failed to update user %s: %s" % (user['userid'], str(e))) + + return dict( + changed=True, + msg="successfully updated the user %s: %s" % (user['userid'], result)) + + def create_user(self, userid, name, group, password, email): + """ Creates the user in manageiq. + + Returns: + the created user id, name, created_on timestamp, + updated_on timestamp, userid and current_group_id. + """ + # check for required arguments + for key, value in dict(name=name, group=group, password=password).items(): + if value in (None, ''): + self.module.fail_json(msg="missing required argument: %s" % (key)) + + group_id = self.group_id(group) + url = '%s/users' % (self.api_url) + + resource = {'userid': userid, 'name': name, 'password': password, 'group': {'id': group_id}} + if email is not None: + resource['email'] = email + + # try to create a new user + try: + result = self.client.post(url, action='create', resource=resource) + except Exception as e: + self.module.fail_json(msg="failed to create user %s: %s" % (userid, str(e))) + + return dict( + changed=True, + msg="successfully created the user %s: %s" % (userid, result['results'])) + + +def main(): + module = AnsibleModule( + argument_spec=dict( + manageiq_connection=dict(required=True, type='dict', + options=manageiq_argument_spec()), + userid=dict(required=True, type='str'), + name=dict(), + password=dict(no_log=True), + group=dict(), + email=dict(), + state=dict(choices=['absent', 'present'], default='present') + ), + ) + + userid = module.params['userid'] + name = module.params['name'] + password = module.params['password'] + group = module.params['group'] + email = module.params['email'] + state = module.params['state'] + + manageiq = ManageIQ(module) + manageiq_user = ManageIQUser(manageiq) + + user = manageiq_user.user(userid) + + # user should not exist + if state == "absent": + # if we have a user, delete it + if user: + res_args = manageiq_user.delete_user(user) + # if we do not have a user, nothing to do + else: + res_args = dict( + changed=False, + msg="user %s: does not exist in manageiq" % (userid)) + + # user shoult exist + if state == "present": + # if we have a user, edit it + if user: + res_args = manageiq_user.edit_user(user, name, group, password, email) + # if we do not have a user, create it + else: + res_args = manageiq_user.create_user(userid, name, group, password, email) + + module.exit_json(**res_args) + + +if __name__ == "__main__": + main() diff --git a/lib/ansible/utils/module_docs_fragments/manageiq.py b/lib/ansible/utils/module_docs_fragments/manageiq.py new file mode 100644 index 00000000000..35aa80da245 --- /dev/null +++ b/lib/ansible/utils/module_docs_fragments/manageiq.py @@ -0,0 +1,55 @@ +# +# (c) 2017, Daniel Korn +# +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . + + +class ModuleDocFragment(object): + + # Standard ManageIQ documentation fragment + DOCUMENTATION = """ +options: + manageiq_connection: + required: true + description: + - ManageIQ connection configuration information. + suboptions: + url: + required: true + description: + - ManageIQ environment url. C(MIQ_URL) env var if set. otherwise, it is required to pass it. + username: + required: true + description: + - ManageIQ username. C(MIQ_USERNAME) env var if set. otherwise, it is required to pass it. + password: + required: true + description: + - ManageIQ password. C(MIQ_PASSWORD) env var if set. otherwise, it is required to pass it. + verify_ssl: + required: false + default: true + description: + - Whether SSL certificates should be verified for HTTPS requests. defaults to True. + ca_bundle_path: + required: false + default: null + description: + - The path to a CA bundle file or directory with certificates. defaults to None. + +requirements: + - 'manageiq-client U(https://github.com/ManageIQ/manageiq-api-client-python/)' +"""