1#!/usr/bin/python 2# (c) 2017, NetApp, Inc 3# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) 4 5from __future__ import absolute_import, division, print_function 6 7__metaclass__ = type 8 9ANSIBLE_METADATA = {'metadata_version': '1.1', 10 'status': ['preview'], 11 'supported_by': 'certified'} 12 13DOCUMENTATION = ''' 14 15module: na_elementsw_volume_pair 16 17short_description: NetApp Element Software Volume Pair 18extends_documentation_fragment: 19 - netapp.solidfire 20version_added: '2.7' 21author: NetApp Ansible Team (@carchi8py) <ng-ansibleteam@netapp.com> 22description: 23- Create, delete volume pair 24 25options: 26 27 state: 28 description: 29 - Whether the specified volume pair should exist or not. 30 choices: ['present', 'absent'] 31 default: present 32 33 src_volume: 34 description: 35 - Source volume name or volume ID 36 required: true 37 38 src_account: 39 description: 40 - Source account name or ID 41 required: true 42 43 dest_volume: 44 description: 45 - Destination volume name or volume ID 46 required: true 47 48 dest_account: 49 description: 50 - Destination account name or ID 51 required: true 52 53 mode: 54 description: 55 - Mode to start the volume pairing 56 choices: ['async', 'sync', 'snapshotsonly'] 57 default: async 58 59 dest_mvip: 60 description: 61 - Destination IP address of the paired cluster. 62 required: true 63 64 dest_username: 65 description: 66 - Destination username for the paired cluster 67 - Optional if this is same as source cluster username. 68 69 dest_password: 70 description: 71 - Destination password for the paired cluster 72 - Optional if this is same as source cluster password. 73 74''' 75 76EXAMPLES = """ 77 - name: Create volume pair 78 na_elementsw_volume_pair: 79 hostname: "{{ src_cluster_hostname }}" 80 username: "{{ src_cluster_username }}" 81 password: "{{ src_cluster_password }}" 82 state: present 83 src_volume: test1 84 src_account: test2 85 dest_volume: test3 86 dest_account: test4 87 mode: sync 88 dest_mvip: "{{ dest_cluster_hostname }}" 89 90 - name: Delete volume pair 91 na_elementsw_volume_pair: 92 hostname: "{{ src_cluster_hostname }}" 93 username: "{{ src_cluster_username }}" 94 password: "{{ src_cluster_password }}" 95 state: absent 96 src_volume: 3 97 src_account: 1 98 dest_volume: 2 99 dest_account: 1 100 dest_mvip: "{{ dest_cluster_hostname }}" 101 dest_username: "{{ dest_cluster_username }}" 102 dest_password: "{{ dest_cluster_password }}" 103 104""" 105 106RETURN = """ 107 108""" 109 110from ansible.module_utils.basic import AnsibleModule 111from ansible.module_utils._text import to_native 112import ansible.module_utils.netapp as netapp_utils 113from ansible.module_utils.netapp_elementsw_module import NaElementSWModule 114from ansible.module_utils.netapp_module import NetAppModule 115 116HAS_SF_SDK = netapp_utils.has_sf_sdk() 117try: 118 import solidfire.common 119except ImportError: 120 HAS_SF_SDK = False 121 122 123class ElementSWVolumePair(object): 124 ''' class to handle volume pairing operations ''' 125 126 def __init__(self): 127 """ 128 Setup Ansible parameters and SolidFire connection 129 """ 130 self.argument_spec = netapp_utils.ontap_sf_host_argument_spec() 131 self.argument_spec.update(dict( 132 state=dict(required=False, choices=['present', 'absent'], 133 default='present'), 134 src_volume=dict(required=True, type='str'), 135 src_account=dict(required=True, type='str'), 136 dest_volume=dict(required=True, type='str'), 137 dest_account=dict(required=True, type='str'), 138 mode=dict(required=False, type='str', 139 choices=['async', 'sync', 'snapshotsonly'], 140 default='async'), 141 dest_mvip=dict(required=True, type='str'), 142 dest_username=dict(required=False, type='str'), 143 dest_password=dict(required=False, type='str', no_log=True) 144 )) 145 146 self.module = AnsibleModule( 147 argument_spec=self.argument_spec, 148 supports_check_mode=True 149 ) 150 151 if HAS_SF_SDK is False: 152 self.module.fail_json(msg="Unable to import the SolidFire Python SDK") 153 else: 154 self.elem = netapp_utils.create_sf_connection(module=self.module) 155 156 self.elementsw_helper = NaElementSWModule(self.elem) 157 self.na_helper = NetAppModule() 158 self.parameters = self.na_helper.set_parameters(self.module.params) 159 # get element_sw_connection for destination cluster 160 # overwrite existing source host, user and password with destination credentials 161 self.module.params['hostname'] = self.parameters['dest_mvip'] 162 # username and password is same as source, 163 # if dest_username and dest_password aren't specified 164 if self.parameters.get('dest_username'): 165 self.module.params['username'] = self.parameters['dest_username'] 166 if self.parameters.get('dest_password'): 167 self.module.params['password'] = self.parameters['dest_password'] 168 self.dest_elem = netapp_utils.create_sf_connection(module=self.module) 169 self.dest_elementsw_helper = NaElementSWModule(self.dest_elem) 170 171 def check_if_already_paired(self, vol_id): 172 """ 173 Check for idempotency 174 A volume can have only one pair 175 Return paired-volume-id if volume is paired already 176 None if volume is not paired 177 """ 178 paired_volumes = self.elem.list_volumes(volume_ids=[vol_id], 179 is_paired=True) 180 for vol in paired_volumes.volumes: 181 for pair in vol.volume_pairs: 182 if pair is not None: 183 return pair.remote_volume_id 184 return None 185 186 def pair_volumes(self): 187 """ 188 Start volume pairing on source, and complete on target volume 189 """ 190 try: 191 pair_key = self.elem.start_volume_pairing( 192 volume_id=self.parameters['src_vol_id'], 193 mode=self.parameters['mode']) 194 self.dest_elem.complete_volume_pairing( 195 volume_pairing_key=pair_key.volume_pairing_key, 196 volume_id=self.parameters['dest_vol_id']) 197 except solidfire.common.ApiServerError as err: 198 self.module.fail_json(msg="Error pairing volume id %s" 199 % (self.parameters['src_vol_id']), 200 exception=to_native(err)) 201 202 def pairing_exists(self, src_id, dest_id): 203 src_paired = self.check_if_already_paired(self.parameters['src_vol_id']) 204 dest_paired = self.check_if_already_paired(self.parameters['dest_vol_id']) 205 if src_paired is not None or dest_paired is not None: 206 return True 207 return None 208 209 def unpair_volumes(self): 210 """ 211 Delete volume pair 212 """ 213 try: 214 self.elem.remove_volume_pair(volume_id=self.parameters['src_vol_id']) 215 self.dest_elem.remove_volume_pair(volume_id=self.parameters['dest_vol_id']) 216 except solidfire.common.ApiServerError as err: 217 self.module.fail_json(msg="Error unpairing volume ids %s and %s" 218 % (self.parameters['src_vol_id'], 219 self.parameters['dest_vol_id']), 220 exception=to_native(err)) 221 222 def get_account_id(self, account, type): 223 """ 224 Get source and destination account IDs 225 """ 226 try: 227 if type == 'src': 228 self.parameters['src_account_id'] = self.elementsw_helper.account_exists(account) 229 elif type == 'dest': 230 self.parameters['dest_account_id'] = self.dest_elementsw_helper.account_exists(account) 231 except solidfire.common.ApiServerError as err: 232 self.module.fail_json(msg="Error: either account %s or %s does not exist" 233 % (self.parameters['src_account'], 234 self.parameters['dest_account']), 235 exception=to_native(err)) 236 237 def get_volume_id(self, volume, type): 238 """ 239 Get source and destination volume IDs 240 """ 241 if type == 'src': 242 self.parameters['src_vol_id'] = self.elementsw_helper.volume_exists(volume, self.parameters['src_account_id']) 243 if self.parameters['src_vol_id'] is None: 244 self.module.fail_json(msg="Error: source volume %s does not exist" 245 % (self.parameters['src_volume'])) 246 elif type == 'dest': 247 self.parameters['dest_vol_id'] = self.dest_elementsw_helper.volume_exists(volume, self.parameters['dest_account_id']) 248 if self.parameters['dest_vol_id'] is None: 249 self.module.fail_json(msg="Error: destination volume %s does not exist" 250 % (self.parameters['dest_volume'])) 251 252 def get_ids(self): 253 """ 254 Get IDs for volumes and accounts 255 """ 256 self.get_account_id(self.parameters['src_account'], 'src') 257 self.get_account_id(self.parameters['dest_account'], 'dest') 258 self.get_volume_id(self.parameters['src_volume'], 'src') 259 self.get_volume_id(self.parameters['dest_volume'], 'dest') 260 261 def apply(self): 262 """ 263 Call create / delete volume pair methods 264 """ 265 self.get_ids() 266 paired = self.pairing_exists(self.parameters['src_vol_id'], 267 self.parameters['dest_vol_id']) 268 # calling helper to determine action 269 cd_action = self.na_helper.get_cd_action(paired, self.parameters) 270 if cd_action == "create": 271 self.pair_volumes() 272 elif cd_action == "delete": 273 self.unpair_volumes() 274 self.module.exit_json(changed=self.na_helper.changed) 275 276 277def main(): 278 """ Apply volume pair actions """ 279 vol_obj = ElementSWVolumePair() 280 vol_obj.apply() 281 282 283if __name__ == '__main__': 284 main() 285