1#!/usr/bin/python 2 3# (c) 2018, NetApp, Inc 4# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) 5 6from __future__ import absolute_import, division, print_function 7 8__metaclass__ = type 9 10ANSIBLE_METADATA = {'metadata_version': '1.1', 11 'status': ['preview'], 12 'supported_by': 'community'} 13 14DOCUMENTATION = """ 15--- 16module: netapp_e_iscsi_target 17short_description: NetApp E-Series manage iSCSI target configuration 18description: 19 - Configure the settings of an E-Series iSCSI target 20version_added: '2.7' 21author: Michael Price (@lmprice) 22extends_documentation_fragment: 23 - netapp.eseries 24options: 25 name: 26 description: 27 - The name/alias to assign to the iSCSI target. 28 - This alias is often used by the initiator software in order to make an iSCSI target easier to identify. 29 aliases: 30 - alias 31 ping: 32 description: 33 - Enable ICMP ping responses from the configured iSCSI ports. 34 type: bool 35 default: yes 36 chap_secret: 37 description: 38 - Enable Challenge-Handshake Authentication Protocol (CHAP), utilizing this value as the password. 39 - When this value is specified, we will always trigger an update (changed=True). We have no way of verifying 40 whether or not the password has changed. 41 - The chap secret may only use ascii characters with values between 32 and 126 decimal. 42 - The chap secret must be no less than 12 characters, but no greater than 57 characters in length. 43 - The chap secret is cleared when not specified or an empty string. 44 aliases: 45 - chap 46 - password 47 unnamed_discovery: 48 description: 49 - When an initiator initiates a discovery session to an initiator port, it is considered an unnamed 50 discovery session if the iSCSI target iqn is not specified in the request. 51 - This option may be disabled to increase security if desired. 52 type: bool 53 default: yes 54 log_path: 55 description: 56 - A local path (on the Ansible controller), to a file to be used for debug logging. 57 required: no 58notes: 59 - Check mode is supported. 60 - Some of the settings are dependent on the settings applied to the iSCSI interfaces. These can be configured using 61 M(netapp_e_iscsi_interface). 62 - This module requires a Web Services API version of >= 1.3. 63""" 64 65EXAMPLES = """ 66 - name: Enable ping responses and unnamed discovery sessions for all iSCSI ports 67 netapp_e_iscsi_target: 68 api_url: "https://localhost:8443/devmgr/v2" 69 api_username: admin 70 api_password: myPassword 71 ssid: "1" 72 validate_certs: no 73 name: myTarget 74 ping: yes 75 unnamed_discovery: yes 76 77 - name: Set the target alias and the CHAP secret 78 netapp_e_iscsi_target: 79 ssid: "{{ ssid }}" 80 api_url: "{{ netapp_api_url }}" 81 api_username: "{{ netapp_api_username }}" 82 api_password: "{{ netapp_api_password }}" 83 name: myTarget 84 chap: password1234 85""" 86 87RETURN = """ 88msg: 89 description: Success message 90 returned: on success 91 type: str 92 sample: The iSCSI target settings have been updated. 93alias: 94 description: 95 - The alias assigned to the iSCSI target. 96 returned: on success 97 sample: myArray 98 type: str 99iqn: 100 description: 101 - The iqn (iSCSI Qualified Name), assigned to the iSCSI target. 102 returned: on success 103 sample: iqn.1992-08.com.netapp:2800.000a132000b006d2000000005a0e8f45 104 type: str 105""" 106import json 107import logging 108from pprint import pformat 109 110from ansible.module_utils.basic import AnsibleModule 111from ansible.module_utils.netapp import request, eseries_host_argument_spec 112from ansible.module_utils._text import to_native 113 114HEADERS = { 115 "Content-Type": "application/json", 116 "Accept": "application/json", 117} 118 119 120class IscsiTarget(object): 121 def __init__(self): 122 argument_spec = eseries_host_argument_spec() 123 argument_spec.update(dict( 124 name=dict(type='str', required=False, aliases=['alias']), 125 ping=dict(type='bool', required=False, default=True), 126 chap_secret=dict(type='str', required=False, aliases=['chap', 'password'], no_log=True), 127 unnamed_discovery=dict(type='bool', required=False, default=True), 128 log_path=dict(type='str', required=False), 129 )) 130 131 self.module = AnsibleModule(argument_spec=argument_spec, supports_check_mode=True, ) 132 args = self.module.params 133 134 self.name = args['name'] 135 self.ping = args['ping'] 136 self.chap_secret = args['chap_secret'] 137 self.unnamed_discovery = args['unnamed_discovery'] 138 139 self.ssid = args['ssid'] 140 self.url = args['api_url'] 141 self.creds = dict(url_password=args['api_password'], 142 validate_certs=args['validate_certs'], 143 url_username=args['api_username'], ) 144 145 self.check_mode = self.module.check_mode 146 self.post_body = dict() 147 self.controllers = list() 148 149 log_path = args['log_path'] 150 151 # logging setup 152 self._logger = logging.getLogger(self.__class__.__name__) 153 154 if log_path: 155 logging.basicConfig( 156 level=logging.DEBUG, filename=log_path, filemode='w', 157 format='%(relativeCreated)dms %(levelname)s %(module)s.%(funcName)s:%(lineno)d\n %(message)s') 158 159 if not self.url.endswith('/'): 160 self.url += '/' 161 162 if self.chap_secret: 163 if len(self.chap_secret) < 12 or len(self.chap_secret) > 57: 164 self.module.fail_json(msg="The provided CHAP secret is not valid, it must be between 12 and 57" 165 " characters in length.") 166 167 for c in self.chap_secret: 168 ordinal = ord(c) 169 if ordinal < 32 or ordinal > 126: 170 self.module.fail_json(msg="The provided CHAP secret is not valid, it may only utilize ascii" 171 " characters with decimal values between 32 and 126.") 172 173 @property 174 def target(self): 175 """Provide information on the iSCSI Target configuration 176 177 Sample: 178 { 179 'alias': 'myCustomName', 180 'ping': True, 181 'unnamed_discovery': True, 182 'chap': False, 183 'iqn': 'iqn.1992-08.com.netapp:2800.000a132000b006d2000000005a0e8f45', 184 } 185 """ 186 target = dict() 187 try: 188 (rc, data) = request(self.url + 'storage-systems/%s/graph/xpath-filter?query=/storagePoolBundle/target' 189 % self.ssid, headers=HEADERS, **self.creds) 190 # This likely isn't an iSCSI-enabled system 191 if not data: 192 self.module.fail_json( 193 msg="This storage-system doesn't appear to have iSCSI interfaces. Array Id [%s]." % (self.ssid)) 194 195 data = data[0] 196 197 chap = any( 198 [auth for auth in data['configuredAuthMethods']['authMethodData'] if auth['authMethod'] == 'chap']) 199 200 target.update(dict(alias=data['alias']['iscsiAlias'], 201 iqn=data['nodeName']['iscsiNodeName'], 202 chap=chap)) 203 204 (rc, data) = request(self.url + 'storage-systems/%s/graph/xpath-filter?query=/sa/iscsiEntityData' 205 % self.ssid, headers=HEADERS, **self.creds) 206 207 data = data[0] 208 target.update(dict(ping=data['icmpPingResponseEnabled'], 209 unnamed_discovery=data['unnamedDiscoverySessionsEnabled'])) 210 211 except Exception as err: 212 self.module.fail_json( 213 msg="Failed to retrieve the iSCSI target information. Array Id [%s]. Error [%s]." 214 % (self.ssid, to_native(err))) 215 216 return target 217 218 def apply_iscsi_settings(self): 219 """Update the iSCSI target alias and CHAP settings""" 220 update = False 221 target = self.target 222 223 body = dict() 224 225 if self.name is not None and self.name != target['alias']: 226 update = True 227 body['alias'] = self.name 228 229 # If the CHAP secret was provided, we trigger an update. 230 if self.chap_secret: 231 update = True 232 body.update(dict(enableChapAuthentication=True, 233 chapSecret=self.chap_secret)) 234 # If no secret was provided, then we disable chap 235 elif target['chap']: 236 update = True 237 body.update(dict(enableChapAuthentication=False)) 238 239 if update and not self.check_mode: 240 try: 241 request(self.url + 'storage-systems/%s/iscsi/target-settings' % self.ssid, method='POST', 242 data=json.dumps(body), headers=HEADERS, **self.creds) 243 except Exception as err: 244 self.module.fail_json( 245 msg="Failed to update the iSCSI target settings. Array Id [%s]. Error [%s]." 246 % (self.ssid, to_native(err))) 247 248 return update 249 250 def apply_target_changes(self): 251 update = False 252 target = self.target 253 254 body = dict() 255 256 if self.ping != target['ping']: 257 update = True 258 body['icmpPingResponseEnabled'] = self.ping 259 260 if self.unnamed_discovery != target['unnamed_discovery']: 261 update = True 262 body['unnamedDiscoverySessionsEnabled'] = self.unnamed_discovery 263 264 self._logger.info(pformat(body)) 265 if update and not self.check_mode: 266 try: 267 request(self.url + 'storage-systems/%s/iscsi/entity' % self.ssid, method='POST', 268 data=json.dumps(body), timeout=60, headers=HEADERS, **self.creds) 269 except Exception as err: 270 self.module.fail_json( 271 msg="Failed to update the iSCSI target settings. Array Id [%s]. Error [%s]." 272 % (self.ssid, to_native(err))) 273 return update 274 275 def update(self): 276 update = self.apply_iscsi_settings() 277 update = self.apply_target_changes() or update 278 279 target = self.target 280 data = dict((key, target[key]) for key in target if key in ['iqn', 'alias']) 281 282 self.module.exit_json(msg="The interface settings have been updated.", changed=update, **data) 283 284 def __call__(self, *args, **kwargs): 285 self.update() 286 287 288def main(): 289 iface = IscsiTarget() 290 iface() 291 292 293if __name__ == '__main__': 294 main() 295