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