Why Gemfury? Push, build, and install  RubyGems npm packages Python packages Maven artifacts PHP packages Go Modules Debian packages RPM packages NuGet packages

Repository URL to install this package:

Details    
ansible / netapp / cloudmanager / plugins / modules / na_cloudmanager_snapmirror.py
Size: Mime:
#!/usr/bin/python

# (c) 2021, NetApp, Inc
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)

'''
na_cloudmanager_snapmirror
'''

from __future__ import absolute_import, division, print_function
__metaclass__ = type

DOCUMENTATION = '''

module: na_cloudmanager_snapmirror
short_description: NetApp Cloud Manager SnapMirror
extends_documentation_fragment:
    - netapp.cloudmanager.netapp.cloudmanager
version_added: '21.6.0'
author: NetApp Ansible Team (@carchi8py) <ng-ansibleteam@netapp.com>

description:
- Create or Delete SnapMirror relationship on Cloud Manager.

options:

  state:
    description:
    - Whether the specified snapmirror relationship should exist or not.
    choices: ['present', 'absent']
    default: 'present'
    type: str

  source_working_environment_name:
    description:
    - The working environment name of the source volume.
    type: str

  destination_working_environment_name:
    description:
    - The working environment name of the destination volume.
    type: str

  source_working_environment_id:
    description:
    - The public ID of the working environment of the source volume.
    type: str

  destination_working_environment_id:
    description:
    - The public ID of the working environment of the destination volume.
    type: str

  destination_aggregate_name:
    description:
    - The aggregate in which the volume will be created.
    - If not provided, Cloud Manager chooses the best aggregate for you.
    type: str

  policy:
    description:
    - The SnapMirror policy name.
    type: str
    default: 'MirrorAllSnapshots'

  max_transfer_rate:
    description:
    - Maximum transfer rate limit KB/s.
    - Use 0 for no limit, otherwise use number between 1024 and 2,147,482,624.
    type: int
    default: 100000

  source_svm_name:
    description:
    - The name of the source SVM.
    - The default SVM name is used, if a name is not provided.
    type: str

  destination_svm_name:
    description:
    - The name of the destination SVM.
    - The default SVM name is used, if a name is not provided.
    type: str

  source_volume_name:
    description:
    - The name of the source volume.
    required: true
    type: str

  destination_volume_name:
    description:
    - The name of the destination volume to be created for snapmirror relationship.
    required: true
    type: str

  schedule:
    description:
    - The name of the Schedule.
    type: str
    default: '1hour'

  provider_volume_type:
    description:
    - The underlying cloud provider volume type.
    - For AWS ['gp3', 'gp2', 'io1', 'st1', 'sc1'].
    - For Azure ['Premium_LRS','Standard_LRS','StandardSSD_LRS'].
    - For GCP ['pd-balanced','pd-ssd','pd-standard'].
    type: str

  capacity_tier:
    description:
    - The volume capacity tier for tiering cold data to object storage.
    - The default values for each cloud provider are as follows, Amazon 'S3', Azure 'Blob', GCP 'cloudStorage'.
    - If NONE, the capacity tier will not be set on volume creation.
    type: str
    choices: ['S3', 'Blob', 'cloudStorage', 'NONE']

  tenant_id:
    description:
    - The NetApp account ID that the Connector will be associated with. To be used only when using FSx.
    type: str
    version_added: 21.14.0

  client_id:
    description:
    - The connector ID of the Cloud Manager Connector.
    required: true
    type: str

notes:
- Support check_mode.
'''

EXAMPLES = '''
- name: Create snapmirror with working_environment_name
  netapp.cloudmanager.na_cloudmanager_snapmirror:
    state: present
    source_working_environment_name: source
    destination_working_environment_name: dest
    source_volume_name: source
    destination_volume_name: source_copy
    policy: MirrorAllSnapshots
    schedule: 5min
    max_transfer_rate: 102400
    client_id: client_id
    refresh_token: refresh_token

- name: Delete snapmirror
  netapp.cloudmanager.na_cloudmanager_snapmirror:
    state: absent
    source_working_environment_name: source
    destination_working_environment_name: dest
    source_volume_name: source
    destination_volume_name: source_copy
    client_id: client_id
    refresh_token: refresh_token
'''

