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 / elementsw / plugins / modules / na_elementsw_cluster.py
Size: Mime:
#!/usr/bin/python
# (c) 2018, NetApp, Inc
# GNU General Public License v3.0+ (see COPYING or
# https://www.gnu.org/licenses/gpl-3.0.txt)

'''
Element Software Initialize Cluster
'''
from __future__ import absolute_import, division, print_function
__metaclass__ = type


ANSIBLE_METADATA = {'metadata_version': '1.1',
                    'status': ['preview'],
                    'supported_by': 'certified'}


DOCUMENTATION = '''

module: na_elementsw_cluster

short_description: NetApp Element Software Create Cluster
extends_documentation_fragment:
    - netapp.elementsw.netapp.solidfire
version_added: 2.7.0
author: NetApp Ansible Team (@carchi8py) <ng-ansibleteam@netapp.com>
description:
  - Initialize Element Software node ownership to form a cluster.
  - If the cluster does not exist, username/password are still required but ignored for initial creation.
  - username/password are used as the node credentials to see if the cluster already exists.
  - username/password can also be used to set the cluster credentials.
  - If the cluster already exists, no error is returned, but changed is set to false.
  - Cluster modifications are not supported and are ignored.

options:
    management_virtual_ip:
        description:
        - Floating (virtual) IP address for the cluster on the management network.
        required: true
        type: str

    storage_virtual_ip:
        description:
        - Floating (virtual) IP address for the cluster on the storage (iSCSI) network.
        required: true
        type: str

    replica_count:
        description:
        - Number of replicas of each piece of data to store in the cluster.
        default: 2
        type: int

    cluster_admin_username:
        description:
        - Username for the cluster admin.
        - If not provided, default to username.
        type: str

    cluster_admin_password:
        description:
        - Initial password for the cluster admin account.
        - If not provided, default to password.
        type: str

    accept_eula:
        description:
        - Required to indicate your acceptance of the End User License Agreement when creating this cluster.
        - To accept the EULA, set this parameter to true.
        type: bool

    nodes:
        description:
        - Storage IP (SIP) addresses of the initial set of nodes making up the cluster.
        - nodes IP must be in the list.
        required: true
        type: list
        elements: str

    attributes:
        description:
        - List of name-value pairs in JSON object format.
        type: dict

    timeout:
        description:
          - Time to wait for cluster creation to complete.
        default: 100
        type: int
        version_added: 20.8.0

    fail_if_cluster_already_exists_with_larger_ensemble:
        description:
          - If the cluster exists, the default is to verify that I(nodes) is a superset of the existing ensemble.
          - A superset is accepted because some nodes may have a different role.
          - But the module reports an error if the existing ensemble contains a node not listed in I(nodes).
          - This checker is disabled when this option is set to false.
        default: true
        type: bool
        version_added: 20.8.0

    encryption:
        description: to enable or disable encryption at rest
        type: bool
        version_added: 20.10.0

    order_number:
        description: (experimental) order number as provided by NetApp
        type: str
        version_added: 20.10.0

    serial_number:
        description: (experimental) serial number as provided by NetApp
        type: str
        version_added: 20.10.0
'''

EXAMPLES = """

  - name: Initialize new cluster
    tags:
    - elementsw_cluster
    na_elementsw_cluster:
      hostname: "{{ elementsw_hostname }}"
      username: "{{ elementsw_username }}"
      password: "{{ elementsw_password }}"
      management_virtual_ip: 10.226.108.32
      storage_virtual_ip: 10.226.109.68
      replica_count: 2
      accept_eula: true
      nodes:
      - 10.226.109.72
      - 10.226.109.74
"""

RETURN = """

msg:
    description: Success message
    returned: success
    type: str

"""
import traceback

from ansible.module_utils.basic import AnsibleModule
from ansible.module_utils._text import to_native
import ansible_collections.netapp.elementsw.plugins.module_utils.netapp as netapp_utils
from ansible_collections.netapp.elementsw.plugins.module_utils.netapp_elementsw_module import NaElementSWModule

HAS_SF_SDK = netapp_utils.has_sf_sdk()


