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_volume_copy 18short_description: NetApp E-Series create volume copy pairs 19description: 20 - Create and delete snapshots images on volume groups for NetApp E-series storage arrays. 21version_added: '2.2' 22author: Kevin Hulquest (@hulquest) 23extends_documentation_fragment: 24 - netapp.eseries 25options: 26 api_username: 27 required: true 28 description: 29 - The username to authenticate with the SANtricity WebServices Proxy or embedded REST API. 30 api_password: 31 required: true 32 description: 33 - The password to authenticate with the SANtricity WebServices Proxy or embedded REST API. 34 api_url: 35 required: true 36 description: 37 - The url to the SANtricity WebServices Proxy or embedded REST API, for example C(https://prod-1.wahoo.acme.com/devmgr/v2). 38 validate_certs: 39 required: false 40 default: true 41 description: 42 - Should https certificates be validated? 43 source_volume_id: 44 description: 45 - The id of the volume copy source. 46 - If used, must be paired with destination_volume_id 47 - Mutually exclusive with volume_copy_pair_id, and search_volume_id 48 destination_volume_id: 49 description: 50 - The id of the volume copy destination. 51 - If used, must be paired with source_volume_id 52 - Mutually exclusive with volume_copy_pair_id, and search_volume_id 53 volume_copy_pair_id: 54 description: 55 - The id of a given volume copy pair 56 - Mutually exclusive with destination_volume_id, source_volume_id, and search_volume_id 57 - Can use to delete or check presence of volume pairs 58 - Must specify this or (destination_volume_id and source_volume_id) 59 state: 60 description: 61 - Whether the specified volume copy pair should exist or not. 62 required: True 63 choices: ['present', 'absent'] 64 create_copy_pair_if_does_not_exist: 65 description: 66 - Defines if a copy pair will be created if it does not exist. 67 - If set to True destination_volume_id and source_volume_id are required. 68 type: bool 69 default: True 70 start_stop_copy: 71 description: 72 - starts a re-copy or stops a copy in progress 73 - "Note: If you stop the initial file copy before it it done the copy pair will be destroyed" 74 - Requires volume_copy_pair_id 75 search_volume_id: 76 description: 77 - Searches for all valid potential target and source volumes that could be used in a copy_pair 78 - Mutually exclusive with volume_copy_pair_id, destination_volume_id and source_volume_id 79""" 80RESULTS = """ 81""" 82EXAMPLES = """ 83--- 84msg: 85 description: Success message 86 returned: success 87 type: str 88 sample: Json facts for the volume copy that was created. 89""" 90RETURN = """ 91msg: 92 description: Success message 93 returned: success 94 type: str 95 sample: Created Volume Copy Pair with ID 96""" 97 98import json 99 100from ansible.module_utils.basic import AnsibleModule 101from ansible.module_utils._text import to_native 102from ansible.module_utils.netapp import request 103 104HEADERS = { 105 "Content-Type": "application/json", 106 "Accept": "application/json", 107} 108 109 110def find_volume_copy_pair_id_from_source_volume_id_and_destination_volume_id(params): 111 get_status = 'storage-systems/%s/volume-copy-jobs' % params['ssid'] 112 url = params['api_url'] + get_status 113 114 (rc, resp) = request(url, method='GET', url_username=params['api_username'], 115 url_password=params['api_password'], headers=HEADERS, 116 validate_certs=params['validate_certs']) 117 118 volume_copy_pair_id = None 119 for potential_copy_pair in resp: 120 if potential_copy_pair['sourceVolume'] == params['source_volume_id']: 121 if potential_copy_pair['sourceVolume'] == params['source_volume_id']: 122 volume_copy_pair_id = potential_copy_pair['id'] 123 124 return volume_copy_pair_id 125 126 127def create_copy_pair(params): 128 get_status = 'storage-systems/%s/volume-copy-jobs' % params['ssid'] 129 url = params['api_url'] + get_status 130 131 rData = { 132 "sourceId": params['source_volume_id'], 133 "targetId": params['destination_volume_id'] 134 } 135 136 (rc, resp) = request(url, data=json.dumps(rData), ignore_errors=True, method='POST', 137 url_username=params['api_username'], url_password=params['api_password'], headers=HEADERS, 138 validate_certs=params['validate_certs']) 139 if rc != 200: 140 return False, (rc, resp) 141 else: 142 return True, (rc, resp) 143 144 145def delete_copy_pair_by_copy_pair_id(params): 146 get_status = 'storage-systems/%s/volume-copy-jobs/%s?retainRepositories=false' % ( 147 params['ssid'], params['volume_copy_pair_id']) 148 url = params['api_url'] + get_status 149 150 (rc, resp) = request(url, ignore_errors=True, method='DELETE', 151 url_username=params['api_username'], url_password=params['api_password'], headers=HEADERS, 152 validate_certs=params['validate_certs']) 153 if rc != 204: 154 return False, (rc, resp) 155 else: 156 return True, (rc, resp) 157 158 159def find_volume_copy_pair_id_by_volume_copy_pair_id(params): 160 get_status = 'storage-systems/%s/volume-copy-jobs/%s?retainRepositories=false' % ( 161 params['ssid'], params['volume_copy_pair_id']) 162 url = params['api_url'] + get_status 163 164 (rc, resp) = request(url, ignore_errors=True, method='DELETE', 165 url_username=params['api_username'], url_password=params['api_password'], headers=HEADERS, 166 validate_certs=params['validate_certs']) 167 if rc != 200: 168 return False, (rc, resp) 169 else: 170 return True, (rc, resp) 171 172 173def start_stop_copy(params): 174 get_status = 'storage-systems/%s/volume-copy-jobs-control/%s?control=%s' % ( 175 params['ssid'], params['volume_copy_pair_id'], params['start_stop_copy']) 176 url = params['api_url'] + get_status 177 178 (response_code, response_data) = request(url, ignore_errors=True, method='POST', 179 url_username=params['api_username'], url_password=params['api_password'], 180 headers=HEADERS, 181 validate_certs=params['validate_certs']) 182 183 if response_code == 200: 184 return True, response_data[0]['percentComplete'] 185 else: 186 return False, response_data 187 188 189def check_copy_status(params): 190 get_status = 'storage-systems/%s/volume-copy-jobs-control/%s' % ( 191 params['ssid'], params['volume_copy_pair_id']) 192 url = params['api_url'] + get_status 193 194 (response_code, response_data) = request(url, ignore_errors=True, method='GET', 195 url_username=params['api_username'], url_password=params['api_password'], 196 headers=HEADERS, 197 validate_certs=params['validate_certs']) 198 199 if response_code == 200: 200 if response_data['percentComplete'] != -1: 201 202 return True, response_data['percentComplete'] 203 else: 204 return False, response_data['percentComplete'] 205 else: 206 return False, response_data 207 208 209def find_valid_copy_pair_targets_and_sources(params): 210 get_status = 'storage-systems/%s/volumes' % params['ssid'] 211 url = params['api_url'] + get_status 212 213 (response_code, response_data) = request(url, ignore_errors=True, method='GET', 214 url_username=params['api_username'], url_password=params['api_password'], 215 headers=HEADERS, 216 validate_certs=params['validate_certs']) 217 218 if response_code == 200: 219 source_capacity = None 220 candidates = [] 221 for volume in response_data: 222 if volume['id'] == params['search_volume_id']: 223 source_capacity = volume['capacity'] 224 else: 225 candidates.append(volume) 226 227 potential_sources = [] 228 potential_targets = [] 229 230 for volume in candidates: 231 if volume['capacity'] > source_capacity: 232 if volume['volumeCopyTarget'] is False: 233 if volume['volumeCopySource'] is False: 234 potential_targets.append(volume['id']) 235 else: 236 if volume['volumeCopyTarget'] is False: 237 if volume['volumeCopySource'] is False: 238 potential_sources.append(volume['id']) 239 240 return potential_targets, potential_sources 241 242 else: 243 raise Exception("Response [%s]" % response_code) 244 245 246def main(): 247 module = AnsibleModule(argument_spec=dict( 248 source_volume_id=dict(type='str'), 249 destination_volume_id=dict(type='str'), 250 copy_priority=dict(required=False, default=0, type='int'), 251 ssid=dict(required=True, type='str'), 252 api_url=dict(required=True), 253 api_username=dict(required=False), 254 api_password=dict(required=False, no_log=True), 255 validate_certs=dict(required=False, default=True), 256 targetWriteProtected=dict(required=False, default=True, type='bool'), 257 onlineCopy=dict(required=False, default=False, type='bool'), 258 volume_copy_pair_id=dict(type='str'), 259 status=dict(required=True, choices=['present', 'absent'], type='str'), 260 create_copy_pair_if_does_not_exist=dict(required=False, default=True, type='bool'), 261 start_stop_copy=dict(required=False, choices=['start', 'stop'], type='str'), 262 search_volume_id=dict(type='str'), 263 ), 264 mutually_exclusive=[['volume_copy_pair_id', 'destination_volume_id'], 265 ['volume_copy_pair_id', 'source_volume_id'], 266 ['volume_copy_pair_id', 'search_volume_id'], 267 ['search_volume_id', 'destination_volume_id'], 268 ['search_volume_id', 'source_volume_id'], 269 ], 270 required_together=[['source_volume_id', 'destination_volume_id'], 271 ], 272 required_if=[["create_copy_pair_if_does_not_exist", True, ['source_volume_id', 'destination_volume_id'], ], 273 ["start_stop_copy", 'stop', ['volume_copy_pair_id'], ], 274 ["start_stop_copy", 'start', ['volume_copy_pair_id'], ], 275 ] 276 277 ) 278 params = module.params 279 280 if not params['api_url'].endswith('/'): 281 params['api_url'] += '/' 282 283 # Check if we want to search 284 if params['search_volume_id'] is not None: 285 try: 286 potential_targets, potential_sources = find_valid_copy_pair_targets_and_sources(params) 287 except Exception as e: 288 module.fail_json(msg="Failed to find valid copy pair candidates. Error [%s]" % to_native(e)) 289 290 module.exit_json(changed=False, 291 msg=' Valid source devices found: %s Valid target devices found: %s' % (len(potential_sources), len(potential_targets)), 292 search_volume_id=params['search_volume_id'], 293 valid_targets=potential_targets, 294 valid_sources=potential_sources) 295 296 # Check if we want to start or stop a copy operation 297 if params['start_stop_copy'] == 'start' or params['start_stop_copy'] == 'stop': 298 299 # Get the current status info 300 currenty_running, status_info = check_copy_status(params) 301 302 # If we want to start 303 if params['start_stop_copy'] == 'start': 304 305 # If we have already started 306 if currenty_running is True: 307 module.exit_json(changed=False, msg='Volume Copy Pair copy has started.', 308 volume_copy_pair_id=params['volume_copy_pair_id'], percent_done=status_info) 309 # If we need to start 310 else: 311 312 start_status, info = start_stop_copy(params) 313 314 if start_status is True: 315 module.exit_json(changed=True, msg='Volume Copy Pair copy has started.', 316 volume_copy_pair_id=params['volume_copy_pair_id'], percent_done=info) 317 else: 318 module.fail_json(msg="Could not start volume copy pair Error: %s" % info) 319 320 # If we want to stop 321 else: 322 # If it has already stopped 323 if currenty_running is False: 324 module.exit_json(changed=False, msg='Volume Copy Pair copy is stopped.', 325 volume_copy_pair_id=params['volume_copy_pair_id']) 326 327 # If we need to stop it 328 else: 329 start_status, info = start_stop_copy(params) 330 331 if start_status is True: 332 module.exit_json(changed=True, msg='Volume Copy Pair copy has been stopped.', 333 volume_copy_pair_id=params['volume_copy_pair_id']) 334 else: 335 module.fail_json(msg="Could not stop volume copy pair Error: %s" % info) 336 337 # If we want the copy pair to exist we do this stuff 338 if params['status'] == 'present': 339 340 # We need to check if it exists first 341 if params['volume_copy_pair_id'] is None: 342 params['volume_copy_pair_id'] = find_volume_copy_pair_id_from_source_volume_id_and_destination_volume_id( 343 params) 344 345 # If no volume copy pair is found we need need to make it. 346 if params['volume_copy_pair_id'] is None: 347 348 # In order to create we can not do so with just a volume_copy_pair_id 349 350 copy_began_status, (rc, resp) = create_copy_pair(params) 351 352 if copy_began_status is True: 353 module.exit_json(changed=True, msg='Created Volume Copy Pair with ID: %s' % resp['id']) 354 else: 355 module.fail_json(msg="Could not create volume copy pair Code: %s Error: %s" % (rc, resp)) 356 357 # If it does exist we do nothing 358 else: 359 # We verify that it exists 360 exist_status, (exist_status_code, exist_status_data) = find_volume_copy_pair_id_by_volume_copy_pair_id( 361 params) 362 363 if exist_status: 364 module.exit_json(changed=False, 365 msg=' Volume Copy Pair with ID: %s exists' % params['volume_copy_pair_id']) 366 else: 367 if exist_status_code == 404: 368 module.fail_json( 369 msg=' Volume Copy Pair with ID: %s does not exist. Can not create without source_volume_id and destination_volume_id' % 370 params['volume_copy_pair_id']) 371 else: 372 module.fail_json(msg="Could not find volume copy pair Code: %s Error: %s" % ( 373 exist_status_code, exist_status_data)) 374 375 module.fail_json(msg="Done") 376 377 # If we want it to not exist we do this 378 else: 379 380 if params['volume_copy_pair_id'] is None: 381 params['volume_copy_pair_id'] = find_volume_copy_pair_id_from_source_volume_id_and_destination_volume_id( 382 params) 383 384 # We delete it by the volume_copy_pair_id 385 delete_status, (delete_status_code, delete_status_data) = delete_copy_pair_by_copy_pair_id(params) 386 387 if delete_status is True: 388 module.exit_json(changed=True, 389 msg=' Volume Copy Pair with ID: %s was deleted' % params['volume_copy_pair_id']) 390 else: 391 if delete_status_code == 404: 392 module.exit_json(changed=False, 393 msg=' Volume Copy Pair with ID: %s does not exist' % params['volume_copy_pair_id']) 394 else: 395 module.fail_json(msg="Could not delete volume copy pair Code: %s Error: %s" % ( 396 delete_status_code, delete_status_data)) 397 398 399if __name__ == '__main__': 400 main() 401