RETURN = r''' # '''


from ansible.module_utils.basic import AnsibleModule
import ansible_collections.netapp.cloudmanager.plugins.module_utils.netapp as netapp_utils
from ansible_collections.netapp.cloudmanager.plugins.module_utils.netapp_module import NetAppModule
from ansible_collections.netapp.cloudmanager.plugins.module_utils.netapp import CloudManagerRestAPI

PROVIDER_TO_CAPACITY_TIER = {'amazon': 'S3', 'azure': 'Blob', 'gcp': 'cloudStorage'}


class NetAppCloudmanagerSnapmirror:

    def __init__(self):
        """
        Parse arguments, setup state variables,
        check parameters and ensure request module is installed
        """
        self.argument_spec = netapp_utils.cloudmanager_host_argument_spec()
        self.argument_spec.update(dict(
            state=dict(required=False, choices=['present', 'absent'], default='present'),
            source_working_environment_id=dict(required=False, type='str'),
            destination_working_environment_id=dict(required=False, type='str'),
            source_working_environment_name=dict(required=False, type='str'),
            destination_working_environment_name=dict(required=False, type='str'),
            destination_aggregate_name=dict(required=False, type='str'),
            policy=dict(required=False, type='str', default='MirrorAllSnapshots'),
            max_transfer_rate=dict(required=False, type='int', default='100000'),
            schedule=dict(required=False, type='str', default='1hour'),
            source_svm_name=dict(required=False, type='str'),
            destination_svm_name=dict(required=False, type='str'),
            source_volume_name=dict(required=True, type='str'),
            destination_volume_name=dict(required=True, type='str'),
            capacity_tier=dict(required=False, type='str', choices=['NONE', 'S3', 'Blob', 'cloudStorage']),
            provider_volume_type=dict(required=False, type='str'),
            tenant_id=dict(required=False, type='str'),
            client_id=dict(required=True, type='str'),
        ))
        self.module = AnsibleModule(
            argument_spec=self.argument_spec,
            required_one_of=[
                ['source_working_environment_id', 'source_working_environment_name'],
                ['refresh_token', 'sa_client_id'],
            ],
            required_together=(['source_working_environment_id', 'destination_working_environment_id'],
                               ['source_working_environment_name', 'destination_working_environment_name'],
                               ['sa_client_id', 'sa_secret_key'],
                               ),
            supports_check_mode=True
        )

        self.na_helper = NetAppModule()
        # set up state variables
        self.parameters = self.na_helper.set_parameters(self.module.params)

        self.rest_api = CloudManagerRestAPI(self.module)
        self.rest_api.url += self.rest_api.environment_data['CLOUD_MANAGER_HOST']
        self.rest_api.api_root_path = None
        self.headers = {
            'X-Agent-Id': self.rest_api.format_client_id(self.parameters['client_id'])
        }
        if self.rest_api.simulator:
            self.headers.update({'x-simulator': 'true'})

    def get_snapmirror(self):
        source_we_info, dest_we_info, err = self.na_helper.get_working_environment_detail_for_snapmirror(self.rest_api, self.headers)
        if err is not None:
            self.module.fail_json(changed=False, msg=err)

        get_url = '/occm/api/replication/status/%s' % source_we_info['publicId']
        snapmirror_info, err, dummy = self.rest_api.send_request("GET", get_url, None, header=self.headers)
        if err is not None:
            self.module.fail_json(changed=False, msg='Error getting snapmirror relationship %s: %s.' % (err, snapmirror_info))
        sm_found = False
        snapmirror = None
        for sm in snapmirror_info:
            if sm['destination']['volumeName'] == self.parameters['destination_volume_name']:
                sm_found = True
                snapmirror = sm
                break

        if not sm_found:
            return None
        result = {
            'source_working_environment_id': source_we_info['publicId'],
            'destination_svm_name': snapmirror['destination']['svmName'],
            'destination_working_environment_id': dest_we_info['publicId'],
        }
        if not dest_we_info['publicId'].startswith('fs-'):
            result['cloud_provider_name'] = dest_we_info['cloudProviderName']
        return result

    def create_snapmirror(self):
        snapmirror_build_data = {}
        replication_request = {}
        replication_volume = {}
        source_we_info, dest_we_info, err = self.na_helper.get_working_environment_detail_for_snapmirror(self.rest_api, self.headers)
        if err is not None:
            self.module.fail_json(changed=False, msg=err)
        if self.parameters.get('capacity_tier') is not None:
            if self.parameters['capacity_tier'] == 'NONE':
                self.parameters.pop('capacity_tier')
        else:
            if dest_we_info.get('cloudProviderName'):
                self.parameters['capacity_tier'] = PROVIDER_TO_CAPACITY_TIER[dest_we_info['cloudProviderName'].lower()]

        interclusterlifs_info = self.get_interclusterlifs(source_we_info['publicId'], dest_we_info['publicId'])

        if source_we_info['workingEnvironmentType'] != 'ON_PREM':
            source_volumes = self.get_volumes(source_we_info, self.parameters['source_volume_name'])
        else:
            source_volumes = self.get_volumes_on_prem(source_we_info, self.parameters['source_volume_name'])

        if len(source_volumes) == 0:
            self.module.fail_json(changed=False, msg='source volume not found')

        vol_found = False
        vol_dest_quote = {}
        source_volume_resp = {}
        for vol in source_volumes:
            if vol['name'] == self.parameters['source_volume_name']:
                vol_found = True
                vol_dest_quote = vol
                source_volume_resp = vol
                if self.parameters.get('source_svm_name') is not None and vol['svmName'] != self.parameters['source_svm_name']:
                    vol_found = False
                if vol_found:
                    break

        if not vol_found:
            self.module.fail_json(changed=False, msg='source volume not found')

        if self.parameters.get('source_svm_name') is None:
            self.parameters['source_svm_name'] = source_volume_resp['svmName']

        if self.parameters.get('destination_svm_name') is None:
            if dest_we_info.get('svmName') is not None:
                self.parameters['destination_svm_name'] = dest_we_info['svmName']
            else:
                self.parameters['destination_working_environment_name'] = dest_we_info['name']
                dest_working_env_detail, err = self.na_helper.get_working_environment_details_by_name(self.rest_api,
                                                                                                      self.headers,
                                                                                                      self.parameters['destination_working_environment_name'])
                if err:
                    self.module.fail_json(changed=False, msg='Error getting destination info %s: %s.' % (err, dest_working_env_detail))
                self.parameters['destination_svm_name'] = dest_working_env_detail['svmName']

        if dest_we_info.get('workingEnvironmentType') and dest_we_info['workingEnvironmentType'] != 'ON_PREM'\
                and not dest_we_info['publicId'].startswith('fs-'):
            quote = self.build_quote_request(source_we_info, dest_we_info, vol_dest_quote)
            quote_response = self.quote_volume(quote)
            replication_volume['numOfDisksApprovedToAdd'] = int(quote_response['numOfDisks'])
            if 'iops' in quote:
                replication_volume['iops'] = quote['iops']
            if 'throughput' in quote:
                replication_volume['throughput'] = quote['throughput']
            if self.parameters.get('destination_aggregate_name') is not None:
                replication_volume['advancedMode'] = True
            else:
                replication_volume['advancedMode'] = False
                replication_volume['destinationAggregateName'] = quote_response['aggregateName']
        if self.parameters.get('provider_volume_type') is None:
            replication_volume['destinationProviderVolumeType'] = source_volume_resp['providerVolumeType']

        if self.parameters.get('capacity_tier') is not None:
            replication_volume['destinationCapacityTier'] = self.parameters['capacity_tier']
        replication_request['sourceWorkingEnvironmentId'] = source_we_info['publicId']
        if dest_we_info['publicId'].startswith('fs-'):
            replication_request['destinationFsxId'] = dest_we_info['publicId']
        else:
            replication_request['destinationWorkingEnvironmentId'] = dest_we_info['publicId']
        replication_volume['sourceVolumeName'] = self.parameters['source_volume_name']
        replication_volume['destinationVolumeName'] = self.parameters['destination_volume_name']
        replication_request['policyName'] = self.parameters['policy']
        replication_request['scheduleName'] = self.parameters['schedule']
        replication_request['maxTransferRate'] = self.parameters['max_transfer_rate']
        replication_volume['sourceSvmName'] = source_volume_resp['svmName']
        replication_volume['destinationSvmName'] = self.parameters['destination_svm_name']
        replication_request['sourceInterclusterLifIps'] = [interclusterlifs_info['interClusterLifs'][0]['address']]
        replication_request['destinationInterclusterLifIps'] = [interclusterlifs_info['peerInterClusterLifs'][0]['address']]

        snapmirror_build_data['replicationRequest'] = replication_request
        snapmirror_build_data['replicationVolume'] = replication_volume

        if dest_we_info['publicId'].startswith('fs-'):
            api = '/occm/api/replication/fsx'
        elif dest_we_info['workingEnvironmentType'] != 'ON_PREM':
            api = '/occm/api/replication/vsa'
        else:
            api = '/occm/api/replication/onprem'

        response, err, on_cloud_request_id = self.rest_api.send_request("POST", api, None, snapmirror_build_data, header=self.headers)
        if err is not None:
            self.module.fail_json(changed=False, msg='Error creating snapmirror relationship %s: %s.' % (err, response))
        wait_on_completion_api_url = '/occm/api/audit/activeTask/%s' % (str(on_cloud_request_id))
        err = self.rest_api.wait_on_completion(wait_on_completion_api_url, "snapmirror", "create", 20, 5)
        if err is not None:
            self.module.fail_json(changed=False, msg=err)

    def get_volumes(self, working_environment_detail, name):
        self.na_helper.set_api_root_path(working_environment_detail, self.rest_api)
        response, err, dummy = self.rest_api.send_request("GET", "%s/volumes?workingEnvironmentId=%s&name=%s" % (
            self.rest_api.api_root_path, working_environment_detail['publicId'], name), None, header=self.headers)
        if err is not None:
            self.module.fail_json(changed=False, msg='Error getting volume %s: %s.' % (err, response))
        return response

    def quote_volume(self, quote):
        response, err, on_cloud_request_id = self.rest_api.send_request("POST", '%s/volumes/quote' %
                                                                        self.rest_api.api_root_path, None, quote, header=self.headers)
        if err is not None:
            self.module.fail_json(changed=False, msg='Error quoting destination volume %s: %s.' % (err, response))
        wait_on_completion_api_url = '/occm/api/audit/activeTask/%s' % (str(on_cloud_request_id))
        err = self.rest_api.wait_on_completion(wait_on_completion_api_url, "volume", "quote", 20, 5)
        if err is not None:
            self.module.fail_json(changed=False, msg=err)
        return response

    def get_volumes_on_prem(self, working_environment_detail, name):
        response, err, dummy = self.rest_api.send_request("GET", "/occm/api/onprem/volumes?workingEnvironmentId=%s&name=%s" %
                                                          (working_environment_detail['publicId'], name), None, header=self.headers)
        if err is not None:
            self.module.fail_json(changed=False, msg='Error getting volume on prem %s: %s.' % (err, response))
        return response

    def get_aggregate_detail(self, working_environment_detail, aggregate_name):
        if working_environment_detail['workingEnvironmentType'] == 'ON_PREM':
            api = "/occm/api/onprem/aggregates?workingEnvironmentId=%s" % working_environment_detail['publicId']
        else:
            self.na_helper.set_api_root_path(working_environment_detail, self.rest_api)
            api_root_path = self.rest_api.api_root_path
            if working_environment_detail['cloudProviderName'] != "Amazon":
                api = '%s/aggregates/%s'
            else:
                api = '%s/aggregates?workingEnvironmentId=%s'
            api = api % (api_root_path, working_environment_detail['publicId'])
        response, error, dummy = self.rest_api.get(api, header=self.headers)
        if error:
            self.module.fail_json(msg="Error: Failed to get aggregate list: %s" % str(error))
        for aggr in response:
            if aggr['name'] == aggregate_name:
                return aggr
        return None

    def build_quote_request(self, source_we_info, dest_we_info, vol_dest_quote):
        quote = dict()
        quote['size'] = {'size': vol_dest_quote['size']['size'], 'unit': vol_dest_quote['size']['unit']}
        quote['name'] = self.parameters['destination_volume_name']
        quote['snapshotPolicyName'] = vol_dest_quote['snapshotPolicy']
        quote['enableDeduplication'] = vol_dest_quote['deduplication']
        quote['enableThinProvisioning'] = vol_dest_quote['thinProvisioning']
        quote['enableCompression'] = vol_dest_quote['compression']
        quote['verifyNameUniqueness'] = True
        quote['replicationFlow'] = True

        # Use source working environment to get physical properties info of volumes
        aggregate = self.get_aggregate_detail(source_we_info, vol_dest_quote['aggregateName'])
        if aggregate is None:
            self.module.fail_json(changed=False, msg='Error getting aggregate on source volume')
        # All the volumes in one aggregate have the same physical properties
        if source_we_info['workingEnvironmentType'] != 'ON_PREM':
            if aggregate['providerVolumes'][0]['diskType'] == 'gp3' or aggregate['providerVolumes'][0]['diskType'] == 'io1'\
                    or aggregate['providerVolumes'][0]['diskType'] == 'io2':
                quote['iops'] = aggregate['providerVolumes'][0]['iops']
            if aggregate['providerVolumes'][0]['diskType'] == 'gp3':
                quote['throughput'] = aggregate['providerVolumes'][0]['throughput']
            quote['workingEnvironmentId'] = dest_we_info['publicId']
            quote['svmName'] = self.parameters['destination_svm_name']
        if self.parameters.get('capacity_tier') is not None:
            quote['capacityTier'] = self.parameters['capacity_tier']

        if self.parameters.get('provider_volume_type') is None:
            quote['providerVolumeType'] = vol_dest_quote['providerVolumeType']
        else:
            quote['providerVolumeType'] = self.parameters['provider_volume_type']

        return quote

    def delete_snapmirror(self, sm_detail):
        api_delete = '/occm/api/replication/%s/%s/%s' %\
                     (sm_detail['destination_working_environment_id'], sm_detail['destination_svm_name'], self.parameters['destination_volume_name'])
        dummy, err, dummy_second = self.rest_api.send_request("DELETE", api_delete, None, None, header=self.headers)
        if err is not None:
            self.module.fail_json(changed=False, msg='Error deleting snapmirror relationship %s: %s.' % (err, dummy))

    def get_interclusterlifs(self, source_we_id, dest_we_id):
        api_get = '/occm/api/replication/intercluster-lifs?peerWorkingEnvironmentId=%s&workingEnvironmentId=%s' % (dest_we_id, source_we_id)
        response, err, dummy_second = self.rest_api.send_request("GET", api_get, None, header=self.headers)
        if err is not None:
            self.module.fail_json(changed=False, msg='Error getting interclusterlifs %s: %s.' % (err, response))
        return response

    def apply(self):
        current = self.get_snapmirror()
        cd_action = self.na_helper.get_cd_action(current, self.parameters)
        if self.na_helper.changed and not self.module.check_mode:
            if cd_action == 'create':
                self.create_snapmirror()
            elif cd_action == 'delete':
                self.delete_snapmirror(current)
        self.module.exit_json(changed=self.na_helper.changed)


def main():
    '''Main Function'''
    volume = NetAppCloudmanagerSnapmirror()
    volume.apply()


if __name__ == '__main__':
    main()