class ElementSWCluster(object):
    """
    Element Software Initialize node with ownership for cluster formation
    """

    def __init__(self):
        self.argument_spec = netapp_utils.ontap_sf_host_argument_spec()
        self.argument_spec.update(dict(
            management_virtual_ip=dict(required=True, type='str'),
            storage_virtual_ip=dict(required=True, type='str'),
            replica_count=dict(required=False, type='int', default=2),
            cluster_admin_username=dict(required=False, type='str'),
            cluster_admin_password=dict(required=False, type='str', no_log=True),
            accept_eula=dict(required=False, type='bool'),
            nodes=dict(required=True, type='list', elements='str'),
            attributes=dict(required=False, type='dict', default=None),
            timeout=dict(required=False, type='int', default=100),
            fail_if_cluster_already_exists_with_larger_ensemble=dict(required=False, type='bool', default=True),
            encryption=dict(required=False, type='bool'),
            order_number=dict(required=False, type='str'),
            serial_number=dict(required=False, type='str'),
        ))

        self.module = AnsibleModule(
            argument_spec=self.argument_spec,
            supports_check_mode=True
        )

        input_params = self.module.params

        self.management_virtual_ip = input_params['management_virtual_ip']
        self.storage_virtual_ip = input_params['storage_virtual_ip']
        self.replica_count = input_params['replica_count']
        self.accept_eula = input_params.get('accept_eula')
        self.attributes = input_params.get('attributes')
        self.nodes = input_params['nodes']
        self.cluster_admin_username = input_params['username'] if input_params.get('cluster_admin_username') is None else input_params['cluster_admin_username']
        self.cluster_admin_password = input_params['password'] if input_params.get('cluster_admin_password') is None else input_params['cluster_admin_password']
        self.fail_if_cluster_already_exists_with_larger_ensemble = input_params['fail_if_cluster_already_exists_with_larger_ensemble']
        self.encryption = input_params['encryption']
        self.order_number = input_params['order_number']
        self.serial_number = input_params['serial_number']
        self.debug = list()

        if HAS_SF_SDK is False:
            self.module.fail_json(msg="Unable to import the SolidFire Python SDK")

        # 442 for node APIs, 443 (default) for cluster APIs
        for role, port in [('node', 442), ('cluster', 443)]:
            try:
                # even though username/password should be optional, create_sf_connection fails if not set
                conn = netapp_utils.create_sf_connection(module=self.module, raise_on_connection_error=True, port=port, timeout=input_params['timeout'])
                if role == 'node':
                    self.sfe_node = conn
                else:
                    self.sfe_cluster = conn
            except netapp_utils.solidfire.common.ApiConnectionError as exc:
                if str(exc) == "Bad Credentials":
                    msg = 'Most likely the cluster is already created.'
                    msg += '  Make sure to use valid %s credentials for username and password.' % 'node' if port == 442 else 'cluster'
                    msg += '  Even though credentials are not required for the first create, they are needed to check whether the cluster already exists.'
                    msg += '  Cluster reported: %s' % repr(exc)
                else:
                    msg = 'Failed to create connection: %s' % repr(exc)
                self.module.fail_json(msg=msg)
            except Exception as exc:
                self.module.fail_json(msg='Failed to connect: %s' % repr(exc))

        self.elementsw_helper = NaElementSWModule(self.sfe_cluster)

        # add telemetry attributes
        if self.attributes is not None:
            self.attributes.update(self.elementsw_helper.set_element_attributes(source='na_elementsw_cluster'))
        else:
            self.attributes = self.elementsw_helper.set_element_attributes(source='na_elementsw_cluster')

    def get_node_cluster_info(self):
        """
        Get Cluster Info - using node API
        """
        try:
            info = self.sfe_node.get_config()
            self.debug.append(repr(info.config.cluster))
            return info.config.cluster
        except Exception as exc:
            self.debug.append("port: %s, %s" % (str(self.sfe_node._port), repr(exc)))
            return None

    def check_cluster_exists(self):
        """
        validate if cluster exists with list of nodes
        error out if something is found but with different nodes
        return a tuple (found, info)
            found is True if found, False if not found
        """
        info = self.get_node_cluster_info()
        if info is None:
            return False
        ensemble = getattr(info, 'ensemble', None)
        if not ensemble:
            return False
        # format is 'id:IP'
        nodes = [x.split(':', 1)[1] for x in ensemble]
        current_ensemble_nodes = set(nodes) if ensemble else set()
        requested_nodes = set(self.nodes) if self.nodes else set()
        extra_ensemble_nodes = current_ensemble_nodes - requested_nodes
        # TODO: the cluster may have more nodes than what is reported in ensemble:
        # nodes_not_in_ensemble = requested_nodes - current_ensemble_nodes
        # So it's OK to find some missing nodes, but not very deterministic.
        # eg some kind of backup nodes could be in nodes_not_in_ensemble.
        if extra_ensemble_nodes and self.fail_if_cluster_already_exists_with_larger_ensemble:
            msg = 'Error: found existing cluster with more nodes in ensemble.  Cluster: %s, extra nodes: %s' %\
                  (getattr(info, 'cluster', 'not found'), extra_ensemble_nodes)
            msg += '.  Cluster info: %s' % repr(info)
            self.module.fail_json(msg=msg)
        if extra_ensemble_nodes:
            self.debug.append("Extra ensemble nodes: %s" % extra_ensemble_nodes)
        nodes_not_in_ensemble = requested_nodes - current_ensemble_nodes
        if nodes_not_in_ensemble:
            self.debug.append("Extra requested nodes not in ensemble: %s" % nodes_not_in_ensemble)
        return True

    def create_cluster_api(self, options):
        ''' Call send_request directly rather than using the SDK if new fields are present
            The new SDK will support these in version 1.17 (Nov or Feb)
        '''
        extra_options = ['enableSoftwareEncryptionAtRest', 'orderNumber', 'serialNumber']
        if not any((item in options for item in extra_options)):
            # use SDK
            return self.sfe_cluster.create_cluster(**options)

        # call directly the API as the SDK is not updated yet
        params = {
            "mvip": options['mvip'],
            "svip": options['svip'],
            "repCount": options['rep_count'],
            "username": options['username'],
            "password": options['password'],
            "nodes": options['nodes'],
        }
        if options['accept_eula'] is not None:
            params["acceptEula"] = options['accept_eula']
        if options['attributes'] is not None:
            params["attributes"] = options['attributes']
        for option in extra_options:
            if options.get(option):
                params[option] = options[option]

        # There is no adaptor.
        return self.sfe_cluster.send_request(
            'CreateCluster',
            netapp_utils.solidfire.CreateClusterResult,
            params,
            since=None
        )

    def create_cluster(self):
        """
        Create Cluster
        """
        options = {
            'mvip': self.management_virtual_ip,
            'svip': self.storage_virtual_ip,
            'rep_count': self.replica_count,
            'accept_eula': self.accept_eula,
            'nodes': self.nodes,
            'attributes': self.attributes,
            'username': self.cluster_admin_username,
            'password': self.cluster_admin_password
        }
        if self.encryption is not None:
            options['enableSoftwareEncryptionAtRest'] = self.encryption
        if self.order_number is not None:
            options['orderNumber'] = self.order_number
        if self.serial_number is not None:
            options['serialNumber'] = self.serial_number

        return_msg = 'created'
        try:
            # does not work as node even though documentation says otherwise
            # running as node, this error is reported: 500 xUnknownAPIMethod  method=CreateCluster
            self.create_cluster_api(options)
        except netapp_utils.solidfire.common.ApiServerError as exc:
            # not sure how this can happen, but the cluster may already exists
            if 'xClusterAlreadyCreated' not in str(exc.message):
                self.module.fail_json(msg='Error creating cluster %s' % to_native(exc), exception=traceback.format_exc())
            return_msg = 'already_exists: %s' % str(exc.message)
        except Exception as exc:
            self.module.fail_json(msg='Error creating cluster %s' % to_native(exc), exception=traceback.format_exc())
        return return_msg

    def apply(self):
        """
        Check connection and initialize node with cluster ownership
        """
        changed = False
        result_message = None
        exists = self.check_cluster_exists()
        if exists:
            result_message = "cluster already exists"
        else:
            changed = True
            if not self.module.check_mode:
                result_message = self.create_cluster()
                if result_message.startswith('already_exists:'):
                    changed = False
        self.module.exit_json(changed=changed, msg=result_message, debug=self.debug)


def main():
    """
    Main function
    """
    na_elementsw_cluster = ElementSWCluster()
    na_elementsw_cluster.apply()


if __name__ == '__main__':
    main()