├── dellemc_ansible ├── utils │ ├── __init__.py │ └── dellemc_ansible_isilon_utils.py ├── docs │ ├── Ansible for Dell EMC Isilon v1.1 Product Guide.pdf │ └── Ansible for Dell EMC Isilon v1.1 Release Notes.pdf ├── doc_fragments │ └── dellemc_isilon.py └── isilon │ └── library │ ├── dellemc_isilon_gatherfacts.py │ ├── dellemc_isilon_accesszone.py │ ├── dellemc_isilon_group.py │ ├── dellemc_isilon_snapshotschedule.py │ ├── dellemc_isilon_nfs.py │ ├── dellemc_isilon_smartquota.py │ └── dellemc_isilon_user.py ├── README.md └── LICENSE /dellemc_ansible/utils/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /dellemc_ansible/docs/Ansible for Dell EMC Isilon v1.1 Product Guide.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dell/ansible-isilon/HEAD/dellemc_ansible/docs/Ansible for Dell EMC Isilon v1.1 Product Guide.pdf -------------------------------------------------------------------------------- /dellemc_ansible/docs/Ansible for Dell EMC Isilon v1.1 Release Notes.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dell/ansible-isilon/HEAD/dellemc_ansible/docs/Ansible for Dell EMC Isilon v1.1 Release Notes.pdf -------------------------------------------------------------------------------- /dellemc_ansible/doc_fragments/dellemc_isilon.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Copyright: (c) 2019, Dell EMC. 3 | 4 | from __future__ import absolute_import, division, print_function 5 | __metaclass__ = type 6 | 7 | 8 | class ModuleDocFragment(object): 9 | 10 | DOCUMENTATION = r''' 11 | options: 12 | - See respective platform section for more details 13 | requirements: 14 | - See respective platform section for more details 15 | notes: 16 | - Ansible modules are available for EMC Isilon Storage Platform 17 | ''' 18 | # Documentation fragment for Isilon (dellemc_isilon) 19 | DELLEMC_ISILON = r''' 20 | options: 21 | onefs_host: 22 | description: 23 | - IP address or FQDN of the Isilon cluster. 24 | type: str 25 | required: True 26 | port_no: 27 | description: 28 | - Port number of the Isilon cluster.It defaults to 8080 if 29 | not specified. 30 | type: str 31 | required: False 32 | default: '8080' 33 | verify_ssl: 34 | description: 35 | - boolean variable to specify whether to validate SSL 36 | certificate or not. 37 | - True - indicates that the SSL certificate should be 38 | verified. 39 | - False - indicates that the SSL certificate should not be 40 | verified. 41 | type: bool 42 | required: True 43 | choices: [True, False] 44 | api_user: 45 | type: str 46 | description: 47 | - username of the Isilon cluster. 48 | required: True 49 | api_password: 50 | type: str 51 | description: 52 | - the password of the Isilon cluster. 53 | required: True 54 | requirements: 55 | - A DellEMC Isilon Storage device. 56 | - Ansible 2.7 and above. 57 | notes: 58 | - The modules prefixed with dellemc_isilon are built to support the 59 | DellEMC Isilon storage platform. 60 | ''' 61 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## *DEPRECATED* 2 | > #### *Warning: 'Isilon' has been rebranded as 'PowerScale'. Hence, this repository is no longer being maintained. For any updates and new features, please refer [ansible-powerscale](https://github.com/dell/ansible-powerscale).* 3 | 4 | # Ansible Modules for Dell EMC Isilon 5 | The Ansible Modules for Dell EMC Isilon allow Data Center and IT administrators to use RedHat Ansible to automate and orchestrate the configuartion and management of Dell EMC Isilon arrays. 6 | 7 | The capabilities of the Ansible modules are managing users, groups, access zones, file system, nfs exports, smb shares, snapshots, snapshot schedules and smart quotas; and to gather facts from the array. The tasks can be executed by running simple playbooks written in yaml syntax. 8 | 9 | ## Supported Platforms 10 | * Dell EMC Isilon Arrays version 8.0 and above. 11 | 12 | ## Prerequisites 13 | * Ansible 2.7 or higher 14 | * Python >= 2.7.12 15 | * Red Hat Enterprise Linux 7.6 16 | * Python SDK for Isilon ( version 8.1.1 ) 17 | 18 | ## Idempotency 19 | The modules are written in such a way that all requests are idempotent and hence fault-tolerant. It essentially means that the result of a successfully performed request is independent of the number of times it is executed. 20 | 21 | ## List of Ansible Modules for Dell EMC Isilon 22 | * File System Module 23 | * Access Zone Module 24 | * Users Module 25 | * Groups Module 26 | * Snapshot Module 27 | * Snapshot Schedule Module 28 | * NFS Module 29 | * SMB Module 30 | * Smart Quota Module 31 | * Gather Facts Module 32 | 33 | ## Installation of SDK 34 | Install python sdk named 'isi-sdk-8-1-1'. It can be installed using pip, based on appropriate python version. 35 | 36 | ## Installation of Ansible Modules 37 | ``` 38 | git clone https://github.com/dell/ansible-isilon.git 39 | cd ansible-isilon/dellemc_ansible 40 | ``` 41 | * For Python 2.7 42 | ``` 43 | mkdir -p /usr/lib/python2.7/site-packages/ansible/module_utils/storage/dell 44 | cp utils/* /usr/lib/python2.7/site-packages/ansible/module_utils/storage/dell 45 | cp isilon/library/* /usr/lib/python2.7/site-packages/ansible/modules/storage/dellemc/ 46 | cp doc_fragments/dellemc_isilon.py /usr/lib/python2.7/site-packages/ansible/plugins/doc_fragments/ 47 | ``` 48 | * For Python 3.5 49 | ``` 50 | mkdir -p /usr/lib/python3.5/site-packages/ansible/module_utils/storage/dell 51 | cp utils/* /usr/lib/python3.5/site-packages/ansible/module_utils/storage/dell 52 | cp isilon/library/* /usr/lib/python3.5/site-packages/ansible/modules/storage/dellemc/ 53 | cp doc_fragments/dellemc_isilon.py /usr/lib/python3.5/site-packages/ansible/plugins/doc_fragments/ 54 | ``` 55 | 56 | ## Documentation 57 | Check documentation from each module's file in /ansible-isilon/dellemc_ansible/isilon/library/ 58 | 59 | ## Examples 60 | Check examples from each module's file in /ansible-isilon/dellemc_ansible/isilon/library/ 61 | 62 | ## Results 63 | Each module returns the updated state and details of the entity. 64 | For example, if you are using the group module, all calls will return the updated details of the group. 65 | Sample result is shown in each module's documentation. 66 | 67 | ## Support 68 | * Ansible modules for Isilon are supported by Dell EMC and are provided under the terms of the license attached to the source code. 69 | * For any setup, configuration issues, questions or feedback, join the [Dell EMC Automation community](https://www.dell.com/community/Automation/bd-p/Automation). 70 | * For any Dell EMC storage issues, please contact Dell support at: https://www.dell.com/support. 71 | * Dell EMC does not provide support for any source code modifications. 72 | -------------------------------------------------------------------------------- /dellemc_ansible/utils/dellemc_ansible_isilon_utils.py: -------------------------------------------------------------------------------- 1 | """ import isilon sdk""" 2 | try: 3 | import isi_sdk_8_1_1 as isi_sdk 4 | from isi_sdk_8_1_1.rest import ApiException 5 | 6 | HAS_ISILON_SDK = True 7 | 8 | except ImportError: 9 | HAS_ISILON_SDK = False 10 | 11 | 12 | '''import pkg_resources''' 13 | try: 14 | from pkg_resources import parse_version 15 | import pkg_resources 16 | PKG_RSRC_IMPORTED = True 17 | except ImportError: 18 | PKG_RSRC_IMPORTED = False 19 | 20 | 21 | import logging 22 | import math 23 | import urllib3 24 | urllib3.disable_warnings() 25 | from decimal import Decimal 26 | 27 | 28 | ''' Check and Get required libraries ''' 29 | 30 | 31 | def has_isilon_sdk(): 32 | return HAS_ISILON_SDK 33 | 34 | 35 | def get_isilon_sdk(): 36 | return isi_sdk 37 | 38 | 39 | ''' 40 | Check if required Isilon SDK version is installed 41 | ''' 42 | 43 | 44 | def isilon_sdk_version_check(): 45 | try: 46 | supported_version = False 47 | if not PKG_RSRC_IMPORTED: 48 | unsupported_version_message = "Unable to import " \ 49 | "'pkg_resources', please install" \ 50 | " the required package" 51 | else: 52 | min_ver = '0.2.7' 53 | curr_version = pkg_resources.require("isi-sdk-8-1-1")[0].version 54 | unsupported_version_message =\ 55 | "isilon sdk {0} is not supported by this module. Minimum " \ 56 | "supported version is : {1} ".format(curr_version, min_ver) 57 | supported_version = parse_version(curr_version) >= parse_version( 58 | min_ver) 59 | 60 | isi_sdk_version = dict( 61 | supported_version=supported_version, 62 | unsupported_version_message=unsupported_version_message) 63 | 64 | return isi_sdk_version 65 | 66 | except Exception as e: 67 | unsupported_version_message = \ 68 | "Unable to get the isilon sdk version," \ 69 | " failed with Error {0} ".format(str(e)) 70 | isi_sdk_version = dict( 71 | supported_version=False, 72 | unsupported_version_message=unsupported_version_message) 73 | return isi_sdk_version 74 | 75 | 76 | ''' 77 | This method provides common access parameters required for the Ansible Modules on Isilon 78 | options: 79 | onefshost: 80 | description: 81 | - IP of the Isilon OneFS host 82 | required: true 83 | port_no: 84 | decription: 85 | - The port number through which all the requests will be addressed by the OneFS host. 86 | verifyssl: 87 | description: 88 | - Boolean value to inform system whether to verify ssl certificate or not. 89 | api_user: 90 | description: 91 | - User name to access OneFS 92 | api_password: 93 | description: 94 | - password to access OneFS 95 | ''' 96 | 97 | 98 | def get_isilon_management_host_parameters(): 99 | return dict( 100 | onefs_host=dict(type='str', required=True), 101 | verify_ssl=dict(type='bool', required=True), 102 | port_no=dict(type='str'), 103 | api_user=dict(type='str', required=True), 104 | api_password=dict(type='str', required=True, no_log=True) 105 | ) 106 | 107 | 108 | ''' 109 | This method is to establish connection to Isilon 110 | using its SDK. 111 | parameters: 112 | module_params - Ansible module parameters which contain below OneFS details 113 | to establish connection on to OneFS 114 | - onefshost: IP of OneFS host. 115 | - verifyssl: Boolean value to inform system whether to verify ssl certificate or not. 116 | - port_no: The port no of the OneFS host. 117 | - username: Username to access OneFS 118 | - password: Password to access OneFS 119 | returns configuration object 120 | ''' 121 | 122 | 123 | def get_isilon_connection(module_params): 124 | if HAS_ISILON_SDK: 125 | conn = isi_sdk.Configuration() 126 | if module_params['port_no'] is not None: 127 | conn.host = module_params['onefs_host'] + ":" + module_params[ 128 | 'port_no'] 129 | else: 130 | conn.host = module_params['onefs_host'] 131 | conn.verify_ssl = module_params['verify_ssl'] 132 | conn.username = module_params['api_user'] 133 | conn.password = module_params['api_password'] 134 | api_client = isi_sdk.ApiClient(conn) 135 | return api_client 136 | 137 | 138 | ''' 139 | This method is to initialize logger and return the logger object 140 | parameters: 141 | - module_name: Name of module to be part of log message. 142 | - log_file_name: name of the file in which the log meessages get appended. 143 | - log_devel: log level. 144 | returns logger object 145 | ''' 146 | 147 | 148 | def get_logger(module_name, log_file_name='dellemc_ansible_provisioning.log', 149 | log_devel=logging.INFO): 150 | FORMAT = '%(asctime)-15s %(filename)s %(levelname)s : %(message)s' 151 | logging.basicConfig(filename=log_file_name, format=FORMAT) 152 | LOG = logging.getLogger(module_name) 153 | LOG.setLevel(log_devel) 154 | return LOG 155 | 156 | 157 | ''' 158 | Convert the given size to bytes 159 | ''' 160 | KB_IN_BYTES = 1024 161 | MB_IN_BYTES = 1024 * 1024 162 | GB_IN_BYTES = 1024 * 1024 * 1024 163 | TB_IN_BYTES = 1024 * 1024 * 1024 * 1024 164 | 165 | 166 | def get_size_bytes(size, cap_units): 167 | if size is not None and size > 0: 168 | if cap_units in ('kb', 'KB'): 169 | return size * KB_IN_BYTES 170 | elif cap_units in ('mb', 'MB'): 171 | return size * MB_IN_BYTES 172 | elif cap_units in ('gb', 'GB'): 173 | return size * GB_IN_BYTES 174 | elif cap_units in ('tb', 'TB'): 175 | return size * TB_IN_BYTES 176 | else: 177 | return size 178 | else: 179 | return 0 180 | 181 | 182 | ''' 183 | Convert size in byte with actual unit like KB,MB,GB,TB,PB etc. 184 | ''' 185 | 186 | 187 | def convert_size_with_unit(size_bytes): 188 | if not isinstance(size_bytes, int): 189 | raise ValueError('This method takes Integer type argument only') 190 | if size_bytes == 0: 191 | return "0B" 192 | size_name = ("B", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB") 193 | i = int(math.floor(math.log(size_bytes, 1024))) 194 | p = math.pow(1024, i) 195 | s = round(size_bytes / p, 2) 196 | return "%s %s" % (s, size_name[i]) 197 | 198 | 199 | ''' 200 | Convert the given size to size in GB, size is restricted to 2 decimal places 201 | ''' 202 | 203 | 204 | def get_size_in_gb(size, cap_units): 205 | size_in_bytes = get_size_bytes(size, cap_units) 206 | size = Decimal(size_in_bytes / GB_IN_BYTES) 207 | size_in_gb = round(size, 2) 208 | return size_in_gb 209 | 210 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /dellemc_ansible/isilon/library/dellemc_isilon_gatherfacts.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # Copyright: (c) 2019, DellEMC 3 | 4 | """Ansible module for Gathering information about DellEMC Isilon""" 5 | 6 | from __future__ import (absolute_import, division, print_function) 7 | 8 | __metaclass__ = type 9 | ANSIBLE_METADATA = {'metadata_version': '1.1', 10 | 'status': ['preview'], 11 | 'supported_by': 'community' 12 | } 13 | 14 | DOCUMENTATION = r''' 15 | --- 16 | module: dellemc_isilon_gatherfacts 17 | 18 | version_added: '2.7' 19 | 20 | short_description: Gathering information about DellEMC Isilon Storage 21 | 22 | description: 23 | - Gathering information about DellEMC Isilon Storage System includes 24 | Get attributes of the Isilon cluster, 25 | Get list of access zones in an Isilon cluster, 26 | Get list of nodes in an Isilon cluster, 27 | Get list of authentication providers for an access zone, 28 | Get list of users and groups for an access zone. 29 | 30 | extends_documentation_fragment: 31 | - dellemc_isilon.dellemc_isilon 32 | 33 | author: 34 | - Ambuj Dubey (ambuj.dubey@dell.com) 35 | 36 | options: 37 | access_zone: 38 | description: 39 | - The access zone. If no Access Zone is specified, the 'System' access 40 | zone would be taken by default. 41 | default: 'System' 42 | type: str 43 | gather_subset: 44 | description: 45 | - List of string variables to specify the Isilon Storage System entities 46 | for which information is required. 47 | - List of all Isilon Storage System entities supported by the module - 48 | - attributes 49 | - access_zones 50 | - nodes 51 | - providers 52 | - users 53 | - groups 54 | - The list of attributes, access_zones and nodes is for the entire Isilon 55 | cluster 56 | - The list of providers, users and groups is specific to the specified 57 | access zone 58 | required: True 59 | choices: [attributes, access_zones, nodes, providers, users, groups] 60 | type: list 61 | ''' 62 | 63 | EXAMPLES = r''' 64 | - name: Get attributes of the Isilon cluster 65 | dellemc_isilon_gatherfacts: 66 | onefs_host: "{{onefs_host}}" 67 | port_no: "{{isilonport}}" 68 | verify_ssl: "{{verify_ssl}}" 69 | api_user: "{{api_user}}" 70 | api_password: "{{api_password}}" 71 | gather_subset: 72 | - attributes 73 | 74 | - name: Get access_zones of the Isilon cluster 75 | dellemc_isilon_gatherfacts: 76 | onefs_host: "{{onefs_host}}" 77 | port_no: "{{isilonport}}" 78 | verify_ssl: "{{verify_ssl}}" 79 | api_user: "{{api_user}}" 80 | api_password: "{{api_password}}" 81 | gather_subset: 82 | - access_zones 83 | 84 | - name: Get nodes of the Isilon cluster 85 | dellemc_isilon_gatherfacts: 86 | onefs_host: "{{onefs_host}}" 87 | port_no: "{{isilonport}}" 88 | verify_ssl: "{{verify_ssl}}" 89 | api_user: "{{api_user}}" 90 | api_password: "{{api_password}}" 91 | gather_subset: 92 | - nodes 93 | 94 | - name: Get list of authentication providers for an access zone of the 95 | Isilon cluster 96 | dellemc_isilon_gatherfacts: 97 | onefs_host: "{{onefs_host}}" 98 | port_no: "{{isilonport}}" 99 | verify_ssl: "{{verify_ssl}}" 100 | api_user: "{{api_user}}" 101 | api_password: "{{api_password}}" 102 | access_zone: "{{access_zone}}" 103 | gather_subset: 104 | - providers 105 | 106 | - name: Get list of users for an access zone of the Isilon cluster 107 | dellemc_isilon_gatherfacts: 108 | onefs_host: "{{onefs_host}}" 109 | port_no: "{{isilonport}}" 110 | verify_ssl: "{{verify_ssl}}" 111 | api_user: "{{api_user}}" 112 | api_password: "{{api_password}}" 113 | access_zone: "{{access_zone}}" 114 | gather_subset: 115 | - users 116 | 117 | - name: Get list of groups for an access zone of the Isilon cluster 118 | dellemc_isilon_gatherfacts: 119 | onefs_host: "{{onefs_host}}" 120 | port_no: "{{isilonport}}" 121 | verify_ssl: "{{verify_ssl}}" 122 | api_user: "{{api_user}}" 123 | api_password: "{{api_password}}" 124 | access_zone: "{{access_zone}}" 125 | gather_subset: 126 | - groups 127 | ''' 128 | 129 | RETURN = r''' ''' 130 | 131 | import logging 132 | from ansible.module_utils.basic import AnsibleModule 133 | from ansible.module_utils.storage.dell \ 134 | import dellemc_ansible_isilon_utils as utils 135 | import re 136 | 137 | LOG = utils.get_logger('dellemc_isilon_gatherfacts', 138 | log_devel=logging.INFO) 139 | HAS_ISILON_SDK = utils.has_isilon_sdk() 140 | ISILON_SDK_VERSION_CHECK = utils.isilon_sdk_version_check() 141 | 142 | 143 | class IsilonGatherFacts(object): 144 | """Class with Gather Fact operations""" 145 | 146 | def __init__(self): 147 | """Define all the parameters required by this module""" 148 | 149 | self.module_params = utils \ 150 | .get_isilon_management_host_parameters() 151 | self.module_params.update(get_isilon_gatherfacts_parameters()) 152 | 153 | # initialize the Ansible module 154 | self.module = AnsibleModule(argument_spec=self.module_params, 155 | supports_check_mode=False 156 | ) 157 | 158 | if HAS_ISILON_SDK is False: 159 | self.module.fail_json(msg='Ansible modules for Isilon ' 160 | 'require the Isilon python library' 161 | ' to be installed. Please install' 162 | ' the library before using these ' 163 | 'modules.') 164 | 165 | if ISILON_SDK_VERSION_CHECK and \ 166 | not ISILON_SDK_VERSION_CHECK['supported_version']: 167 | err_msg = ISILON_SDK_VERSION_CHECK['unsupported_version_message'] 168 | LOG.error(err_msg) 169 | self.module.fail_json(msg=err_msg) 170 | 171 | self.api_client = utils.get_isilon_connection(self.module.params) 172 | self.isi_sdk = utils.get_isilon_sdk() 173 | LOG.info('Got python SDK instance for provisioning on Isilon ') 174 | 175 | self.cluster_api = self.isi_sdk.ClusterApi(self.api_client) 176 | self.zone_api = self.isi_sdk.ZonesApi(self.api_client) 177 | self.auth_api = self.isi_sdk.AuthApi(self.api_client) 178 | 179 | def get_attributes_list(self): 180 | """Get the list of attributes of a given Isilon Storage""" 181 | try: 182 | config = (self.cluster_api.get_cluster_config()).to_dict() 183 | ips = self.cluster_api.get_cluster_external_ips() 184 | external_ip_str = ','.join(ips) 185 | external_ips = {"External IPs": external_ip_str} 186 | logon_msg = (self.cluster_api.get_cluster_identity()).to_dict() 187 | contact_info = (self.cluster_api.get_cluster_owner()).to_dict() 188 | cluster_version = (self.cluster_api.get_cluster_version())\ 189 | .to_dict() 190 | attribute = {"Config": config, "Contact_Info": contact_info, 191 | "External_IP": external_ips, 192 | "Logon_msg": logon_msg, 193 | "Cluster_Version": cluster_version} 194 | LOG.info("Got Attributes of Isilon cluster %s", 195 | self.module.params['onefs_host']) 196 | return attribute 197 | except Exception as e: 198 | error_msg = ( 199 | 'Get Attributes List for Isilon cluster: {0} failed' 200 | ' with error: {1}' .format( 201 | self.module.params['onefs_host'], 202 | self.determine_error(e))) 203 | LOG.error(error_msg) 204 | self.module.fail_json(msg=error_msg) 205 | 206 | def get_access_zones_list(self): 207 | """Get the list of access_zones of a given Isilon Storage""" 208 | try: 209 | access_zones_list = (self.zone_api.list_zones()).to_dict() 210 | LOG.info("Got Access zones from Isilon cluster %s", 211 | self.module.params['onefs_host']) 212 | return access_zones_list 213 | except Exception as e: 214 | error_msg = ( 215 | 'Get Access zone List for Isilon cluster: {0} failed' 216 | 'with error: {1}' .format( 217 | self.module.params['onefs_host'], 218 | self.determine_error(e))) 219 | LOG.error(error_msg) 220 | self.module.fail_json(msg=error_msg) 221 | 222 | def get_nodes_list(self): 223 | """Get the list of nodes of a given Isilon Storage""" 224 | try: 225 | nodes_list = (self.cluster_api.get_cluster_nodes()).to_dict() 226 | LOG.info('Got Nodes from Isilon cluster %s', 227 | self.module.params['onefs_host']) 228 | return nodes_list 229 | except Exception as e: 230 | error_msg = ( 231 | 'Get Nodes List for Isilon cluster: {0} failed with' 232 | 'error: {1}' .format( 233 | self.module.params['onefs_host'], 234 | self.determine_error(e))) 235 | LOG.error(error_msg) 236 | self.module.fail_json(msg=error_msg) 237 | 238 | def get_providers_list(self, access_zone): 239 | """Get the list of authentication providers for an access zone of a 240 | given Isilon Storage""" 241 | try: 242 | providers_list = (self.auth_api 243 | .get_providers_summary(zone=access_zone))\ 244 | .to_dict() 245 | LOG.info('Got authentication Providers from Isilon cluster %s', 246 | self.module.params['onefs_host']) 247 | return providers_list 248 | except Exception as e: 249 | error_msg = ( 250 | 'Get authentication Providers List for Isilon' 251 | ' cluster: {0} and access zone: {1} failed with' 252 | ' error: {2}' .format( 253 | self.module.params['onefs_host'], 254 | access_zone, 255 | self.determine_error(e))) 256 | LOG.error(error_msg) 257 | self.module.fail_json(msg=error_msg) 258 | 259 | def get_users_list(self, access_zone): 260 | """Get the list of users for an access zone of a given Isilon 261 | Storage""" 262 | try: 263 | users_list = (self.auth_api.list_auth_users(zone=access_zone))\ 264 | .to_dict() 265 | LOG.info('Got Users from Isilon cluster %s', 266 | self.module.params['onefs_host']) 267 | return users_list 268 | except Exception as e: 269 | error_msg = ( 270 | 'Get Users List for Isilon cluster: {0} and access zone: {1} ' 271 | 'failed with error: {2}' .format( 272 | self.module.params['onefs_host'], 273 | access_zone, 274 | self.determine_error(e))) 275 | LOG.error(error_msg) 276 | self.module.fail_json(msg=error_msg) 277 | 278 | def get_groups_list(self, access_zone): 279 | """Get the list of groups for an access zone of a given Isilon 280 | Storage""" 281 | try: 282 | group_list = ( 283 | self.auth_api.list_auth_groups( 284 | zone=access_zone)).to_dict() 285 | LOG.info('Got Groups from Isilon cluster %s', 286 | self.module.params['onefs_host']) 287 | return group_list 288 | except Exception as e: 289 | error_msg = ('Get Group List for Isilon cluster: {0} and' 290 | 'access zone: {1} failed with error: {2}'.format( 291 | self.module.params['onefs_host'], 292 | access_zone, 293 | self.determine_error(e))) 294 | LOG.error(error_msg) 295 | self.module.fail_json(msg=error_msg) 296 | 297 | def determine_error(self, error_obj): 298 | '''Format the error object''' 299 | if isinstance(error_obj, utils.ApiException): 300 | error = re.sub("[\n \"]+", ' ', str(error_obj.body)) 301 | else: 302 | error = str(error_obj) 303 | return error 304 | 305 | def perform_module_operation(self): 306 | """Perform different actions on Gatherfacts based on user parameter 307 | chosen in playbook 308 | """ 309 | access_zone = self.module.params['access_zone'] 310 | subset = self.module.params['gather_subset'] 311 | if not subset: 312 | self.module.fail_json(msg="Please specify gather_subset") 313 | 314 | attributes = [] 315 | access_zones = [] 316 | nodes = [] 317 | providers = [] 318 | users = [] 319 | groups = [] 320 | if 'attributes' in str(subset): 321 | attributes = self.get_attributes_list() 322 | if 'access_zones' in str(subset): 323 | access_zones = self.get_access_zones_list() 324 | if 'nodes' in str(subset): 325 | nodes = self.get_nodes_list() 326 | if 'providers' in str(subset): 327 | providers = self.get_providers_list(access_zone) 328 | if 'users' in str(subset): 329 | users = self.get_users_list(access_zone) 330 | if 'groups' in str(subset): 331 | groups = self.get_groups_list(access_zone) 332 | self.module.exit_json( 333 | Attributes=attributes, 334 | AccessZones=access_zones, 335 | Nodes=nodes, 336 | Providers=providers, 337 | Users=users, 338 | Groups=groups) 339 | 340 | 341 | def get_isilon_gatherfacts_parameters(): 342 | """This method provide parameter required for the ansible gatherfacts 343 | modules on Isilon""" 344 | return dict( 345 | access_zone=dict(required=False, type='str', 346 | default='System'), 347 | gather_subset=dict(type='list', required=True, 348 | choices=['attributes', 349 | 'access_zones', 350 | 'nodes', 351 | 'providers', 352 | 'users', 353 | 'groups' 354 | ]), 355 | ) 356 | 357 | 358 | def main(): 359 | """Create Isilon GatherFacts object and perform action on it 360 | based on user input from playbook""" 361 | obj = IsilonGatherFacts() 362 | obj.perform_module_operation() 363 | 364 | 365 | if __name__ == '__main__': 366 | main() 367 | -------------------------------------------------------------------------------- /dellemc_ansible/isilon/library/dellemc_isilon_accesszone.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # Copyright: (c) 2019, DellEMC 3 | 4 | """Ansible module for managing access zones on Isilon""" 5 | 6 | from __future__ import absolute_import, division, print_function 7 | 8 | __metaclass__ = type 9 | ANSIBLE_METADATA = {'metadata_version': '1.1', 10 | 'status': ['preview'], 11 | 'supported_by': 'community' 12 | } 13 | 14 | DOCUMENTATION = r''' 15 | --- 16 | module: dellemc_isilon_accesszone 17 | 18 | version_added: '2.7' 19 | 20 | short_description: Manage access zones on Isilon 21 | 22 | description: 23 | - Managing access zones on Isilon storage system includes getting details of 24 | access zone and modifying smb and nfs settings. 25 | 26 | extends_documentation_fragment: 27 | - dellemc_isilon.dellemc_isilon 28 | 29 | author: 30 | - Akash Shendge (@shenda1) 31 | 32 | options: 33 | az_name: 34 | description: 35 | - The name of the access zone. 36 | type: str 37 | required: True 38 | 39 | smb: 40 | description: 41 | - Specifies the default SMB setting parameters of access zone. 42 | type: dict 43 | suboptions: 44 | create_permissions: 45 | description: 46 | - Sets the default source permissions to apply when a file or 47 | directory is created. 48 | type: str 49 | choices: [default acl, Inherit mode bits, Use create mask and mode] 50 | default: default acl 51 | directory_create_mask: 52 | description: 53 | - Specifies UNIX mask bits(octal) that are removed when a directory 54 | is created, restricting permissions. 55 | - Mask bits are applied before mode bits are applied. 56 | type: str 57 | directory_create_mode: 58 | description: 59 | - Specifies UNIX mode bits(octal) that are added when a directory is 60 | created, enabling permissions. 61 | type: str 62 | file_create_mask: 63 | description: 64 | - Specifies UNIX mask bits(octal) that are removed when a file is 65 | created, restricting permissions. 66 | type: str 67 | file_create_mode: 68 | description: 69 | - Specifies UNIX mode bits(octal) that are added when a file is 70 | created, enabling permissions. 71 | type: str 72 | access_based_enumeration: 73 | description: 74 | - Allows access based enumeration only on the files and folders that 75 | the requesting user can access. 76 | type: bool 77 | access_based_enumeration_root_only: 78 | description: 79 | - Access-based enumeration on only the root directory of the share. 80 | type: bool 81 | ntfs_acl_support: 82 | description: 83 | - Allows ACLs to be stored and edited from SMB clients. 84 | type: bool 85 | oplocks: 86 | description: 87 | - An oplock allows clients to provide performance improvements by 88 | using locally-cached information. 89 | type: bool 90 | 91 | nfs: 92 | description: 93 | - Specifies the default NFS setting parameters of access zone. 94 | type: dict 95 | suboptions: 96 | commit_asynchronous: 97 | description: 98 | - Set to True if NFS commit requests execute asynchronously. 99 | type: bool 100 | nfsv4_domain: 101 | description: 102 | - Specifies the domain or realm through which users and groups are 103 | associated. 104 | type: str 105 | nfsv4_allow_numeric_ids: 106 | description: 107 | - If true, sends owners and groups as UIDs and GIDs when look up 108 | fails or if the 'nfsv4_no_name' property is set to 1. 109 | type: bool 110 | nfsv4_no_domain: 111 | description: 112 | - If true, sends owners and groups without a domain name. 113 | type: bool 114 | nfsv4_no_domain_uids: 115 | description: 116 | - If true, sends UIDs and GIDs without a domain name. 117 | type: bool 118 | nfsv4_no_names: 119 | description: 120 | - If true, sends owners and groups as UIDs and GIDs. 121 | type: bool 122 | 123 | state: 124 | description: 125 | - Define whether the access zone should exist or not. 126 | - present - indicates that the access zone should exist on the system. 127 | - absent - indicates that the access zone should not exist on the system. 128 | choices: [absent, present] 129 | type: str 130 | required: True 131 | 132 | notes: 133 | - Creation/Deletion of access zone is not allowed through Ansible module. 134 | ''' 135 | 136 | EXAMPLES = r''' 137 | - name: Get details of access zone including smb and nfs settings 138 | dellemc_isilon_accesszone: 139 | onefs_host: "{{onefs_host}}" 140 | api_user: "{{api_user}}" 141 | api_password: "{{api_password}}" 142 | verify_ssl: "{{verify_ssl}}" 143 | az_name: "{{access zone}}" 144 | state: "present" 145 | 146 | - name: Modify smb settings of access zone 147 | dellemc_isilon_accesszone: 148 | onefs_host: "{{onefs_host}}" 149 | api_user: "{{api_user}}" 150 | api_password: "{{api_password}}" 151 | verify_ssl: "{{verify_ssl}}" 152 | az_name: "{{access zone}}" 153 | state: "present" 154 | smb: 155 | create_permissions: 'default acl' 156 | directory_create_mask: '777' 157 | directory_create_mode: '700' 158 | file_create_mask: '700' 159 | file_create_mode: '100' 160 | access_based_enumeration: true 161 | access_based_enumeration_root_only: false 162 | ntfs_acl_support: true 163 | oplocks: true 164 | 165 | - name: Modify nfs settings of access zone 166 | dellemc_isilon_accesszone: 167 | onefs_host: "{{onefs_host}}" 168 | api_user: "{{api_user}}" 169 | api_password: "{{api_password}}" 170 | verify_ssl: "{{verify_ssl}}" 171 | az_name: "{{access zone}}" 172 | state: "present" 173 | nfs: 174 | commit_asynchronous: false 175 | nfsv4_allow_numeric_ids: false 176 | nfsv4_domain: 'localhost' 177 | nfsv4_no_domain: false 178 | nfsv4_no_domain_uids: false 179 | nfsv4_no_names: false 180 | 181 | - name: Modify smb and nfs settings of access zone 182 | dellemc_isilon_accesszone: 183 | onefs_host: "{{onefs_host}}" 184 | api_user: "{{api_user}}" 185 | api_password: "{{api_password}}" 186 | verify_ssl: "{{verify_ssl}}" 187 | az_name: "{{access zone}}" 188 | state: "present" 189 | smb: 190 | create_permissions: 'default acl' 191 | directory_create_mask: '777' 192 | directory_create_mode: '700' 193 | file_create_mask: '700' 194 | file_create_mode: '100' 195 | access_based_enumeration: true 196 | access_based_enumeration_root_only: false 197 | ntfs_acl_support: true 198 | oplocks: true 199 | nfs: 200 | commit_asynchronous: false 201 | nfsv4_allow_numeric_ids: false 202 | nfsv4_domain: 'localhost' 203 | nfsv4_no_domain: false 204 | nfsv4_no_domain_uids: false 205 | nfsv4_no_names: false 206 | 207 | ''' 208 | 209 | RETURN = r''' 210 | changed: 211 | description: Whether or not the resource has changed 212 | returned: always 213 | type: bool 214 | 215 | smb_modify_flag: 216 | description: Whether or not the default SMB settings of access zone has 217 | changed 218 | returned: on success 219 | type: bool 220 | 221 | nfs_modify_flag: 222 | description: Whether or not the default NFS settings of access zone has 223 | changed 224 | returned: on success 225 | type: bool 226 | 227 | access_zone_details: 228 | description: The access zone details 229 | returned: When access zone exists 230 | type: complex 231 | contains: 232 | nfs_settings: 233 | description: NFS settings of access zone 234 | type: complex 235 | contains: 236 | export_settings: 237 | description: Default values for NFS exports 238 | type: complex 239 | contains: 240 | commit_asynchronous: 241 | description: 242 | - Set to True if NFS commit requests execute asynchronously 243 | type: bool 244 | zone_settings: 245 | description: NFS server settings for this zone 246 | type: complex 247 | contains: 248 | nfsv4_domain: 249 | description: 250 | - Specifies the domain or realm through which users and groups are associated 251 | type: str 252 | nfsv4_allow_numeric_ids: 253 | description: 254 | - If true, sends owners and groups as UIDs and GIDs when look up fails or if the 'nfsv4_no_name' property is set to 1 255 | type: bool 256 | nfsv4_no_domain: 257 | description: 258 | - If true, sends owners and groups without a domain name 259 | type: bool 260 | nfsv4_no_domain_uids: 261 | description: 262 | - If true, sends UIDs and GIDs without a domain name 263 | type: bool 264 | nfsv4_no_names: 265 | description: 266 | - If true, sends owners and groups as UIDs and GIDs 267 | type: bool 268 | smb_settings: 269 | description: SMB settings of access zone 270 | type: complex 271 | contains: 272 | directory_create_mask(octal): 273 | description: 274 | - UNIX mask bits for directory in octal format 275 | type: str 276 | directory_create_mode(octal): 277 | description: 278 | - UNIX mode bits for directory in octal format 279 | type: str 280 | file_create_mask(octal): 281 | description: 282 | - UNIX mask bits for file in octal format 283 | type: str 284 | file_create_mode(octal): 285 | description: 286 | - UNIX mode bits for file in octal format 287 | type: str 288 | ''' 289 | 290 | import logging 291 | import re 292 | from ansible.module_utils.basic import AnsibleModule 293 | from ansible.module_utils.storage.dell \ 294 | import dellemc_ansible_isilon_utils as utils 295 | 296 | LOG = utils.get_logger('dellemc_isilon_accesszone', log_devel=logging.INFO) 297 | HAS_ISILON_SDK = utils.has_isilon_sdk() 298 | 299 | ISILON_SDK_VERSION_CHECK = utils.isilon_sdk_version_check() 300 | 301 | 302 | class IsilonAccessZone(object): 303 | """Class with access zone operations""" 304 | 305 | def __init__(self): 306 | """ Define all parameters required by this module""" 307 | self.module_params = utils.get_isilon_management_host_parameters() 308 | self.module_params.update(get_isilon_accesszone_parameters()) 309 | 310 | # initialize the Ansible module 311 | self.module = AnsibleModule( 312 | argument_spec=self.module_params, 313 | supports_check_mode=False 314 | ) 315 | 316 | if HAS_ISILON_SDK is False: 317 | self.module.fail_json(msg="Ansible modules for Isilon require the" 318 | " isi_sdk_8_1_1 python library to be " 319 | "installed. Please install the library " 320 | "before using these modules.") 321 | 322 | if ISILON_SDK_VERSION_CHECK and \ 323 | not ISILON_SDK_VERSION_CHECK['supported_version']: 324 | err_msg = ISILON_SDK_VERSION_CHECK['unsupported_version_message'] 325 | LOG.error(err_msg) 326 | self.module.fail_json(msg=err_msg) 327 | 328 | self.api_client = utils.get_isilon_connection(self.module.params) 329 | self.api_instance = utils.isi_sdk.ZonesApi(self.api_client) 330 | self.api_protocol = utils.isi_sdk.ProtocolsApi(self.api_client) 331 | LOG.info('Got the isi_sdk instance for authorization on to Isilon') 332 | 333 | def get_details(self, name): 334 | """ Get access zone details""" 335 | try: 336 | nfs_settings = {} 337 | api_response = self.api_instance.get_zone(name).to_dict() 338 | nfs_export_settings = self.api_protocol.get_nfs_settings_export( 339 | zone=name).to_dict() 340 | nfs_export_settings['export_settings'] = nfs_export_settings[ 341 | 'settings'] 342 | del nfs_export_settings['settings'] 343 | nfs_zone_settings = self.api_protocol.get_nfs_settings_zone( 344 | zone=name).to_dict() 345 | nfs_zone_settings['zone_settings'] = nfs_zone_settings['settings'] 346 | del nfs_zone_settings['settings'] 347 | 348 | nfs_settings['nfs_settings'] = nfs_export_settings 349 | nfs_settings['nfs_settings'].update(nfs_zone_settings) 350 | 351 | api_response.update(nfs_settings) 352 | smb_settings = self.api_protocol.get_smb_settings_share( 353 | zone=name).to_dict() 354 | smb_settings['settings']['directory_create_mask(octal)'] = \ 355 | "{0:o}".format(smb_settings['settings'] 356 | ['directory_create_mask']) 357 | smb_settings['settings']['directory_create_mode(octal)'] = \ 358 | "{0:o}".format(smb_settings['settings'] 359 | ['directory_create_mode']) 360 | smb_settings['settings']['file_create_mask(octal)'] = \ 361 | "{0:o}".format(smb_settings['settings'] 362 | ['file_create_mask']) 363 | smb_settings['settings']['file_create_mode(octal)'] = \ 364 | "{0:o}".format(smb_settings['settings'] 365 | ['file_create_mode']) 366 | smb_settings['smb_settings'] = smb_settings['settings'] 367 | del smb_settings['settings'] 368 | api_response.update(smb_settings) 369 | return api_response 370 | except utils.ApiException as e: 371 | if str(e.status) == '404': 372 | error_message = "Access zone {0} details are not found".\ 373 | format(name) 374 | LOG.info(error_message) 375 | return None 376 | else: 377 | error_msg = self.determine_error(error_obj=e) 378 | error_message = 'Get details of access zone {0} failed with ' \ 379 | 'error: {1}'.format(name, error_msg) 380 | LOG.error(error_message) 381 | self.module.fail_json(msg=error_message) 382 | except Exception as e: 383 | error_message = 'Get details of access zone {0} failed with ' \ 384 | 'error: {1}'.format(name, str(e)) 385 | LOG.error(error_message) 386 | self.module.fail_json(msg=error_message) 387 | 388 | def is_smb_modification_required(self, smb_playbook, access_zone_details): 389 | """ Check if default smb settings of access zone needs to be modified 390 | """ 391 | # Convert octal parameters to decimal for comparison 392 | try: 393 | if 'directory_create_mask' in smb_playbook and \ 394 | smb_playbook['directory_create_mask'] is not None: 395 | smb_playbook['directory_create_mask'] = int( 396 | smb_playbook['directory_create_mask'], 8) 397 | if 'directory_create_mode' in smb_playbook and \ 398 | smb_playbook['directory_create_mode'] is not None: 399 | smb_playbook['directory_create_mode'] = int( 400 | smb_playbook['directory_create_mode'], 8) 401 | if 'file_create_mask' in smb_playbook and \ 402 | smb_playbook['file_create_mask'] is not None: 403 | smb_playbook['file_create_mask'] = int( 404 | smb_playbook['file_create_mask'], 8) 405 | if 'file_create_mode' in smb_playbook and \ 406 | smb_playbook['file_create_mode'] is not None: 407 | smb_playbook['file_create_mode'] = int( 408 | smb_playbook['file_create_mode'], 8) 409 | except Exception as e: 410 | error_msg = self.determine_error(error_obj=e) 411 | error_message = 'Conversion from octal to decimal failed with ' \ 412 | 'error: {0}'.format(error_msg) 413 | LOG.error(error_message) 414 | self.module.fail_json(msg=error_message) 415 | 416 | for key in smb_playbook.keys(): 417 | if smb_playbook[key] != access_zone_details['smb_settings'][key]: 418 | LOG.info("First Key Modification %s", key) 419 | return True 420 | return False 421 | 422 | def smb_modify(self, name, smb): 423 | """ Modify smb settings of access zone """ 424 | try: 425 | self.api_protocol.update_smb_settings_share(smb, zone=name) 426 | LOG.info("Modification Successful") 427 | return True 428 | except Exception as e: 429 | error_msg = self.determine_error(error_obj=e) 430 | error_message = 'Modify SMB share settings of access zone {0} ' \ 431 | 'failed with error: {1}'.format(name, error_msg) 432 | LOG.error(error_message) 433 | self.module.fail_json(msg=error_message) 434 | 435 | def is_nfs_modification_required(self, nfs_playbook, access_zone_details): 436 | """ Check if default nfs settings of access zone needs to be modified 437 | """ 438 | nfs_export_flag = False 439 | nfs_zone_flag = False 440 | 441 | for key in nfs_playbook.keys(): 442 | if key in access_zone_details['nfs_settings']['export_settings'].\ 443 | keys(): 444 | if nfs_playbook[key] != access_zone_details['nfs_settings'][ 445 | 'export_settings'][key]: 446 | LOG.info("First Key Modification %s", key) 447 | nfs_export_flag = True 448 | 449 | for key in nfs_playbook.keys(): 450 | if key in access_zone_details['nfs_settings']['zone_settings'].\ 451 | keys(): 452 | if nfs_playbook[key] != access_zone_details['nfs_settings'][ 453 | 'zone_settings'][key]: 454 | LOG.info("First Key Modification %s", key) 455 | nfs_zone_flag = True 456 | 457 | return nfs_export_flag, nfs_zone_flag 458 | 459 | def nfs_modify(self, name, nfs, nfs_export_flag, nfs_zone_flag): 460 | """ Modify nfs settings of access zone """ 461 | nfs_export_dict = {} 462 | nfs_zone_dict = {} 463 | 464 | if nfs_export_flag: 465 | if 'commit_asynchronous' in nfs: 466 | nfs_export_dict['commit_asynchronous'] = nfs[ 467 | 'commit_asynchronous'] 468 | 469 | if nfs_zone_flag: 470 | if 'nfsv4_domain' in nfs: 471 | nfs_zone_dict['nfsv4_domain'] = nfs['nfsv4_domain'] 472 | if 'nfsv4_allow_numeric_ids' in nfs: 473 | nfs_zone_dict['nfsv4_allow_numeric_ids'] = nfs[ 474 | 'nfsv4_allow_numeric_ids'] 475 | if 'nfsv4_no_domain' in nfs: 476 | nfs_zone_dict['nfsv4_no_domain'] = nfs['nfsv4_no_domain'] 477 | if 'nfsv4_no_domain_uids' in nfs: 478 | nfs_zone_dict['nfsv4_no_domain_uids'] = nfs[ 479 | 'nfsv4_no_domain_uids'] 480 | if 'nfsv4_no_names' in nfs: 481 | nfs_zone_dict['nfsv4_no_names'] = nfs['nfsv4_no_names'] 482 | 483 | try: 484 | if nfs_export_flag: 485 | self.api_protocol.update_nfs_settings_export(nfs_export_dict, 486 | zone=name) 487 | 488 | if nfs_zone_flag: 489 | self.api_protocol.update_nfs_settings_zone(nfs_zone_dict, 490 | zone=name) 491 | return True 492 | except Exception as e: 493 | error_msg = self.determine_error(error_obj=e) 494 | error_message = 'Modify NFS export settings of access zone {0} ' \ 495 | 'failed with error: {1}'.format(name, error_msg) 496 | LOG.error(error_message) 497 | self.module.fail_json(msg=error_message) 498 | 499 | def determine_error(self, error_obj): 500 | """Determine the error message to return""" 501 | if isinstance(error_obj, utils.ApiException): 502 | error = error_obj.body 503 | error = re.sub('[^A-Za-z:.,]+', ' ', str(error)) 504 | else: 505 | error = error_obj 506 | return error 507 | 508 | def perform_module_operation(self): 509 | """ 510 | Perform different actions on access zone module based on parameters 511 | chosen in playbook 512 | """ 513 | name = self.module.params['az_name'] 514 | state = self.module.params['state'] 515 | smb = self.module.params['smb'] 516 | nfs = self.module.params['nfs'] 517 | 518 | # result is a dictionary that contains changed status and access zone 519 | # details 520 | result = dict( 521 | changed=False, 522 | smb_modify_flag=False, 523 | nfs_modify_flag=False, 524 | access_zone_details='' 525 | ) 526 | 527 | access_zone_details = self.get_details(name) 528 | 529 | if state == 'present' and not access_zone_details: 530 | error_message = 'Access zone {0} not found - Creation of access' \ 531 | ' zone is not allowed through Ansible module'.\ 532 | format(name) 533 | LOG.error(error_message) 534 | self.module.fail_json(msg=error_message) 535 | 536 | if state == 'absent' and access_zone_details: 537 | error_message = 'Deletion of access zone is not allowed through' \ 538 | ' Ansible module' 539 | LOG.error(error_message) 540 | self.module.fail_json(msg=error_message) 541 | 542 | if state == 'present' and smb is not None: 543 | smb_modify_flag = self.is_smb_modification_required( 544 | smb, access_zone_details) 545 | LOG.info("SMB modification flag %s", smb_modify_flag) 546 | 547 | if smb_modify_flag: 548 | result['smb_modify_flag'] = self.smb_modify(name, smb) 549 | 550 | if state == 'present' and nfs is not None: 551 | nfs_export_flag, nfs_zone_flag = self.\ 552 | is_nfs_modification_required(nfs, access_zone_details) 553 | LOG.info("NFS modification flag %s %s", nfs_export_flag, 554 | nfs_zone_flag) 555 | 556 | if nfs_export_flag or nfs_zone_flag: 557 | result['nfs_modify_flag'] = self.nfs_modify( 558 | name, nfs, nfs_export_flag, nfs_zone_flag) 559 | 560 | result['access_zone_details'] = access_zone_details 561 | if result['smb_modify_flag'] or result['nfs_modify_flag']: 562 | access_zone_details = self.get_details(name) 563 | result['access_zone_details'] = access_zone_details 564 | result['changed'] = True 565 | self.module.exit_json(**result) 566 | 567 | 568 | def get_isilon_accesszone_parameters(): 569 | """This method provide parameter required for the ansible access zone 570 | modules on Isilon""" 571 | return dict( 572 | az_name=dict(required=True, type='str'), 573 | smb=dict(required=False, type='dict'), 574 | nfs=dict(required=False, type='dict'), 575 | state=dict(required=True, type='str', choices=['present', 'absent']) 576 | ) 577 | 578 | 579 | def main(): 580 | """ Create Isilon access zone object and perform action on it 581 | based on user input from playbook""" 582 | obj = IsilonAccessZone() 583 | obj.perform_module_operation() 584 | 585 | 586 | if __name__ == '__main__': 587 | main() 588 | -------------------------------------------------------------------------------- /dellemc_ansible/isilon/library/dellemc_isilon_group.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # Copyright: (c) 2019, DellEMC 3 | """ Ansible module for managing Groups on Isilon""" 4 | 5 | from __future__ import (absolute_import, division, print_function) 6 | 7 | __metaclass__ = type 8 | ANSIBLE_METADATA = {'metadata_version': '1.1', 9 | 'status': ['preview'], 10 | 'supported_by': 'community'} 11 | 12 | DOCUMENTATION = r''' 13 | --- 14 | module: dellemc_isilon_group 15 | 16 | version_added: "2.7" 17 | 18 | short_description: Manage Groups on the Isilon Storage System 19 | description: 20 | - Managing Groups on the Isilon Storage System includes create group, 21 | delete group, get group, add users and remove users. 22 | 23 | extends_documentation_fragment: 24 | - dellemc_isilon.dellemc_isilon 25 | author: 26 | - P Srinivas Rao (@srinivas-rao5) srinivas_rao5@dell.com 27 | options: 28 | group_name: 29 | description: 30 | - The name of the group. 31 | - Required at the time of group creation, for rest of the operations 32 | either group_name or group_id is required. 33 | type: str 34 | group_id: 35 | description: 36 | - The group_id is auto generated at the time of creation. 37 | - For all other operations either group_name or group_id is needed. 38 | type: str 39 | access_zone: 40 | description: 41 | - This option mentions the zone in which a group is created. 42 | - For creation, access_zone acts as an attribute for the group. 43 | - For all other operations access_zone acts as a filter. 44 | type: str 45 | default: 'system' 46 | provider_type: 47 | description: 48 | - This option defines the type which will be used to 49 | authenticate the group members. 50 | - Creation, Deletion and Modification is allowed only for local group. 51 | - Details of groups of all provider types can be fetched. 52 | - If the provider_type is 'ads' then domain name of the Active 53 | Directory Server has to be mentioned in the group_name. 54 | The format for the group_name should be 'DOMAIN_NAME\group_name' 55 | or "DOMAIN_NAME\\group_name". 56 | - This option acts as a filter for all operations except creation. 57 | type: str 58 | default: 'local' 59 | choices: [ 'local', 'file', 'ldap', 'ads'] 60 | state: 61 | description: 62 | - The state option is used to determine whether the group 63 | will exist or not. 64 | type: str 65 | required: True 66 | choices: [ 'absent', 'present'] 67 | users: 68 | description: 69 | - Either user_name or user_id is needed to add or remove the user 70 | from the group. 71 | - users can be part of multiple groups. 72 | type: list 73 | user_state: 74 | description: 75 | - The user_state option is used to determine whether the users 76 | will exist for a particular group or not. 77 | - It is required when users are added or removed from a group. 78 | type: str 79 | choices: ['present-in-group', 'absent-in-group'] 80 | ''' 81 | 82 | EXAMPLES = r''' 83 | - name: Create a Group 84 | dellemc_isilon_group: 85 | onefs_host: "{{onefs_host}}" 86 | api_user: "{{api_user}}" 87 | api_password: "{{api_password}}" 88 | verify_ssl: "{{verify_ssl}}" 89 | access_zone: "{{access_zone}}" 90 | provider_type: "{{provider_type}}" 91 | group_name: "{{group_name}}" 92 | state: "present" 93 | 94 | - name: Create Group with Users 95 | dellemc_isilon_group: 96 | onefs_host: "{{onefs_host}}" 97 | api_user: "{{api_user}}" 98 | api_password: "{{api_password}}" 99 | verify_ssl: "{{verify_ssl}}" 100 | provider_type: "{{provider_type}}" 101 | access_zone: "{{access_zone}}" 102 | group_name: "{{group_name}}" 103 | users: 104 | - user_name: "{{user_name}}" 105 | - user_id: "{{user_id}}" 106 | - user_name: "{{user_name_2}}" 107 | user_state: "present-in-group" 108 | state: "present" 109 | 110 | - name: Get Details of the Group using Group Id 111 | dellemc_isilon_group: 112 | onefs_host: "{{onefs_host}}" 113 | api_user: "{{api_user}}" 114 | api_password: "{{api_password}}" 115 | verify_ssl: "{{verify_ssl}}" 116 | provider_type: "{{provider_type}}" 117 | access_zone: "{{access_zone}}" 118 | group_id: "{{group_id}}" 119 | state: "present" 120 | 121 | - name: Delete the Group using Group Name 122 | dellemc_isilon_group: 123 | onefs_host: "{{onefs_host}}" 124 | api_user: "{{api_user}}" 125 | api_password: "{{api_password}}" 126 | verify_ssl: "{{verify_ssl}}" 127 | provider_type: "{{provider_type}}" 128 | access_zone: "{{access_zone}}" 129 | group_name: "{{group_name}}" 130 | state: "absent" 131 | 132 | - name: Add Users to a Group 133 | dellemc_isilon_group: 134 | onefs_host: "{{onefs_host}}" 135 | api_user: "{{api_user}}" 136 | api_password: "{{api_password}}" 137 | verify_ssl: "{{verify_ssl}}" 138 | provider_type: "{{provider_type}}" 139 | access_zone: "{{access_zone}}" 140 | group_id: "{{group_id}}" 141 | users: 142 | - user_name: "{{user_name}}" 143 | - user_id: "{{user_id}}" 144 | - user_name: "{{user_name_2}}" 145 | user_state: "present-in-group" 146 | state: "present" 147 | 148 | - name: Remove Users from a Group 149 | dellemc_isilon_group: 150 | onefs_host: "{{onefs_host}}" 151 | api_user: "{{api_user}}" 152 | api_password: "{{api_password}}" 153 | verify_ssl: "{{verify_ssl}}" 154 | provider_type: "{{provider_type}}" 155 | access_zone: "{{access_zone}}" 156 | group_id: "{{group_id}}" 157 | users: 158 | - user_name: "{{user_name_1}}" 159 | - user_id: "{{user_id}}" 160 | - user_name: "{{user_name_2}}" 161 | user_state: "absent-in-group" 162 | state: "present" 163 | ''' 164 | 165 | RETURN = r''' 166 | changed: 167 | description: Whether or not the resource has changed 168 | returned: always 169 | type: bool 170 | group_details: 171 | description: Details of the group 172 | returned: When group exists 173 | type: complex 174 | contains: 175 | gid: 176 | description: 177 | - The details of the primary group for the user. 178 | type: complex 179 | contains: 180 | id: 181 | description: 182 | - The id of the group. 183 | type: str 184 | name: 185 | description: 186 | - The name of the group. 187 | type_of_resource: 188 | description: 189 | - The resource's type is mentioned. 190 | type: str 191 | sample: "group" 192 | name: 193 | description: 194 | - The name of the group. 195 | type: str 196 | provider: 197 | description: 198 | - The provider contains the provider type and access zone. 199 | type: str 200 | sample: "lsa-local-provider:system" 201 | members: 202 | description: 203 | - The list of sid's the members of group. 204 | type: complex 205 | contains: 206 | sid: 207 | description: 208 | - The details of the associated resource. 209 | type: complex 210 | contains: 211 | id: 212 | description: 213 | - The unique security identifier of the 214 | resource. 215 | type: str 216 | name: 217 | description: 218 | - The name of the resource. 219 | type: str 220 | type_of_resource: 221 | description: 222 | - The resource's type is mentioned. 223 | type: str 224 | sample: "user" 225 | ''' 226 | 227 | from ansible.module_utils.basic import AnsibleModule 228 | from ansible.module_utils.storage.dell \ 229 | import dellemc_ansible_isilon_utils as utils 230 | import re 231 | 232 | LOG = utils.get_logger('dellemc_isilon_group', log_devel=utils.logging.INFO) 233 | HAS_ISILON_SDK = utils.has_isilon_sdk() 234 | ISILON_SDK_VERSION_CHECK = utils.isilon_sdk_version_check() 235 | 236 | 237 | class IsilonGroup(object): 238 | """Class with group operations""" 239 | 240 | def __init__(self): 241 | """ Define all parameters required by this module""" 242 | 243 | self.module_params = utils.get_isilon_management_host_parameters() 244 | self.module_params.update(get_isilon_group_parameters()) 245 | 246 | mutually_exclusive = [['group_name', 'group_id']] 247 | 248 | required_one_of = [['group_name', 'group_id']] 249 | # initialize the ansible module 250 | self.module = AnsibleModule(argument_spec=self.module_params, 251 | supports_check_mode=False, 252 | mutually_exclusive=mutually_exclusive, 253 | required_one_of=required_one_of) 254 | 255 | # result is a dictionary that contains changed status and 256 | # group details 257 | self.result = {"changed": False} 258 | if HAS_ISILON_SDK is False: 259 | self.module.fail_json( 260 | msg="Ansible modules for Isilon require the isilon " 261 | "python library to be installed. Please install" 262 | " the library before using these modules.") 263 | 264 | if ISILON_SDK_VERSION_CHECK and \ 265 | not ISILON_SDK_VERSION_CHECK['supported_version']: 266 | err_msg = ISILON_SDK_VERSION_CHECK['unsupported_version_message'] 267 | LOG.error(err_msg) 268 | self.module.fail_json(msg=err_msg) 269 | 270 | self.api_client = utils.get_isilon_connection(self.module.params) 271 | self.api_instance = utils.isi_sdk.AuthApi(self.api_client) 272 | self.group_api_instance = utils.isi_sdk.AuthGroupsApi( 273 | self.api_client) 274 | 275 | LOG.info('Got the isi_sdk instance for authorization on to Isilon') 276 | 277 | def check_provider_type(self, provider, message): 278 | """ Check the provider and return the updated provider""" 279 | if provider.lower() != "local": 280 | error_message = \ 281 | "%s group is allowed only" \ 282 | " if provider_type is local, got '%s' provider" \ 283 | % (message, provider) 284 | LOG.error(error_message) 285 | self.module.fail_json(msg=error_message) 286 | return provider 287 | 288 | def create_user_objects(self, users, user_state): 289 | users_list = [] 290 | if user_state == 'present-in-group' and users: 291 | for user in users: 292 | if not isinstance(user, dict): 293 | self.module.fail_json( 294 | msg="Key Value pair is allowed, Provided %s." 295 | % user) 296 | if len(user.keys()) != 1: 297 | self.module.fail_json( 298 | msg="One Key per dictionary is allowed, %s" 299 | " given" % user.keys()) 300 | if 'user_name' in user: 301 | user = utils.isi_sdk.AuthAccessAccessItemFileGroup( 302 | "USER:" + user['user_name']) 303 | users_list.append(user) 304 | elif 'user_id' in user: 305 | user = utils.isi_sdk.AuthAccessAccessItemFileGroup( 306 | "UID:" + user['user_id']) 307 | users_list.append(user) 308 | else: 309 | error = 'user_id or user_name is expected,' \ 310 | ' "%s" given.' % list(user.keys())[0] 311 | self.module.fail_json(msg=error) 312 | return users_list 313 | 314 | def create_group(self, group_name, zone, provider, users_list): 315 | """Create Group in Isilon""" 316 | try: 317 | LOG.info("Creating Group %s", group_name) 318 | provider = self.check_provider_type(provider, 'Create') 319 | auth_group = utils.isi_sdk.AuthGroupCreateParams( 320 | name=group_name, members=users_list) 321 | api_response = self.api_instance.create_auth_group( 322 | auth_group=auth_group, zone=zone, provider=provider) 323 | LOG.info("The group is created with id: %s", str(api_response)) 324 | return True 325 | except Exception as e: 326 | error = self.determine_error(error_obj=e) 327 | error_message = "Create Group %s failed with %s" \ 328 | % (group_name, error) 329 | LOG.error(error_message) 330 | self.module.fail_json(msg=error_message) 331 | 332 | def delete_group(self, group, zone, provider): 333 | """Delete Group in Isilon""" 334 | try: 335 | LOG.info("Deleting Group %s", group) 336 | provider = self.check_provider_type(provider, 'Delete') 337 | self.api_instance.delete_auth_group( 338 | group, zone=zone, provider=provider) 339 | return True 340 | except Exception as e: 341 | error = self.determine_error(error_obj=e) 342 | error_message = "Delete %s failed with %s" \ 343 | % (group, error) 344 | LOG.error(error_message) 345 | self.module.fail_json(msg=error_message) 346 | 347 | def get_group_details(self, group, zone, provider): 348 | """Get the Group Details in Isilon""" 349 | try: 350 | LOG.info("Getting Details of group %s ", group) 351 | api_response = self.api_instance.get_auth_group( 352 | auth_group_id=group, 353 | provider=provider, zone=zone) 354 | LOG.info("Group Details: %s", str(api_response)) 355 | api_response_dict = api_response.groups[0].to_dict() 356 | group_user_details = self.get_group_members( 357 | group, zone, provider) 358 | if group_user_details: 359 | api_response_dict['members'] = group_user_details 360 | else: 361 | api_response_dict['members'] = [] 362 | return api_response_dict 363 | 364 | except utils.ApiException as e: 365 | if str(e.status) == "404": 366 | error_message = "Get Group Details %s failed with %s" \ 367 | % (group, self.determine_error(e)) 368 | LOG.info(error_message) 369 | return None 370 | else: 371 | error_message = "Get Group Details %s failed with %s" \ 372 | % (group, self.determine_error(e)) 373 | LOG.error(error_message) 374 | self.module.fail_json(msg=error_message) 375 | 376 | except Exception as e: 377 | error_message = "Get Group Details %s failed with %s" \ 378 | % (group, self.determine_error(e)) 379 | LOG.error(error_message) 380 | self.module.fail_json(msg=error_message) 381 | 382 | def get_group_members(self, group, zone, provider): 383 | """Get the Group Member Details in Isilon""" 384 | try: 385 | LOG.info("Getting members of group %s", group) 386 | provider = 'local' if not provider else provider 387 | api_response = self.group_api_instance.list_group_members( 388 | group, zone=zone, provider=provider) 389 | api_response_dict = api_response.to_dict() 390 | LOG.info("Group Members: %s", api_response_dict['members']) 391 | return api_response_dict['members'] 392 | except Exception as e: 393 | error = self.determine_error(error_obj=e) 394 | error_message = "Get Users for group %s failed with %s" \ 395 | % (group, error) 396 | LOG.info(error_message) 397 | self.module.fail_json(msg=error_message) 398 | 399 | def add_user_to_group(self, group, user, 400 | zone, provider): 401 | """ Add a User to a Group in Isilon """ 402 | try: 403 | message = "Adding user %s to group %s" % (user, group) 404 | LOG.info(message) 405 | group_member = utils.isi_sdk.AuthAccessAccessItemFileGroup(user) 406 | provider = self.check_provider_type(provider, 'Add User to') 407 | api_response = self.group_api_instance.create_group_member( 408 | group_member, group, zone=zone, provider=provider) 409 | LOG.info(api_response) 410 | return True 411 | except Exception as e: 412 | error = self.determine_error(error_obj=e) 413 | error_message = "Add user %s to group failed with %s " \ 414 | % (user, error) 415 | LOG.error(error_message) 416 | self.module.fail_json(msg=error_message) 417 | 418 | def remove_user_from_group(self, group, user, zone, provider): 419 | """ Remove a user from a Group in Isilon""" 420 | try: 421 | message = "Removing user %s from group %s" % (user, group) 422 | LOG.info(message) 423 | provider = self.check_provider_type(provider, 'Remove User from') 424 | self.group_api_instance.delete_group_member( 425 | user, group, zone=zone, provider=provider) 426 | return True 427 | 428 | except Exception as e: 429 | error = self.determine_error(error_obj=e) 430 | error_message = "Remove user %s from group failed with %s" \ 431 | % (group, error) 432 | LOG.error(error_message) 433 | self.module.fail_json(msg=error_message) 434 | 435 | def get_user_name(self, user_id, zone): 436 | """Get the Member Name in Isilon""" 437 | try: 438 | LOG.info("Getting User name using User id %s", user_id) 439 | mapping_identity_id = 'UID:' + user_id 440 | api_response = self.api_instance.get_mapping_identity( 441 | mapping_identity_id, nocreate=True, zone=zone) 442 | return api_response.identities[0].targets[0].target.name 443 | except Exception as e: 444 | error = self.determine_error(error_obj=e) 445 | error_message = "Get user_name for %s failed with %s" \ 446 | % (user_id, error) 447 | LOG.error(error_message) 448 | self.module.fail_json(msg=error_message) 449 | 450 | def is_user_part_of_group( 451 | self, group, user_name, user_id, zone, provider): 452 | """Check if Member is part of the Group or not""" 453 | if user_id: 454 | LOG.info("User Id given, getting corresponding User name") 455 | user_name = self.get_user_name(user_id, zone) 456 | group_members = self.get_group_members(group, zone, provider) 457 | if len(group_members) == 0: 458 | return False 459 | for user_details in group_members: 460 | LOG.info("user_details['name'] %s", user_details['name']) 461 | if user_details['name'].lower() == user_name.lower(): 462 | return True 463 | return False 464 | 465 | def update_group(self, group, user_name, user_id, 466 | user_state, access_zone, provider_type): 467 | """Update the group members in Isilon""" 468 | changed = False 469 | user_flag = self.is_user_part_of_group(group, user_name, user_id, 470 | access_zone, provider_type) 471 | 472 | user = "USER:" + user_name if user_name else "UID:" + user_id 473 | if user_state == 'present-in-group' and not user_flag: 474 | changed = self.add_user_to_group(group, user, access_zone, 475 | provider_type) 476 | 477 | if user_state == 'absent-in-group' and user_flag: 478 | changed = self.remove_user_from_group(group, user, access_zone, 479 | provider_type) 480 | return changed 481 | 482 | def determine_error(self, error_obj): 483 | """Determine the error message to return""" 484 | if isinstance(error_obj, utils.ApiException): 485 | error = error_obj.body 486 | error = re.sub('[^A-Za-z:.,]+', ' ', str(error)) 487 | else: 488 | error = str(error_obj) 489 | return error 490 | 491 | def perform_module_operation(self): 492 | """ 493 | Perform different actions on group module based on parameters 494 | chosen in playbook 495 | """ 496 | group_name = self.module.params['group_name'] 497 | group_id = self.module.params['group_id'] 498 | access_zone = self.module.params['access_zone'] 499 | provider_type = self.module.params['provider_type'] 500 | state = self.module.params['state'] 501 | users = self.module.params['users'] 502 | user_state = self.module.params['user_state'] 503 | if group_name and not group_id: 504 | group = 'GROUP:' + group_name 505 | elif group_id and not group_name: 506 | group = 'GID:' + group_id 507 | else: 508 | self.module.fail_json(msg="Invalid group_name or group_id" 509 | " provided. Enter a valid string.") 510 | 511 | if not users and user_state: 512 | self.module.fail_json(msg="'user_state' is given," 513 | " 'users' are not specified") 514 | 515 | if not user_state and users: 516 | self.module.fail_json(msg="'user_state' is not specified," 517 | " 'users' are given") 518 | 519 | changed = False 520 | if state == 'present': 521 | group_details = self.get_group_details( 522 | group, access_zone, provider_type) 523 | if not group_details: 524 | if group_id: 525 | error_message = 'Group with id %s not found' \ 526 | " on the system" % group_id 527 | LOG.error(error_message) 528 | self.module.fail_json(msg=error_message) 529 | LOG.info("Create a Group %s ", group_name) 530 | users_list = self.create_user_objects(users, user_state) 531 | self.create_group(group_name, access_zone, provider_type, 532 | users_list) 533 | changed = True 534 | else: 535 | if user_state and users: 536 | user_modified_flag = False 537 | for user in users: 538 | if not isinstance(user, dict): 539 | self.module.fail_json( 540 | msg="Key Value pair is allowed, Provided %s." 541 | % user) 542 | 543 | if len(user.keys()) != 1: 544 | self.module.fail_json( 545 | msg="One Key per dictionary is allowed, %s" 546 | " given" % list(user.keys())) 547 | 548 | if 'user_name' in user: 549 | user_modified_flag = self.update_group( 550 | group, user['user_name'], 551 | None, user_state, access_zone, provider_type) 552 | 553 | elif 'user_id' in user: 554 | user_modified_flag = self.update_group( 555 | group, None, user['user_id'], 556 | user_state, access_zone, provider_type) 557 | else: 558 | error = 'user_id or user_name is expected,' \ 559 | ' "%s" given.' % list(user.keys())[0] 560 | self.module.fail_json(msg=error) 561 | 562 | if user_modified_flag and not changed: 563 | changed = True 564 | else: 565 | if group_name: 566 | LOG.info("Delete Group %s ", group_name) 567 | else: 568 | LOG.info("Delete Group %s ", group_id) 569 | group_details = self.get_group_details( 570 | group, access_zone, provider_type) 571 | if group_details: 572 | changed = self.delete_group( 573 | group, access_zone, provider_type) 574 | 575 | group_details = self.get_group_details( 576 | group, access_zone, provider_type) 577 | 578 | self.result["changed"] = changed 579 | self.result["group_details"] = group_details 580 | self.module.exit_json(**self.result) 581 | 582 | 583 | def get_isilon_group_parameters(): 584 | """This method provide parameter required for the ansible group 585 | module on Isilon""" 586 | return dict( 587 | group_name=dict(required=False, type='str'), 588 | group_id=dict(required=False, type='str'), 589 | access_zone=dict(required=False, type='str', default='system'), 590 | provider_type=dict(required=False, type='str', 591 | choices=['local', 'file', 'ldap', 'ads'], 592 | default='local'), 593 | state=dict(required=True, type='str', choices=['present', 'absent']), 594 | users=dict(required=False, type='list'), 595 | user_state=dict(required=False, type='str', 596 | choices=['present-in-group', 'absent-in-group']) 597 | ) 598 | 599 | 600 | def main(): 601 | """ Create Isilon Group object and perform actions on it 602 | based on user input from playbook""" 603 | obj = IsilonGroup() 604 | obj.perform_module_operation() 605 | 606 | 607 | if __name__ == '__main__': 608 | main() 609 | -------------------------------------------------------------------------------- /dellemc_ansible/isilon/library/dellemc_isilon_snapshotschedule.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # Copyright: (c) 2019, DellEMC 3 | 4 | """Ansible module for managing snapshot schedules on Isilon""" 5 | 6 | from __future__ import absolute_import, division, print_function 7 | 8 | __metaclass__ = type 9 | ANSIBLE_METADATA = {'metadata_version': '1.1', 10 | 'status': ['preview'], 11 | 'supported_by': 'community' 12 | } 13 | 14 | DOCUMENTATION = r''' 15 | --- 16 | module: dellemc_isilon_snapshotschedule 17 | version_added: '2.7' 18 | short_description: Manage snapshot schedules on Dell EMC Isilon. 19 | description: 20 | - Managing snapshot schedules on Isilon. 21 | - Create snapshot schedule. 22 | - Modify snapshot schedule. 23 | - Get details of snapshot schedule. 24 | - Delete snapshot schedule. 25 | 26 | extends_documentation_fragment: 27 | - dellemc_isilon.dellemc_isilon 28 | 29 | author: 30 | - Akash Shendge (@shenda1) 31 | 32 | options: 33 | name: 34 | description: 35 | - The name of the snapshot schedule. 36 | type: str 37 | required: true 38 | path: 39 | description: 40 | - The path on which the snapshot will be taken. This path is relative 41 | to the base path of the Access Zone. 42 | - For 'System' access zone, path is absolute. 43 | - This parameter is required at the time of creation. 44 | - Modification of path is not allowed through Ansible module. 45 | type: str 46 | access_zone: 47 | description: 48 | - The effective path where the snapshot is created will 49 | be determined by the base path of the Access Zone and the 50 | path provided by the user in the playbook. 51 | type: str 52 | default: System 53 | new_name: 54 | description: 55 | - The new name of the snapshot schedule. 56 | type: str 57 | desired_retention: 58 | description: 59 | - The number of hours/days for which snapshots created by this snapshot 60 | schedule should be retained. 61 | - If retention is not specified at the time of creation, then the 62 | snapshots created by the snapshot schedule will be retained forever. 63 | - Minimum retention duration is 2 hours. 64 | - For large durations (beyond days/weeks), Isilon may round off the 65 | retention to a somewhat larger value to match a whole number of 66 | days/weeks. 67 | type: int 68 | retention_unit: 69 | description: 70 | - The retention unit for the snapshot created by this schedule. 71 | type: str 72 | choices: [hours, days] 73 | default: hours 74 | alias: 75 | description: 76 | - The alias will point to the latest snapshot created by the snapshot 77 | schedule. 78 | type: str 79 | pattern: 80 | description: 81 | - Pattern expanded with strftime to create snapshot names. 82 | - This parameter is required at the time of creation. 83 | type: str 84 | schedule: 85 | description: 86 | - The isidate compatible natural language description of the schedule. 87 | - It specifies the frequency of the schedule. 88 | - This parameter is required at the time of creation. 89 | type: str 90 | state: 91 | description: 92 | - Defines whether the snapshot schedule should exist or not. 93 | type: str 94 | required: true 95 | choices: [absent, present] 96 | ''' 97 | 98 | EXAMPLES = r''' 99 | - name: Create snapshot schedule 100 | dellemc_isilon_snapshotschedule: 101 | onefs_host: "{{onefs_host}}" 102 | verify_ssl: "{{verify_ssl}}" 103 | api_user: "{{api_user}}" 104 | api_password: "{{api_password}}" 105 | name: "{{name}}" 106 | access_zone: '{{access_zone}}' 107 | path: '{{path1}}' 108 | alias: "{{alias1}}" 109 | desired_retention: "{{desired_retention1}}" 110 | pattern: "{{pattern1}}" 111 | schedule: "{{schedule1}}" 112 | state: "{{state_present}}" 113 | 114 | - name: Get details of snapshot schedule 115 | dellemc_isilon_snapshotschedule: 116 | onefs_host: "{{onefs_host}}" 117 | verify_ssl: "{{verify_ssl}}" 118 | api_user: "{{api_user}}" 119 | api_password: "{{api_password}}" 120 | name: "{{name}}" 121 | state: "{{state_present}}" 122 | 123 | - name: Rename snapshot schedule 124 | dellemc_isilon_snapshotschedule: 125 | onefs_host: "{{onefs_host}}" 126 | verify_ssl: "{{verify_ssl}}" 127 | api_user: "{{api_user}}" 128 | api_password: "{{api_password}}" 129 | name: "{{name}}" 130 | new_name: "{{new_name}}" 131 | state: "{{state_present}}" 132 | 133 | - name: Modify alias of snapshot schedule 134 | dellemc_isilon_snapshotschedule: 135 | onefs_host: "{{onefs_host}}" 136 | verify_ssl: "{{verify_ssl}}" 137 | api_user: "{{api_user}}" 138 | api_password: "{{api_password}}" 139 | name: "{{new_name}}" 140 | alias: "{{alias2}}" 141 | state: "{{state_present}}" 142 | 143 | - name: Modify pattern of snapshot schedule 144 | dellemc_isilon_snapshotschedule: 145 | onefs_host: "{{onefs_host}}" 146 | verify_ssl: "{{verify_ssl}}" 147 | api_user: "{{api_user}}" 148 | api_password: "{{api_password}}" 149 | name: "{{new_name}}" 150 | pattern: "{{pattern2}}" 151 | state: "{{state_present}}" 152 | 153 | - name: Modify schedule of snapshot schedule 154 | dellemc_isilon_snapshotschedule: 155 | onefs_host: "{{onefs_host}}" 156 | verify_ssl: "{{verify_ssl}}" 157 | api_user: "{{api_user}}" 158 | api_password: "{{api_password}}" 159 | name: "{{new_name}}" 160 | schedule: "{{schedule2}}" 161 | state: "{{state_present}}" 162 | 163 | - name: Modify retention of snapshot schedule 164 | dellemc_isilon_snapshotschedule: 165 | onefs_host: "{{onefs_host}}" 166 | verify_ssl: "{{verify_ssl}}" 167 | api_user: "{{api_user}}" 168 | api_password: "{{api_password}}" 169 | name: "{{new_name}}" 170 | desired_retention: 2 171 | retention_unit: "{{retention_unit_days}}" 172 | state: "{{state_present}}" 173 | 174 | - name: Delete snapshot schedule 175 | dellemc_isilon_snapshotschedule: 176 | onefs_host: "{{onefs_host}}" 177 | verify_ssl: "{{verify_ssl}}" 178 | api_user: "{{api_user}}" 179 | api_password: "{{api_password}}" 180 | name: "{{new_name}}" 181 | state: "{{state_absent}}" 182 | 183 | - name: Delete snapshot schedule - Idempotency 184 | dellemc_isilon_snapshotschedule: 185 | onefs_host: "{{onefs_host}}" 186 | verify_ssl: "{{verify_ssl}}" 187 | api_user: "{{api_user}}" 188 | api_password: "{{api_password}}" 189 | name: "{{new_name}}" 190 | state: "{{state_absent}}" 191 | ''' 192 | 193 | RETURN = r''' 194 | changed: 195 | description: Whether or not the resource has changed 196 | returned: always 197 | type: bool 198 | 199 | snapshot_schedule_details: 200 | description: Details of the snapshot schedule including snapshot details 201 | returned: When snapshot schedule exists 202 | type: complex 203 | contains: 204 | schedules: 205 | description: Details of snapshot schedule 206 | type: complex 207 | contains: 208 | duration: 209 | description: 210 | - Time in seconds added to creation time to construction expiration time 211 | type: int 212 | id: 213 | description: 214 | - The system ID given to the schedule 215 | type: int 216 | next_run: 217 | description: 218 | - Unix Epoch time of next snapshot to be created 219 | type: int 220 | next_snapshot: 221 | description: 222 | - Formatted name of next snapshot to be created 223 | type: str 224 | snapshot_list: 225 | description: List of snapshots taken by this schedule 226 | type: complex 227 | contains: 228 | snapshots: 229 | description: Details of snapshot 230 | type: complex 231 | contains: 232 | created: 233 | description: 234 | - The Unix Epoch time the snapshot was created 235 | type: int 236 | expires: 237 | description: 238 | - The Unix Epoch time the snapshot will expire and be eligible for automatic deletion. 239 | type: int 240 | id: 241 | description: 242 | - The system ID given to the snapshot. This is useful for tracking the status of delete pending snapshots 243 | type: int 244 | name: 245 | description: 246 | - The user or system supplied snapshot name. This will be null for snapshots pending delete 247 | type: str 248 | size: 249 | description: 250 | - The amount of storage in bytes used to store this snapshot 251 | type: int 252 | total: 253 | description: 254 | - Total number of items available 255 | type: int 256 | ''' 257 | 258 | import logging 259 | import re 260 | from ansible.module_utils.basic import AnsibleModule 261 | from ansible.module_utils.storage.dell \ 262 | import dellemc_ansible_isilon_utils as utils 263 | 264 | LOG = utils.get_logger('dellemc_isilon_snapshotschedule', 265 | log_devel=logging.INFO) 266 | HAS_ISILON_SDK = utils.has_isilon_sdk() 267 | 268 | ISILON_SDK_VERSION_CHECK = utils.isilon_sdk_version_check() 269 | 270 | 271 | class IsilonSnapshotSchedule(object): 272 | 273 | """Class with snapshot schedule operations""" 274 | 275 | def __init__(self): 276 | """ Define all parameters required by this module""" 277 | self.module_params = utils.get_isilon_management_host_parameters() 278 | self.module_params.update(get_isilon_snapshotschedule_parameters()) 279 | 280 | # initialize the Ansible module 281 | self.module = AnsibleModule( 282 | argument_spec=self.module_params, 283 | supports_check_mode=False 284 | ) 285 | 286 | if HAS_ISILON_SDK is False: 287 | self.module.fail_json(msg="Ansible modules for Isilon require the" 288 | " Isilon python library to be " 289 | "installed. Please install the library " 290 | "before using these modules.") 291 | 292 | if ISILON_SDK_VERSION_CHECK and \ 293 | not ISILON_SDK_VERSION_CHECK['supported_version']: 294 | err_msg = ISILON_SDK_VERSION_CHECK['unsupported_version_message'] 295 | LOG.error(err_msg) 296 | self.module.fail_json(msg=err_msg) 297 | 298 | self.api_client = utils.get_isilon_connection(self.module.params) 299 | self.isi_sdk = utils.get_isilon_sdk() 300 | self.api_instance = utils.isi_sdk.SnapshotApi(self.api_client) 301 | self.zone_summary_api = utils.isi_sdk.ZonesSummaryApi(self.api_client) 302 | LOG.info('Got python SDK instance for provisioning on Isilon') 303 | 304 | def get_details(self, name): 305 | """Get snapshot schedule details""" 306 | try: 307 | api_response = self.api_instance.get_snapshot_schedule(name).\ 308 | to_dict() 309 | snapshot_list = self.api_instance.list_snapshot_snapshots( 310 | schedule=name).to_dict() 311 | api_response['snapshot_list'] = snapshot_list 312 | return api_response 313 | except utils.ApiException as e: 314 | if str(e.status) == "404": 315 | error_message = "Snapshot schedule {0} details are not found"\ 316 | .format(name) 317 | LOG.info(error_message) 318 | return None 319 | else: 320 | error_msg = self.determine_error(error_obj=e) 321 | error_message = 'Get details of snapshot schedule {0} ' \ 322 | 'failed with error: {1}'.format(name, 323 | error_msg) 324 | LOG.error(error_message) 325 | self.module.fail_json(msg=error_message) 326 | except Exception as e: 327 | error_message = 'Get details of snapshot schedule {0} failed ' \ 328 | 'with error: {1}'.format(name, str(e)) 329 | LOG.error(error_message) 330 | self.module.fail_json(msg=error_message) 331 | 332 | def get_zone_base_path(self, access_zone): 333 | """Returns the base path of the Access Zone.""" 334 | try: 335 | zone_path = (self.zone_summary_api. 336 | get_zones_summary_zone(access_zone)).to_dict() 337 | return zone_path['summary']['path'] 338 | except Exception as e: 339 | error_msg = self.determine_error(error_obj=e) 340 | error_message = 'Unable to fetch base path of Access Zone {0} ' \ 341 | ',failed with error: {1}'.format(access_zone, 342 | str(error_msg)) 343 | LOG.error(error_message) 344 | self.module.fail_json(msg=error_message) 345 | 346 | def validate_desired_retention(self, desired_retention): 347 | """Validates the specified desired retention""" 348 | if desired_retention <= 0: 349 | self.module.fail_json(msg="Please provide a valid integer " 350 | "as the desired retention.") 351 | 352 | def check_snapshot_schedule_modified(self, snapshot_schedule_details, 353 | alias, desired_retention, 354 | retention_unit, effective_path, 355 | pattern, schedule): 356 | """Determines whether the snapshot schedule needs to be modified""" 357 | modified = False 358 | snapshot_schedule_modify = {} 359 | 360 | if effective_path is not None and effective_path != \ 361 | snapshot_schedule_details['schedules'][0]['path']: 362 | error_message = 'Modification of path of snapshot schedule is ' \ 363 | 'not allowed through Ansible Module.' 364 | LOG.error(error_message) 365 | self.module.fail_json(msg=error_message) 366 | 367 | if alias is not None and alias != \ 368 | snapshot_schedule_details['schedules'][0]['alias']: 369 | LOG.info("Alias Modification") 370 | snapshot_schedule_modify['alias'] = alias 371 | modified = True 372 | 373 | if pattern is not None and pattern != snapshot_schedule_details[ 374 | 'schedules'][0]['pattern']: 375 | LOG.info("Pattern Modification") 376 | snapshot_schedule_modify['pattern'] = pattern 377 | modified = True 378 | 379 | if schedule is not None and schedule != snapshot_schedule_details[ 380 | 'schedules'][0]['schedule']: 381 | LOG.info("Schedule Modification") 382 | snapshot_schedule_modify['schedule'] = schedule 383 | modified = True 384 | 385 | if desired_retention is not None: 386 | retention_in_sec = 0 387 | if retention_unit == 'days': 388 | retention_in_sec = desired_retention * 24 * 60 * 60 389 | else: 390 | retention_in_sec = desired_retention * 60 * 60 391 | 392 | if retention_in_sec != snapshot_schedule_details['schedules'][ 393 | 0]['duration']: 394 | if retention_in_sec < 7200: 395 | self.module.fail_json(msg="The snapshot desired retention" 396 | " must be at least 2 hours") 397 | LOG.info("Retention Modification: new value=%s, old value=%s", 398 | retention_in_sec, snapshot_schedule_details 399 | ['schedules'][0]['duration']) 400 | snapshot_schedule_modify['duration'] = retention_in_sec 401 | modified = True 402 | return modified, snapshot_schedule_modify 403 | 404 | def create_snapshot_schedule(self, name, alias, effective_path, 405 | desired_retention, retention_unit, 406 | pattern, schedule): 407 | """Create snapshot schedule""" 408 | 409 | if effective_path is None: 410 | self.module.fail_json(msg="Path is mandatory while creating " 411 | "snapshot schedule") 412 | 413 | if pattern is None: 414 | self.module.fail_json(msg="Pattern is mandatory while creating " 415 | "snapshot schedule") 416 | 417 | if schedule is None: 418 | self.module.fail_json(msg="Schedule is mandatory while creating " 419 | "snapshot schedule") 420 | 421 | duration = 0 422 | if desired_retention is not None: 423 | if retention_unit == 'hours': 424 | duration = desired_retention * 60 * 60 425 | else: 426 | duration = desired_retention * 24 * 60 * 60 427 | 428 | if duration < 7200: 429 | self.module.fail_json(msg="The snapshot desired retention " 430 | "must be at least 2 hours") 431 | 432 | try: 433 | snapshot_schedule_create_param = utils.isi_sdk.\ 434 | SnapshotScheduleCreateParams(name=name, alias=alias, 435 | path=effective_path, 436 | duration=duration, 437 | pattern=pattern, 438 | schedule=schedule) 439 | self.api_instance.create_snapshot_schedule( 440 | snapshot_schedule_create_param) 441 | return True 442 | except Exception as e: 443 | error_msg = self.determine_error(error_obj=e) 444 | error_message = 'Failed to create snapshot schedule: {0} for ' \ 445 | 'path {1} with error: {2}'.format( 446 | name, effective_path, error_msg) 447 | LOG.error(error_message) 448 | self.module.fail_json(msg=error_message) 449 | 450 | def validate_new_name(self, new_name): 451 | """validate if the snapshot schedule with new_name already exists""" 452 | 453 | snapshot_schedules = self.get_details(new_name) 454 | 455 | if snapshot_schedules is not None: 456 | error_message = 'Snapshot schedule with name {0} already exists'.\ 457 | format(new_name) 458 | LOG.error(error_message) 459 | self.module.fail_json(msg=error_message) 460 | 461 | def rename_snapshot_schedule(self, snapshot_schedule_details, new_name): 462 | """Rename snapshot schedule""" 463 | 464 | try: 465 | snapshot_schedule_update_param = utils.isi_sdk.SnapshotSchedule( 466 | name=new_name) 467 | self.api_instance.update_snapshot_schedule( 468 | snapshot_schedule_update_param, 469 | snapshot_schedule_details['schedules'][0]['name']) 470 | return True 471 | except Exception as e: 472 | error_msg = self.determine_error(error_obj=e) 473 | error_message = 'Failed to rename snapshot schedule {0} with ' \ 474 | 'error : {1}'.\ 475 | format(snapshot_schedule_details['schedules'][0]['name'], 476 | error_msg) 477 | LOG.error(error_message) 478 | self.module.fail_json(msg=error_message) 479 | 480 | def modify_snapshot_schedule(self, name, 481 | snapshot_schedule_modification_details): 482 | """Modify snapshot schedule""" 483 | snapshot_schedule_update_param = self.isi_sdk.SnapshotSchedule() 484 | 485 | try: 486 | if 'alias' in snapshot_schedule_modification_details: 487 | snapshot_schedule_update_param.alias = \ 488 | snapshot_schedule_modification_details['alias'] 489 | if 'pattern' in snapshot_schedule_modification_details: 490 | snapshot_schedule_update_param.pattern = \ 491 | snapshot_schedule_modification_details['pattern'] 492 | if 'schedule' in snapshot_schedule_modification_details: 493 | snapshot_schedule_update_param.schedule = \ 494 | snapshot_schedule_modification_details['schedule'] 495 | if 'duration' in snapshot_schedule_modification_details: 496 | snapshot_schedule_update_param.duration = \ 497 | snapshot_schedule_modification_details['duration'] 498 | 499 | self.api_instance.update_snapshot_schedule( 500 | snapshot_schedule_update_param, name) 501 | return True 502 | except Exception as e: 503 | error_msg = self.determine_error(error_obj=e) 504 | error_message = 'Failed to modify snapshot schedule {0} with ' \ 505 | 'error : {1}'.format(name, error_msg) 506 | LOG.error(error_message) 507 | self.module.fail_json(msg=error_message) 508 | 509 | def delete_snapshot_schedule(self, name): 510 | """Delete snapshot schedule""" 511 | try: 512 | self.api_instance.delete_snapshot_schedule(name) 513 | return True 514 | except Exception as e: 515 | error_msg = self.determine_error(error_obj=e) 516 | error_message = 'Failed to delete snapshot schedule: {0} with ' \ 517 | 'error: {1}'.format(name, error_msg) 518 | LOG.error(error_message) 519 | self.module.fail_json(msg=error_message) 520 | 521 | def determine_error(self, error_obj): 522 | """Determine the error message to return""" 523 | if isinstance(error_obj, utils.ApiException): 524 | error = re.sub("[\n \"]+", ' ', str(error_obj.body)) 525 | else: 526 | error = error_obj 527 | return error 528 | 529 | def perform_module_operation(self): 530 | """ 531 | Perform different actions on snapshot schedule module based on 532 | parameters chosen in playbook 533 | """ 534 | name = self.module.params['name'] 535 | state = self.module.params['state'] 536 | access_zone = self.module.params['access_zone'] 537 | path = self.module.params['path'] 538 | new_name = self.module.params['new_name'] 539 | pattern = self.module.params['pattern'] 540 | schedule = self.module.params['schedule'] 541 | desired_retention = self.module.params['desired_retention'] 542 | retention_unit = self.module.params['retention_unit'] 543 | alias = self.module.params['alias'] 544 | 545 | # result is a dictionary that contains changed status and snapshot 546 | # schedule details 547 | result = dict( 548 | changed=False, 549 | snapshot_schedule_details='' 550 | ) 551 | 552 | effective_path = None 553 | if access_zone is not None: 554 | if access_zone.lower() == 'system': 555 | effective_path = path 556 | else: 557 | if path: 558 | effective_path = self.get_zone_base_path(access_zone) + \ 559 | path 560 | 561 | if desired_retention: 562 | self.validate_desired_retention(desired_retention) 563 | 564 | snapshot_schedule_details = self.get_details(name) 565 | 566 | is_schedule_modified = False 567 | snapshot_schedule_modification_details = dict() 568 | 569 | if snapshot_schedule_details is not None: 570 | is_schedule_modified, snapshot_schedule_modification_details = \ 571 | self.check_snapshot_schedule_modified( 572 | snapshot_schedule_details, alias, desired_retention, 573 | retention_unit, effective_path, pattern, schedule) 574 | 575 | if state == 'present' and not snapshot_schedule_details: 576 | LOG.info("Creating new snapshot schedule: %s for path: %s", 577 | name, effective_path) 578 | result['changed'] = self.create_snapshot_schedule( 579 | name, alias, effective_path, desired_retention, 580 | retention_unit, pattern, schedule) or result['changed'] 581 | 582 | if state == 'present' and new_name is not None: 583 | if len(new_name) == 0: 584 | self.module.fail_json(msg="Please provide valid string for " 585 | "new_name") 586 | 587 | if snapshot_schedule_details is None: 588 | self.module.fail_json(msg="Snapshot schedule not found.") 589 | 590 | if new_name != snapshot_schedule_details['schedules'][0]['name']: 591 | self.validate_new_name(new_name) 592 | LOG.info("Renaming snapshot schedule %s to new name %s", 593 | name, new_name) 594 | result['changed'] = self.rename_snapshot_schedule( 595 | snapshot_schedule_details, new_name) or result['changed'] 596 | name = new_name 597 | 598 | if state == 'present' and is_schedule_modified: 599 | LOG.info("Modifying snapshot schedule %s", name) 600 | result['changed'] = self.modify_snapshot_schedule( 601 | name, snapshot_schedule_modification_details) or \ 602 | result['changed'] 603 | 604 | if state == 'absent' and snapshot_schedule_details: 605 | LOG.info("Deleting snapshot schedule %s", name) 606 | result['changed'] = self.delete_snapshot_schedule(name) or \ 607 | result['changed'] 608 | 609 | snapshot_schedule_details = self.get_details(name) 610 | result['snapshot_schedule_details'] = snapshot_schedule_details 611 | self.module.exit_json(**result) 612 | 613 | 614 | def get_isilon_snapshotschedule_parameters(): 615 | """This method provide parameters required for the ansible snapshot 616 | schedule module on Isilon""" 617 | return dict( 618 | name=dict(required=True, type='str'), 619 | access_zone=dict(type='str', default='System'), 620 | path=dict(type='str'), 621 | new_name=dict(type='str'), 622 | pattern=dict(type='str'), 623 | schedule=dict(type='str'), 624 | desired_retention=dict(type='int'), 625 | retention_unit=dict(type='str', choices=['hours', 'days'], 626 | default='hours'), 627 | alias=dict(required=False, type='str'), 628 | state=dict(required=True, type='str', choices=['present', 'absent']) 629 | ) 630 | 631 | 632 | def main(): 633 | """ Create Isilon snapshot schedule object and perform action on it 634 | based on user input from playbook""" 635 | obj = IsilonSnapshotSchedule() 636 | obj.perform_module_operation() 637 | 638 | 639 | if __name__ == '__main__': 640 | main() 641 | -------------------------------------------------------------------------------- /dellemc_ansible/isilon/library/dellemc_isilon_nfs.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # Copyright: (c) 2020, DellEMC 3 | 4 | """Ansible module for managing NFS Exports on DellEMC Isilon""" 5 | 6 | from __future__ import absolute_import, division, print_function 7 | 8 | __metaclass__ = type 9 | ANSIBLE_METADATA = {'metadata_version': '1.1', 10 | 'status': ['preview'], 11 | 'supported_by': 'community' 12 | } 13 | 14 | 15 | DOCUMENTATION = r''' 16 | --- 17 | module: dellemc_isilon_nfs 18 | version_added: '2.7' 19 | short_description: Manage NFS exports on a DellEMC Isilon system 20 | description: 21 | - Managing NFS exports on an Isilon system includes creating NFS export for 22 | a directory in an access zone, adding or removing clients, modifying 23 | different parameters of the export and deleting export. 24 | 25 | extends_documentation_fragment: 26 | - dellemc_isilon.dellemc_isilon 27 | 28 | author: 29 | - Manisha Agrawal(@agrawm3) manisha.agrawal@dell.com 30 | 31 | options: 32 | path: 33 | description: 34 | - Specifies the filesystem path. It is absolute path for System access zone 35 | and relative if using non-System access zone. For example, if your access 36 | zone is 'Ansible' and it has a base path '/ifs/ansible' and the path 37 | specified is '/user1', then the effective path would be '/ifs/ansible/user1'. 38 | If your access zone is System, and you have 'directory1' in the access 39 | zone, the path provided should be '/ifs/directory1'. 40 | - The directory on the path must exist - the NFS module will not create 41 | the directory. 42 | - Ansible module will only support exports with a unique path. 43 | - If there multiple exports present with the same path, fetching details, 44 | creation, modification or deletion of such exports will fail. 45 | required: True 46 | type: str 47 | access_zone: 48 | description: 49 | - Specifies the zone in which the export is valid. 50 | - Access zone once set cannot be changed. 51 | type: str 52 | default: System 53 | clients: 54 | description: 55 | - Specifies the clients to the export. The type of access to clients in 56 | this list is determined by the 'read_only' parameter. 57 | - This list can be changed anytime during the lifetime of the NFS export. 58 | type: list 59 | root_clients: 60 | description: 61 | - Specifies the clients with root access to the export. 62 | - This list can be changed anytime during the lifetime of the NFS export. 63 | type: list 64 | read_only_clients: 65 | description: 66 | - Specifies the clients with read-only access to the export, even when the 67 | export is read/write. 68 | - This list can be changed anytime during the lifetime of the NFS export. 69 | type: list 70 | read_write_clients: 71 | description: 72 | - Specifies the clients with both read and write access to the export, 73 | even when the export is set to read-only. 74 | - This list can be changed anytime during the lifetime of the NFS export. 75 | type: list 76 | read_only: 77 | description: 78 | - Specifies whether the export is read-only or read-write. This parameter 79 | only has effect on the 'clients' list and not the other three types of 80 | clients. 81 | - This setting can be modified any time. If it is not set at the time of 82 | creation, the export will be of type read/write. 83 | type: bool 84 | sub_directories_mountable: 85 | description: 86 | - True if all directories under the specified paths are mountable. If not 87 | set, sub-directories will not be mountable. 88 | - This setting can be modified any time. 89 | type: bool 90 | description: 91 | description: 92 | - Optional description field for the NFS export. 93 | - Can be modified by passing new value. 94 | type: str 95 | state: 96 | description: 97 | - Define whether the NFS export should exist or not. 98 | - present indicates that the NFS export should exist in system. 99 | - absent indicates that the NFS export should not exist in system. 100 | required: True 101 | type: str 102 | choices: [absent, present] 103 | client_state: 104 | description: 105 | - Define whether the clients can access the NFS export. 106 | - present-in-export indicates that the clients can access the NFS export. 107 | - absent-in-export indicates that the client cannot access the NFS export. 108 | - Required when adding or removing access of clients from the export. 109 | - While removing clients, only the specified clients will be removed from 110 | the export, others will remain as is. 111 | required: False 112 | type: str 113 | choices: [present-in-export, absent-in-export] 114 | ''' 115 | 116 | EXAMPLES = r''' 117 | 118 | - name: Create NFS Export 119 | dellemc_isilon_nfs: 120 | onefs_host: "{{onefs_host}}" 121 | api_user: "{{api_user}}" 122 | api_password: "{{api_password}}" 123 | verify_ssl: "{{verify_ssl}}" 124 | path: "{{path}}" 125 | access_zone: "{{access_zone}}" 126 | read_only_clients: 127 | - "{{client1}}" 128 | - "{{client2}}" 129 | read_only: True 130 | clients: ["{{client3}}"] 131 | client_state: 'present-in-export' 132 | state: 'present' 133 | 134 | - name: Get NFS Export 135 | dellemc_isilon_nfs: 136 | onefs_host: "{{onefs_host}}" 137 | api_user: "{{api_user}}" 138 | api_password: "{{api_password}}" 139 | verify_ssl: "{{verify_ssl}}" 140 | path: "{{path}}" 141 | access_zone: "{{access_zone}}" 142 | state: 'present' 143 | 144 | - name: Add a root client 145 | dellemc_isilon_nfs: 146 | onefs_host: "{{onefs_host}}" 147 | api_user: "{{api_user}}" 148 | api_password: "{{api_password}}" 149 | verify_ssl: "{{verify_ssl}}" 150 | path: "{{path}}" 151 | access_zone: "{{access_zone}}" 152 | root_clients: 153 | - "{{client4}}" 154 | client_state: 'present-in-export' 155 | state: 'present' 156 | 157 | - name: Set sub_directories_mountable flag to True 158 | dellemc_isilon_nfs: 159 | onefs_host: "{{onefs_host}}" 160 | api_user: "{{api_user}}" 161 | api_password: "{{api_password}}" 162 | verify_ssl: "{{verify_ssl}}" 163 | path: "{{path}}" 164 | access_zone: "{{access_zone}}" 165 | sub_directories_mountable: True 166 | state: 'present' 167 | 168 | - name: Remove a root client 169 | dellemc_isilon_nfs: 170 | onefs_host: "{{onefs_host}}" 171 | api_user: "{{api_user}}" 172 | api_password: "{{api_password}}" 173 | verify_ssl: "{{verify_ssl}}" 174 | path: "{{path}}" 175 | access_zone: "{{access_zone}}" 176 | root_clients: 177 | - "{{client4}}" 178 | client_state: 'absent-in-export' 179 | state: 'present' 180 | 181 | - name: Modify description 182 | dellemc_isilon_nfs: 183 | onefs_host: "{{onefs_host}}" 184 | api_user: "{{api_user}}" 185 | api_password: "{{api_password}}" 186 | verify_ssl: "{{verify_ssl}}" 187 | path: "{{path}}" 188 | access_zone: "{{access_zone}}" 189 | description: "new description" 190 | state: 'present' 191 | 192 | - name: Set read_only flag to False 193 | dellemc_isilon_nfs: 194 | onefs_host: "{{onefs_host}}" 195 | api_user: "{{api_user}}" 196 | api_password: "{{api_password}}" 197 | verify_ssl: "{{verify_ssl}}" 198 | path: "{{path}}" 199 | access_zone: "{{access_zone}}" 200 | read_only: False 201 | state: 'present' 202 | 203 | - name: Delete NFS Export 204 | dellemc_isilon_nfs: 205 | onefs_host: "{{onefs_host}}" 206 | api_user: "{{api_user}}" 207 | api_password: "{{api_password}}" 208 | verify_ssl: "{{verify_ssl}}" 209 | path: "{{path}}" 210 | access_zone: "{{access_zone}}" 211 | state: 'absent' 212 | 213 | ''' 214 | 215 | RETURN = r''' 216 | changed: 217 | description: A boolean indicating if the task had to make changes. 218 | returned: always 219 | type: bool 220 | NFS_export_details: 221 | description: The updated NFS Export details. 222 | type: complex 223 | returned: always 224 | contains: 225 | all_dirs: 226 | description: 227 | - sub_directories_mountable flag value. 228 | type: bool 229 | id: 230 | description: 231 | - The ID of the NFS Export, generated by the array. 232 | type: int 233 | sample: 12 234 | paths: 235 | description: 236 | - The filesystem path. 237 | type: list 238 | sample: ['/ifs/dir/filepath'] 239 | zone: 240 | description: 241 | - Specifies the zone in which the export is valid. 242 | type: string 243 | sample: 'System' 244 | read_only: 245 | description: 246 | - Specifies whether the export is read-only or read-write. 247 | type: bool 248 | read_only_clients: 249 | description: 250 | - The list of read only clients for the NFS Export. 251 | type: list 252 | sample: ['client_ip', 'client_ip'] 253 | read_write_clients: 254 | description: 255 | - The list of read write clients for the NFS Export. 256 | type: list 257 | sample: ['client_ip', 'client_ip'] 258 | root_clients: 259 | description: 260 | - The list of root clients for the NFS Export. 261 | type: list 262 | sample: ['client_ip', 'client_ip'] 263 | clients: 264 | description: 265 | - The list of clients for the NFS Export. 266 | type: list 267 | sample: ['client_ip', 'client_ip'] 268 | description: 269 | description: 270 | - Description for the export. 271 | type: string 272 | ''' 273 | 274 | from ansible.module_utils.basic import AnsibleModule 275 | from ansible.module_utils.storage.dell \ 276 | import dellemc_ansible_isilon_utils as utils 277 | import logging 278 | import re 279 | 280 | LOG = utils.get_logger( 281 | module_name='dellemc_isilon_nfs', 282 | log_devel=logging.INFO) 283 | 284 | HAS_ISILON_SDK = utils.has_isilon_sdk() 285 | 286 | ISILON_SDK_VERSION_CHECK = utils.isilon_sdk_version_check() 287 | 288 | 289 | class IsilonNfsExport(object): 290 | 291 | '''Class with NFS export operations''' 292 | 293 | def __init__(self): 294 | ''' Define all parameters required by this module''' 295 | self.module_params = utils.get_isilon_management_host_parameters() 296 | self.module_params.update(self.get_isilon_nfs_parameters()) 297 | # Initialize the ansible module 298 | self.module = AnsibleModule( 299 | argument_spec=self.module_params, 300 | supports_check_mode=False 301 | ) 302 | # Result is a dictionary that contains changed status, NFS export 303 | # details 304 | self.result = { 305 | "changed": False, 306 | "NFS_export_details": {} 307 | } 308 | if HAS_ISILON_SDK is False: 309 | self.module.fail_json(msg="Ansible modules for Isilon require " 310 | "the Isilon SDK to be installed. Please " 311 | "install the library before using these " 312 | "modules.") 313 | 314 | if ISILON_SDK_VERSION_CHECK and \ 315 | not ISILON_SDK_VERSION_CHECK['supported_version']: 316 | err_msg = ISILON_SDK_VERSION_CHECK['unsupported_version_message'] 317 | LOG.error(err_msg) 318 | self.module.fail_json(msg=err_msg) 319 | 320 | self.api_client = utils.get_isilon_connection(self.module.params) 321 | self.isi_sdk = utils.get_isilon_sdk() 322 | LOG.info('Got python SDK instance for provisioning on Isilon ') 323 | 324 | self.protocol_api = self.isi_sdk.ProtocolsApi(self.api_client) 325 | self.zone_summary_api = self.isi_sdk.ZonesSummaryApi(self.api_client) 326 | 327 | def get_zone_base_path(self, access_zone): 328 | """Returns the base path of the Access Zone.""" 329 | try: 330 | zone_path = (self.zone_summary_api. 331 | get_zones_summary_zone(access_zone)).to_dict() 332 | return zone_path['summary']['path'] 333 | except Exception as e: 334 | error_msg = self.determine_error(error_obj=e) 335 | error_message = 'Unable to fetch base path of Access Zone {0} ' \ 336 | 'failed with error: {1}'.format(access_zone, 337 | str(error_msg)) 338 | LOG.error(error_message) 339 | self.module.fail_json(msg=error_message) 340 | 341 | def get_nfs_export(self, path, access_zone): 342 | ''' 343 | Get details of an NFS export using filesystem path and access zone 344 | ''' 345 | LOG.info( 346 | "Getting NFS export details for path: {0} and access zone: " 347 | "{1}".format( 348 | path, access_zone)) 349 | try: 350 | NfsExportsExtendedObj = self.protocol_api.list_nfs_exports( 351 | path=path, zone=access_zone) 352 | if NfsExportsExtendedObj.total > 1: 353 | error_msg = 'Multiple NFS Exports found' 354 | LOG.error(error_msg) 355 | self.module.fail_json(msg=error_msg) 356 | 357 | elif NfsExportsExtendedObj.total == 0: 358 | LOG.info( 359 | 'NFS Export for given path: {0} and access zone: {1} not found'.format( 360 | path, access_zone)) 361 | return {} 362 | 363 | else: 364 | nfs_export = NfsExportsExtendedObj.exports[0] 365 | return nfs_export.to_dict() 366 | 367 | except Exception as e: 368 | error_msg = ( 369 | "Got error {0} while getting NFS export details for path: " 370 | "{1} and access zone: {2}" .format( 371 | self.determine_error(e), 372 | path, 373 | access_zone)) 374 | LOG.error(error_msg) 375 | self.module.fail_json(msg=error_msg) 376 | 377 | def _create_client_lists_from_playbook(self): 378 | all_client_list = [ 379 | self.module.params['clients'], 380 | self.module.params['read_only_clients'], 381 | self.module.params['read_write_clients'], 382 | self.module.params['root_clients']] 383 | return all_client_list 384 | 385 | def _get_nfs_export_from_id(self, nfs_export_id, access_zone): 386 | ''' 387 | Get details of an NFS export using NFS export ID and access zone 388 | ''' 389 | LOG.info( 390 | "Getting NFS export details for id: {0} and access zone: {1}".format( 391 | nfs_export_id, access_zone)) 392 | try: 393 | NfsExportsObj = self.protocol_api.get_nfs_export( 394 | nfs_export_id, zone=access_zone) 395 | nfs_export = NfsExportsObj.exports[0] 396 | return nfs_export.to_dict() 397 | 398 | except Exception as e: 399 | error_msg = ( 400 | "Got error {0} while getting NFS export details for ID: " 401 | "{1} and access zone: {2}" .format( 402 | self.determine_error(e), 403 | nfs_export_id, 404 | access_zone)) 405 | LOG.error(error_msg) 406 | self.module.fail_json(msg=error_msg) 407 | 408 | def _create_nfs_export_create_params_object(self, path): 409 | try: 410 | nfs_export = self.isi_sdk.NfsExportCreateParams( 411 | paths=[path], 412 | clients=self.module.params['clients'], 413 | read_only_clients=self.module.params['read_only_clients'], 414 | read_write_clients=self.module.params['read_write_clients'], 415 | root_clients=self.module.params['root_clients'], 416 | read_only=self.module.params['read_only'], 417 | all_dirs=self.module.params['sub_directories_mountable'], 418 | description=self.module.params['description'], 419 | zone=self.module.params['access_zone']) 420 | return nfs_export 421 | except Exception as e: 422 | errorMsg = 'Create NfsExportCreateParams object for path {0}' \ 423 | 'failed with error {1}'.format( 424 | path, self.determine_error(e)) 425 | LOG.error(errorMsg) 426 | self.module.fail_json(msg=errorMsg) 427 | 428 | def create_nfs_export(self, path, access_zone): 429 | ''' 430 | Create NFS export for given path and access_zone 431 | ''' 432 | nfs_export = self._create_nfs_export_create_params_object(path) 433 | try: 434 | msg = ( 435 | "Creating NFS export with parameters:nfs_export={0}") 436 | LOG.info(msg.format(nfs_export)) 437 | response = self.protocol_api.create_nfs_export(nfs_export, zone=access_zone) 438 | self.result['NFS_export_details'] = self._get_nfs_export_from_id(response.id, access_zone=access_zone) 439 | return True 440 | 441 | except Exception as e: 442 | errorMsg = 'Create NFS export for path: {0} and access zone: {1}' \ 443 | ' failed with error: {2}'.format( 444 | path, access_zone, self.determine_error(e)) 445 | LOG.error(errorMsg) 446 | self.module.fail_json(msg=errorMsg) 447 | 448 | def _create_current_client_dict_from_playbook(self): 449 | client_dict_playbook_input = { 450 | 'read_only_clients': self.module.params['read_only_clients'], 451 | 'clients': self.module.params['clients'], 452 | 'root_clients': self.module.params['root_clients'], 453 | 'read_write_clients': self.module.params['read_write_clients']} 454 | 455 | return client_dict_playbook_input 456 | 457 | def _create_current_client_dict(self): 458 | current_client_dict = { 459 | 'read_only_clients': self.result['NFS_export_details']['read_only_clients'], 460 | 'clients': self.result['NFS_export_details']['clients'], 461 | 'root_clients': self.result['NFS_export_details']['root_clients'], 462 | 'read_write_clients': self.result['NFS_export_details']['read_write_clients']} 463 | 464 | return current_client_dict 465 | 466 | def _check_add_clients(self, nfs_export): 467 | ''' 468 | Check if clients are to be added to NFS export 469 | ''' 470 | playbook_client_dict = self._create_current_client_dict_from_playbook() 471 | current_client_dict = self._create_current_client_dict() 472 | mod_flag = False 473 | 474 | mod_flag1 = False 475 | if playbook_client_dict['clients']: 476 | for client in playbook_client_dict['clients']: 477 | if client not in current_client_dict['clients']: 478 | current_client_dict['clients'].append(client) 479 | mod_flag1 = True 480 | 481 | if mod_flag1: 482 | nfs_export.clients = current_client_dict['clients'] 483 | 484 | mod_flag2 = False 485 | if playbook_client_dict['read_write_clients']: 486 | for client in playbook_client_dict['read_write_clients']: 487 | if client not in current_client_dict['read_write_clients']: 488 | current_client_dict['read_write_clients'].append(client) 489 | mod_flag2 = True 490 | 491 | if mod_flag2: 492 | nfs_export.read_write_clients = current_client_dict['read_write_clients'] 493 | 494 | mod_flag3 = False 495 | if playbook_client_dict['read_only_clients']: 496 | for client in playbook_client_dict['read_only_clients']: 497 | if client not in current_client_dict['read_only_clients']: 498 | current_client_dict['read_only_clients'].append(client) 499 | mod_flag3 = True 500 | 501 | if mod_flag3: 502 | nfs_export.read_only_clients = current_client_dict['read_only_clients'] 503 | 504 | mod_flag4 = False 505 | if playbook_client_dict['root_clients']: 506 | for client in playbook_client_dict['root_clients']: 507 | if client not in current_client_dict['root_clients']: 508 | current_client_dict['root_clients'].append(client) 509 | mod_flag4 = True 510 | 511 | if mod_flag4: 512 | nfs_export.root_clients = current_client_dict['root_clients'] 513 | 514 | mod_flag = mod_flag1 or mod_flag2 or mod_flag3 or mod_flag4 515 | return mod_flag, nfs_export 516 | 517 | def _check_remove_clients(self, nfs_export): 518 | ''' 519 | Check if clients are to be removed from NFS export 520 | ''' 521 | playbook_client_dict = self._create_current_client_dict_from_playbook() 522 | current_client_dict = self._create_current_client_dict() 523 | mod_flag = False 524 | 525 | mod_flag1 = False 526 | if playbook_client_dict['clients']: 527 | for client in playbook_client_dict['clients']: 528 | if client in current_client_dict['clients']: 529 | current_client_dict['clients'].remove(client) 530 | mod_flag1 = True 531 | 532 | if mod_flag1: 533 | nfs_export.clients = current_client_dict['clients'] 534 | 535 | mod_flag2 = False 536 | if playbook_client_dict['read_write_clients']: 537 | for client in playbook_client_dict['read_write_clients']: 538 | if client in current_client_dict['read_write_clients']: 539 | current_client_dict['read_write_clients'].remove(client) 540 | mod_flag2 = True 541 | 542 | if mod_flag2: 543 | nfs_export.read_write_clients = current_client_dict['read_write_clients'] 544 | 545 | mod_flag3 = False 546 | if playbook_client_dict['read_only_clients']: 547 | for client in playbook_client_dict['read_only_clients']: 548 | if client in current_client_dict['read_only_clients']: 549 | current_client_dict['read_only_clients'].remove(client) 550 | mod_flag3 = True 551 | 552 | if mod_flag3: 553 | nfs_export.read_only_clients = current_client_dict['read_only_clients'] 554 | 555 | mod_flag4 = False 556 | if playbook_client_dict['root_clients']: 557 | for client in playbook_client_dict['root_clients']: 558 | if client in current_client_dict['root_clients']: 559 | current_client_dict['root_clients'].remove(client) 560 | mod_flag4 = True 561 | 562 | if mod_flag4: 563 | nfs_export.root_clients = current_client_dict['root_clients'] 564 | 565 | mod_flag = mod_flag1 or mod_flag2 or mod_flag3 or mod_flag4 566 | return mod_flag, nfs_export 567 | 568 | def _check_mod_field(self, field_name_playbook, field_name_isilon): 569 | _field_mod = False 570 | if self.module.params[field_name_playbook] is None: 571 | field_value = self.result['NFS_export_details'][field_name_isilon] 572 | elif self.module.params[field_name_playbook] == \ 573 | self.result['NFS_export_details'][field_name_isilon]: 574 | field_value = self.result['NFS_export_details'][field_name_isilon] 575 | else: 576 | field_value = self.module.params[field_name_playbook] 577 | _field_mod = True 578 | return _field_mod, field_value 579 | 580 | def modify_nfs_export(self, path, access_zone): 581 | ''' 582 | Modify NFS export in system 583 | ''' 584 | nfs_export = self.isi_sdk.NfsExport() 585 | client_flag = False 586 | if self.module.params['client_state'] == 'present-in-export': 587 | client_flag, nfs_export = self._check_add_clients(nfs_export) 588 | if self.module.params['client_state'] == 'absent-in-export': 589 | client_flag, nfs_export = self._check_remove_clients(nfs_export) 590 | 591 | read_only_flag, read_only_value = self._check_mod_field( 592 | 'read_only', 'read_only') 593 | all_dirs_flag, all_dirs_value = self._check_mod_field( 594 | 'sub_directories_mountable', 'all_dirs') 595 | description_flag, description_value = self._check_mod_field( 596 | 'description', 'description') 597 | 598 | if all( 599 | field_mod_flag is False for field_mod_flag in [ 600 | client_flag, 601 | read_only_flag, 602 | all_dirs_flag, 603 | description_flag]): 604 | LOG.info( 605 | 'No change detected for the NFS Export, returning changed = False') 606 | return False 607 | else: 608 | 609 | nfs_export.read_only = read_only_value if read_only_flag else None 610 | nfs_export.all_dirs = all_dirs_value if all_dirs_flag else None 611 | nfs_export.description = description_value if description_flag else None 612 | LOG.info('Modifying NFS Export with %s details', nfs_export) 613 | 614 | try: 615 | self.protocol_api.update_nfs_export( 616 | nfs_export, 617 | self.result['NFS_export_details']['id'], 618 | zone=self.result['NFS_export_details']['zone']) 619 | # update result with updated details 620 | self.result['NFS_export_details'] = self.get_nfs_export( 621 | path, access_zone) 622 | return True 623 | 624 | except Exception as e: 625 | errorMsg = 'Modify NFS export for path: {0} and access zone:' \ 626 | ' {1} failed with error: {2}'.format( 627 | path, access_zone, self.determine_error(e)) 628 | LOG.error(errorMsg) 629 | self.module.fail_json(msg=errorMsg) 630 | 631 | def delete_nfs_export(self): 632 | ''' 633 | Delete NFS export from system 634 | ''' 635 | nfs_export = self.result['NFS_export_details'] 636 | try: 637 | msg = ('Deleting NFS export with path: {0}, zone: {1} and ID: {2}'.format( 638 | nfs_export['paths'][0], nfs_export['zone'], nfs_export['id'])) 639 | LOG.info(msg) 640 | self.protocol_api.delete_nfs_export( 641 | nfs_export['id'], zone=nfs_export['zone']) 642 | 643 | self.result['NFS_export_details'] = {} 644 | return True 645 | except Exception as e: 646 | errorMsg = ( 647 | 'Delete NFS export with path: {0}, zone: {1}, id: {2} failed' 648 | 'with error {3}'.format( 649 | nfs_export['paths'][0], 650 | nfs_export['zone'], 651 | nfs_export['id'], 652 | self.determine_error(e))) 653 | LOG.error(errorMsg) 654 | self.module.fail_json(msg=errorMsg) 655 | 656 | def determine_error(self, error_obj): 657 | '''Format the error object''' 658 | if isinstance(error_obj, utils.ApiException): 659 | error = re.sub("[\n \"]+", ' ', str(error_obj.body)) 660 | else: 661 | error = str(error_obj) 662 | return error 663 | 664 | def determine_path(self): 665 | path = self.module.params['path'] 666 | access_zone = self.module.params['access_zone'] 667 | 668 | if access_zone.lower() != 'system': 669 | if not path.startswith('/'): 670 | path = "/" + path 671 | path = self.get_zone_base_path(access_zone) + path 672 | 673 | return path 674 | 675 | def _validate_input(self): 676 | all_client_list = self._create_client_lists_from_playbook() 677 | if self.module.params['client_state'] is not None and all( 678 | client_list is None for client_list in all_client_list): 679 | error_msg = 'Invalid input: Client state is given, clients not specified' 680 | LOG.error(error_msg) 681 | self.module.fail_json(msg=error_msg) 682 | if self.module.params['client_state'] is None and any( 683 | client_list is not None for client_list in all_client_list): 684 | error_msg = 'Invalid input: Clients are given, client state not specified' 685 | LOG.error(error_msg) 686 | self.module.fail_json(msg=error_msg) 687 | 688 | def perform_module_operation(self): 689 | ''' 690 | Perform different actions on NFS exports based on user parameter 691 | chosen in playbook 692 | ''' 693 | state = self.module.params['state'] 694 | access_zone = self.module.params['access_zone'] 695 | path = self.determine_path() 696 | changed = False 697 | 698 | self.result['NFS_export_details'] = self.get_nfs_export( 699 | path, access_zone) 700 | 701 | self._validate_input() 702 | if state == 'present' and self.result['NFS_export_details']: 703 | # check for modification 704 | changed = self.modify_nfs_export(path, access_zone) or changed 705 | 706 | if state == 'present' and not self.result['NFS_export_details']: 707 | # create NFS export 708 | changed = self.create_nfs_export(path, access_zone) 709 | 710 | if state == 'absent' and self.result['NFS_export_details']: 711 | # delete nfs export 712 | changed = self.delete_nfs_export() or changed 713 | 714 | # Update the module's final state 715 | LOG.info('changed {0}'.format(changed)) 716 | self.result['changed'] = changed 717 | self.module.exit_json(**self.result) 718 | 719 | def get_isilon_nfs_parameters(self): 720 | return dict( 721 | path=dict(required=True, type='str'), 722 | access_zone=dict(type='str', default='System'), 723 | clients=dict(type='list'), 724 | root_clients=dict(type='list'), 725 | read_only_clients=dict(type='list'), 726 | read_write_clients=dict(type='list'), 727 | client_state=dict(type='str', 728 | choices=['present-in-export', 729 | 'absent-in-export']), 730 | description=dict(type='str'), 731 | read_only=dict(type='bool'), 732 | sub_directories_mountable=dict(type='bool'), 733 | state=dict(required=True, type='str', choices=['present', 734 | 'absent']) 735 | ) 736 | 737 | 738 | def main(): 739 | ''' Create Isilon_NFS export object and perform action on it 740 | based on user input from playbook''' 741 | obj = IsilonNfsExport() 742 | obj.perform_module_operation() 743 | 744 | 745 | if __name__ == '__main__': 746 | main() 747 | -------------------------------------------------------------------------------- /dellemc_ansible/isilon/library/dellemc_isilon_smartquota.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # Copyright: (c) 2020, DellEMC 3 | """ Ansible module for managing Smart Quota on Isilon""" 4 | from __future__ import (absolute_import, division, print_function) 5 | 6 | __metaclass__ = type 7 | ANSIBLE_METADATA = {'metadata_version': '1.1', 8 | 'status': ['preview'], 9 | 'supported_by': 'community'} 10 | 11 | DOCUMENTATION = r''' 12 | --- 13 | module: dellemc_isilon_smartquota 14 | 15 | version_added: '2.7' 16 | 17 | short_description: Manage Smart Quotas on Isilon 18 | 19 | description: 20 | - Managing Smart Quotas on Isilon storage system includes getting details, 21 | modifying, creating and deleting Smart Quotas. 22 | 23 | extends_documentation_fragment: 24 | - dellemc_isilon.dellemc_isilon 25 | 26 | author: 27 | - P Srinivas Rao (@srinivas-rao5) srinivas_rao5@dell.com 28 | 29 | options: 30 | path: 31 | description: 32 | - The path on which the quota will be imposed. 33 | - For system access zone, the path is absolute. For all other access 34 | zones, the path is a relative path from the base of the access zone. 35 | type: str 36 | required: True 37 | quota_type: 38 | description: 39 | - The type of quota which will be imposed on path. 40 | type: str 41 | required: True 42 | choices: ['user', 'group', 'directory'] 43 | user_name: 44 | description: 45 | - The name of the user account for which 46 | quota operations will be performed. 47 | type: str 48 | group_name: 49 | description: 50 | - The name of the group for which quota operations will be performed. 51 | type: str 52 | access_zone: 53 | description: 54 | - This option mentions the zone in which user/group exists. 55 | - For non system access zone, path relative to non system Access Zone's 56 | base directory has to be given. 57 | - For system access zone, absolute path has to be given. 58 | type: str 59 | default: 'system' 60 | provider_type: 61 | description: 62 | - This option defines the type which is used to 63 | authenticate the user/group. 64 | - If the provider_type is 'ads' then domain name of the Active 65 | Directory Server has to be mentioned in the user_name. 66 | The format for the user_name should be 'DOMAIN_NAME\user_name' 67 | or "DOMAIN_NAME\\user_name". 68 | - This option acts as a filter for all operations except creation. 69 | type: str 70 | default: 'local' 71 | choices: [ 'local', 'file', 'ldap', 'ads'] 72 | quota: 73 | description: 74 | - Specifies Smart Quota parameters. 75 | type: dict 76 | suboptions: 77 | include_snapshots: 78 | description: 79 | - Whether to include the snapshots in the quota or not. 80 | type: bool 81 | default: False 82 | include_overheads: 83 | description: 84 | - Whether to include the data protection overheads 85 | in the quota or not. 86 | - If not passed during quota creation then quota will be created 87 | excluding the overheads. 88 | type: bool 89 | advisory_limit_size: 90 | description: 91 | - The threshold value after which the advisory notification 92 | will be sent. 93 | type: int 94 | soft_limit_size: 95 | description: 96 | - Threshold value after which soft limit exceeded notification 97 | will be sent and soft_grace period will start. 98 | - Write access will be restricted after grace period expires. 99 | - Both soft_grace_period and soft_limit_size are required to modify 100 | soft threshold for the quota. 101 | type: int 102 | soft_grace_period: 103 | description: 104 | - Grace Period after the soft limit for quota is exceeded. 105 | - After the grace period, the write access to the quota will be 106 | restricted. 107 | - Both soft_grace_period and soft_limit_size are required to modify 108 | soft threshold for the quota. 109 | type: int 110 | period_unit: 111 | description: 112 | - Unit of the time period for soft_grace_period. 113 | - For months the number of days is assumed to be 30 days. 114 | - This parameter is required only if the soft_grace_period, 115 | is specified. 116 | type: str 117 | choices: ['days', 'weeks', 'months'] 118 | hard_limit_size: 119 | description: 120 | - Threshold value after which hard limit exceeded 121 | notification will be sent. 122 | - Write access will be restricted after hard limit is exceeded. 123 | type: int 124 | cap_unit: 125 | description: 126 | - Unit of storage for the hard, soft and advisory limits. 127 | - This parameter is required if any of the hard, soft or advisory 128 | limits is specified. 129 | type: str 130 | choices: ['GB', 'TB'] 131 | state: 132 | description: 133 | - Define whether the Smart Quota should exist or not. 134 | - present - indicates that the Smart Quota should exist on the system. 135 | - absent - indicates that the Smart Quota should not exist on the system. 136 | choices: ['absent', 'present'] 137 | type: str 138 | required: True 139 | 140 | notes: 141 | - To perform any operation, path, quota_type and state are 142 | mandatory parameters. 143 | - There can be two quotas for each type per directory, one with snapshots 144 | included, and one without snapshots included. 145 | - Once the limits are assigned then the quota can't be converted to 146 | accounting only modification to the threshold limits is permitted. 147 | ''' 148 | EXAMPLES = r''' 149 | 150 | - name: Create a Quota for a User excluding snapshot. 151 | dellemc_isilon_smartquota: 152 | onefs_host: "{{onefs_host}}" 153 | verify_ssl: "{{verify_ssl}}" 154 | api_user: "{{api_user}}" 155 | api_password: "{{api_password}}" 156 | path: "{{path}}" 157 | quota_type: "user" 158 | user_name: "{{user_name}}" 159 | access_zone: "{{access_zone}}" 160 | provider_type: "{{provider_type}}" 161 | quota: 162 | include_overheads: False 163 | advisory_limit_size: "{{advisory_limit_size}}" 164 | soft_limit_size: "{{soft_limit_size}}" 165 | soft_grace_period: "{{soft_grace_period}}" 166 | period_unit: "{{period_unit}}" 167 | hard_limit_size: "{{hard_limit_size}}" 168 | cap_unit: "{{cap_unit}}" 169 | state: "present" 170 | 171 | - name: Create a Quota for a Directory for accounting includes snapshots and data protection overheads. 172 | dellemc_isilon_smartquota: 173 | onefs_host: "{{onefs_host}}" 174 | verify_ssl: "{{verify_ssl}}" 175 | api_user: "{{api_user}}" 176 | api_password: "{{api_password}}" 177 | path: "{{path}}" 178 | quota_type: "directory" 179 | provider_type: "{{provider_type}}" 180 | quota: 181 | include_snapshots: "True" 182 | include_overheads: "True" 183 | state: "present" 184 | 185 | - name: Get a Quota Details for a Group 186 | dellemc_isilon_smartquota: 187 | onefs_host: "{{onefs_host}}" 188 | verify_ssl: "{{verify_ssl}}" 189 | api_user: "{{api_user}}" 190 | api_password: "{{api_password}}" 191 | path: "{{path}}" 192 | quota_type: "group" 193 | group_name: "{{user_name}}" 194 | access_zone: "{{access_zone}}" 195 | provider_type: "{{provider_type}}" 196 | quota: 197 | include_snapshots: "True" 198 | state: "present" 199 | 200 | - name: Update Quota for a User 201 | dellemc_isilon_smartquota: 202 | onefs_host: "{{onefs_host}}" 203 | verify_ssl: "{{verify_ssl}}" 204 | api_user: "{{api_user}}" 205 | api_password: "{{api_password}}" 206 | path: "{{path}}" 207 | quota_type: "user" 208 | user_name: "{{user_name}}" 209 | access_zone: "{{access_zone}}" 210 | provider_type: "{{provider_type}}" 211 | quota: 212 | include_snapshots: "{{include_snapshots}}" 213 | include_overheads: "{{include_overheads}}" 214 | advisory_limit_size: "{{new_advisory_limit_size}}" 215 | hard_limit_size: "{{new_hard_limit_size}}" 216 | cap_unit: "{{cap_unit}}" 217 | state: "present" 218 | 219 | - name: Delete a Quota for a Directory 220 | dellemc_isilon_smartquota: 221 | onefs_host: "{{onefs_host}}" 222 | verify_ssl: "{{verify_ssl}}" 223 | api_user: "{{api_user}}" 224 | api_password: "{{api_password}}" 225 | path: "{{path}}" 226 | quota_type: "{{user_quota_type}}" 227 | quota: 228 | include_snapshots: "True" 229 | state: "absent" 230 | ''' 231 | RETURN = r''' 232 | changed: 233 | description: Whether or not the resource has changed 234 | returned: always 235 | type: bool 236 | sample: True 237 | 238 | quota_details: 239 | description: The quota details. 240 | type: complex 241 | returned: When Quota exists. 242 | contains: 243 | id: 244 | description: 245 | - The ID of the Quota. 246 | type: str 247 | sample: "2nQKAAEAAAAAAAAAAAAAQIMCAAAAAAAA" 248 | enforced: 249 | description: 250 | - Whether the limits are enforced on Quota or not. 251 | type: bool 252 | sample: True 253 | thresholds: 254 | description: 255 | - Includes information about all the limits imposed on quota. 256 | - The limits are mentioned in bytes and soft_grace is in seconds. 257 | type: dict 258 | sample: { 259 | "advisory": 3221225472, 260 | "advisory(GB)": "3.0", 261 | "advisory_exceeded": false, 262 | "advisory_last_exceeded": 0, 263 | "hard": 6442450944, 264 | "hard(GB)": "6.0", 265 | "hard_exceeded": false, 266 | "hard_last_exceeded": 0, 267 | "soft": 5368709120, 268 | "soft(GB)": "5.0", 269 | "soft_exceeded": false, 270 | "soft_grace": 3024000, 271 | "soft_last_exceeded": 0 272 | } 273 | type: 274 | description: 275 | - The type of Quota. 276 | type: str 277 | sample: "directory" 278 | usage: 279 | description: 280 | - The Quota usage. 281 | type: dict 282 | sample: { 283 | "inodes": 1, 284 | "logical": 0, 285 | "physical": 2048 286 | } 287 | ''' 288 | 289 | from ansible.module_utils.basic import AnsibleModule 290 | from ansible.module_utils.storage.dell \ 291 | import dellemc_ansible_isilon_utils as utils 292 | import re 293 | 294 | LOG = utils.get_logger('dellemc_isilon_smartquota', 295 | log_devel=utils.logging.INFO) 296 | HAS_ISILON_SDK = utils.has_isilon_sdk() 297 | ISILON_SDK_VERSION_CHECK = utils.isilon_sdk_version_check() 298 | 299 | 300 | class IsilonSmartQuota(object): 301 | """Class with Smart Quota operations""" 302 | 303 | def __init__(self): 304 | """ Define all parameters required by this module""" 305 | 306 | self.module_params = utils.get_isilon_management_host_parameters() 307 | self.module_params.update(get_isilon_smartquota_parameters()) 308 | mut_ex_args = [['group_name', 'user_name']] 309 | req_if_args = [ 310 | ['quota_type', 'user', ['user_name']], 311 | ['quota_type', 'group', ['group_name']] 312 | ] 313 | 314 | # initialize the ansible module 315 | self.module = AnsibleModule(argument_spec=self.module_params, 316 | supports_check_mode=False, 317 | mutually_exclusive=mut_ex_args, 318 | required_if=req_if_args) 319 | 320 | # result is a dictionary that contains changed status and 321 | # smart quota details 322 | self.result = {"changed": False} 323 | if HAS_ISILON_SDK is False: 324 | self.module.fail_json( 325 | msg="Ansible modules for Isilon require the isilon " 326 | "python library to be installed. Please install" 327 | " the library before using these modules.") 328 | 329 | if ISILON_SDK_VERSION_CHECK and \ 330 | not ISILON_SDK_VERSION_CHECK['supported_version']: 331 | err_msg = ISILON_SDK_VERSION_CHECK['unsupported_version_message'] 332 | LOG.error(err_msg) 333 | self.module.fail_json(msg=err_msg) 334 | 335 | self.api_client = utils.get_isilon_connection(self.module.params) 336 | self.auth_api_instance = utils.isi_sdk.AuthApi(self.api_client) 337 | self.zone_summary_api = utils.isi_sdk.ZonesSummaryApi( 338 | self.api_client) 339 | self.quota_api_instance = utils.isi_sdk.QuotaApi(self.api_client) 340 | 341 | LOG.info('Got the isi_sdk instance for Smart Quota Operations') 342 | 343 | def get_zone_base_path(self, access_zone): 344 | """ 345 | Get the base path of the Access Zone. 346 | :param access_zone: Name of the Access Zone. 347 | :return: Base Path of the Access Zone. 348 | """ 349 | try: 350 | zone_path = (self.zone_summary_api. 351 | get_zones_summary_zone(access_zone)).to_dict() 352 | zone_base_path = zone_path['summary']['path'] 353 | LOG.info("Successfully got zone_base_path for %s is %s", 354 | access_zone, zone_base_path) 355 | return zone_base_path 356 | except Exception as e: 357 | error_message = 'Unable to fetch base path of Access Zone %s' \ 358 | ',failed with error: %s' \ 359 | % (access_zone, determine_error(e)) 360 | LOG.error(error_message) 361 | self.module.fail_json(msg=error_message) 362 | 363 | def create(self, path, quota_type, zone, 364 | quota_dict, persona=None): 365 | """ 366 | Create a Smart Quota. 367 | :param path: The path for which quota has to be created. 368 | :param quota_type: The type of the quota. 369 | :param zone: The zone in which user/group exists. 370 | :param quota_dict: Threshold limits dictionary containing all limits. 371 | :param persona: User/Group object. 372 | :return: Quota Id. 373 | """ 374 | if quota_dict: 375 | threshold_obj = utils.isi_sdk.QuotaQuotaThresholds( 376 | quota_dict['advisory'], hard=quota_dict['hard'], 377 | soft=quota_dict['soft'], 378 | soft_grace=quota_dict['soft_grace']) 379 | else: 380 | threshold_obj = utils.isi_sdk.QuotaQuotaThresholds() 381 | 382 | enforced = False 383 | if quota_dict and (quota_dict['hard'] or quota_dict['soft'] 384 | or quota_dict['advisory']): 385 | enforced = True 386 | 387 | # if not passed during creation of Quota 388 | # Set include_overheads as False 389 | if quota_dict is None or quota_dict['include_overheads'] is None: 390 | include_overhead = False 391 | else: 392 | include_overhead = quota_dict['include_overheads'] 393 | if quota_dict is None or quota_dict['include_snapshots'] is None: 394 | include_snapshots = False 395 | else: 396 | include_snapshots = quota_dict['include_snapshots'] 397 | 398 | quota_params_obj = utils.isi_sdk.QuotaQuotaCreateParams( 399 | include_snapshots=include_snapshots, path=path, 400 | enforced=enforced, 401 | persona=persona, 402 | thresholds_include_overhead=include_overhead, 403 | thresholds=threshold_obj, type=quota_type) 404 | try: 405 | api_response = self.quota_api_instance.create_quota_quota( 406 | quota_quota=quota_params_obj, zone=zone) 407 | message = "Quota created, %s" % api_response 408 | LOG.info(message) 409 | return api_response 410 | except utils.ApiException as e: 411 | error_message = "Create quota for %s failed with %s" \ 412 | % (path, determine_error(e)) 413 | LOG.error(error_message) 414 | self.module.fail_json(msg=error_message) 415 | 416 | def update(self, quota_dict, quota_id, enforced, path): 417 | """ 418 | Update the attributes for a Smart Quota. 419 | :param quota_dict: Threshold limits dictionary containing all limits. 420 | :param quota_id: Id of the quota. 421 | :param enforced: Boolean value whether to enforce limits or not. 422 | :param path: The path for which quota has to be updated. 423 | :return: True if the operation is successful. 424 | """ 425 | threshold_obj = utils.isi_sdk.QuotaQuotaThresholds( 426 | advisory=quota_dict['advisory'], hard=quota_dict['hard'], 427 | soft=quota_dict['soft'], soft_grace=quota_dict['soft_grace']) 428 | quota_params_obj = utils.isi_sdk.QuotaQuota( 429 | enforced=enforced, 430 | thresholds_include_overhead=quota_dict['include_overheads'], 431 | thresholds=threshold_obj) 432 | try: 433 | self.quota_api_instance.update_quota_quota( 434 | quota_quota=quota_params_obj, quota_quota_id=quota_id) 435 | msg = "Quota Updated successfully for path %s" % path 436 | LOG.info(msg) 437 | return True 438 | except utils.ApiException as e: 439 | error_message = "Update quota for path %s failed with %s" \ 440 | % (path, determine_error(e)) 441 | LOG.error(error_message) 442 | self.module.fail_json(msg=error_message) 443 | 444 | def get_sid(self, name, type, provider, zone): 445 | """ 446 | Get the User/Group Account's SID in Isilon. 447 | :param name: Name of the resource. 448 | :param type: Whether resource is of User or Group. 449 | :param provider: Authentication type for the resource. 450 | :param zone: Access Zone in which resource exists. 451 | :return: sid of the resource. 452 | """ 453 | try: 454 | if type == 'user': 455 | api_response = self.auth_api_instance.get_auth_user( 456 | auth_user_id='USER:' + name, 457 | zone=zone, provider=provider) 458 | msg = "SID of the user: %s" % api_response.users[0].sid.id 459 | LOG.info(msg) 460 | return api_response.users[0].sid.id 461 | 462 | elif type == 'group': 463 | api_response = self.auth_api_instance.get_auth_group( 464 | auth_group_id='GROUP:' + name, zone=zone, 465 | provider=provider) 466 | msg = "SID of the group: %s" % api_response.groups[0].sid.id 467 | LOG.info(msg) 468 | return api_response.groups[0].sid.id 469 | 470 | except Exception as e: 471 | error_message = "Failed to get {0} details for " \ 472 | "AccessZone:{1} and Provider:{2} " \ 473 | "with error {3}" \ 474 | .format(name, zone, provider, determine_error(e)) 475 | LOG.error(error_message) 476 | self.module.fail_json(msg=error_message) 477 | 478 | def get_quota_details(self, include_snapshots, 479 | zone, type, path, persona=None): 480 | """ 481 | Get the details of the Smart Quota. 482 | :param include_snapshots: Whether to include snapshots or not. 483 | :param zone: Access Zone in which User/Group/Quota exists. 484 | :param type: The type of the quota. 485 | :param path: The path for which quota exists. 486 | :param persona: User/Group object. 487 | :return: if exists returns details of the Quota and Quota's Id, 488 | else returns None. 489 | """ 490 | try: 491 | if type == 'directory': 492 | api_response = self.quota_api_instance.list_quota_quotas( 493 | include_snapshots=include_snapshots, zone=zone, 494 | type=type, path=path) 495 | else: 496 | api_response = self.quota_api_instance.list_quota_quotas( 497 | include_snapshots=include_snapshots, zone=zone, 498 | persona=persona, type=type, path=path) 499 | if api_response.quotas: 500 | quota_id = api_response.quotas[0].id 501 | quota = api_response.quotas[0].to_dict() 502 | msg = "Get Quota Details Successful. Quota Details: %s"\ 503 | % quota 504 | LOG.info(msg) 505 | return quota, quota_id 506 | LOG.info("Get Quota Details Failed. Quota does not exist.") 507 | return None, None 508 | except Exception as e: 509 | error_message = "Get Quota Details for %s failed with %s" \ 510 | % (path, determine_error(e)) 511 | LOG.error(error_message) 512 | self.module.fail_json(msg=error_message) 513 | 514 | def delete(self, quota_id, path): 515 | """ 516 | Delete the Smart Quota. 517 | :param quota_id: The Id of the Quota. 518 | :param path: The path for which quota exists. 519 | :return: True, if the delete operation is successful. 520 | """ 521 | try: 522 | self.quota_api_instance.delete_quota_quota(quota_id) 523 | msg = "Quota Deleted Successfully for Path %s" % path 524 | LOG.info(msg) 525 | return True 526 | except Exception as e: 527 | error_message = "Delete quota for %s failed with %s" \ 528 | % (path, determine_error(e)) 529 | LOG.error(error_message) 530 | self.module.fail_json(msg=error_message) 531 | 532 | def convert_quota_thresholds(self, quota): 533 | """ 534 | Convert the threshold limits to appropriate units. 535 | :param quota: Threshold limits dictionary containing all limits. 536 | :return: Converted Threshold limits dictionary. 537 | """ 538 | limit_params = ['advisory_limit_size', 'soft_limit_size', 539 | 'hard_limit_size', 'soft_grace_period'] 540 | for limit in quota.keys(): 541 | if limit in limit_params: 542 | if quota[limit] is not None and quota[limit] <= 0: 543 | self.module.fail_json( 544 | msg="Invalid %s provided, must be greater than 0" 545 | % limit) 546 | if limit == 'soft_grace_period': 547 | if quota[limit] is not None and quota[limit] > 0: 548 | quota[limit] = period_to_seconds( 549 | quota[limit], quota['period_unit']) 550 | elif quota[limit] is not None and quota[limit] > 0: 551 | quota[limit] = utils.get_size_bytes( 552 | quota[limit], quota['cap_unit']) 553 | quota['advisory'] = quota.pop('advisory_limit_size') 554 | quota['soft'] = quota.pop('soft_limit_size') 555 | quota['hard'] = quota.pop('hard_limit_size') 556 | quota['soft_grace'] = quota.pop('soft_grace_period') 557 | return quota 558 | 559 | def perform_module_operation(self): 560 | """ 561 | Perform different actions on Smart Quota module based on parameters 562 | chosen in playbook 563 | """ 564 | quota_type = self.module.params['quota_type'] 565 | user_name = self.module.params['user_name'] 566 | group_name = self.module.params['group_name'] 567 | 568 | access_zone = self.module.params['access_zone'] 569 | if access_zone == "" or access_zone.isspace(): 570 | self.module.fail_json(msg="Invalid Access_zone provided," 571 | " Please a provide valid Access_zone") 572 | 573 | provider_type = self.module.params['provider_type'] 574 | state = self.module.params['state'] 575 | 576 | quota = self.module.params['quota'] 577 | if quota: 578 | self.convert_quota_thresholds(quota) 579 | include_snapshots = quota.get('include_snapshots') 580 | else: 581 | include_snapshots = False 582 | message = "Quota Dictionary after conversion: %s" % str(quota) 583 | LOG.debug(message) 584 | 585 | path = self.module.params['path'] 586 | if path == "" or path.isspace(): 587 | self.module.fail_json(msg="Invalid path provided," 588 | " Please a provide valid path") 589 | # If Access_Zone is System then absolute path is required 590 | # else relative path is taken 591 | if access_zone.lower() == "system": 592 | complete_path = path 593 | else: 594 | complete_path = self.get_zone_base_path(access_zone) + path 595 | 596 | changed = False 597 | # Get the sid(security identifier) for User 598 | sid = None 599 | if quota_type == "user": 600 | sid = self.get_sid(user_name, quota_type, 601 | provider_type, access_zone) 602 | # Get the sid(security identifier) for Group 603 | if quota_type == "group": 604 | sid = self.get_sid(group_name, quota_type, 605 | provider_type, access_zone) 606 | 607 | # Throw error if quota_type is directory 608 | # and parameters for user and group are provided 609 | if quota_type == 'directory': 610 | provider_type = None 611 | if user_name or group_name or provider_type: 612 | self.module.fail_json( 613 | msg="quota_type is directory given," 614 | " user_name/group_name/provider_type not required.") 615 | 616 | # Throw error if limits and cap_unit are not passed together 617 | if quota and (quota['advisory'] or quota['soft'] or quota['hard']) \ 618 | and not quota['cap_unit']: 619 | self.module.fail_json(msg="advisory/soft/hard limit provided," 620 | " cap_unit not provided") 621 | if quota and quota['cap_unit'] \ 622 | and not(quota['advisory'] or quota['soft'] or quota['hard']): 623 | self.module.fail_json( 624 | msg="cap_unit provided," 625 | " advisory/soft/hard limit not provided") 626 | 627 | # Get the details of the Quota 628 | quota_details, quota_id = self.get_quota_details( 629 | include_snapshots, access_zone, quota_type, complete_path, sid) 630 | 631 | # Create a Quota 632 | if state == "present" and not quota_details: 633 | LOG.info("Create a Quota") 634 | persona_obj = None 635 | if quota_type != "directory": 636 | persona_obj = \ 637 | utils.isi_sdk.AuthAccessAccessItemFileGroup(id=sid) 638 | self.create(complete_path, quota_type, access_zone, quota, 639 | persona_obj) 640 | changed = True 641 | 642 | # Update a Quota 643 | if state == "present" and quota_details and quota: 644 | modify_flag = False 645 | if quota: 646 | modify_flag = to_modify_quota( 647 | quota, quota_details["thresholds"], 648 | quota_details["thresholds_include_overhead"]) 649 | enforce_limit = False 650 | if quota_details["enforced"] or quota['advisory'] or \ 651 | quota['hard'] or quota['soft']: 652 | enforce_limit = True 653 | if modify_flag: 654 | LOG.info("Updating the Quota") 655 | changed = self.update(quota, quota_id, enforce_limit, path) 656 | 657 | # Delete Quota 658 | if state == "absent" and quota_details: 659 | LOG.info("Delete Quota") 660 | changed = self.delete(quota_id, complete_path) 661 | 662 | quota_details, quota_id = self.get_quota_details( 663 | include_snapshots, access_zone, quota_type, complete_path, sid) 664 | if quota_type != "directory" and quota_details: 665 | quota_details['persona']['type'] = quota_type 666 | quota_details['persona']['name'] = \ 667 | user_name if user_name else group_name 668 | quota_details = add_limits_with_unit(quota_details) 669 | self.result["changed"] = changed 670 | self.result["quota_details"] = quota_details 671 | self.module.exit_json(**self.result) 672 | 673 | 674 | def add_limits_with_unit(quota_details): 675 | """ 676 | Adds limits to the quota details with units. 677 | :param quota_details: details of the Quota 678 | :return: updated quota details if quota details exists else None 679 | """ 680 | if quota_details is None: 681 | return None 682 | limit_list = ['hard', 'soft', 'advisory'] 683 | for limit in limit_list: 684 | if quota_details['thresholds'][limit]: 685 | size_with_unit = utils.convert_size_with_unit( 686 | quota_details['thresholds'][limit]).split(" ") 687 | new_limit = limit + "("+size_with_unit[1]+")" 688 | quota_details['thresholds'][new_limit] = size_with_unit[0] 689 | return quota_details 690 | 691 | 692 | def to_modify_quota(input_quota, array_quota, array_include_overhead): 693 | """ 694 | 695 | :param input_quota: Threshold limits dictionary passed by the user. 696 | :param array_quota: Threshold limits dictionary got from the Isilon Array 697 | :param array_include_overhead: Whether Quota Include Overheads or not. 698 | :return: True if the quota is to be modified else returns False. 699 | """ 700 | if input_quota['include_overheads'] is not None \ 701 | and input_quota['include_overheads'] != array_include_overhead: 702 | return True 703 | for limit in input_quota: 704 | if limit in array_quota and input_quota[limit] is not None and\ 705 | input_quota[limit] != array_quota[limit]: 706 | return True 707 | return False 708 | 709 | 710 | def determine_error(error_obj): 711 | """Determine the error message to return""" 712 | if isinstance(error_obj, utils.ApiException): 713 | error = re.sub("[\n \"]+", ' ', str(error_obj.body)) 714 | else: 715 | error = str(error_obj) 716 | return error 717 | 718 | 719 | def period_to_seconds(period, period_unit): 720 | """ Convert the given period to seconds""" 721 | if period_unit == 'days': 722 | return period * 86400 723 | if period_unit == 'weeks': 724 | return period * 7 * 86400 725 | if period_unit == 'months': 726 | return period * 30 * 86400 727 | 728 | 729 | def make_threshold_obj(advisory, soft, soft_grace, hard): 730 | """Make threshold object for quota""" 731 | thresholds = utils.isi_sdk.QuotaQuotaThresholds( 732 | advisory=advisory, hard=hard, soft=soft, soft_grace=soft_grace) 733 | return thresholds 734 | 735 | 736 | def get_isilon_smartquota_parameters(): 737 | """This method provides parameters required for the ansible Smart Quota 738 | module on Isilon""" 739 | return dict( 740 | path=dict(required=True, type='str'), 741 | user_name=dict(type='str'), 742 | group_name=dict(type='str'), 743 | access_zone=dict(type='str', default='system'), 744 | provider_type=dict(type='str', default='local', 745 | choices=['local', 'file', 'ldap', 'ads']), 746 | quota_type=dict(required=True, type='str', 747 | choices=['user', 'group', 'directory']), 748 | quota=dict( 749 | type='dict', options=dict( 750 | include_snapshots=dict(type='bool', default=False), 751 | include_overheads=dict(type='bool'), 752 | advisory_limit_size=dict(type='int'), 753 | soft_limit_size=dict(type='int'), 754 | hard_limit_size=dict(type='int'), 755 | soft_grace_period=dict(type='int'), 756 | period_unit=dict(type='str', 757 | choices=['days', 'weeks', 'months']), 758 | cap_unit=dict(type='str', choices=['GB', 'TB']) 759 | ), 760 | required_together=[ 761 | ['soft_grace_period', 'period_unit'], 762 | ['soft_grace_period', 'soft_limit_size'] 763 | ] 764 | ), 765 | state=dict(required=True, type='str', choices=['present', 'absent']) 766 | ) 767 | 768 | 769 | def main(): 770 | """ Create Isilon Smart Quota object and perform actions on it 771 | based on user input from playbook""" 772 | obj = IsilonSmartQuota() 773 | obj.perform_module_operation() 774 | 775 | 776 | if __name__ == '__main__': 777 | main() 778 | -------------------------------------------------------------------------------- /dellemc_ansible/isilon/library/dellemc_isilon_user.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # Copyright: (c) 2019, DellEMC 3 | """ Ansible module for managing Users on Isilon""" 4 | from __future__ import (absolute_import, division, print_function) 5 | 6 | __metaclass__ = type 7 | ANSIBLE_METADATA = {'metadata_version': '1.1', 8 | 'status': ['preview'], 9 | 'supported_by': 'community'} 10 | 11 | DOCUMENTATION = r''' 12 | --- 13 | module: dellemc_isilon_user 14 | short_description: Manage users on the Isilon Storage System 15 | description: 16 | - Managing Users on the Isilon Storage System includes create user, 17 | delete user, update user, get user, add role and remove role. 18 | version_added: "2.7" 19 | extends_documentation_fragment: 20 | - dellemc_isilon.dellemc_isilon 21 | author: 22 | - P Srinivas Rao (@srinivas-rao5) srinivas_rao5@dell.com 23 | options: 24 | user_name: 25 | description: 26 | - The name of the user account. 27 | - Required at the time of user creation, for rest of the operations 28 | either user_name or user_id is required. 29 | type: str 30 | user_id: 31 | description: 32 | - The user_id is auto generated at the time of creation. 33 | - For all other operations either user_name or user_id is needed. 34 | type: str 35 | password: 36 | description: 37 | - The password for the user account. 38 | - Required only in the creation of a user account. 39 | - If given in other operations then the password will be ignored. 40 | type: str 41 | access_zone: 42 | description: 43 | - This option mentions the zone in which a user is created. 44 | - For creation, access_zone acts as an attribute for the user. 45 | - For all other operations access_zone acts as a filter. 46 | type: str 47 | default: 'system' 48 | provider_type: 49 | description: 50 | - This option defines the type which will be used to 51 | authenticate the user. 52 | - Creation, Modification and Deletion is allowed for local users. 53 | - Adding and removing roles is allowed for all users of 54 | system access zone. 55 | - Getting user details is allowed for all users. 56 | - If the provider_type is 'ads' then domain name of the Active 57 | Directory Server has to be mentioned in the user_name. 58 | The format for the user_name should be 'DOMAIN_NAME\user_name' 59 | or "DOMAIN_NAME\\user_name". 60 | - This option acts as a filter for all operations except creation. 61 | type: str 62 | default: 'local' 63 | choices: [ 'local', 'file', 'ldap', 'ads'] 64 | enabled: 65 | description: 66 | - Enabled is a bool variable which is used to enable or disable 67 | the user account. 68 | type: bool 69 | primary_group: 70 | description: 71 | - A user can be member of multiple group of which one group has 72 | to be assigned as primary group. 73 | - This group will be used for access checks and 74 | can also be used when creating files. 75 | - A user can be added to the group using Group Name. 76 | type: str 77 | home_directory: 78 | description: 79 | - The path specified in this option acts as a home directory 80 | for the user. 81 | - The directory which is given should not be already in use. 82 | - For user in system access zone, absolute path has to be given. 83 | - For users in non system access zone, path relative to 84 | non system Access Zone's base directory has to be given. 85 | type: str 86 | shell: 87 | description: 88 | - This option is for choosing the type of shell for the user account. 89 | type: str 90 | full_name: 91 | description: 92 | - The additional information about the user can be provided using 93 | full_name option. 94 | type: str 95 | email: 96 | description: 97 | - The email id of the user can be added using email option. 98 | - The email id can be set at the time of creation and modified later. 99 | type: str 100 | state: 101 | description: 102 | - The state option is used to mention the existence of 103 | the user account. 104 | type: str 105 | required: True 106 | choices: [ 'absent', 'present' ] 107 | role_name: 108 | description: 109 | - The name of the role which a user will be assigned. 110 | - User can be added to multiple roles. 111 | type: str 112 | role_state: 113 | description: 114 | - The role_state option is used to mention the existence of the role 115 | for a particular user. 116 | - It is required when a role is added or removed from user. 117 | type: str 118 | choices: ['present-for-user', 'absent-for-user'] 119 | ''' 120 | 121 | EXAMPLES = r''' 122 | - name: Get User Details using user name 123 | dellemc_isilon_user: 124 | onefs_host: "{{onefs_host}}" 125 | port_no: "{{port_no}}" 126 | api_user: "{{api_user}}" 127 | api_password: "{{api_password}}" 128 | verify_ssl: "{{verify_ssl}}" 129 | access_zone: "{{access_zone}}" 130 | provider_type: "{{provider_type}}" 131 | user_name: "{{account_name}}" 132 | state: "present" 133 | 134 | - name: Create User 135 | dellemc_isilon_user: 136 | onefs_host: "{{onefs_host}}" 137 | port_no: "{{port_no}}" 138 | api_user: "{{api_user}}" 139 | api_password: "{{api_password}}" 140 | verify_ssl: "{{verify_ssl}}" 141 | access_zone: "{{access_zone}}" 142 | provider_type: "{{provider_type}}" 143 | user_name: "{{account_name}}" 144 | password: "{{account_password}}" 145 | primary_group: "{{primary_group}}" 146 | enabled: "{{enabled}}" 147 | email: "{{email}}" 148 | full_name: "{{full_name}}" 149 | home_directory: "{{home_directory}}" 150 | shell: "{{shell}}" 151 | role_name: "{{role_name}}" 152 | role_state: "present-for-user" 153 | state: "present" 154 | 155 | - name: Update User's Full Name and email using user name 156 | dellemc_isilon_user: 157 | onefs_host: "{{onefs_host}}" 158 | port_no: "{{port_no}}" 159 | api_user: "{{api_user}}" 160 | api_password: "{{api_password}}" 161 | verify_ssl: "{{verify_ssl}}" 162 | access_zone: "{{access_zone}}" 163 | provider_type: "{{provider_type}}" 164 | user_name: "{{account_name}}" 165 | email: "{{new_email}}" 166 | full_name: "{{full_name}}" 167 | state: "present" 168 | 169 | - name: Disable User Account using User Id 170 | dellemc_isilon_user: 171 | onefs_host: "{{onefs_host}}" 172 | port_no: "{{port_no}}" 173 | api_user: "{{api_user}}" 174 | api_password: "{{api_password}}" 175 | verify_ssl: "{{verify_ssl}}" 176 | access_zone: "{{access_zone}}" 177 | provider_type: "{{provider_type}}" 178 | user_id: "{{id}}" 179 | enabled: "False" 180 | state: "present" 181 | 182 | - name: Add user to a role using Username 183 | dellemc_isilon_user: 184 | onefs_host: "{{onefs_host}}" 185 | port_no: "{{port_no}}" 186 | api_user: "{{api_user}}" 187 | api_password: "{{api_password}}" 188 | verify_ssl: "{{verify_ssl}}" 189 | user_name: "{{account_name}}" 190 | provider_type: "{{provider_type}}" 191 | role_name: "{{role_name}}" 192 | role_state: "present-for-user" 193 | state: "present" 194 | 195 | - name: Remove user from a role using User id 196 | dellemc_isilon_user: 197 | onefs_host: "{{onefs_host}}" 198 | port_no: "{{port_no}}" 199 | api_user: "{{api_user}}" 200 | api_password: "{{api_password}}" 201 | verify_ssl: "{{verify_ssl}}" 202 | user_id: "{{id}}" 203 | role_name: "{{role_name}}" 204 | role_state: "absent-for-user" 205 | state: "present" 206 | 207 | - name: Delete User using user name 208 | dellemc_isilon_user: 209 | onefs_host: "{{onefs_host}}" 210 | port_no: "{{port_no}}" 211 | api_user: "{{api_user}}" 212 | api_password: "{{api_password}}" 213 | verify_ssl: "{{verify_ssl}}" 214 | access_zone: "{{access_zone}}" 215 | provider_type: "{{provider_type}}" 216 | user_name: "{{account_name}}" 217 | state: "absent" 218 | ''' 219 | 220 | RETURN = r''' 221 | changed: 222 | description: Whether or not the resource has changed 223 | returned: always 224 | type: bool 225 | user_details: 226 | description: Details of the user. 227 | returned: When user exists 228 | type: complex 229 | contains: 230 | email: 231 | description: 232 | - The email of the user. 233 | type: str 234 | enabled: 235 | description: 236 | - Enabled is a bool variable which is used to enable or 237 | disable the user account. 238 | type: bool 239 | gecos: 240 | description: 241 | - The full description of the user. 242 | type: str 243 | gid: 244 | description: 245 | - The details of the primary group for the user. 246 | type: complex 247 | contains: 248 | id: 249 | description: 250 | - The id of the primary group. 251 | type: str 252 | name: 253 | description: 254 | - The name of the primary group. 255 | type: 256 | description: 257 | - The resource's type is mentioned. 258 | type: str 259 | home_directory: 260 | description: 261 | - The directory path acts as the home directory 262 | for the user's account. 263 | type: str 264 | name: 265 | description: 266 | - The name of the user. 267 | type: str 268 | provider: 269 | description: 270 | - The provider contains the provider type and access zone. 271 | type: str 272 | roles: 273 | description: 274 | - The list of all the roles of which user is a member. 275 | returned: For all users in system access zone. 276 | type: list 277 | shell: 278 | description: 279 | - The type of shell for the user account. 280 | type: str 281 | uid: 282 | description: 283 | - Details about the id and name of the user. 284 | type: complex 285 | contains: 286 | id: 287 | description: 288 | - The id of the user. 289 | type: str 290 | name: 291 | description: 292 | - The name of the user. 293 | type: 294 | description: 295 | - The resource's type is mentioned. 296 | type: str 297 | ''' 298 | 299 | from ansible.module_utils.basic import AnsibleModule 300 | from ansible.module_utils.storage.dell \ 301 | import dellemc_ansible_isilon_utils as utils 302 | import logging 303 | import re 304 | 305 | LOG = utils.get_logger('dellemc_isilon_user', log_devel=utils.logging.INFO) 306 | HAS_ISILON_SDK = utils.has_isilon_sdk() 307 | ISILON_SDK_VERSION_CHECK = utils.isilon_sdk_version_check() 308 | 309 | 310 | class IsilonUser(object): 311 | """Class with user operations""" 312 | 313 | def __init__(self): 314 | """ Define all parameters required by this module""" 315 | 316 | self.module_params = utils.get_isilon_management_host_parameters() 317 | self.module_params.update(get_isilon_user_parameters()) 318 | 319 | mutually_exclusive = [['user_name', 'user_id']] 320 | 321 | required_one_of = [ 322 | ['user_name', 'user_id'] 323 | ] 324 | 325 | # initialize the ansible module 326 | self.module = AnsibleModule(argument_spec=self.module_params, 327 | supports_check_mode=False, 328 | mutually_exclusive=mutually_exclusive, 329 | required_one_of=required_one_of) 330 | 331 | # result is a dictionary that contains changed status and 332 | # user details 333 | self.result = {"changed": False} 334 | if HAS_ISILON_SDK is False: 335 | self.module.fail_json( 336 | msg="Ansible modules for Isilon require the isilon " 337 | "python library to be installed. Please install" 338 | " the library before using these modules.") 339 | 340 | if ISILON_SDK_VERSION_CHECK and \ 341 | not ISILON_SDK_VERSION_CHECK['supported_version']: 342 | err_msg = ISILON_SDK_VERSION_CHECK['unsupported_version_message'] 343 | LOG.error(err_msg) 344 | self.module.fail_json(msg=err_msg) 345 | 346 | self.api_client = utils.get_isilon_connection(self.module.params) 347 | self.api_instance = utils.isi_sdk.AuthApi(self.api_client) 348 | self.role_api_instance = utils.isi_sdk.AuthRolesApi( 349 | self.api_client) 350 | self.zone_summary_api = utils.isi_sdk.ZonesSummaryApi(self.api_client) 351 | 352 | LOG.info('Got the isi_sdk instance for authorization on to Isilon') 353 | 354 | def get_zone_base_path(self, access_zone): 355 | """Returns the base path of the Access Zone.""" 356 | try: 357 | zone_path = (self.zone_summary_api. 358 | get_zones_summary_zone(access_zone)).to_dict() 359 | zone_base_path = zone_path['summary']['path'] 360 | LOG.info("Successfully got zone_base_path for %s is %s", 361 | access_zone, zone_base_path) 362 | return zone_base_path 363 | except Exception as e: 364 | error_message = 'Unable to fetch base path of Access Zone %s ' \ 365 | ',failed with error: %s', access_zone, \ 366 | self.determine_error(e) 367 | LOG.error(error_message) 368 | self.module.fail_json(msg=error_message) 369 | 370 | def check_provider_type(self, provider, message): 371 | """ Check the provider and return the updated provider""" 372 | if provider.lower() != "local": 373 | error_message = \ 374 | "%s user is allowed only" \ 375 | " if provider_type is local, got '%s' provider" \ 376 | % (message, provider) 377 | LOG.error(error_message) 378 | self.module.fail_json(msg=error_message) 379 | return provider 380 | 381 | def determine_error(self, error_obj): 382 | """Determine the error message to return""" 383 | if isinstance(error_obj, utils.ApiException): 384 | error = re.sub("[\n \"]+", ' ', str(error_obj.body)) 385 | else: 386 | error = str(error_obj) 387 | return error 388 | 389 | def create_user(self, user_name, password, zone, provider, 390 | enabled, primary_group, home_directory, shell, 391 | full_name, email): 392 | """Create User in Isilon""" 393 | try: 394 | if primary_group: 395 | primary_group = utils.isi_sdk.AuthAccessAccessItemFileGroup( 396 | "GROUP:" + primary_group) 397 | 398 | provider = self.check_provider_type(provider, 'Create') 399 | auth_user = utils.isi_sdk.AuthUserCreateParams( 400 | name=user_name, password=password, enabled=enabled, 401 | primary_group=primary_group, home_directory=home_directory, 402 | shell=shell, gecos=full_name, email=email) 403 | 404 | api_response = self.api_instance.create_auth_user( 405 | auth_user=auth_user, 406 | zone=zone, provider=provider) 407 | 408 | LOG.info('User is created with the SID: %s', str(api_response)) 409 | except Exception as e: 410 | error = self.determine_error(error_obj=e) 411 | error_message = \ 412 | "Create User '%s' failed with %s" % (user_name, error) 413 | LOG.error(error_message) 414 | self.module.fail_json(msg=error_message) 415 | 416 | def delete_user(self, auth_user_id, zone, provider): 417 | """Delete User in Isilon""" 418 | try: 419 | provider = self.check_provider_type(provider, 'Delete') 420 | self.api_instance.delete_auth_user( 421 | auth_user_id=auth_user_id, zone=zone, provider=provider) 422 | LOG.info("User %s is deleted", auth_user_id) 423 | return True 424 | except Exception as e: 425 | error = self.determine_error(error_obj=e) 426 | error_message = "Delete User '%s' failed with %s"\ 427 | % (auth_user_id, error) 428 | LOG.error(error_message) 429 | self.module.fail_json(msg=error_message) 430 | 431 | def is_user_modified(self, user_details): 432 | """ Determines whether the user details are to be updated or not.""" 433 | if self.module.params['enabled'] is not None: 434 | if self.module.params['enabled'] != user_details['enabled']: 435 | return True 436 | 437 | parameter_list = ['primary_group', 'shell', 'email', 'full_name', 438 | 'home_directory'] 439 | case_sensitive_parameters = ['full_name', 'home_directory'] 440 | 441 | for parameter in parameter_list: 442 | if self.module.params[parameter]: 443 | if user_details[parameter]: 444 | if parameter not in case_sensitive_parameters: 445 | if self.module.params[parameter].lower() != \ 446 | user_details[parameter].lower(): 447 | return True 448 | else: 449 | if self.module.params[parameter] != \ 450 | user_details[parameter]: 451 | return True 452 | else: 453 | return True 454 | return False 455 | 456 | def update_user(self, auth_user_id, zone, provider, enabled, 457 | primary_group, home_directory, shell, 458 | full_name, email): 459 | """Update the User Account details in Isilon""" 460 | try: 461 | if primary_group: 462 | primary_group = utils.isi_sdk.AuthAccessAccessItemFileGroup( 463 | "GROUP:" + primary_group) 464 | auth_user = utils.isi_sdk.AuthUser(primary_group=primary_group, 465 | home_directory=home_directory, 466 | shell=shell, gecos=full_name, 467 | email=email, enabled=enabled) 468 | provider = self.check_provider_type(provider, 'Update') 469 | self.api_instance.update_auth_user( 470 | auth_user=auth_user, auth_user_id=auth_user_id, 471 | zone=zone, provider=provider) 472 | LOG.info("User %s is updated", auth_user_id) 473 | return True 474 | except Exception as e: 475 | error = self.determine_error(error_obj=e) 476 | error_message = "Update User '%s' failed with %s" \ 477 | % (auth_user_id, error) 478 | LOG.error(error_message) 479 | self.module.fail_json(msg=error_message) 480 | 481 | def get_user_details(self, auth_user_id, zone, provider): 482 | """Get the User Account Details in Isilon""" 483 | try: 484 | api_response = self.api_instance.get_auth_user( 485 | auth_user_id=auth_user_id, 486 | zone=zone, provider=provider) 487 | LOG.info('User details are %s', str(api_response)) 488 | api_response_dict = api_response.users[0].to_dict() 489 | return api_response_dict 490 | 491 | except utils.ApiException as e: 492 | if str(e.status) == "404": 493 | error_message = "Get User Details %s failed with %s" \ 494 | % (auth_user_id, self.determine_error(e)) 495 | LOG.info(error_message) 496 | return None 497 | else: 498 | error_message = "Get User Details %s failed with %s" \ 499 | % (auth_user_id, self.determine_error(e)) 500 | LOG.error(error_message) 501 | self.module.fail_json(msg=error_message) 502 | 503 | except Exception as e: 504 | error_message = "Get User Details %s failed with %s" \ 505 | % (auth_user_id, self.determine_error(e)) 506 | LOG.error(error_message) 507 | self.module.fail_json(msg=error_message) 508 | 509 | def add_role_to_user(self, auth_user_id, role_name): 510 | """Add a Role to a User in Isilon""" 511 | try: 512 | role_member = utils.isi_sdk.AuthAccessAccessItemFileGroup( 513 | id=auth_user_id) 514 | self.role_api_instance.create_role_member( 515 | role_member, role=role_name) 516 | message = 'User %s added to role %s ' \ 517 | % (auth_user_id, role_name) 518 | LOG.info(message) 519 | return True 520 | except Exception as e: 521 | error = self.determine_error(error_obj=e) 522 | error_message = "Add user %s to role %s failed with %s " \ 523 | % (auth_user_id, role_name, error) 524 | LOG.error(error_message) 525 | self.module.fail_json(msg=error_message) 526 | 527 | def remove_role_from_user(self, role_member_id, role_name): 528 | """ Remove a Role from a User in Isilon""" 529 | try: 530 | self.role_api_instance.delete_role_member( 531 | role_member_id, role=role_name) 532 | message = 'User %s removed from role %s ' \ 533 | % (role_member_id, role_name) 534 | LOG.info(message) 535 | return True 536 | except Exception as e: 537 | error = self.determine_error(error_obj=e) 538 | error_message = "Remove user %s from role %s failed with %s " \ 539 | % (role_member_id, role_name, error) 540 | LOG.error(error_message) 541 | self.module.fail_json(msg=error_message) 542 | 543 | def is_user_part_of_role(self, user_name, user_id, role_name): 544 | """ Determines if the user is part of a given role or not.""" 545 | if role_name: 546 | roles_for_user = self.get_roles_for_user(user_name, user_id) 547 | debug_message = "roles for users %s" % roles_for_user 548 | LOG.debug(debug_message) 549 | if role_name.lower() in roles_for_user: 550 | return True 551 | else: 552 | return False 553 | else: 554 | return False 555 | 556 | def get_roles_for_user(self, user_name, user_id): 557 | """ Get the roles for the user """ 558 | roles_for_user = [] 559 | try: 560 | api_response = self.api_instance.list_auth_roles() 561 | if user_name is not None: 562 | for role_name in api_response.roles: 563 | if role_name.members: 564 | for member in role_name.members: 565 | if member.name and (member.name.lower() 566 | == user_name.lower()): 567 | roles_for_user.append(role_name.id.lower()) 568 | return roles_for_user 569 | else: 570 | user_id = "UID:" + user_id 571 | for role_name in api_response.roles: 572 | if role_name.members: 573 | for member in role_name.members: 574 | if member.id == user_id: 575 | roles_for_user.append(role_name.id.lower()) 576 | return roles_for_user 577 | except utils.ApiException as e: 578 | error_message = "Exception when calling" \ 579 | " AuthApi->list_auth_roles: %s\n" % e 580 | LOG.error(error_message) 581 | 582 | def perform_module_operation(self): 583 | """ 584 | Perform different actions on user module based on parameters 585 | chosen in playbook 586 | """ 587 | user_name = self.module.params['user_name'] 588 | user_id = self.module.params['user_id'] 589 | password = self.module.params['password'] 590 | access_zone = self.module.params['access_zone'] 591 | provider_type = self.module.params['provider_type'] 592 | enabled = self.module.params['enabled'] 593 | primary_group = self.module.params['primary_group'] 594 | shell = self.module.params['shell'] 595 | full_name = self.module.params['full_name'] 596 | email = self.module.params['email'] 597 | state = self.module.params['state'] 598 | role_name = self.module.params['role_name'] 599 | role_state = self.module.params['role_state'] 600 | 601 | if self.module.params['home_directory'] and \ 602 | access_zone.lower() != 'system': 603 | 604 | self.module.params['home_directory'] = \ 605 | self.get_zone_base_path(access_zone) + \ 606 | self.module.params['home_directory'] 607 | 608 | home_directory = self.module.params['home_directory'] 609 | if user_name and not user_id: 610 | auth_user_id = 'USER:' + user_name 611 | elif user_id and not user_name: 612 | auth_user_id = 'UID:' + user_id 613 | else: 614 | self.module.fail_json(msg="Invalid user_name or user_id" 615 | " provided. Enter a valid string.") 616 | 617 | if email and re.search( 618 | r'^\w+([\.-]?\w+)*@\w+([\.-]?\w+)*(\.\w{2,3})+$', 619 | email) is None: 620 | self.module.fail_json(msg="Email is not in the correct format") 621 | if (role_name and role_state is None) or \ 622 | (not role_name and role_state is not None): 623 | self.module.fail_json( 624 | msg="role_name and role_state both are required" 625 | " to add or remove user from a role") 626 | 627 | if role_name and access_zone.lower() != 'system': 628 | self.module.fail_json( 629 | msg="roles can be assigned to users and groups of" 630 | " System Access Zone, got %s" % access_zone) 631 | if state == "present": 632 | # Get the details of the user. 633 | user_details = self.get_user_details( 634 | auth_user_id, access_zone, provider_type) 635 | if user_details: 636 | # Error is none so user exists, hence getting the list 637 | # of roles for the user if the User is in System Access Zone 638 | get_roles_flag = True 639 | if (not role_name) and (role_state is None) and \ 640 | (access_zone.lower() != "system"): 641 | get_roles_flag = False 642 | user_details['roles'] = [] 643 | if get_roles_flag: 644 | user_details['roles'] = self.get_roles_for_user( 645 | user_name, user_id) 646 | # If User Details is None, User is created. 647 | if not user_details: 648 | if user_id: 649 | error_message = "User with user_id '%s'" \ 650 | " not found on the system" % user_id 651 | LOG.error(error_message) 652 | self.module.fail_json(msg=error_message) 653 | if not password: 654 | error_message = "Unable to create a user, 'password' is"\ 655 | " missing" 656 | LOG.error(error_message) 657 | self.module.fail_json(msg=error_message) 658 | 659 | self.create_user(user_name, password, access_zone, 660 | provider_type, enabled, primary_group, 661 | home_directory, shell, full_name, email) 662 | 663 | if role_state == "present-for-user": 664 | self.add_role_to_user(auth_user_id, role_name) 665 | changed = True 666 | 667 | else: 668 | LOG.info("Update the user details.") 669 | # Check for changes in role 670 | role_flag = self.is_user_part_of_role( 671 | user_name, user_id, role_name) 672 | role_changed = False 673 | message = "role_flag %s" % role_flag 674 | LOG.info(message) 675 | 676 | if role_flag: 677 | if role_state == "absent-for-user": 678 | role_changed = self.remove_role_from_user( 679 | auth_user_id, role_name) 680 | else: 681 | if role_state == "present-for-user": 682 | role_changed = self.add_role_to_user( 683 | auth_user_id, role_name) 684 | 685 | old_user_details = get_user_params_from_details(user_details) 686 | modified = self.is_user_modified(old_user_details) 687 | 688 | user_details_changed = False 689 | if home_directory and \ 690 | user_details['home_directory'] == home_directory: 691 | home_directory = None 692 | 693 | if modified: 694 | user_details_changed = self.update_user( 695 | auth_user_id, access_zone, provider_type, enabled, 696 | primary_group, home_directory, shell, full_name, 697 | email) 698 | 699 | if user_details_changed or role_changed: 700 | changed = True 701 | else: 702 | changed = False 703 | 704 | # State == Absent (Delete User Account) 705 | else: 706 | if provider_type.lower() != 'local': 707 | self.module.fail_json( 708 | msg="Cannot Delete user from %s provider_type" 709 | % provider_type) 710 | user_details = self.get_user_details( 711 | auth_user_id, access_zone, provider_type) 712 | if user_details: 713 | get_roles_flag = True 714 | if (not role_name) and (role_state is None) and \ 715 | (access_zone.lower() != "system"): 716 | get_roles_flag = False 717 | roles_for_user = [] 718 | if get_roles_flag: 719 | roles_for_user = self.get_roles_for_user( 720 | user_name, user_id) 721 | 722 | if get_roles_flag and len(roles_for_user) != 0: 723 | for role in roles_for_user: 724 | self.remove_role_from_user(auth_user_id, role) 725 | changed = self.delete_user(auth_user_id, access_zone, 726 | provider_type) 727 | else: 728 | changed = False 729 | ''' 730 | Finally update the module changed state and saving updated user 731 | details 732 | ''' 733 | user_details = self.get_user_details( 734 | auth_user_id, access_zone, provider_type) 735 | if user_details and access_zone.lower() == 'system': 736 | get_roles_flag = True 737 | if (not role_name) and (role_state is None) and \ 738 | (access_zone.lower() != "system"): 739 | get_roles_flag = False 740 | user_details['roles'] = [] 741 | if get_roles_flag: 742 | user_details['roles'] = self.get_roles_for_user( 743 | user_name, user_id) 744 | self.result["changed"] = changed 745 | self.result["user_details"] = user_details 746 | self.module.exit_json(**self.result) 747 | 748 | 749 | def get_user_params_from_details(user_details): 750 | user_params = { 751 | 'user_id': user_details['uid']['id'].split(":")[1], 752 | 'user_name': user_details['id'], 753 | 'access_zone': user_details['provider'].split(":")[1], 754 | "provider_type": 755 | user_details['provider'].split(":")[0].split("-")[1], 756 | 'enabled': bool(user_details['enabled']), 757 | 'primary_group': user_details['primary_group_sid']['name'], 758 | 'home_directory': user_details['home_directory'], 759 | 'shell': user_details['shell'], 760 | 'full_name': user_details['gecos'], 761 | 'email': user_details['email']} 762 | return user_params 763 | 764 | 765 | def get_isilon_user_parameters(): 766 | """This method provide parameter required for the ansible user 767 | modules on Isilon""" 768 | return dict( 769 | user_name=dict(required=False, type='str'), 770 | user_id=dict(required=False, type='str'), 771 | password=dict(required=False, type='str', no_log=True), 772 | access_zone=dict(required=False, type='str', default='system'), 773 | provider_type=dict(required=False, type='str', 774 | choices=['local', 'file', 'ldap', 'ads'], 775 | default='local'), 776 | enabled=dict(required=False, type='bool'), 777 | primary_group=dict(required=False, type='str'), 778 | home_directory=dict(required=False, type='str'), 779 | shell=dict(required=False, type='str'), 780 | full_name=dict(required=False, type='str'), 781 | email=dict(required=False, type='str'), 782 | state=dict(required=True, type='str', 783 | choices=['present', 'absent']), 784 | role_name=dict(required=False, type='str'), 785 | role_state=dict(required=False, type='str', 786 | choices=['present-for-user', 'absent-for-user']) 787 | ) 788 | 789 | 790 | def main(): 791 | """ Create Isilon User object and perform actions on it 792 | based on user input from playbook""" 793 | obj = IsilonUser() 794 | obj.perform_module_operation() 795 | 796 | 797 | if __name__ == '__main__': 798 | main() 799 | --------------------------------------------------------------------------------