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