1#!/usr/bin/python 2 3# (c) 2016, 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__metaclass__ = type 8 9 10ANSIBLE_METADATA = {'metadata_version': '1.1', 11 'status': ['preview'], 12 'supported_by': 'community'} 13 14 15DOCUMENTATION = """ 16--- 17module: netapp_e_amg_sync 18short_description: NetApp E-Series conduct synchronization actions on asynchronous mirror groups. 19description: 20 - Allows for the initialization, suspension and resumption of an asynchronous mirror group's synchronization for NetApp E-series storage arrays. 21version_added: '2.2' 22author: Kevin Hulquest (@hulquest) 23options: 24 api_username: 25 required: true 26 description: 27 - The username to authenticate with the SANtricity WebServices Proxy or embedded REST API. 28 api_password: 29 required: true 30 description: 31 - The password to authenticate with the SANtricity WebServices Proxy or embedded REST API. 32 api_url: 33 required: true 34 description: 35 - The url to the SANtricity WebServices Proxy or embedded REST API. 36 validate_certs: 37 required: false 38 default: true 39 description: 40 - Should https certificates be validated? 41 type: bool 42 ssid: 43 description: 44 - The ID of the storage array containing the AMG you wish to target 45 name: 46 description: 47 - The name of the async mirror group you wish to target 48 required: yes 49 state: 50 description: 51 - The synchronization action you'd like to take. 52 - If C(running) then it will begin syncing if there is no active sync or will resume a suspended sync. If there is already a sync in 53 progress, it will return with an OK status. 54 - If C(suspended) it will suspend any ongoing sync action, but return OK if there is no active sync or if the sync is already suspended 55 choices: 56 - running 57 - suspended 58 required: yes 59 delete_recovery_point: 60 description: 61 - Indicates whether the failures point can be deleted on the secondary if necessary to achieve the synchronization. 62 - If true, and if the amount of unsynchronized data exceeds the CoW repository capacity on the secondary for any member volume, the last 63 failures point will be deleted and synchronization will continue. 64 - If false, the synchronization will be suspended if the amount of unsynchronized data exceeds the CoW Repository capacity on the secondary 65 and the failures point will be preserved. 66 - "NOTE: This only has impact for newly launched syncs." 67 type: bool 68 default: no 69""" 70EXAMPLES = """ 71 - name: start AMG async 72 netapp_e_amg_sync: 73 name: "{{ amg_sync_name }}" 74 state: running 75 ssid: "{{ ssid }}" 76 api_url: "{{ netapp_api_url }}" 77 api_username: "{{ netapp_api_username }}" 78 api_password: "{{ netapp_api_password }}" 79""" 80RETURN = """ 81json: 82 description: The object attributes of the AMG. 83 returned: success 84 type: str 85 example: 86 { 87 "changed": false, 88 "connectionType": "fc", 89 "groupRef": "3700000060080E5000299C24000006EF57ACAC70", 90 "groupState": "optimal", 91 "id": "3700000060080E5000299C24000006EF57ACAC70", 92 "label": "made_with_ansible", 93 "localRole": "primary", 94 "mirrorChannelRemoteTarget": "9000000060080E5000299C24005B06E557AC7EEC", 95 "orphanGroup": false, 96 "recoveryPointAgeAlertThresholdMinutes": 20, 97 "remoteRole": "secondary", 98 "remoteTarget": { 99 "nodeName": { 100 "ioInterfaceType": "fc", 101 "iscsiNodeName": null, 102 "remoteNodeWWN": "20040080E5299F1C" 103 }, 104 "remoteRef": "9000000060080E5000299C24005B06E557AC7EEC", 105 "scsiinitiatorTargetBaseProperties": { 106 "ioInterfaceType": "fc", 107 "iscsiinitiatorTargetBaseParameters": null 108 } 109 }, 110 "remoteTargetId": "ansible2", 111 "remoteTargetName": "Ansible2", 112 "remoteTargetWwn": "60080E5000299F880000000056A25D56", 113 "repositoryUtilizationWarnThreshold": 80, 114 "roleChangeProgress": "none", 115 "syncActivity": "idle", 116 "syncCompletionTimeAlertThresholdMinutes": 10, 117 "syncIntervalMinutes": 10, 118 "worldWideName": "60080E5000299C24000006EF57ACAC70" 119 } 120""" 121import json 122 123from ansible.module_utils.api import basic_auth_argument_spec 124from ansible.module_utils.basic import AnsibleModule 125from ansible.module_utils.six.moves.urllib.error import HTTPError 126from ansible.module_utils.urls import open_url 127 128 129def request(url, data=None, headers=None, method='GET', use_proxy=True, 130 force=False, last_mod_time=None, timeout=10, validate_certs=True, 131 url_username=None, url_password=None, http_agent=None, force_basic_auth=True, ignore_errors=False): 132 try: 133 r = open_url(url=url, data=data, headers=headers, method=method, use_proxy=use_proxy, 134 force=force, last_mod_time=last_mod_time, timeout=timeout, validate_certs=validate_certs, 135 url_username=url_username, url_password=url_password, http_agent=http_agent, 136 force_basic_auth=force_basic_auth) 137 except HTTPError as e: 138 r = e.fp 139 140 try: 141 raw_data = r.read() 142 if raw_data: 143 data = json.loads(raw_data) 144 else: 145 raw_data = None 146 except Exception: 147 if ignore_errors: 148 pass 149 else: 150 raise Exception(raw_data) 151 152 resp_code = r.getcode() 153 154 if resp_code >= 400 and not ignore_errors: 155 raise Exception(resp_code, data) 156 else: 157 return resp_code, data 158 159 160class AMGsync(object): 161 def __init__(self): 162 argument_spec = basic_auth_argument_spec() 163 argument_spec.update(dict( 164 api_username=dict(type='str', required=True), 165 api_password=dict(type='str', required=True, no_log=True), 166 api_url=dict(type='str', required=True), 167 name=dict(required=True, type='str'), 168 ssid=dict(required=True, type='str'), 169 state=dict(required=True, type='str', choices=['running', 'suspended']), 170 delete_recovery_point=dict(required=False, type='bool', default=False) 171 )) 172 self.module = AnsibleModule(argument_spec=argument_spec) 173 args = self.module.params 174 self.name = args['name'] 175 self.ssid = args['ssid'] 176 self.state = args['state'] 177 self.delete_recovery_point = args['delete_recovery_point'] 178 try: 179 self.user = args['api_username'] 180 self.pwd = args['api_password'] 181 self.url = args['api_url'] 182 except KeyError: 183 self.module.fail_json(msg="You must pass in api_username" 184 "and api_password and api_url to the module.") 185 self.certs = args['validate_certs'] 186 187 self.post_headers = { 188 "Accept": "application/json", 189 "Content-Type": "application/json" 190 } 191 self.amg_id, self.amg_obj = self.get_amg() 192 193 def get_amg(self): 194 endpoint = self.url + '/storage-systems/%s/async-mirrors' % self.ssid 195 (rc, amg_objs) = request(endpoint, url_username=self.user, url_password=self.pwd, validate_certs=self.certs, 196 headers=self.post_headers) 197 try: 198 amg_id = filter(lambda d: d['label'] == self.name, amg_objs)[0]['id'] 199 amg_obj = filter(lambda d: d['label'] == self.name, amg_objs)[0] 200 except IndexError: 201 self.module.fail_json( 202 msg="There is no async mirror group %s associated with storage array %s" % (self.name, self.ssid)) 203 return amg_id, amg_obj 204 205 @property 206 def current_state(self): 207 amg_id, amg_obj = self.get_amg() 208 return amg_obj['syncActivity'] 209 210 def run_sync_action(self): 211 # If we get to this point we know that the states differ, and there is no 'err' state, 212 # so no need to revalidate 213 214 post_body = dict() 215 if self.state == 'running': 216 if self.current_state == 'idle': 217 if self.delete_recovery_point: 218 post_body.update(dict(deleteRecoveryPointIfNecessary=self.delete_recovery_point)) 219 suffix = 'sync' 220 else: 221 # In a suspended state 222 suffix = 'resume' 223 else: 224 suffix = 'suspend' 225 226 endpoint = self.url + "/storage-systems/%s/async-mirrors/%s/%s" % (self.ssid, self.amg_id, suffix) 227 228 (rc, resp) = request(endpoint, method='POST', url_username=self.user, url_password=self.pwd, 229 validate_certs=self.certs, data=json.dumps(post_body), headers=self.post_headers, 230 ignore_errors=True) 231 232 if not str(rc).startswith('2'): 233 self.module.fail_json(msg=str(resp['errorMessage'])) 234 235 return resp 236 237 def apply(self): 238 state_map = dict( 239 running=['active'], 240 suspended=['userSuspended', 'internallySuspended', 'paused'], 241 err=['unkown', '_UNDEFINED']) 242 243 if self.current_state not in state_map[self.state]: 244 if self.current_state in state_map['err']: 245 self.module.fail_json( 246 msg="The sync is a state of '%s', this requires manual intervention. " + 247 "Please investigate and try again" % self.current_state) 248 else: 249 self.amg_obj = self.run_sync_action() 250 251 (ret, amg) = self.get_amg() 252 self.module.exit_json(changed=False, **amg) 253 254 255def main(): 256 sync = AMGsync() 257 sync.apply() 258 259 260if __name__ == '__main__': 261 main() 262