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