Migrated to netapp.elementsw
This commit is contained in:
parent
0a9205b7c0
commit
cae9d633b0
26 changed files with 0 additions and 6935 deletions
|
@ -1,378 +0,0 @@
|
|||
#!/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 Access Group Manager
|
||||
"""
|
||||
|
||||
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_access_group
|
||||
|
||||
short_description: NetApp Element Software Manage Access Groups
|
||||
extends_documentation_fragment:
|
||||
- netapp.solidfire
|
||||
version_added: '2.7'
|
||||
author: NetApp Ansible Team (@carchi8py) <ng-ansibleteam@netapp.com>
|
||||
description:
|
||||
- Create, destroy, or update access groups on Element Software Cluster.
|
||||
|
||||
options:
|
||||
|
||||
state:
|
||||
description:
|
||||
- Whether the specified access group should exist or not.
|
||||
required: true
|
||||
choices: ['present', 'absent']
|
||||
|
||||
from_name:
|
||||
description:
|
||||
- ID or Name of the access group to rename.
|
||||
- Required to create a new access group called 'name' by renaming 'from_name'.
|
||||
version_added: '2.8'
|
||||
|
||||
name:
|
||||
description:
|
||||
- Name for the access group for create, modify and delete operations.
|
||||
required: True
|
||||
aliases:
|
||||
- src_access_group_id
|
||||
|
||||
initiators:
|
||||
description:
|
||||
- List of initiators to include in the access group. If unspecified, the access group will start out without configured initiators.
|
||||
|
||||
volumes:
|
||||
description:
|
||||
- List of volumes to initially include in the volume access group. If unspecified, the access group will start without any volumes.
|
||||
- It accepts either volume_name or volume_id
|
||||
|
||||
account_id:
|
||||
description:
|
||||
- Account ID for the owner of this volume.
|
||||
- It accepts either account_name or account_id
|
||||
- if account_id is digit, it will consider as account_id
|
||||
- If account_id is string, it will consider as account_name
|
||||
version_added: '2.8'
|
||||
|
||||
virtual_network_id:
|
||||
description:
|
||||
- The ID of the Element SW Software Cluster Virtual Network ID to associate the access group with.
|
||||
|
||||
virtual_network_tags:
|
||||
description:
|
||||
- The ID of the VLAN Virtual Network Tag to associate the access group with.
|
||||
|
||||
attributes:
|
||||
description: List of Name/Value pairs in JSON object format.
|
||||
|
||||
'''
|
||||
|
||||
EXAMPLES = """
|
||||
- name: Create Access Group
|
||||
na_elementsw_access_group:
|
||||
hostname: "{{ elementsw_hostname }}"
|
||||
username: "{{ elementsw_username }}"
|
||||
password: "{{ elementsw_password }}"
|
||||
state: present
|
||||
name: AnsibleAccessGroup
|
||||
volumes: [7,8]
|
||||
account_id: 1
|
||||
|
||||
- name: Modify Access Group
|
||||
na_elementsw_access_group:
|
||||
hostname: "{{ elementsw_hostname }}"
|
||||
username: "{{ elementsw_username }}"
|
||||
password: "{{ elementsw_password }}"
|
||||
state: present
|
||||
name: AnsibleAccessGroup-Renamed
|
||||
account_id: 1
|
||||
attributes: {"volumes": [1,2,3], "virtual_network_id": 12345}
|
||||
|
||||
- name: Rename Access Group
|
||||
na_elementsw_access_group:
|
||||
hostname: "{{ elementsw_hostname }}"
|
||||
username: "{{ elementsw_username }}"
|
||||
password: "{{ elementsw_password }}"
|
||||
state: present
|
||||
from_name: AnsibleAccessGroup
|
||||
name: AnsibleAccessGroup-Renamed
|
||||
|
||||
- name: Delete Access Group
|
||||
na_elementsw_access_group:
|
||||
hostname: "{{ elementsw_hostname }}"
|
||||
username: "{{ elementsw_username }}"
|
||||
password: "{{ elementsw_password }}"
|
||||
state: absent
|
||||
name: 1
|
||||
"""
|
||||
|
||||
|
||||
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.module_utils.netapp as netapp_utils
|
||||
from ansible.module_utils.netapp_elementsw_module import NaElementSWModule
|
||||
|
||||
HAS_SF_SDK = netapp_utils.has_sf_sdk()
|
||||
try:
|
||||
import solidfire.common
|
||||
except ImportError:
|
||||
HAS_SF_SDK = False
|
||||
|
||||
|
||||
class ElementSWAccessGroup(object):
|
||||
"""
|
||||
Element Software Volume Access Group
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
|
||||
self.argument_spec = netapp_utils.ontap_sf_host_argument_spec()
|
||||
self.argument_spec.update(dict(
|
||||
state=dict(required=True, choices=['present', 'absent']),
|
||||
from_name=dict(required=False, type='str'),
|
||||
name=dict(required=True, aliases=["src_access_group_id"], type='str'),
|
||||
initiators=dict(required=False, type='list'),
|
||||
volumes=dict(required=False, type='list'),
|
||||
account_id=dict(required=False, type='str'),
|
||||
virtual_network_id=dict(required=False, type='list'),
|
||||
virtual_network_tags=dict(required=False, type='list'),
|
||||
attributes=dict(required=False, type='dict'),
|
||||
))
|
||||
|
||||
self.module = AnsibleModule(
|
||||
argument_spec=self.argument_spec,
|
||||
required_if=[
|
||||
('state', 'present', ['account_id'])
|
||||
],
|
||||
supports_check_mode=True
|
||||
)
|
||||
|
||||
input_params = self.module.params
|
||||
|
||||
# Set up state variables
|
||||
self.state = input_params['state']
|
||||
self.from_name = input_params['from_name']
|
||||
self.access_group_name = input_params['name']
|
||||
self.initiators = input_params['initiators']
|
||||
self.volumes = input_params['volumes']
|
||||
self.account_id = input_params['account_id']
|
||||
self.virtual_network_id = input_params['virtual_network_id']
|
||||
self.virtual_network_tags = input_params['virtual_network_tags']
|
||||
self.attributes = input_params['attributes']
|
||||
|
||||
if HAS_SF_SDK is False:
|
||||
self.module.fail_json(msg="Unable to import the SolidFire Python SDK")
|
||||
else:
|
||||
self.sfe = netapp_utils.create_sf_connection(module=self.module)
|
||||
|
||||
self.elementsw_helper = NaElementSWModule(self.sfe)
|
||||
|
||||
# add telemetry attributes
|
||||
if self.attributes is not None:
|
||||
self.attributes.update(self.elementsw_helper.set_element_attributes(source='na_elementsw_access_group'))
|
||||
else:
|
||||
self.attributes = self.elementsw_helper.set_element_attributes(source='na_elementsw_access_group')
|
||||
|
||||
def get_access_group(self, name):
|
||||
"""
|
||||
Get Access Group
|
||||
:description: Get Access Group object for a given name
|
||||
|
||||
:return: object (Group object)
|
||||
:rtype: object (Group object)
|
||||
"""
|
||||
access_groups_list = self.sfe.list_volume_access_groups()
|
||||
group_obj = None
|
||||
|
||||
for group in access_groups_list.volume_access_groups:
|
||||
# Check and get access_group object for a given name
|
||||
if str(group.volume_access_group_id) == name:
|
||||
group_obj = group
|
||||
elif group.name == name:
|
||||
group_obj = group
|
||||
|
||||
return group_obj
|
||||
|
||||
def get_account_id(self):
|
||||
# Validate account id
|
||||
# Return account_id if found, None otherwise
|
||||
try:
|
||||
account_id = self.elementsw_helper.account_exists(self.account_id)
|
||||
return account_id
|
||||
except solidfire.common.ApiServerError:
|
||||
return None
|
||||
|
||||
def get_volume_id(self):
|
||||
# Validate volume_ids
|
||||
# Return volume ids if found, fail if not found
|
||||
volume_ids = []
|
||||
for volume in self.volumes:
|
||||
volume_id = self.elementsw_helper.volume_exists(volume, self.account_id)
|
||||
if volume_id:
|
||||
volume_ids.append(volume_id)
|
||||
else:
|
||||
self.module.fail_json(msg='Specified volume %s does not exist' % volume)
|
||||
return volume_ids
|
||||
|
||||
def create_access_group(self):
|
||||
"""
|
||||
Create the Access Group
|
||||
"""
|
||||
try:
|
||||
self.sfe.create_volume_access_group(name=self.access_group_name,
|
||||
initiators=self.initiators,
|
||||
volumes=self.volumes,
|
||||
virtual_network_id=self.virtual_network_id,
|
||||
virtual_network_tags=self.virtual_network_tags,
|
||||
attributes=self.attributes)
|
||||
except Exception as e:
|
||||
self.module.fail_json(msg="Error creating volume access group %s: %s" %
|
||||
(self.access_group_name, to_native(e)), exception=traceback.format_exc())
|
||||
|
||||
def delete_access_group(self):
|
||||
"""
|
||||
Delete the Access Group
|
||||
"""
|
||||
try:
|
||||
self.sfe.delete_volume_access_group(volume_access_group_id=self.group_id)
|
||||
|
||||
except Exception as e:
|
||||
self.module.fail_json(msg="Error deleting volume access group %s: %s" %
|
||||
(self.access_group_name, to_native(e)),
|
||||
exception=traceback.format_exc())
|
||||
|
||||
def update_access_group(self):
|
||||
"""
|
||||
Update the Access Group if the access_group already exists
|
||||
"""
|
||||
try:
|
||||
self.sfe.modify_volume_access_group(volume_access_group_id=self.group_id,
|
||||
virtual_network_id=self.virtual_network_id,
|
||||
virtual_network_tags=self.virtual_network_tags,
|
||||
initiators=self.initiators,
|
||||
volumes=self.volumes,
|
||||
attributes=self.attributes)
|
||||
except Exception as e:
|
||||
self.module.fail_json(msg="Error updating volume access group %s: %s" %
|
||||
(self.access_group_name, to_native(e)), exception=traceback.format_exc())
|
||||
|
||||
def rename_access_group(self):
|
||||
"""
|
||||
Rename the Access Group to the new name
|
||||
"""
|
||||
try:
|
||||
self.sfe.modify_volume_access_group(volume_access_group_id=self.from_group_id,
|
||||
virtual_network_id=self.virtual_network_id,
|
||||
virtual_network_tags=self.virtual_network_tags,
|
||||
name=self.access_group_name,
|
||||
initiators=self.initiators,
|
||||
volumes=self.volumes,
|
||||
attributes=self.attributes)
|
||||
except Exception as e:
|
||||
self.module.fail_json(msg="Error updating volume access group %s: %s" %
|
||||
(self.from_name, to_native(e)), exception=traceback.format_exc())
|
||||
|
||||
def apply(self):
|
||||
"""
|
||||
Process the access group operation on the Element Software Cluster
|
||||
"""
|
||||
changed = False
|
||||
update_group = False
|
||||
|
||||
input_account_id = self.account_id
|
||||
if self.account_id is not None:
|
||||
self.account_id = self.get_account_id()
|
||||
if self.state == 'present' and self.volumes is not None:
|
||||
if self.account_id:
|
||||
self.volumes = self.get_volume_id()
|
||||
else:
|
||||
self.module.fail_json(msg='Error: Specified account id "%s" does not exist.' % str(input_account_id))
|
||||
|
||||
group_detail = self.get_access_group(self.access_group_name)
|
||||
|
||||
if group_detail is not None:
|
||||
# If access group found
|
||||
self.group_id = group_detail.volume_access_group_id
|
||||
|
||||
if self.state == "absent":
|
||||
self.delete_access_group()
|
||||
changed = True
|
||||
else:
|
||||
# If state - present, check for any parameter of existing group needs modification.
|
||||
if self.volumes is not None and len(self.volumes) > 0:
|
||||
# Compare the volume list
|
||||
if not group_detail.volumes:
|
||||
# If access group does not have any volume attached
|
||||
update_group = True
|
||||
changed = True
|
||||
else:
|
||||
for volumeID in group_detail.volumes:
|
||||
if volumeID not in self.volumes:
|
||||
update_group = True
|
||||
changed = True
|
||||
break
|
||||
|
||||
elif self.initiators is not None and group_detail.initiators != self.initiators:
|
||||
update_group = True
|
||||
changed = True
|
||||
|
||||
elif self.virtual_network_id is not None or self.virtual_network_tags is not None:
|
||||
update_group = True
|
||||
changed = True
|
||||
|
||||
if update_group:
|
||||
self.update_access_group()
|
||||
|
||||
else:
|
||||
# access_group does not exist
|
||||
if self.state == "present" and self.from_name is not None:
|
||||
group_detail = self.get_access_group(self.from_name)
|
||||
if group_detail is not None:
|
||||
# If resource pointed by from_name exists, rename the access_group to name
|
||||
self.from_group_id = group_detail.volume_access_group_id
|
||||
self.rename_access_group()
|
||||
changed = True
|
||||
else:
|
||||
# If resource pointed by from_name does not exists, error out
|
||||
self.module.fail_json(msg="Resource does not exist : %s" % self.from_name)
|
||||
elif self.state == "present":
|
||||
# If from_name is not defined, Create from scratch.
|
||||
self.create_access_group()
|
||||
changed = True
|
||||
|
||||
self.module.exit_json(changed=changed)
|
||||
|
||||
|
||||
def main():
|
||||
"""
|
||||
Main function
|
||||
"""
|
||||
na_elementsw_access_group = ElementSWAccessGroup()
|
||||
na_elementsw_access_group.apply()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
|
@ -1,333 +0,0 @@
|
|||
#!/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 Account Manager
|
||||
"""
|
||||
|
||||
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_account
|
||||
|
||||
short_description: NetApp Element Software Manage Accounts
|
||||
extends_documentation_fragment:
|
||||
- netapp.solidfire
|
||||
version_added: '2.7'
|
||||
author: NetApp Ansible Team (@carchi8py) <ng-ansibleteam@netapp.com>
|
||||
description:
|
||||
- Create, destroy, or update accounts on Element SW
|
||||
|
||||
options:
|
||||
|
||||
state:
|
||||
description:
|
||||
- Whether the specified account should exist or not.
|
||||
required: true
|
||||
choices: ['present', 'absent']
|
||||
|
||||
element_username:
|
||||
description:
|
||||
- Unique username for this account. (May be 1 to 64 characters in length).
|
||||
required: true
|
||||
aliases:
|
||||
- account_id
|
||||
|
||||
from_name:
|
||||
description:
|
||||
- ID or Name of the account to rename.
|
||||
- Required to create an account called 'element_username' by renaming 'from_name'.
|
||||
version_added: '2.8'
|
||||
|
||||
initiator_secret:
|
||||
description:
|
||||
- CHAP secret to use for the initiator. Should be 12-16 characters long and impenetrable.
|
||||
- The CHAP initiator secrets must be unique and cannot be the same as the target CHAP secret.
|
||||
- If not specified, a random secret is created.
|
||||
|
||||
target_secret:
|
||||
description:
|
||||
- CHAP secret to use for the target (mutual CHAP authentication).
|
||||
- Should be 12-16 characters long and impenetrable.
|
||||
- The CHAP target secrets must be unique and cannot be the same as the initiator CHAP secret.
|
||||
- If not specified, a random secret is created.
|
||||
|
||||
attributes:
|
||||
description: List of Name/Value pairs in JSON object format.
|
||||
|
||||
status:
|
||||
description:
|
||||
- Status of the account.
|
||||
|
||||
'''
|
||||
|
||||
EXAMPLES = """
|
||||
- name: Create Account
|
||||
na_elementsw_account:
|
||||
hostname: "{{ elementsw_hostname }}"
|
||||
username: "{{ elementsw_username }}"
|
||||
password: "{{ elementsw_password }}"
|
||||
state: present
|
||||
element_username: TenantA
|
||||
|
||||
- name: Modify Account
|
||||
na_elementsw_account:
|
||||
hostname: "{{ elementsw_hostname }}"
|
||||
username: "{{ elementsw_username }}"
|
||||
password: "{{ elementsw_password }}"
|
||||
state: present
|
||||
status: locked
|
||||
element_username: TenantA
|
||||
|
||||
- name: Rename Account
|
||||
na_elementsw_account:
|
||||
hostname: "{{ elementsw_hostname }}"
|
||||
username: "{{ elementsw_username }}"
|
||||
password: "{{ elementsw_password }}"
|
||||
state: present
|
||||
element_username: TenantA_Renamed
|
||||
from_name: TenantA
|
||||
|
||||
- name: Rename and modify Account
|
||||
na_elementsw_account:
|
||||
hostname: "{{ elementsw_hostname }}"
|
||||
username: "{{ elementsw_username }}"
|
||||
password: "{{ elementsw_password }}"
|
||||
state: present
|
||||
status: locked
|
||||
element_username: TenantA_Renamed
|
||||
from_name: TenantA
|
||||
|
||||
- name: Delete Account
|
||||
na_elementsw_account:
|
||||
hostname: "{{ elementsw_hostname }}"
|
||||
username: "{{ elementsw_username }}"
|
||||
password: "{{ elementsw_password }}"
|
||||
state: absent
|
||||
element_username: TenantA_Renamed
|
||||
"""
|
||||
|
||||
RETURN = """
|
||||
|
||||
"""
|
||||
import traceback
|
||||
|
||||
from ansible.module_utils.basic import AnsibleModule
|
||||
from ansible.module_utils._text import to_native
|
||||
import ansible.module_utils.netapp as netapp_utils
|
||||
from ansible.module_utils.netapp_elementsw_module import NaElementSWModule
|
||||
|
||||
|
||||
HAS_SF_SDK = netapp_utils.has_sf_sdk()
|
||||
|
||||
|
||||
class ElementSWAccount(object):
|
||||
"""
|
||||
Element SW Account
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
self.argument_spec = netapp_utils.ontap_sf_host_argument_spec()
|
||||
self.argument_spec.update(dict(
|
||||
state=dict(required=True, choices=['present', 'absent']),
|
||||
element_username=dict(required=True, aliases=["account_id"], type='str'),
|
||||
from_name=dict(required=False, default=None),
|
||||
initiator_secret=dict(required=False, type='str'),
|
||||
target_secret=dict(required=False, type='str'),
|
||||
attributes=dict(required=False, type='dict'),
|
||||
status=dict(required=False, type='str'),
|
||||
))
|
||||
|
||||
self.module = AnsibleModule(
|
||||
argument_spec=self.argument_spec,
|
||||
supports_check_mode=True
|
||||
)
|
||||
|
||||
params = self.module.params
|
||||
|
||||
# set up state variables
|
||||
self.state = params.get('state')
|
||||
self.element_username = params.get('element_username')
|
||||
self.from_name = params.get('from_name')
|
||||
self.initiator_secret = params.get('initiator_secret')
|
||||
self.target_secret = params.get('target_secret')
|
||||
self.attributes = params.get('attributes')
|
||||
self.status = params.get('status')
|
||||
|
||||
if HAS_SF_SDK is False:
|
||||
self.module.fail_json(msg="Unable to import the Element SW Python SDK")
|
||||
else:
|
||||
self.sfe = netapp_utils.create_sf_connection(module=self.module)
|
||||
|
||||
self.elementsw_helper = NaElementSWModule(self.sfe)
|
||||
|
||||
# add telemetry attributes
|
||||
if self.attributes is not None:
|
||||
self.attributes.update(self.elementsw_helper.set_element_attributes(source='na_elementsw_account'))
|
||||
else:
|
||||
self.attributes = self.elementsw_helper.set_element_attributes(source='na_elementsw_account')
|
||||
|
||||
def get_account(self, username):
|
||||
"""
|
||||
Get Account
|
||||
:description: Get Account object from account id or name
|
||||
|
||||
:return: Details about the account. None if not found.
|
||||
:rtype: object (Account object)
|
||||
"""
|
||||
|
||||
account_list = self.sfe.list_accounts()
|
||||
|
||||
for account in account_list.accounts:
|
||||
# Check and get account object for a given name
|
||||
if str(account.account_id) == username:
|
||||
return account
|
||||
elif account.username == username:
|
||||
return account
|
||||
return None
|
||||
|
||||
def create_account(self):
|
||||
"""
|
||||
Create the Account
|
||||
"""
|
||||
try:
|
||||
self.sfe.add_account(username=self.element_username,
|
||||
initiator_secret=self.initiator_secret,
|
||||
target_secret=self.target_secret,
|
||||
attributes=self.attributes)
|
||||
except Exception as e:
|
||||
self.module.fail_json(msg='Error creating account %s: %s' % (self.element_username, to_native(e)),
|
||||
exception=traceback.format_exc())
|
||||
|
||||
def delete_account(self):
|
||||
"""
|
||||
Delete the Account
|
||||
"""
|
||||
try:
|
||||
self.sfe.remove_account(account_id=self.account_id)
|
||||
|
||||
except Exception as e:
|
||||
self.module.fail_json(msg='Error deleting account %s: %s' % (self.account_id, to_native(e)),
|
||||
exception=traceback.format_exc())
|
||||
|
||||
def rename_account(self):
|
||||
"""
|
||||
Rename the Account
|
||||
"""
|
||||
try:
|
||||
self.sfe.modify_account(account_id=self.account_id,
|
||||
username=self.element_username,
|
||||
status=self.status,
|
||||
initiator_secret=self.initiator_secret,
|
||||
target_secret=self.target_secret,
|
||||
attributes=self.attributes)
|
||||
|
||||
except Exception as e:
|
||||
self.module.fail_json(msg='Error renaming account %s: %s' % (self.account_id, to_native(e)),
|
||||
exception=traceback.format_exc())
|
||||
|
||||
def update_account(self):
|
||||
"""
|
||||
Update the Account if account already exists
|
||||
"""
|
||||
try:
|
||||
self.sfe.modify_account(account_id=self.account_id,
|
||||
status=self.status,
|
||||
initiator_secret=self.initiator_secret,
|
||||
target_secret=self.target_secret,
|
||||
attributes=self.attributes)
|
||||
|
||||
except Exception as e:
|
||||
self.module.fail_json(msg='Error updating account %s: %s' % (self.account_id, to_native(e)),
|
||||
exception=traceback.format_exc())
|
||||
|
||||
def apply(self):
|
||||
"""
|
||||
Process the account operation on the Element OS Cluster
|
||||
"""
|
||||
changed = False
|
||||
update_account = False
|
||||
account_detail = self.get_account(self.element_username)
|
||||
|
||||
if account_detail is None and self.state == 'present':
|
||||
changed = True
|
||||
|
||||
elif account_detail is not None:
|
||||
# If account found
|
||||
self.account_id = account_detail.account_id
|
||||
|
||||
if self.state == 'absent':
|
||||
changed = True
|
||||
else:
|
||||
# If state - present, check for any parameter of existing account needs modification.
|
||||
if account_detail.username is not None and self.element_username is not None and \
|
||||
account_detail.username != self.element_username:
|
||||
update_account = True
|
||||
changed = True
|
||||
elif account_detail.status is not None and self.status is not None \
|
||||
and account_detail.status != self.status:
|
||||
update_account = True
|
||||
changed = True
|
||||
|
||||
elif account_detail.initiator_secret is not None and self.initiator_secret is not None \
|
||||
and account_detail.initiator_secret != self.initiator_secret:
|
||||
update_account = True
|
||||
changed = True
|
||||
|
||||
elif account_detail.target_secret is not None and self.target_secret is not None \
|
||||
and account_detail.target_secret != self.target_secret:
|
||||
update_account = True
|
||||
changed = True
|
||||
|
||||
elif account_detail.attributes is not None and self.attributes is not None \
|
||||
and account_detail.attributes != self.attributes:
|
||||
update_account = True
|
||||
changed = True
|
||||
if changed:
|
||||
if self.module.check_mode:
|
||||
# Skipping the changes
|
||||
pass
|
||||
else:
|
||||
if self.state == 'present':
|
||||
if update_account:
|
||||
self.update_account()
|
||||
else:
|
||||
if self.from_name is not None:
|
||||
# If from_name is defined
|
||||
account_exists = self.get_account(self.from_name)
|
||||
if account_exists is not None:
|
||||
# If resource pointed by from_name exists, rename the account to name
|
||||
self.account_id = account_exists.account_id
|
||||
self.rename_account()
|
||||
else:
|
||||
# If resource pointed by from_name does not exists, error out
|
||||
self.module.fail_json(msg="Resource does not exist : %s" % self.from_name)
|
||||
else:
|
||||
# If from_name is not defined, create from scratch.
|
||||
self.create_account()
|
||||
elif self.state == 'absent':
|
||||
self.delete_account()
|
||||
|
||||
self.module.exit_json(changed=changed)
|
||||
|
||||
|
||||
def main():
|
||||
"""
|
||||
Main function
|
||||
"""
|
||||
na_elementsw_account = ElementSWAccount()
|
||||
na_elementsw_account.apply()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
|
@ -1,229 +0,0 @@
|
|||
#!/usr/bin/python
|
||||
|
||||
# (c) 2017, NetApp, Inc
|
||||
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
|
||||
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_admin_users
|
||||
|
||||
short_description: NetApp Element Software Manage Admin Users
|
||||
extends_documentation_fragment:
|
||||
- netapp.solidfire
|
||||
version_added: '2.7'
|
||||
author: NetApp Ansible Team (@carchi8py) <ng-ansibleteam@netapp.com>
|
||||
description:
|
||||
- Create, destroy, or update admin users on SolidFire
|
||||
|
||||
options:
|
||||
|
||||
state:
|
||||
description:
|
||||
- Whether the specified account should exist or not.
|
||||
required: true
|
||||
choices: ['present', 'absent']
|
||||
|
||||
element_username:
|
||||
description:
|
||||
- Unique username for this account. (May be 1 to 64 characters in length).
|
||||
required: true
|
||||
|
||||
element_password:
|
||||
description:
|
||||
- The password for the new admin account. Setting the password attribute will always reset your password, even if the password is the same
|
||||
|
||||
acceptEula:
|
||||
description:
|
||||
- Boolean, true for accepting Eula, False Eula
|
||||
type: bool
|
||||
|
||||
access:
|
||||
description:
|
||||
- A list of type the admin has access to
|
||||
'''
|
||||
|
||||
EXAMPLES = """
|
||||
- name: Add admin user
|
||||
na_elementsw_admin_users:
|
||||
state: present
|
||||
username: "{{ admin_user_name }}"
|
||||
password: "{{ admin_password }}"
|
||||
hostname: "{{ hostname }}"
|
||||
element_username: carchi8py
|
||||
element_password: carchi8py
|
||||
acceptEula: True
|
||||
access: accounts,drives
|
||||
|
||||
- name: modify admin user
|
||||
na_elementsw_admin_users:
|
||||
state: present
|
||||
username: "{{ admin_user_name }}"
|
||||
password: "{{ admin_password }}"
|
||||
hostname: "{{ hostname }}"
|
||||
element_username: carchi8py
|
||||
element_password: carchi8py12
|
||||
acceptEula: True
|
||||
access: accounts,drives,nodes
|
||||
|
||||
- name: delete admin user
|
||||
na_elementsw_admin_users:
|
||||
state: absent
|
||||
username: "{{ admin_user_name }}"
|
||||
password: "{{ admin_password }}"
|
||||
hostname: "{{ hostname }}"
|
||||
element_username: carchi8py
|
||||
"""
|
||||
|
||||
RETURN = """
|
||||
|
||||
"""
|
||||
|
||||
import traceback
|
||||
|
||||
from ansible.module_utils.basic import AnsibleModule
|
||||
from ansible.module_utils._text import to_native
|
||||
import ansible.module_utils.netapp as netapp_utils
|
||||
from ansible.module_utils.netapp_elementsw_module import NaElementSWModule
|
||||
|
||||
|
||||
HAS_SF_SDK = netapp_utils.has_sf_sdk()
|
||||
|
||||
|
||||
class NetAppElementSWAdminUser(object):
|
||||
"""
|
||||
Class to set, modify and delete admin users on ElementSW box
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
"""
|
||||
Initialize the NetAppElementSWAdminUser class.
|
||||
"""
|
||||
self.argument_spec = netapp_utils.ontap_sf_host_argument_spec()
|
||||
self.argument_spec.update(dict(
|
||||
state=dict(required=True, choices=['present', 'absent']),
|
||||
element_username=dict(required=True, type='str'),
|
||||
element_password=dict(required=False, type='str', no_log=True),
|
||||
acceptEula=dict(required=False, type='bool'),
|
||||
access=dict(required=False, type='list')
|
||||
))
|
||||
|
||||
self.module = AnsibleModule(
|
||||
argument_spec=self.argument_spec,
|
||||
supports_check_mode=True
|
||||
)
|
||||
|
||||
param = self.module.params
|
||||
# set up state variables
|
||||
self.state = param['state']
|
||||
self.element_username = param['element_username']
|
||||
self.element_password = param['element_password']
|
||||
self.acceptEula = param['acceptEula']
|
||||
self.access = param['access']
|
||||
|
||||
if HAS_SF_SDK is False:
|
||||
self.module.fail_json(msg="Unable to import the SolidFire Python SDK")
|
||||
else:
|
||||
self.sfe = netapp_utils.create_sf_connection(module=self.module)
|
||||
|
||||
self.elementsw_helper = NaElementSWModule(self.sfe)
|
||||
|
||||
# add telemetry attributes
|
||||
self.attributes = self.elementsw_helper.set_element_attributes(source='na_elementsw_admin_users')
|
||||
|
||||
def does_admin_user_exist(self):
|
||||
"""
|
||||
Checks to see if an admin user exists or not
|
||||
:return: True if the user exist, False if it dose not exist
|
||||
"""
|
||||
admins_list = self.sfe.list_cluster_admins()
|
||||
for admin in admins_list.cluster_admins:
|
||||
if admin.username == self.element_username:
|
||||
return True
|
||||
return False
|
||||
|
||||
def get_admin_user(self):
|
||||
"""
|
||||
Get the admin user object
|
||||
:return: the admin user object
|
||||
"""
|
||||
admins_list = self.sfe.list_cluster_admins()
|
||||
for admin in admins_list.cluster_admins:
|
||||
if admin.username == self.element_username:
|
||||
return admin
|
||||
return None
|
||||
|
||||
def modify_admin_user(self):
|
||||
"""
|
||||
Modify a admin user. If a password is set the user will be modified as there is no way to
|
||||
compare a new password with an existing one
|
||||
:return: if a user was modified or not
|
||||
"""
|
||||
changed = False
|
||||
admin_user = self.get_admin_user()
|
||||
if self.access is not None and len(self.access) > 0:
|
||||
for access in self.access:
|
||||
if access not in admin_user.access:
|
||||
changed = True
|
||||
if changed:
|
||||
self.sfe.modify_cluster_admin(cluster_admin_id=admin_user.cluster_admin_id,
|
||||
access=self.access,
|
||||
password=self.element_password,
|
||||
attributes=self.attributes)
|
||||
|
||||
return changed
|
||||
|
||||
def add_admin_user(self):
|
||||
"""
|
||||
Add's a new admin user to the element cluster
|
||||
:return: nothing
|
||||
"""
|
||||
self.sfe.add_cluster_admin(username=self.element_username,
|
||||
password=self.element_password,
|
||||
access=self.access,
|
||||
accept_eula=self.acceptEula,
|
||||
attributes=self.attributes)
|
||||
|
||||
def delete_admin_user(self):
|
||||
"""
|
||||
Deletes an existing admin user from the element cluster
|
||||
:return: nothing
|
||||
"""
|
||||
admin_user = self.get_admin_user()
|
||||
self.sfe.remove_cluster_admin(cluster_admin_id=admin_user.cluster_admin_id)
|
||||
|
||||
def apply(self):
|
||||
"""
|
||||
determines which method to call to set, delete or modify admin users
|
||||
:return:
|
||||
"""
|
||||
changed = False
|
||||
if self.state == "present":
|
||||
if self.does_admin_user_exist():
|
||||
changed = self.modify_admin_user()
|
||||
else:
|
||||
self.add_admin_user()
|
||||
changed = True
|
||||
else:
|
||||
if self.does_admin_user_exist():
|
||||
self.delete_admin_user()
|
||||
changed = True
|
||||
|
||||
self.module.exit_json(changed=changed)
|
||||
|
||||
|
||||
def main():
|
||||
v = NetAppElementSWAdminUser()
|
||||
v.apply()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
|
@ -1,235 +0,0 @@
|
|||
#!/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 Backup Manager
|
||||
"""
|
||||
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_backup
|
||||
|
||||
short_description: NetApp Element Software Create Backups
|
||||
extends_documentation_fragment:
|
||||
- netapp.solidfire
|
||||
version_added: '2.7'
|
||||
author: NetApp Ansible Team (@carchi8py) <ng-ansibleteam@netapp.com>
|
||||
description:
|
||||
- Create backup
|
||||
|
||||
options:
|
||||
|
||||
src_volume_id:
|
||||
description:
|
||||
- ID of the backup source volume.
|
||||
required: true
|
||||
aliases:
|
||||
- volume_id
|
||||
|
||||
dest_hostname:
|
||||
description:
|
||||
- hostname for the backup source cluster
|
||||
- will be set equal to hostname if not specified
|
||||
required: false
|
||||
|
||||
dest_username:
|
||||
description:
|
||||
- username for the backup destination cluster
|
||||
- will be set equal to username if not specified
|
||||
required: false
|
||||
|
||||
dest_password:
|
||||
description:
|
||||
- password for the backup destination cluster
|
||||
- will be set equal to password if not specified
|
||||
required: false
|
||||
|
||||
dest_volume_id:
|
||||
description:
|
||||
- ID of the backup destination volume
|
||||
required: true
|
||||
|
||||
format:
|
||||
description:
|
||||
- Backup format to use
|
||||
choices: ['native','uncompressed']
|
||||
required: false
|
||||
default: 'native'
|
||||
|
||||
script:
|
||||
description:
|
||||
- the backup script to be executed
|
||||
required: false
|
||||
|
||||
script_parameters:
|
||||
description:
|
||||
- the backup script parameters
|
||||
required: false
|
||||
|
||||
'''
|
||||
|
||||
EXAMPLES = """
|
||||
na_elementsw_backup:
|
||||
hostname: "{{ source_cluster_hostname }}"
|
||||
username: "{{ source_cluster_username }}"
|
||||
password: "{{ source_cluster_password }}"
|
||||
src_volume_id: 1
|
||||
dest_hostname: "{{ destination_cluster_hostname }}"
|
||||
dest_username: "{{ destination_cluster_username }}"
|
||||
dest_password: "{{ destination_cluster_password }}"
|
||||
dest_volume_id: 3
|
||||
format: native
|
||||
"""
|
||||
|
||||
RETURN = """
|
||||
|
||||
"""
|
||||
|
||||
from ansible.module_utils.basic import AnsibleModule
|
||||
from ansible.module_utils._text import to_native
|
||||
import ansible.module_utils.netapp as netapp_utils
|
||||
from ansible.module_utils.netapp_elementsw_module import NaElementSWModule
|
||||
import time
|
||||
|
||||
HAS_SF_SDK = netapp_utils.has_sf_sdk()
|
||||
try:
|
||||
import solidfire.common
|
||||
except ImportError:
|
||||
HAS_SF_SDK = False
|
||||
|
||||
|
||||
class ElementSWBackup(object):
|
||||
''' class to handle backup operations '''
|
||||
|
||||
def __init__(self):
|
||||
"""
|
||||
Setup Ansible parameters and SolidFire connection
|
||||
"""
|
||||
self.argument_spec = netapp_utils.ontap_sf_host_argument_spec()
|
||||
|
||||
self.argument_spec.update(dict(
|
||||
|
||||
src_volume_id=dict(aliases=['volume_id'], required=True, type='str'),
|
||||
dest_hostname=dict(required=False, type='str'),
|
||||
dest_username=dict(required=False, type='str'),
|
||||
dest_password=dict(required=False, type='str', no_log=True),
|
||||
dest_volume_id=dict(required=True, type='str'),
|
||||
format=dict(required=False, choices=['native', 'uncompressed'], default='native'),
|
||||
script=dict(required=False, type='str'),
|
||||
script_parameters=dict(required=False, type='dict')
|
||||
|
||||
|
||||
))
|
||||
|
||||
self.module = AnsibleModule(
|
||||
argument_spec=self.argument_spec,
|
||||
required_together=[['script', 'script_parameters']],
|
||||
supports_check_mode=True
|
||||
)
|
||||
if HAS_SF_SDK is False:
|
||||
self.module.fail_json(msg="Unable to import the SolidFire Python SDK")
|
||||
|
||||
# If destination cluster details are not specified , set the destination to be the same as the source
|
||||
if self.module.params["dest_hostname"] is None:
|
||||
self.module.params["dest_hostname"] = self.module.params["hostname"]
|
||||
if self.module.params["dest_username"] is None:
|
||||
self.module.params["dest_username"] = self.module.params["username"]
|
||||
if self.module.params["dest_password"] is None:
|
||||
self.module.params["dest_password"] = self.module.params["password"]
|
||||
|
||||
params = self.module.params
|
||||
|
||||
# establish a connection to both source and destination elementsw clusters
|
||||
self.src_connection = netapp_utils.create_sf_connection(self.module)
|
||||
self.module.params["username"] = params["dest_username"]
|
||||
self.module.params["password"] = params["dest_password"]
|
||||
self.module.params["hostname"] = params["dest_hostname"]
|
||||
self.dest_connection = netapp_utils.create_sf_connection(self.module)
|
||||
|
||||
self.elementsw_helper = NaElementSWModule(self.src_connection)
|
||||
|
||||
# add telemetry attributes
|
||||
self.attributes = self.elementsw_helper.set_element_attributes(source='na_elementsw_backup')
|
||||
|
||||
def apply(self):
|
||||
"""
|
||||
Apply backup creation logic
|
||||
"""
|
||||
self.create_backup()
|
||||
self.module.exit_json(changed=True)
|
||||
|
||||
def create_backup(self):
|
||||
"""
|
||||
Create backup
|
||||
"""
|
||||
|
||||
# Start volume write on destination cluster
|
||||
|
||||
try:
|
||||
write_obj = self.dest_connection.start_bulk_volume_write(volume_id=self.module.params["dest_volume_id"],
|
||||
format=self.module.params["format"],
|
||||
attributes=self.attributes)
|
||||
write_key = write_obj.key
|
||||
except solidfire.common.ApiServerError as err:
|
||||
self.module.fail_json(msg="Error starting bulk write on destination cluster", exception=to_native(err))
|
||||
|
||||
# Set script parameters if not passed by user
|
||||
# These parameters are equivalent to the options used when a backup is executed via the GUI
|
||||
|
||||
if self.module.params["script"] is None and self.module.params["script_parameters"] is None:
|
||||
|
||||
self.module.params["script"] = 'bv_internal.py'
|
||||
self.module.params["script_parameters"] = {"write": {
|
||||
"mvip": self.module.params["dest_hostname"],
|
||||
"username": self.module.params["dest_username"],
|
||||
"password": self.module.params["dest_password"],
|
||||
"key": write_key,
|
||||
"endpoint": "solidfire",
|
||||
"format": self.module.params["format"]},
|
||||
"range": {"lba": 0, "blocks": 244224}}
|
||||
|
||||
# Start volume read on source cluster
|
||||
|
||||
try:
|
||||
read_obj = self.src_connection.start_bulk_volume_read(self.module.params["src_volume_id"],
|
||||
self.module.params["format"],
|
||||
script=self.module.params["script"],
|
||||
script_parameters=self.module.params["script_parameters"],
|
||||
attributes=self.attributes)
|
||||
except solidfire.common.ApiServerError as err:
|
||||
self.module.fail_json(msg="Error starting bulk read on source cluster", exception=to_native(err))
|
||||
|
||||
# Poll job status until it has completed
|
||||
# SF will automatically timeout if the job is not successful after certain amount of time
|
||||
|
||||
completed = False
|
||||
while completed is not True:
|
||||
# Sleep between polling iterations to reduce api load
|
||||
time.sleep(2)
|
||||
try:
|
||||
result = self.src_connection.get_async_result(read_obj.async_handle, True)
|
||||
except solidfire.common.ApiServerError as err:
|
||||
self.module.fail_json(msg="Unable to check backup job status", exception=to_native(err))
|
||||
|
||||
if result["status"] != 'running':
|
||||
completed = True
|
||||
if 'error' in result:
|
||||
self.module.fail_json(msg=result['error']['message'])
|
||||
|
||||
|
||||
def main():
|
||||
""" Run backup operation"""
|
||||
vol_obj = ElementSWBackup()
|
||||
vol_obj.apply()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
|
@ -1,152 +0,0 @@
|
|||
#!/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)
|
||||
|
||||
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_check_connections
|
||||
|
||||
short_description: NetApp Element Software Check connectivity to MVIP and SVIP.
|
||||
extends_documentation_fragment:
|
||||
- netapp.solidfire
|
||||
version_added: '2.7'
|
||||
author: NetApp Ansible Team (@carchi8py) <ng-ansibleteam@netapp.com>
|
||||
description:
|
||||
- Used to test the management connection to the cluster.
|
||||
- The test pings the MVIP and SVIP, and executes a simple API method to verify connectivity.
|
||||
|
||||
options:
|
||||
|
||||
skip:
|
||||
description:
|
||||
- Skip checking connection to SVIP or MVIP.
|
||||
choices: ['svip', 'mvip']
|
||||
|
||||
mvip:
|
||||
description:
|
||||
- Optionally, use to test connection of a different MVIP.
|
||||
- This is not needed to test the connection to the target cluster.
|
||||
|
||||
svip:
|
||||
description:
|
||||
- Optionally, use to test connection of a different SVIP.
|
||||
- This is not needed to test the connection to the target cluster.
|
||||
|
||||
'''
|
||||
|
||||
|
||||
EXAMPLES = """
|
||||
- name: Check connections to MVIP and SVIP
|
||||
na_elementsw_check_connections:
|
||||
hostname: "{{ solidfire_hostname }}"
|
||||
username: "{{ solidfire_username }}"
|
||||
password: "{{ solidfire_password }}"
|
||||
"""
|
||||
|
||||
RETURN = """
|
||||
|
||||
"""
|
||||
import traceback
|
||||
|
||||
from ansible.module_utils.basic import AnsibleModule
|
||||
from ansible.module_utils._text import to_native
|
||||
import ansible.module_utils.netapp as netapp_utils
|
||||
from ansible.module_utils.netapp_module import NetAppModule
|
||||
|
||||
|
||||
HAS_SF_SDK = netapp_utils.has_sf_sdk()
|
||||
|
||||
|
||||
class NaElementSWConnection(object):
|
||||
|
||||
def __init__(self):
|
||||
self.argument_spec = netapp_utils.ontap_sf_host_argument_spec()
|
||||
self.argument_spec.update(dict(
|
||||
skip=dict(required=False, type='str', default=None, choices=['mvip', 'svip']),
|
||||
mvip=dict(required=False, type='str', default=None),
|
||||
svip=dict(required=False, type='str', default=None)
|
||||
))
|
||||
|
||||
self.module = AnsibleModule(
|
||||
argument_spec=self.argument_spec,
|
||||
required_if=[
|
||||
('skip', 'svip', ['mvip']),
|
||||
('skip', 'mvip', ['svip'])
|
||||
],
|
||||
supports_check_mode=True
|
||||
)
|
||||
|
||||
self.na_helper = NetAppModule()
|
||||
self.parameters = self.module.params.copy()
|
||||
self.msg = ""
|
||||
|
||||
if HAS_SF_SDK is False:
|
||||
self.module.fail_json(msg="Unable to import the ElementSW Python SDK")
|
||||
else:
|
||||
self.elem = netapp_utils.create_sf_connection(self.module, port=442)
|
||||
|
||||
def check_mvip_connection(self):
|
||||
"""
|
||||
Check connection to MVIP
|
||||
|
||||
:return: true if connection was successful, false otherwise.
|
||||
:rtype: bool
|
||||
"""
|
||||
try:
|
||||
test = self.elem.test_connect_mvip(mvip=self.parameters['mvip'])
|
||||
# Todo - Log details about the test
|
||||
return test.details.connected
|
||||
|
||||
except Exception as e:
|
||||
self.msg += 'Error checking connection to MVIP: %s' % to_native(e)
|
||||
return False
|
||||
|
||||
def check_svip_connection(self):
|
||||
"""
|
||||
Check connection to SVIP
|
||||
|
||||
:return: true if connection was successful, false otherwise.
|
||||
:rtype: bool
|
||||
"""
|
||||
try:
|
||||
test = self.elem.test_connect_svip(svip=self.parameters['svip'])
|
||||
# Todo - Log details about the test
|
||||
return test.details.connected
|
||||
except Exception as e:
|
||||
self.msg += 'Error checking connection to SVIP: %s' % to_native(e)
|
||||
return False
|
||||
|
||||
def apply(self):
|
||||
passed = False
|
||||
if self.parameters.get('skip') is None:
|
||||
# Set failed and msg
|
||||
passed = self.check_mvip_connection()
|
||||
# check if both connections have passed
|
||||
passed &= self.check_svip_connection()
|
||||
elif self.parameters['skip'] == 'mvip':
|
||||
passed |= self.check_svip_connection()
|
||||
elif self.parameters['skip'] == 'svip':
|
||||
passed |= self.check_mvip_connection()
|
||||
if not passed:
|
||||
self.module.fail_json(msg=self.msg)
|
||||
else:
|
||||
self.module.exit_json()
|
||||
|
||||
|
||||
def main():
|
||||
connect_obj = NaElementSWConnection()
|
||||
connect_obj.apply()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
|
@ -1,222 +0,0 @@
|
|||
#!/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.solidfire
|
||||
version_added: '2.7'
|
||||
author: NetApp Ansible Team (@carchi8py) <ng-ansibleteam@netapp.com>
|
||||
description:
|
||||
- Initialize Element Software node ownership to form a cluster.
|
||||
|
||||
options:
|
||||
management_virtual_ip:
|
||||
description:
|
||||
- Floating (virtual) IP address for the cluster on the management network.
|
||||
required: true
|
||||
|
||||
storage_virtual_ip:
|
||||
description:
|
||||
- Floating (virtual) IP address for the cluster on the storage (iSCSI) network.
|
||||
required: true
|
||||
|
||||
replica_count:
|
||||
description:
|
||||
- Number of replicas of each piece of data to store in the cluster.
|
||||
default: '2'
|
||||
|
||||
cluster_admin_username:
|
||||
description:
|
||||
- Username for the cluster admin.
|
||||
- If not provided, default to login username.
|
||||
|
||||
cluster_admin_password:
|
||||
description:
|
||||
- Initial password for the cluster admin account.
|
||||
- If not provided, default to login password.
|
||||
|
||||
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
|
||||
|
||||
attributes:
|
||||
description:
|
||||
- List of name-value pairs in JSON object format.
|
||||
'''
|
||||
|
||||
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.module_utils.netapp as netapp_utils
|
||||
from ansible.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='str', 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),
|
||||
attributes=dict(required=False, type='dict', default=None)
|
||||
))
|
||||
|
||||
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.get('cluster_admin_username')
|
||||
self.cluster_admin_password = input_params.get('cluster_admin_password')
|
||||
|
||||
if HAS_SF_SDK is False:
|
||||
self.module.fail_json(msg="Unable to import the SolidFire Python SDK")
|
||||
else:
|
||||
self.sfe = netapp_utils.create_sf_connection(module=self.module)
|
||||
|
||||
self.elementsw_helper = NaElementSWModule(self.sfe)
|
||||
|
||||
# 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 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
|
||||
}
|
||||
if self.cluster_admin_username is not None:
|
||||
options['username'] = self.cluster_admin_username
|
||||
if self.cluster_admin_password is not None:
|
||||
options['password'] = self.cluster_admin_password
|
||||
try:
|
||||
self.sfe.create_cluster(**options)
|
||||
except Exception as exception_object:
|
||||
self.module.fail_json(msg='Error create cluster %s' % (to_native(exception_object)),
|
||||
exception=traceback.format_exc())
|
||||
|
||||
def check_connection(self):
|
||||
"""
|
||||
Check connections to mvip, svip address.
|
||||
:description: To test connection to given IP addressed for mvip and svip
|
||||
|
||||
:rtype: bool
|
||||
"""
|
||||
try:
|
||||
mvip_test = self.sfe.test_connect_mvip(mvip=self.management_virtual_ip)
|
||||
svip_test = self.sfe.test_connect_svip(svip=self.storage_virtual_ip)
|
||||
|
||||
if mvip_test.details.connected and svip_test.details.connected:
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
except Exception as e:
|
||||
return False
|
||||
|
||||
def apply(self):
|
||||
"""
|
||||
Check connection and initialize node with cluster ownership
|
||||
"""
|
||||
changed = False
|
||||
result_message = None
|
||||
if self.module.supports_check_mode and self.accept_eula:
|
||||
if self.check_connection():
|
||||
self.create_cluster()
|
||||
changed = True
|
||||
else:
|
||||
self.module.fail_json(msg='Error connecting mvip and svip address')
|
||||
else:
|
||||
result_message = "Skipping changes, No change requested"
|
||||
self.module.exit_json(changed=changed, msg=result_message)
|
||||
|
||||
|
||||
def main():
|
||||
"""
|
||||
Main function
|
||||
"""
|
||||
na_elementsw_cluster = ElementSWCluster()
|
||||
na_elementsw_cluster.apply()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
|
@ -1,327 +0,0 @@
|
|||
#!/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 Configure cluster
|
||||
'''
|
||||
from __future__ import absolute_import, division, print_function
|
||||
__metaclass__ = type
|
||||
|
||||
|
||||
ANSIBLE_METADATA = {'metadata_version': '1.1',
|
||||
'status': ['preview'],
|
||||
'supported_by': 'community'}
|
||||
|
||||
|
||||
DOCUMENTATION = '''
|
||||
|
||||
module: na_elementsw_cluster_config
|
||||
|
||||
short_description: Configure Element SW Cluster
|
||||
extends_documentation_fragment:
|
||||
- netapp.solidfire
|
||||
version_added: '2.8'
|
||||
author: NetApp Ansible Team (@carchi8py) <ng-ansibleteam@netapp.com>
|
||||
description:
|
||||
- Configure Element Software cluster.
|
||||
|
||||
options:
|
||||
modify_cluster_full_threshold:
|
||||
description:
|
||||
- The capacity level at which the cluster generates an event
|
||||
- Requires a stage3_block_threshold_percent or
|
||||
- max_metadata_over_provision_factor or
|
||||
- stage2_aware_threshold
|
||||
suboptions:
|
||||
stage3_block_threshold_percent:
|
||||
description:
|
||||
- The percentage below the "Error" threshold that triggers a cluster "Warning" alert
|
||||
|
||||
max_metadata_over_provision_factor:
|
||||
description:
|
||||
- The number of times metadata space can be overprovisioned relative to the amount of space available
|
||||
|
||||
stage2_aware_threshold:
|
||||
description:
|
||||
- The number of nodes of capacity remaining in the cluster before the system triggers a notification
|
||||
|
||||
encryption_at_rest:
|
||||
description:
|
||||
- enable or disable the Advanced Encryption Standard (AES) 256-bit encryption at rest on the cluster
|
||||
choices: ['present', 'absent']
|
||||
|
||||
set_ntp_info:
|
||||
description:
|
||||
- configure NTP on cluster node
|
||||
- Requires a list of one or more ntp_servers
|
||||
suboptions:
|
||||
ntp_servers:
|
||||
description:
|
||||
- list of NTP servers to add to each nodes NTP configuration
|
||||
|
||||
broadcastclient:
|
||||
type: bool
|
||||
default: False
|
||||
description:
|
||||
- Enables every node in the cluster as a broadcast client
|
||||
|
||||
enable_virtual_volumes:
|
||||
type: bool
|
||||
default: True
|
||||
description:
|
||||
- Enable the NetApp SolidFire VVols cluster feature
|
||||
'''
|
||||
|
||||
EXAMPLES = """
|
||||
|
||||
- name: Configure cluster
|
||||
tags:
|
||||
- elementsw_cluster_config
|
||||
na_elementsw_cluster_config:
|
||||
hostname: "{{ elementsw_hostname }}"
|
||||
username: "{{ elementsw_username }}"
|
||||
password: "{{ elementsw_password }}"
|
||||
modify_cluster_full_threshold:
|
||||
stage2_aware_threshold: 2
|
||||
stage3_block_threshold_percent: 10
|
||||
max_metadata_over_provision_factor: 2
|
||||
encryption_at_rest: absent
|
||||
set_ntp_info:
|
||||
broadcastclient: False
|
||||
ntp_servers:
|
||||
- 1.1.1.1
|
||||
- 2.2.2.2
|
||||
enable_virtual_volumes: True
|
||||
"""
|
||||
|
||||
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.module_utils.netapp as netapp_utils
|
||||
from ansible.module_utils.netapp_elementsw_module import NaElementSWModule
|
||||
from ansible.module_utils.netapp_module import NetAppModule
|
||||
|
||||
HAS_SF_SDK = netapp_utils.has_sf_sdk()
|
||||
|
||||
|
||||
class ElementSWClusterConfig(object):
|
||||
"""
|
||||
Element Software Configure Element SW Cluster
|
||||
"""
|
||||
def __init__(self):
|
||||
self.argument_spec = netapp_utils.ontap_sf_host_argument_spec()
|
||||
|
||||
self.argument_spec.update(dict(
|
||||
modify_cluster_full_threshold=dict(
|
||||
type='dict',
|
||||
options=dict(
|
||||
stage2_aware_threshold=dict(type='int', default=None),
|
||||
stage3_block_threshold_percent=dict(type='int', default=None),
|
||||
max_metadata_over_provision_factor=dict(type='int', default=None)
|
||||
)
|
||||
),
|
||||
encryption_at_rest=dict(type='str', choices=['present', 'absent']),
|
||||
set_ntp_info=dict(
|
||||
type='dict',
|
||||
options=dict(
|
||||
broadcastclient=dict(type='bool', default=False),
|
||||
ntp_servers=dict(type='list')
|
||||
)
|
||||
),
|
||||
enable_virtual_volumes=dict(type='bool', default=True)
|
||||
))
|
||||
|
||||
self.module = AnsibleModule(
|
||||
argument_spec=self.argument_spec,
|
||||
supports_check_mode=True
|
||||
)
|
||||
|
||||
self.na_helper = NetAppModule()
|
||||
self.parameters = self.na_helper.set_parameters(self.module.params)
|
||||
|
||||
if HAS_SF_SDK is False:
|
||||
self.module.fail_json(msg="Unable to import the SolidFire Python SDK")
|
||||
else:
|
||||
self.sfe = netapp_utils.create_sf_connection(module=self.module)
|
||||
|
||||
def get_ntp_details(self):
|
||||
"""
|
||||
get ntp info
|
||||
"""
|
||||
# Get ntp details
|
||||
ntp_details = self.sfe.get_ntp_info()
|
||||
return ntp_details
|
||||
|
||||
def cmp(self, provided_ntp_servers, existing_ntp_servers):
|
||||
# As python3 doesn't have default cmp function, defining manually to provide same functionality.
|
||||
return (provided_ntp_servers > existing_ntp_servers) - (provided_ntp_servers < existing_ntp_servers)
|
||||
|
||||
def get_cluster_details(self):
|
||||
"""
|
||||
get cluster info
|
||||
"""
|
||||
cluster_details = self.sfe.get_cluster_info()
|
||||
return cluster_details
|
||||
|
||||
def get_vvols_status(self):
|
||||
"""
|
||||
get vvols status
|
||||
"""
|
||||
feature_status = self.sfe.get_feature_status(feature='vvols')
|
||||
if feature_status is not None:
|
||||
return feature_status.features[0].enabled
|
||||
return None
|
||||
|
||||
def get_cluster_full_threshold_status(self):
|
||||
"""
|
||||
get cluster full threshold
|
||||
"""
|
||||
cluster_full_threshold_status = self.sfe.get_cluster_full_threshold()
|
||||
return cluster_full_threshold_status
|
||||
|
||||
def setup_ntp_info(self, servers, broadcastclient=None):
|
||||
"""
|
||||
configure ntp
|
||||
"""
|
||||
# Set ntp servers
|
||||
try:
|
||||
self.sfe.set_ntp_info(servers, broadcastclient)
|
||||
except Exception as exception_object:
|
||||
self.module.fail_json(msg='Error configuring ntp %s' % (to_native(exception_object)),
|
||||
exception=traceback.format_exc())
|
||||
|
||||
def set_encryption_at_rest(self, state=None):
|
||||
"""
|
||||
enable/disable encryption at rest
|
||||
"""
|
||||
try:
|
||||
if state == 'present':
|
||||
encryption_state = 'enable'
|
||||
self.sfe.enable_encryption_at_rest()
|
||||
elif state == 'absent':
|
||||
encryption_state = 'disable'
|
||||
self.sfe.disable_encryption_at_rest()
|
||||
except Exception as exception_object:
|
||||
self.module.fail_json(msg='Failed to %s rest encryption %s' % (encryption_state,
|
||||
to_native(exception_object)),
|
||||
exception=traceback.format_exc())
|
||||
|
||||
def enable_feature(self, feature):
|
||||
"""
|
||||
enable feature
|
||||
"""
|
||||
try:
|
||||
self.sfe.enable_feature(feature=feature)
|
||||
except Exception as exception_object:
|
||||
self.module.fail_json(msg='Error enabling %s %s' % (feature, to_native(exception_object)),
|
||||
exception=traceback.format_exc())
|
||||
|
||||
def set_cluster_full_threshold(self, stage2_aware_threshold=None,
|
||||
stage3_block_threshold_percent=None,
|
||||
max_metadata_over_provision_factor=None):
|
||||
"""
|
||||
modify cluster full threshold
|
||||
"""
|
||||
try:
|
||||
self.sfe.modify_cluster_full_threshold(stage2_aware_threshold=stage2_aware_threshold,
|
||||
stage3_block_threshold_percent=stage3_block_threshold_percent,
|
||||
max_metadata_over_provision_factor=max_metadata_over_provision_factor)
|
||||
except Exception as exception_object:
|
||||
self.module.fail_json(msg='Failed to modify cluster full threshold %s' % (to_native(exception_object)),
|
||||
exception=traceback.format_exc())
|
||||
|
||||
def apply(self):
|
||||
"""
|
||||
Cluster configuration
|
||||
"""
|
||||
changed = False
|
||||
result_message = None
|
||||
|
||||
if self.parameters.get('modify_cluster_full_threshold') is not None:
|
||||
# get cluster full threshold
|
||||
cluster_full_threshold_details = self.get_cluster_full_threshold_status()
|
||||
# maxMetadataOverProvisionFactor
|
||||
current_mmopf = cluster_full_threshold_details.max_metadata_over_provision_factor
|
||||
# stage3BlockThresholdPercent
|
||||
current_s3btp = cluster_full_threshold_details.stage3_block_threshold_percent
|
||||
# stage2AwareThreshold
|
||||
current_s2at = cluster_full_threshold_details.stage2_aware_threshold
|
||||
|
||||
# is cluster full threshold state change required?
|
||||
if self.parameters.get("modify_cluster_full_threshold")['max_metadata_over_provision_factor'] is not None and \
|
||||
current_mmopf != self.parameters['modify_cluster_full_threshold']['max_metadata_over_provision_factor'] or \
|
||||
self.parameters.get("modify_cluster_full_threshold")['stage3_block_threshold_percent'] is not None and \
|
||||
current_s3btp != self.parameters['modify_cluster_full_threshold']['stage3_block_threshold_percent'] or \
|
||||
self.parameters.get("modify_cluster_full_threshold")['stage2_aware_threshold'] is not None and \
|
||||
current_s2at != self.parameters['modify_cluster_full_threshold']['stage2_aware_threshold']:
|
||||
changed = True
|
||||
self.set_cluster_full_threshold(self.parameters['modify_cluster_full_threshold']['stage2_aware_threshold'],
|
||||
self.parameters['modify_cluster_full_threshold']['stage3_block_threshold_percent'],
|
||||
self.parameters['modify_cluster_full_threshold']['max_metadata_over_provision_factor'])
|
||||
|
||||
if self.parameters.get('encryption_at_rest') is not None:
|
||||
# get all cluster info
|
||||
cluster_info = self.get_cluster_details()
|
||||
# register rest state
|
||||
current_encryption_at_rest_state = cluster_info.cluster_info.encryption_at_rest_state
|
||||
|
||||
# is encryption state change required?
|
||||
if current_encryption_at_rest_state == 'disabled' and self.parameters['encryption_at_rest'] == 'present' or \
|
||||
current_encryption_at_rest_state == 'enabled' and self.parameters['encryption_at_rest'] == 'absent':
|
||||
changed = True
|
||||
self.set_encryption_at_rest(self.parameters['encryption_at_rest'])
|
||||
|
||||
if self.parameters.get('set_ntp_info') is not None:
|
||||
# get all ntp details
|
||||
ntp_details = self.get_ntp_details()
|
||||
# register list of ntp servers
|
||||
ntp_servers = ntp_details.servers
|
||||
# broadcastclient
|
||||
broadcast_client = ntp_details.broadcastclient
|
||||
|
||||
# has either the broadcastclient or the ntp server list changed?
|
||||
|
||||
if self.parameters.get('set_ntp_info')['broadcastclient'] != broadcast_client or \
|
||||
self.cmp(self.parameters.get('set_ntp_info')['ntp_servers'], ntp_servers) != 0:
|
||||
changed = True
|
||||
self.setup_ntp_info(self.parameters.get('set_ntp_info')['ntp_servers'],
|
||||
self.parameters.get('set_ntp_info')['broadcastclient'])
|
||||
|
||||
if self.parameters.get('enable_virtual_volumes') is not None:
|
||||
# check vvols status
|
||||
current_vvols_status = self.get_vvols_status()
|
||||
|
||||
# has the vvols state changed?
|
||||
if current_vvols_status is False and self.parameters.get('enable_virtual_volumes') is True:
|
||||
changed = True
|
||||
self.enable_feature('vvols')
|
||||
elif current_vvols_status is True and self.parameters.get('enable_virtual_volumes') is not True:
|
||||
# vvols, once enabled, cannot be disabled
|
||||
self.module.fail_json(msg='Error disabling vvols: this feature cannot be undone')
|
||||
|
||||
if self.module.check_mode is True:
|
||||
result_message = "Check mode, skipping changes"
|
||||
self.module.exit_json(changed=changed, msg=result_message)
|
||||
|
||||
|
||||
def main():
|
||||
"""
|
||||
Main function
|
||||
"""
|
||||
na_elementsw_cluster_config = ElementSWClusterConfig()
|
||||
na_elementsw_cluster_config.apply()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
|
@ -1,202 +0,0 @@
|
|||
#!/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)
|
||||
|
||||
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_pair
|
||||
|
||||
short_description: NetApp Element Software Manage Cluster Pair
|
||||
extends_documentation_fragment:
|
||||
- netapp.solidfire
|
||||
version_added: '2.7'
|
||||
author: NetApp Ansible Team (@carchi8py) <ng-ansibleteam@netapp.com>
|
||||
description:
|
||||
- Create, delete cluster pair
|
||||
|
||||
options:
|
||||
|
||||
state:
|
||||
description:
|
||||
- Whether the specified cluster pair should exist or not.
|
||||
choices: ['present', 'absent']
|
||||
default: present
|
||||
|
||||
dest_mvip:
|
||||
description:
|
||||
- Destination IP address of the cluster to be paired.
|
||||
required: true
|
||||
|
||||
dest_username:
|
||||
description:
|
||||
- Destination username for the cluster to be paired.
|
||||
- Optional if this is same as source cluster username.
|
||||
|
||||
dest_password:
|
||||
description:
|
||||
- Destination password for the cluster to be paired.
|
||||
- Optional if this is same as source cluster password.
|
||||
|
||||
'''
|
||||
|
||||
EXAMPLES = """
|
||||
- name: Create cluster pair
|
||||
na_elementsw_cluster_pair:
|
||||
hostname: "{{ src_hostname }}"
|
||||
username: "{{ src_username }}"
|
||||
password: "{{ src_password }}"
|
||||
state: present
|
||||
dest_mvip: "{{ dest_hostname }}"
|
||||
|
||||
- name: Delete cluster pair
|
||||
na_elementsw_cluster_pair:
|
||||
hostname: "{{ src_hostname }}"
|
||||
username: "{{ src_username }}"
|
||||
password: "{{ src_password }}"
|
||||
state: absent
|
||||
dest_mvip: "{{ dest_hostname }}"
|
||||
dest_username: "{{ dest_username }}"
|
||||
dest_password: "{{ dest_password }}"
|
||||
|
||||
"""
|
||||
|
||||
RETURN = """
|
||||
|
||||
"""
|
||||
|
||||
from ansible.module_utils.basic import AnsibleModule
|
||||
from ansible.module_utils._text import to_native
|
||||
import ansible.module_utils.netapp as netapp_utils
|
||||
from ansible.module_utils.netapp_elementsw_module import NaElementSWModule
|
||||
from ansible.module_utils.netapp_module import NetAppModule
|
||||
|
||||
HAS_SF_SDK = netapp_utils.has_sf_sdk()
|
||||
try:
|
||||
import solidfire.common
|
||||
except ImportError:
|
||||
HAS_SF_SDK = False
|
||||
|
||||
|
||||
class ElementSWClusterPair(object):
|
||||
""" class to handle cluster pairing operations """
|
||||
|
||||
def __init__(self):
|
||||
"""
|
||||
Setup Ansible parameters and ElementSW connection
|
||||
"""
|
||||
self.argument_spec = netapp_utils.ontap_sf_host_argument_spec()
|
||||
self.argument_spec.update(dict(
|
||||
state=dict(required=False, choices=['present', 'absent'],
|
||||
default='present'),
|
||||
dest_mvip=dict(required=True, type='str'),
|
||||
dest_username=dict(required=False, type='str'),
|
||||
dest_password=dict(required=False, type='str', no_log=True)
|
||||
))
|
||||
|
||||
self.module = AnsibleModule(
|
||||
argument_spec=self.argument_spec,
|
||||
supports_check_mode=True
|
||||
)
|
||||
|
||||
if HAS_SF_SDK is False:
|
||||
self.module.fail_json(msg="Unable to import the SolidFire Python SDK")
|
||||
else:
|
||||
self.elem = netapp_utils.create_sf_connection(module=self.module)
|
||||
|
||||
self.elementsw_helper = NaElementSWModule(self.elem)
|
||||
self.na_helper = NetAppModule()
|
||||
self.parameters = self.na_helper.set_parameters(self.module.params)
|
||||
# get element_sw_connection for destination cluster
|
||||
# overwrite existing source host, user and password with destination credentials
|
||||
self.module.params['hostname'] = self.parameters['dest_mvip']
|
||||
# username and password is same as source,
|
||||
# if dest_username and dest_password aren't specified
|
||||
if self.parameters.get('dest_username'):
|
||||
self.module.params['username'] = self.parameters['dest_username']
|
||||
if self.parameters.get('dest_password'):
|
||||
self.module.params['password'] = self.parameters['dest_password']
|
||||
self.dest_elem = netapp_utils.create_sf_connection(module=self.module)
|
||||
self.dest_elementsw_helper = NaElementSWModule(self.dest_elem)
|
||||
|
||||
def check_if_already_paired(self, paired_clusters, hostname):
|
||||
for pair in paired_clusters.cluster_pairs:
|
||||
if pair.mvip == hostname:
|
||||
return pair.cluster_pair_id
|
||||
return None
|
||||
|
||||
def get_src_pair_id(self):
|
||||
"""
|
||||
Check for idempotency
|
||||
"""
|
||||
# src cluster and dest cluster exist
|
||||
paired_clusters = self.elem.list_cluster_pairs()
|
||||
return self.check_if_already_paired(paired_clusters, self.parameters['dest_mvip'])
|
||||
|
||||
def get_dest_pair_id(self):
|
||||
"""
|
||||
Getting destination cluster_pair_id
|
||||
"""
|
||||
paired_clusters = self.dest_elem.list_cluster_pairs()
|
||||
return self.check_if_already_paired(paired_clusters, self.parameters['hostname'])
|
||||
|
||||
def pair_clusters(self):
|
||||
"""
|
||||
Start cluster pairing on source, and complete on target cluster
|
||||
"""
|
||||
try:
|
||||
pair_key = self.elem.start_cluster_pairing()
|
||||
self.dest_elem.complete_cluster_pairing(
|
||||
cluster_pairing_key=pair_key.cluster_pairing_key)
|
||||
except solidfire.common.ApiServerError as err:
|
||||
self.module.fail_json(msg="Error pairing cluster %s and %s"
|
||||
% (self.parameters['hostname'],
|
||||
self.parameters['dest_mvip']),
|
||||
exception=to_native(err))
|
||||
|
||||
def unpair_clusters(self, pair_id_source, pair_id_dest):
|
||||
"""
|
||||
Delete cluster pair
|
||||
"""
|
||||
try:
|
||||
self.elem.remove_cluster_pair(cluster_pair_id=pair_id_source)
|
||||
self.dest_elem.remove_cluster_pair(cluster_pair_id=pair_id_dest)
|
||||
except solidfire.common.ApiServerError as err:
|
||||
self.module.fail_json(msg="Error unpairing cluster %s and %s"
|
||||
% (self.parameters['hostname'],
|
||||
self.parameters['dest_mvip']),
|
||||
exception=to_native(err))
|
||||
|
||||
def apply(self):
|
||||
"""
|
||||
Call create / delete cluster pair methods
|
||||
"""
|
||||
pair_id_source = self.get_src_pair_id()
|
||||
# If already paired, find the cluster_pair_id of destination cluster
|
||||
if pair_id_source:
|
||||
pair_id_dest = self.get_dest_pair_id()
|
||||
# calling helper to determine action
|
||||
cd_action = self.na_helper.get_cd_action(pair_id_source, self.parameters)
|
||||
if cd_action == "create":
|
||||
self.pair_clusters()
|
||||
elif cd_action == "delete":
|
||||
self.unpair_clusters(pair_id_source, pair_id_dest)
|
||||
self.module.exit_json(changed=self.na_helper.changed)
|
||||
|
||||
|
||||
def main():
|
||||
""" Apply cluster pair actions """
|
||||
cluster_obj = ElementSWClusterPair()
|
||||
cluster_obj.apply()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
|
@ -1,354 +0,0 @@
|
|||
#!/usr/bin/python
|
||||
# (c) 2019, NetApp, Inc
|
||||
# GNU General Public License v3.0+ (see COPYING or
|
||||
# https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
|
||||
'''
|
||||
Element Software Configure SNMP
|
||||
'''
|
||||
from __future__ import absolute_import, division, print_function
|
||||
__metaclass__ = type
|
||||
|
||||
|
||||
ANSIBLE_METADATA = {'metadata_version': '1.1',
|
||||
'status': ['preview'],
|
||||
'supported_by': 'community'}
|
||||
|
||||
|
||||
DOCUMENTATION = '''
|
||||
|
||||
module: na_elementsw_cluster_snmp
|
||||
|
||||
short_description: Configure Element SW Cluster SNMP
|
||||
extends_documentation_fragment:
|
||||
- netapp.solidfire
|
||||
version_added: '2.8'
|
||||
author: NetApp Ansible Team (@carchi8py) <ng-ansibleteam@netapp.com>
|
||||
description:
|
||||
- Configure Element Software cluster SNMP.
|
||||
|
||||
options:
|
||||
|
||||
state:
|
||||
description:
|
||||
- This module enables you to enable SNMP on cluster nodes. When you enable SNMP, \
|
||||
the action applies to all nodes in the cluster, and the values that are passed replace, \
|
||||
in whole, all values set in any previous call to this module.
|
||||
choices: ['present', 'absent']
|
||||
default: 'present'
|
||||
|
||||
snmp_v3_enabled:
|
||||
description:
|
||||
- Which version of SNMP has to be enabled.
|
||||
type: bool
|
||||
|
||||
networks:
|
||||
description:
|
||||
- List of networks and what type of access they have to the SNMP servers running on the cluster nodes.
|
||||
- This parameter is required if SNMP v3 is disabled.
|
||||
suboptions:
|
||||
access:
|
||||
description:
|
||||
- ro for read-only access.
|
||||
- rw for read-write access.
|
||||
- rosys for read-only access to a restricted set of system information.
|
||||
choices: ['ro', 'rw', 'rosys']
|
||||
cidr:
|
||||
description:
|
||||
- A CIDR network mask. This network mask must be an integer greater than or equal to 0, \
|
||||
and less than or equal to 32. It must also not be equal to 31.
|
||||
community:
|
||||
description:
|
||||
- SNMP community string.
|
||||
network:
|
||||
description:
|
||||
- This parameter along with the cidr variable is used to control which network the access and \
|
||||
community string apply to.
|
||||
- The special value of 'default' is used to specify an entry that applies to all networks.
|
||||
- The cidr mask is ignored when network value is either a host name or default.
|
||||
|
||||
usm_users:
|
||||
description:
|
||||
- List of users and the type of access they have to the SNMP servers running on the cluster nodes.
|
||||
- This parameter is required if SNMP v3 is enabled.
|
||||
suboptions:
|
||||
access:
|
||||
description:
|
||||
- rouser for read-only access.
|
||||
- rwuser for read-write access.
|
||||
- rosys for read-only access to a restricted set of system information.
|
||||
choices: ['rouser', 'rwuser', 'rosys']
|
||||
name:
|
||||
description:
|
||||
- The name of the user. Must contain at least one character, but no more than 32 characters.
|
||||
- Blank spaces are not allowed.
|
||||
password:
|
||||
description:
|
||||
- The password of the user. Must be between 8 and 255 characters long (inclusive).
|
||||
- Blank spaces are not allowed.
|
||||
- Required if 'secLevel' is 'auth' or 'priv.'
|
||||
passphrase:
|
||||
description:
|
||||
- The passphrase of the user. Must be between 8 and 255 characters long (inclusive).
|
||||
- Blank spaces are not allowed.
|
||||
- Required if 'secLevel' is 'priv.'
|
||||
secLevel:
|
||||
description:
|
||||
- To define the security level of a user.
|
||||
choices: ['noauth', 'auth', 'priv']
|
||||
|
||||
'''
|
||||
|
||||
EXAMPLES = """
|
||||
|
||||
- name: configure SnmpNetwork
|
||||
tags:
|
||||
- elementsw_cluster_snmp
|
||||
na_elementsw_cluster_snmp:
|
||||
hostname: "{{ elementsw_hostname }}"
|
||||
username: "{{ elementsw_username }}"
|
||||
password: "{{ elementsw_password }}"
|
||||
state: present
|
||||
snmp_v3_enabled: True
|
||||
usm_users:
|
||||
access: rouser
|
||||
name: testuser
|
||||
password: ChangeMe123
|
||||
passphrase: ChangeMe123
|
||||
secLevel: auth
|
||||
networks:
|
||||
access: ro
|
||||
cidr: 24
|
||||
community: TestNetwork
|
||||
network: 192.168.0.1
|
||||
|
||||
- name: Disable SnmpNetwork
|
||||
tags:
|
||||
- elementsw_cluster_snmp
|
||||
na_elementsw_cluster_snmp:
|
||||
hostname: "{{ elementsw_hostname }}"
|
||||
username: "{{ elementsw_username }}"
|
||||
password: "{{ elementsw_password }}"
|
||||
state: absent
|
||||
|
||||
"""
|
||||
|
||||
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.module_utils.netapp as netapp_utils
|
||||
from ansible.module_utils.netapp_elementsw_module import NaElementSWModule
|
||||
from ansible.module_utils.netapp_module import NetAppModule
|
||||
|
||||
HAS_SF_SDK = netapp_utils.has_sf_sdk()
|
||||
|
||||
|
||||
class ElementSWClusterSnmp(object):
|
||||
"""
|
||||
Element Software Configure Element SW Cluster SnmpNetwork
|
||||
"""
|
||||
def __init__(self):
|
||||
self.argument_spec = netapp_utils.ontap_sf_host_argument_spec()
|
||||
|
||||
self.argument_spec.update(dict(
|
||||
state=dict(type='str', choices=['present', 'absent'], default='present'),
|
||||
snmp_v3_enabled=dict(type='bool'),
|
||||
networks=dict(
|
||||
type='dict',
|
||||
options=dict(
|
||||
access=dict(type='str', choices=['ro', 'rw', 'rosys']),
|
||||
cidr=dict(type='int', default=None),
|
||||
community=dict(type='str', default=None),
|
||||
network=dict(type='str', default=None)
|
||||
)
|
||||
),
|
||||
usm_users=dict(
|
||||
type='dict',
|
||||
options=dict(
|
||||
access=dict(type='str', choices=['rouser', 'rwuser', 'rosys']),
|
||||
name=dict(type='str', default=None),
|
||||
password=dict(type='str', default=None),
|
||||
passphrase=dict(type='str', default=None),
|
||||
secLevel=dict(type='str', choices=['auth', 'noauth', 'priv'])
|
||||
)
|
||||
),
|
||||
))
|
||||
|
||||
self.module = AnsibleModule(
|
||||
argument_spec=self.argument_spec,
|
||||
required_if=[
|
||||
('state', 'present', ['snmp_v3_enabled']),
|
||||
('snmp_v3_enabled', True, ['usm_users']),
|
||||
('snmp_v3_enabled', False, ['networks'])
|
||||
],
|
||||
supports_check_mode=True
|
||||
)
|
||||
|
||||
self.na_helper = NetAppModule()
|
||||
self.parameters = self.na_helper.set_parameters(self.module.params)
|
||||
|
||||
if self.parameters.get('state') == "present":
|
||||
if self.parameters.get('usm_users') is not None:
|
||||
# Getting the configuration details to configure SNMP Version3
|
||||
self.access_usm = self.parameters.get('usm_users')['access']
|
||||
self.name = self.parameters.get('usm_users')['name']
|
||||
self.password = self.parameters.get('usm_users')['password']
|
||||
self.passphrase = self.parameters.get('usm_users')['passphrase']
|
||||
self.secLevel = self.parameters.get('usm_users')['secLevel']
|
||||
if self.parameters.get('networks') is not None:
|
||||
# Getting the configuration details to configure SNMP Version2
|
||||
self.access_network = self.parameters.get('networks')['access']
|
||||
self.cidr = self.parameters.get('networks')['cidr']
|
||||
self.community = self.parameters.get('networks')['community']
|
||||
self.network = self.parameters.get('networks')['network']
|
||||
|
||||
if HAS_SF_SDK is False:
|
||||
self.module.fail_json(msg="Unable to import the SolidFire Python SDK")
|
||||
else:
|
||||
self.sfe = netapp_utils.create_sf_connection(module=self.module)
|
||||
|
||||
def enable_snmp(self):
|
||||
"""
|
||||
enable snmp feature
|
||||
"""
|
||||
try:
|
||||
self.sfe.enable_snmp(snmp_v3_enabled=self.parameters.get('snmp_v3_enabled'))
|
||||
except Exception as exception_object:
|
||||
self.module.fail_json(msg='Error enabling snmp feature %s' % to_native(exception_object),
|
||||
exception=traceback.format_exc())
|
||||
|
||||
def disable_snmp(self):
|
||||
"""
|
||||
disable snmp feature
|
||||
"""
|
||||
try:
|
||||
self.sfe.disable_snmp()
|
||||
except Exception as exception_object:
|
||||
self.module.fail_json(msg='Error disabling snmp feature %s' % to_native(exception_object),
|
||||
exception=traceback.format_exc())
|
||||
|
||||
def configure_snmp(self, actual_networks, actual_usm_users):
|
||||
"""
|
||||
Configure snmp
|
||||
"""
|
||||
try:
|
||||
self.sfe.set_snmp_acl(networks=[actual_networks], usm_users=[actual_usm_users])
|
||||
|
||||
except Exception as exception_object:
|
||||
self.module.fail_json(msg='Error Configuring snmp feature %s' % to_native(exception_object.message),
|
||||
exception=traceback.format_exc())
|
||||
|
||||
def apply(self):
|
||||
"""
|
||||
Cluster SNMP configuration
|
||||
"""
|
||||
changed = False
|
||||
result_message = None
|
||||
update_required = False
|
||||
version_change = False
|
||||
is_snmp_enabled = self.sfe.get_snmp_state().enabled
|
||||
|
||||
if is_snmp_enabled is True:
|
||||
# IF SNMP is already enabled
|
||||
if self.parameters.get('state') == 'absent':
|
||||
# Checking for state change(s) here, and applying it later in the code allows us to support
|
||||
# check_mode
|
||||
changed = True
|
||||
|
||||
elif self.parameters.get('state') == 'present':
|
||||
# Checking if SNMP configuration needs to be updated,
|
||||
is_snmp_v3_enabled = self.sfe.get_snmp_state().snmp_v3_enabled
|
||||
|
||||
if is_snmp_v3_enabled != self.parameters.get('snmp_v3_enabled'):
|
||||
# Checking if there any version changes required
|
||||
version_change = True
|
||||
changed = True
|
||||
|
||||
if is_snmp_v3_enabled is True:
|
||||
# Checking If snmp configuration for usm_users needs modification
|
||||
if len(self.sfe.get_snmp_info().usm_users) == 0:
|
||||
# If snmp is getting configured for first time
|
||||
update_required = True
|
||||
changed = True
|
||||
else:
|
||||
for usm_user in self.sfe.get_snmp_info().usm_users:
|
||||
if usm_user.access != self.access_usm or usm_user.name != self.name or usm_user.password != self.password or \
|
||||
usm_user.passphrase != self.passphrase or usm_user.sec_level != self.secLevel:
|
||||
update_required = True
|
||||
changed = True
|
||||
else:
|
||||
# Checking If snmp configuration for networks needs modification
|
||||
for snmp_network in self.sfe.get_snmp_info().networks:
|
||||
if snmp_network.access != self.access_network or snmp_network.cidr != self.cidr or \
|
||||
snmp_network.community != self.community or snmp_network.network != self.network:
|
||||
update_required = True
|
||||
changed = True
|
||||
|
||||
else:
|
||||
if self.parameters.get('state') == 'present':
|
||||
changed = True
|
||||
|
||||
result_message = ""
|
||||
|
||||
if changed:
|
||||
if self.module.check_mode is True:
|
||||
result_message = "Check mode, skipping changes"
|
||||
|
||||
else:
|
||||
if self.parameters.get('state') == "present":
|
||||
# IF snmp is not enabled, then enable and configure snmp
|
||||
if self.parameters.get('snmp_v3_enabled') is True:
|
||||
# IF SNMP is enabled with version 3
|
||||
usm_users = {'access': self.access_usm,
|
||||
'name': self.name,
|
||||
'password': self.password,
|
||||
'passphrase': self.passphrase,
|
||||
'secLevel': self.secLevel}
|
||||
networks = None
|
||||
else:
|
||||
# IF SNMP is enabled with version 2
|
||||
usm_users = None
|
||||
networks = {'access': self.access_network,
|
||||
'cidr': self.cidr,
|
||||
'community': self.community,
|
||||
'network': self.network}
|
||||
|
||||
if is_snmp_enabled is False or version_change is True:
|
||||
# Enable and configure snmp
|
||||
self.enable_snmp()
|
||||
self.configure_snmp(networks, usm_users)
|
||||
result_message = "SNMP is enabled and configured"
|
||||
|
||||
elif update_required is True:
|
||||
# If snmp is already enabled, update the configuration if required
|
||||
self.configure_snmp(networks, usm_users)
|
||||
result_message = "SNMP is configured"
|
||||
|
||||
elif is_snmp_enabled is True and self.parameters.get('state') == "absent":
|
||||
# If snmp is enabled and state is absent, disable snmp
|
||||
self.disable_snmp()
|
||||
result_message = "SNMP is disabled"
|
||||
|
||||
self.module.exit_json(changed=changed, msg=result_message)
|
||||
|
||||
|
||||
def main():
|
||||
"""
|
||||
Main function
|
||||
"""
|
||||
na_elementsw_cluster_snmp = ElementSWClusterSnmp()
|
||||
na_elementsw_cluster_snmp.apply()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
|
@ -1,321 +0,0 @@
|
|||
#!/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 Node Drives
|
||||
'''
|
||||
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_drive
|
||||
|
||||
short_description: NetApp Element Software Manage Node Drives
|
||||
extends_documentation_fragment:
|
||||
- netapp.solidfire
|
||||
version_added: '2.7'
|
||||
author: NetApp Ansible Team (@carchi8py) <ng-ansibleteam@netapp.com>
|
||||
description:
|
||||
- Add, Erase or Remove drive for nodes on Element Software Cluster.
|
||||
|
||||
options:
|
||||
drive_id:
|
||||
description:
|
||||
- Drive ID or Serial Name of Node drive.
|
||||
- If not specified, add and remove action will be performed on all drives of node_id
|
||||
|
||||
state:
|
||||
description:
|
||||
- Element SW Storage Drive operation state.
|
||||
- present - To add drive of node to participate in cluster data storage.
|
||||
- absent - To remove the drive from being part of active cluster.
|
||||
- clean - Clean-up any residual data persistent on a *removed* drive in a secured method.
|
||||
choices: ['present', 'absent', 'clean']
|
||||
default: 'present'
|
||||
|
||||
node_id:
|
||||
description:
|
||||
- ID or Name of cluster node.
|
||||
required: true
|
||||
|
||||
force_during_upgrade:
|
||||
description:
|
||||
- Flag to force drive operation during upgrade.
|
||||
type: 'bool'
|
||||
|
||||
force_during_bin_sync:
|
||||
description:
|
||||
- Flag to force during a bin sync operation.
|
||||
type: 'bool'
|
||||
'''
|
||||
|
||||
EXAMPLES = """
|
||||
- name: Add drive with status available to cluster
|
||||
tags:
|
||||
- elementsw_add_drive
|
||||
na_element_drive:
|
||||
hostname: "{{ elementsw_hostname }}"
|
||||
username: "{{ elementsw_username }}"
|
||||
password: "{{ elementsw_password }}"
|
||||
state: present
|
||||
drive_id: scsi-SATA_SAMSUNG_MZ7LM48S2UJNX0J3221807
|
||||
force_during_upgrade: false
|
||||
force_during_bin_sync: false
|
||||
node_id: sf4805-meg-03
|
||||
|
||||
- name: Remove active drive from cluster
|
||||
tags:
|
||||
- elementsw_remove_drive
|
||||
na_element_drive:
|
||||
hostname: "{{ elementsw_hostname }}"
|
||||
username: "{{ elementsw_username }}"
|
||||
password: "{{ elementsw_password }}"
|
||||
state: absent
|
||||
force_during_upgrade: false
|
||||
node_id: sf4805-meg-03
|
||||
drive_id: scsi-SATA_SAMSUNG_MZ7LM48S2UJNX0J321208
|
||||
|
||||
- name: Secure Erase drive
|
||||
tags:
|
||||
- elemensw_clean_drive
|
||||
na_elementsw_drive:
|
||||
hostname: "{{ elementsw_hostname }}"
|
||||
username: "{{ elementsw_username }}"
|
||||
password: "{{ elementsw_password }}"
|
||||
state: clean
|
||||
drive_id: scsi-SATA_SAMSUNG_MZ7LM48S2UJNX0J432109
|
||||
node_id: sf4805-meg-03
|
||||
|
||||
- name: Add all the drives of a node to cluster
|
||||
tags:
|
||||
- elementsw_add_node
|
||||
na_elementsw_drive:
|
||||
hostname: "{{ elementsw_hostname }}"
|
||||
username: "{{ elementsw_username }}"
|
||||
password: "{{ elementsw_password }}"
|
||||
state: present
|
||||
force_during_upgrade: false
|
||||
force_during_bin_sync: false
|
||||
node_id: sf4805-meg-03
|
||||
|
||||
"""
|
||||
|
||||
|
||||
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.module_utils.netapp as netapp_utils
|
||||
|
||||
HAS_SF_SDK = netapp_utils.has_sf_sdk()
|
||||
|
||||
|
||||
class ElementSWDrive(object):
|
||||
"""
|
||||
Element Software Storage Drive operations
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
self.argument_spec = netapp_utils.ontap_sf_host_argument_spec()
|
||||
self.argument_spec.update(dict(
|
||||
state=dict(required=False, choices=['present', 'absent', 'clean'], default='present'),
|
||||
drive_id=dict(required=False, type='str'),
|
||||
node_id=dict(required=True, type='str'),
|
||||
force_during_upgrade=dict(required=False, type='bool'),
|
||||
force_during_bin_sync=dict(required=False, type='bool')
|
||||
))
|
||||
|
||||
self.module = AnsibleModule(
|
||||
argument_spec=self.argument_spec,
|
||||
supports_check_mode=True
|
||||
)
|
||||
|
||||
input_params = self.module.params
|
||||
|
||||
self.state = input_params['state']
|
||||
self.drive_id = input_params['drive_id']
|
||||
self.node_id = input_params['node_id']
|
||||
self.force_during_upgrade = input_params['force_during_upgrade']
|
||||
self.force_during_bin_sync = input_params['force_during_bin_sync']
|
||||
|
||||
if HAS_SF_SDK is False:
|
||||
self.module.fail_json(
|
||||
msg="Unable to import the SolidFire Python SDK")
|
||||
else:
|
||||
self.sfe = netapp_utils.create_sf_connection(module=self.module)
|
||||
|
||||
def get_node_id(self):
|
||||
"""
|
||||
Get Node ID
|
||||
:description: Find and retrieve node_id from the active cluster
|
||||
|
||||
:return: node_id (None if not found)
|
||||
:rtype: node_id
|
||||
"""
|
||||
if self.node_id is not None:
|
||||
list_nodes = self.sfe.list_active_nodes()
|
||||
for current_node in list_nodes.nodes:
|
||||
if self.node_id == str(current_node.node_id):
|
||||
return current_node.node_id
|
||||
elif current_node.name == self.node_id:
|
||||
self.node_id = current_node.node_id
|
||||
return current_node.node_id
|
||||
self.node_id = None
|
||||
return self.node_id
|
||||
|
||||
def get_drives_listby_status(self):
|
||||
"""
|
||||
Capture list of drives based on status for a given node_id
|
||||
:description: Capture list of active, failed and available drives from a given node_id
|
||||
|
||||
:return: None
|
||||
"""
|
||||
if self.node_id is not None:
|
||||
list_drives = self.sfe.list_drives()
|
||||
for drive in list_drives.drives:
|
||||
if drive.node_id == self.node_id:
|
||||
if drive.status in ['active', 'failed']:
|
||||
self.active_drives[drive.serial] = drive.drive_id
|
||||
elif drive.status == "available":
|
||||
self.available_drives[drive.serial] = drive.drive_id
|
||||
return None
|
||||
|
||||
def get_active_drives(self, drive_id=None):
|
||||
"""
|
||||
return a list of active drives
|
||||
if drive_id is specified, only [] or [drive_id] is returned
|
||||
else all available drives for this node are returned
|
||||
"""
|
||||
action_list = list()
|
||||
if self.drive_id is not None:
|
||||
if self.drive_id in self.active_drives.values():
|
||||
action_list.append(int(self.drive_id))
|
||||
if self.drive_id in self.active_drives:
|
||||
action_list.append(self.active_drives[self.drive_id])
|
||||
else:
|
||||
action_list.extend(self.active_drives.values())
|
||||
|
||||
return action_list
|
||||
|
||||
def get_available_drives(self, drive_id=None):
|
||||
"""
|
||||
return a list of available drives (not active)
|
||||
if drive_id is specified, only [] or [drive_id] is returned
|
||||
else all available drives for this node are returned
|
||||
"""
|
||||
action_list = list()
|
||||
if self.drive_id is not None:
|
||||
if self.drive_id in self.available_drives.values():
|
||||
action_list.append(int(self.drive_id))
|
||||
if self.drive_id in self.available_drives:
|
||||
action_list.append(self.available_drives[self.drive_id])
|
||||
else:
|
||||
action_list.extend(self.available_drives.values())
|
||||
|
||||
return action_list
|
||||
|
||||
def add_drive(self, drives=None):
|
||||
"""
|
||||
Add Drive available for Cluster storage expansion
|
||||
"""
|
||||
try:
|
||||
self.sfe.add_drives(drives,
|
||||
force_during_upgrade=self.force_during_upgrade,
|
||||
force_during_bin_sync=self.force_during_bin_sync)
|
||||
except Exception as exception_object:
|
||||
self.module.fail_json(msg='Error add drive to cluster %s' % (to_native(exception_object)),
|
||||
exception=traceback.format_exc())
|
||||
|
||||
def remove_drive(self, drives=None):
|
||||
"""
|
||||
Remove Drive active in Cluster
|
||||
"""
|
||||
try:
|
||||
self.sfe.remove_drives(drives,
|
||||
force_during_upgrade=self.force_during_upgrade)
|
||||
except Exception as exception_object:
|
||||
self.module.fail_json(msg='Error remove drive from cluster %s' % (to_native(exception_object)),
|
||||
exception=traceback.format_exc())
|
||||
|
||||
def secure_erase(self, drives=None):
|
||||
"""
|
||||
Secure Erase any residual data existing on a drive
|
||||
"""
|
||||
try:
|
||||
self.sfe.secure_erase_drives(drives)
|
||||
except Exception as exception_object:
|
||||
self.module.fail_json(msg='Error clean data from drive %s' % (to_native(exception_object)),
|
||||
exception=traceback.format_exc())
|
||||
|
||||
def apply(self):
|
||||
"""
|
||||
Check, process and initiate Drive operation
|
||||
"""
|
||||
changed = False
|
||||
result_message = None
|
||||
self.active_drives = {}
|
||||
self.available_drives = {}
|
||||
action_list = []
|
||||
self.get_node_id()
|
||||
self.get_drives_listby_status()
|
||||
|
||||
if self.module.check_mode is False and self.node_id is not None:
|
||||
if self.state == "present":
|
||||
action_list = self.get_available_drives(self.drive_id)
|
||||
if len(action_list) > 0:
|
||||
self.add_drive(action_list)
|
||||
changed = True
|
||||
elif self.drive_id is not None and (self.drive_id in self.active_drives.values() or self.drive_id in self.active_drives):
|
||||
changed = False # No action, so setting changed to false
|
||||
elif self.drive_id is None and len(self.active_drives) > 0:
|
||||
changed = False # No action, so setting changed to false
|
||||
else:
|
||||
self.module.fail_json(msg='Error - no drive(s) in available state on node to be included in cluster')
|
||||
|
||||
elif self.state == "absent":
|
||||
action_list = self.get_active_drives(self.drive_id)
|
||||
if len(action_list) > 0:
|
||||
self.remove_drive(action_list)
|
||||
changed = True
|
||||
|
||||
elif self.state == "clean":
|
||||
action_list = self.get_available_drives(self.drive_id)
|
||||
if len(action_list) > 0:
|
||||
self.secure_erase(action_list)
|
||||
changed = True
|
||||
else:
|
||||
self.module.fail_json(msg='Error - no drive(s) in available state on node to be cleaned')
|
||||
|
||||
else:
|
||||
result_message = "Skipping changes, No change requested"
|
||||
self.module.exit_json(changed=changed, msg=result_message)
|
||||
|
||||
|
||||
def main():
|
||||
"""
|
||||
Main function
|
||||
"""
|
||||
|
||||
na_elementsw_drive = ElementSWDrive()
|
||||
na_elementsw_drive.apply()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
|
@ -1,317 +0,0 @@
|
|||
#!/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 manage initiators
|
||||
'''
|
||||
from __future__ import absolute_import, division, print_function
|
||||
__metaclass__ = type
|
||||
|
||||
|
||||
ANSIBLE_METADATA = {'metadata_version': '1.1',
|
||||
'status': ['preview'],
|
||||
'supported_by': 'community'}
|
||||
|
||||
|
||||
DOCUMENTATION = '''
|
||||
|
||||
module: na_elementsw_initiators
|
||||
|
||||
short_description: Manage Element SW initiators
|
||||
extends_documentation_fragment:
|
||||
- netapp.solidfire
|
||||
version_added: '2.8'
|
||||
author: NetApp Ansible Team (@carchi8py) <ng-ansibleteam@netapp.com>
|
||||
description:
|
||||
- Manage Element Software initiators that allow external clients access to volumes.
|
||||
|
||||
options:
|
||||
initiators:
|
||||
description: A list of objects containing characteristics of each initiator.
|
||||
suboptions:
|
||||
name:
|
||||
description: The name of the initiator.
|
||||
|
||||
alias:
|
||||
description: The friendly name assigned to this initiator.
|
||||
|
||||
initiator_id:
|
||||
description: The numeric ID of the initiator.
|
||||
|
||||
volume_access_groups:
|
||||
description: A list of volumeAccessGroupIDs to which this initiator belongs.
|
||||
|
||||
attributes:
|
||||
description: A set of JSON attributes to assign to this initiator.
|
||||
|
||||
state:
|
||||
description:
|
||||
- Whether the specified initiator should exist or not.
|
||||
choices: ['present', 'absent']
|
||||
default: present
|
||||
'''
|
||||
|
||||
EXAMPLES = """
|
||||
|
||||
- name: Manage initiators
|
||||
tags:
|
||||
- na_elementsw_initiators
|
||||
na_elementsw_initiators:
|
||||
hostname: "{{ elementsw_hostname }}"
|
||||
username: "{{ elementsw_username }}"
|
||||
password: "{{ elementsw_password }}"
|
||||
initiators:
|
||||
- name: a
|
||||
alias: a1
|
||||
initiator_id: 1
|
||||
volume_access_groups:
|
||||
- 1
|
||||
- 2
|
||||
attributes: {"key": "value"}
|
||||
- name: b
|
||||
alias: b2
|
||||
initiator_id: 2
|
||||
volume_access_groups:
|
||||
- 2
|
||||
state: present
|
||||
"""
|
||||
|
||||
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.module_utils.netapp as netapp_utils
|
||||
from ansible.module_utils.netapp_elementsw_module import NaElementSWModule
|
||||
from ansible.module_utils.netapp_module import NetAppModule
|
||||
HAS_SF_SDK = netapp_utils.has_sf_sdk()
|
||||
if HAS_SF_SDK:
|
||||
from solidfire.models import ModifyInitiator
|
||||
|
||||
|
||||
class ElementSWInitiators(object):
|
||||
"""
|
||||
Element Software Manage Element SW initiators
|
||||
"""
|
||||
def __init__(self):
|
||||
self.argument_spec = netapp_utils.ontap_sf_host_argument_spec()
|
||||
|
||||
self.argument_spec.update(dict(
|
||||
initiators=dict(
|
||||
type='list',
|
||||
options=dict(
|
||||
name=dict(type='str', required=True),
|
||||
alias=dict(type='str', default=None),
|
||||
initiator_id=dict(type='int', default=None),
|
||||
volume_access_groups=dict(type='list', default=None),
|
||||
volume_access_group_id=dict(type='int', default=None),
|
||||
attributes=dict(type='dict', default=None),
|
||||
)
|
||||
),
|
||||
state=dict(choices=['present', 'absent'], default='present'),
|
||||
))
|
||||
|
||||
self.module = AnsibleModule(
|
||||
argument_spec=self.argument_spec,
|
||||
supports_check_mode=True
|
||||
)
|
||||
|
||||
self.na_helper = NetAppModule()
|
||||
self.parameters = self.na_helper.set_parameters(self.module.params)
|
||||
|
||||
if HAS_SF_SDK is False:
|
||||
self.module.fail_json(msg="Unable to import the SolidFire Python SDK")
|
||||
else:
|
||||
self.sfe = netapp_utils.create_sf_connection(module=self.module)
|
||||
|
||||
self.elementsw_helper = NaElementSWModule(self.sfe)
|
||||
|
||||
# iterate over each user-provided initiator
|
||||
for initiator in self.parameters.get('initiators'):
|
||||
# add telemetry attributes
|
||||
if 'attributes' in initiator:
|
||||
initiator['attributes'].update(self.elementsw_helper.set_element_attributes(source='na_elementsw_initiators'))
|
||||
else:
|
||||
initiator['attributes'] = self.elementsw_helper.set_element_attributes(source='na_elementsw_initiators')
|
||||
|
||||
def compare_initiators(self, user_initiator, existing_initiator):
|
||||
"""
|
||||
compare user input initiator with existing dict
|
||||
:return: True if matched, False otherwise
|
||||
"""
|
||||
if user_initiator is None or existing_initiator is None:
|
||||
return False
|
||||
for param in user_initiator:
|
||||
# lookup initiator_name instead of name
|
||||
if param == 'name':
|
||||
if user_initiator['name'] == existing_initiator['initiator_name']:
|
||||
pass
|
||||
elif user_initiator[param] == existing_initiator[param]:
|
||||
pass
|
||||
else:
|
||||
return True
|
||||
return False
|
||||
|
||||
def initiator_to_dict(self, initiator_obj):
|
||||
"""
|
||||
converts initiator class object to dict
|
||||
:return: reconstructed initiator dict
|
||||
"""
|
||||
known_params = ['initiator_name',
|
||||
'alias',
|
||||
'initiator_id',
|
||||
'volume_access_groups',
|
||||
'volume_access_group_id',
|
||||
'attributes']
|
||||
initiator_dict = {}
|
||||
|
||||
# missing parameter cause error
|
||||
# so assign defaults
|
||||
for param in known_params:
|
||||
initiator_dict[param] = getattr(initiator_obj, param, None)
|
||||
return initiator_dict
|
||||
|
||||
def find_initiator(self, id=None, name=None):
|
||||
"""
|
||||
find a specific initiator
|
||||
:return: initiator dict
|
||||
"""
|
||||
initiator_details = None
|
||||
if self.all_existing_initiators is None:
|
||||
return initiator_details
|
||||
for initiator in self.all_existing_initiators:
|
||||
# if name is provided or
|
||||
# if id is provided
|
||||
if name is not None:
|
||||
if initiator.initiator_name == name:
|
||||
initiator_details = self.initiator_to_dict(initiator)
|
||||
elif id is not None:
|
||||
if initiator.initiator_id == id:
|
||||
initiator_details = self.initiator_to_dict(initiator)
|
||||
else:
|
||||
# if neither id nor name provided
|
||||
# return everything
|
||||
initiator_details = self.all_existing_initiators
|
||||
return initiator_details
|
||||
|
||||
def create_initiators(self, initiator):
|
||||
"""
|
||||
create initiators
|
||||
"""
|
||||
# create_initiators needs an array
|
||||
# so enclose this initiator in an array
|
||||
initiator_list = [initiator]
|
||||
try:
|
||||
self.sfe.create_initiators(initiator_list)
|
||||
except Exception as exception_object:
|
||||
self.module.fail_json(msg='Error creating initiator %s' % (to_native(exception_object)),
|
||||
exception=traceback.format_exc())
|
||||
|
||||
def delete_initiators(self, initiator):
|
||||
"""
|
||||
delete initiators
|
||||
"""
|
||||
# delete_initiators needs an array
|
||||
# so enclose this initiator in an array
|
||||
initiator_id_array = [initiator]
|
||||
try:
|
||||
self.sfe.delete_initiators(initiator_id_array)
|
||||
except Exception as exception_object:
|
||||
self.module.fail_json(msg='Error deleting initiator %s' % (to_native(exception_object)),
|
||||
exception=traceback.format_exc())
|
||||
|
||||
def modify_initiators(self, initiator, existing_initiator):
|
||||
"""
|
||||
modify initiators
|
||||
"""
|
||||
# create the new initiator dict
|
||||
# by merging old and new values
|
||||
merged_initiator = existing_initiator.copy()
|
||||
merged_initiator.update(initiator)
|
||||
|
||||
# we MUST create an object before sending
|
||||
# the new initiator to modify_initiator
|
||||
initiator_object = ModifyInitiator(initiator_id=merged_initiator['initiator_id'],
|
||||
alias=merged_initiator['alias'],
|
||||
volume_access_group_id=merged_initiator['volume_access_group_id'],
|
||||
attributes=merged_initiator['attributes'])
|
||||
initiator_list = [initiator_object]
|
||||
try:
|
||||
self.sfe.modify_initiators(initiators=initiator_list)
|
||||
except Exception as exception_object:
|
||||
self.module.fail_json(msg='Error modifying initiator %s' % (to_native(exception_object)),
|
||||
exception=traceback.format_exc())
|
||||
|
||||
def apply(self):
|
||||
"""
|
||||
configure initiators
|
||||
"""
|
||||
changed = False
|
||||
modify = None
|
||||
result_message = None
|
||||
|
||||
# get all user provided initiators
|
||||
input_initiators = self.parameters.get('initiators')
|
||||
|
||||
# get all initiators
|
||||
# store in a cache variable
|
||||
self.all_existing_initiators = self.sfe.list_initiators().initiators
|
||||
|
||||
# iterate over each user-provided initiator
|
||||
for in_initiator in input_initiators:
|
||||
if self.parameters.get('state') == 'present':
|
||||
# check if initiator_id is provided and exists
|
||||
if 'initiator_id' in in_initiator and in_initiator['initiator_id'] is not None and \
|
||||
self.find_initiator(id=in_initiator['initiator_id']) is not None:
|
||||
if self.compare_initiators(in_initiator, self.find_initiator(id=in_initiator['initiator_id'])):
|
||||
changed = True
|
||||
result_message = 'modifying initiator(s)'
|
||||
self.modify_initiators(in_initiator, self.find_initiator(id=in_initiator['initiator_id']))
|
||||
# otherwise check if name is provided and exists
|
||||
elif 'name' in in_initiator and in_initiator['name'] is not None and self.find_initiator(name=in_initiator['name']) is not None:
|
||||
if self.compare_initiators(in_initiator, self.find_initiator(name=in_initiator['name'])):
|
||||
changed = True
|
||||
result_message = 'modifying initiator(s)'
|
||||
self.modify_initiators(in_initiator, self.find_initiator(name=in_initiator['name']))
|
||||
# this is a create op if initiator doesn't exist
|
||||
else:
|
||||
changed = True
|
||||
result_message = 'creating initiator(s)'
|
||||
self.create_initiators(in_initiator)
|
||||
elif self.parameters.get('state') == 'absent':
|
||||
# delete_initiators only processes ids
|
||||
# so pass ids of initiators to method
|
||||
if 'name' in in_initiator and in_initiator['name'] is not None and \
|
||||
self.find_initiator(name=in_initiator['name']) is not None:
|
||||
changed = True
|
||||
result_message = 'deleting initiator(s)'
|
||||
self.delete_initiators(self.find_initiator(name=in_initiator['name'])['initiator_id'])
|
||||
elif 'initiator_id' in in_initiator and in_initiator['initiator_id'] is not None and \
|
||||
self.find_initiator(id=in_initiator['initiator_id']) is not None:
|
||||
changed = True
|
||||
result_message = 'deleting initiator(s)'
|
||||
self.delete_initiators(in_initiator['initiator_id'])
|
||||
if self.module.check_mode is True:
|
||||
result_message = "Check mode, skipping changes"
|
||||
self.module.exit_json(changed=changed, msg=result_message)
|
||||
|
||||
|
||||
def main():
|
||||
"""
|
||||
Main function
|
||||
"""
|
||||
na_elementsw_initiators = ElementSWInitiators()
|
||||
na_elementsw_initiators.apply()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
|
@ -1,243 +0,0 @@
|
|||
#!/usr/bin/python
|
||||
|
||||
# (c) 2017, NetApp, Inc
|
||||
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
|
||||
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_ldap
|
||||
|
||||
short_description: NetApp Element Software Manage ldap admin users
|
||||
extends_documentation_fragment:
|
||||
- netapp.solidfire
|
||||
version_added: '2.7'
|
||||
author: NetApp Ansible Team (@carchi8py) <ng-ansibleteam@netapp.com>
|
||||
description:
|
||||
- Enable, disable ldap, and add ldap users
|
||||
|
||||
options:
|
||||
|
||||
state:
|
||||
description:
|
||||
- Whether the specified volume should exist or not.
|
||||
required: true
|
||||
choices: ['present', 'absent']
|
||||
|
||||
authType:
|
||||
description:
|
||||
- Identifies which user authentication method to use.
|
||||
choices: ['DirectBind', 'SearchAndBind']
|
||||
|
||||
groupSearchBaseDn:
|
||||
description:
|
||||
- The base DN of the tree to start the group search (will do a subtree search from here)
|
||||
|
||||
groupSearchType:
|
||||
description:
|
||||
- Controls the default group search filter used
|
||||
choices: ['NoGroup', 'ActiveDirectory', 'MemberDN']
|
||||
|
||||
serverURIs:
|
||||
description:
|
||||
- A comma-separated list of LDAP server URIs
|
||||
|
||||
userSearchBaseDN:
|
||||
description:
|
||||
- The base DN of the tree to start the search (will do a subtree search from here)
|
||||
|
||||
searchBindDN:
|
||||
description:
|
||||
- A dully qualified DN to log in with to perform an LDAp search for the user (needs read access to the LDAP directory).
|
||||
|
||||
searchBindPassword:
|
||||
description:
|
||||
- The password for the searchBindDN account used for searching
|
||||
|
||||
userSearchFilter:
|
||||
description:
|
||||
- the LDAP Filter to use
|
||||
|
||||
userDNTemplate:
|
||||
description:
|
||||
- A string that is used form a fully qualified user DN.
|
||||
|
||||
groupSearchCustomFilter:
|
||||
description:
|
||||
- For use with the CustomFilter Search type
|
||||
'''
|
||||
|
||||
EXAMPLES = """
|
||||
- name: disable ldap authentication
|
||||
na_elementsw_ldap:
|
||||
state: absent
|
||||
username: "{{ admin username }}"
|
||||
password: "{{ admin password }}"
|
||||
hostname: "{{ hostname }}"
|
||||
|
||||
- name: Enable ldap authentication
|
||||
na_elementsw_ldap:
|
||||
state: present
|
||||
username: "{{ admin username }}"
|
||||
password: "{{ admin password }}"
|
||||
hostname: "{{ hostname }}"
|
||||
authType: DirectBind
|
||||
serverURIs: ldap://svmdurlabesx01spd_ldapclnt
|
||||
groupSearchType: MemberDN
|
||||
userDNTemplate: uid=%USERNAME%,cn=users,cn=accounts,dc=corp,dc="{{ company name }}",dc=com
|
||||
|
||||
|
||||
"""
|
||||
|
||||
RETURN = """
|
||||
|
||||
"""
|
||||
import traceback
|
||||
|
||||
from ansible.module_utils.basic import AnsibleModule
|
||||
from ansible.module_utils._text import to_native
|
||||
import ansible.module_utils.netapp as netapp_utils
|
||||
|
||||
HAS_SF_SDK = netapp_utils.has_sf_sdk()
|
||||
try:
|
||||
import solidfire.common
|
||||
except Exception:
|
||||
HAS_SF_SDK = False
|
||||
|
||||
|
||||
class NetappElementLdap(object):
|
||||
|
||||
def __init__(self):
|
||||
self.argument_spec = netapp_utils.ontap_sf_host_argument_spec()
|
||||
self.argument_spec.update(
|
||||
state=dict(type='str', required=True, choices=['absent', 'present']),
|
||||
authType=dict(type='str', choices=['DirectBind', 'SearchAndBind']),
|
||||
groupSearchBaseDn=dict(type='str'),
|
||||
groupSearchType=dict(type='str', choices=['NoGroup', 'ActiveDirectory', 'MemberDN']),
|
||||
serverURIs=dict(type='str'),
|
||||
userSearchBaseDN=dict(type='str'),
|
||||
searchBindDN=dict(type='str'),
|
||||
searchBindPassword=dict(type='str', no_log=True),
|
||||
userSearchFilter=dict(type='str'),
|
||||
userDNTemplate=dict(type='str'),
|
||||
groupSearchCustomFilter=dict(type='str'),
|
||||
)
|
||||
|
||||
self.module = AnsibleModule(
|
||||
argument_spec=self.argument_spec,
|
||||
supports_check_mode=True,
|
||||
)
|
||||
|
||||
param = self.module.params
|
||||
|
||||
# set up state variables
|
||||
self.state = param['state']
|
||||
self.authType = param['authType']
|
||||
self.groupSearchBaseDn = param['groupSearchBaseDn']
|
||||
self.groupSearchType = param['groupSearchType']
|
||||
self.serverURIs = param['serverURIs']
|
||||
if self.serverURIs is not None:
|
||||
self.serverURIs = self.serverURIs.split(',')
|
||||
self.userSearchBaseDN = param['userSearchBaseDN']
|
||||
self.searchBindDN = param['searchBindDN']
|
||||
self.searchBindPassword = param['searchBindPassword']
|
||||
self.userSearchFilter = param['userSearchFilter']
|
||||
self.userDNTemplate = param['userDNTemplate']
|
||||
self.groupSearchCustomFilter = param['groupSearchCustomFilter']
|
||||
|
||||
if HAS_SF_SDK is False:
|
||||
self.module.fail_json(msg="Unable to import the SolidFire Python SDK")
|
||||
else:
|
||||
self.sfe = netapp_utils.create_sf_connection(module=self.module)
|
||||
|
||||
def get_ldap_configuration(self):
|
||||
"""
|
||||
Return ldap configuration if found
|
||||
|
||||
:return: Details about the ldap configuration. None if not found.
|
||||
:rtype: solidfire.models.GetLdapConfigurationResult
|
||||
"""
|
||||
ldap_config = self.sfe.get_ldap_configuration()
|
||||
return ldap_config
|
||||
|
||||
def enable_ldap(self):
|
||||
"""
|
||||
Enable LDAP
|
||||
:return: nothing
|
||||
"""
|
||||
try:
|
||||
self.sfe.enable_ldap_authentication(self.serverURIs, auth_type=self.authType,
|
||||
group_search_base_dn=self.groupSearchBaseDn,
|
||||
group_search_type=self.groupSearchType,
|
||||
group_search_custom_filter=self.groupSearchCustomFilter,
|
||||
search_bind_dn=self.searchBindDN,
|
||||
search_bind_password=self.searchBindPassword,
|
||||
user_search_base_dn=self.userSearchBaseDN,
|
||||
user_search_filter=self.userSearchFilter,
|
||||
user_dntemplate=self.userDNTemplate)
|
||||
except solidfire.common.ApiServerError as error:
|
||||
self.module.fail_json(msg='Error enabling LDAP %s: %s' % (self.account_id, to_native(error)),
|
||||
exception=traceback.format_exc())
|
||||
|
||||
def check_config(self, ldap_config):
|
||||
"""
|
||||
Check to see if the ldap config has been modified.
|
||||
:param ldap_config: The LDAP configuration
|
||||
:return: False if the config is the same as the playbook, True if it is not
|
||||
"""
|
||||
if self.authType != ldap_config.ldap_configuration.auth_type:
|
||||
return True
|
||||
if self.serverURIs != ldap_config.ldap_configuration.server_uris:
|
||||
return True
|
||||
if self.groupSearchBaseDn != ldap_config.ldap_configuration.group_search_base_dn:
|
||||
return True
|
||||
if self.groupSearchType != ldap_config.ldap_configuration.group_search_type:
|
||||
return True
|
||||
if self.groupSearchCustomFilter != ldap_config.ldap_configuration.group_search_custom_filter:
|
||||
return True
|
||||
if self.searchBindDN != ldap_config.ldap_configuration.search_bind_dn:
|
||||
return True
|
||||
if self.searchBindPassword != ldap_config.ldap_configuration.search_bind_password:
|
||||
return True
|
||||
if self.userSearchBaseDN != ldap_config.ldap_configuration.user_search_base_dn:
|
||||
return True
|
||||
if self.userSearchFilter != ldap_config.ldap_configuration.user_search_filter:
|
||||
return True
|
||||
if self.userDNTemplate != ldap_config.ldap_configuration.user_dntemplate:
|
||||
return True
|
||||
return False
|
||||
|
||||
def apply(self):
|
||||
changed = False
|
||||
ldap_config = self.get_ldap_configuration()
|
||||
if self.state == 'absent':
|
||||
if ldap_config and ldap_config.ldap_configuration.enabled:
|
||||
changed = True
|
||||
if self.state == 'present' and self.check_config(ldap_config):
|
||||
changed = True
|
||||
if changed:
|
||||
if self.module.check_mode:
|
||||
pass
|
||||
else:
|
||||
if self.state == 'present':
|
||||
self.enable_ldap()
|
||||
elif self.state == 'absent':
|
||||
self.sfe.disable_ldap_authentication()
|
||||
|
||||
self.module.exit_json(changed=changed)
|
||||
|
||||
|
||||
def main():
|
||||
v = NetappElementLdap()
|
||||
v.apply()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
|
@ -1,297 +0,0 @@
|
|||
#!/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 Node Network Interfaces - Bond 1G and 10G configuration
|
||||
'''
|
||||
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_network_interfaces
|
||||
|
||||
short_description: NetApp Element Software Configure Node Network Interfaces
|
||||
extends_documentation_fragment:
|
||||
- netapp.solidfire
|
||||
version_added: '2.7'
|
||||
author: NetApp Ansible Team (@carchi8py) <ng-ansibleteam@netapp.com>
|
||||
description:
|
||||
- Configure Element SW Node Network Interfaces for Bond 1G and 10G IP address.
|
||||
|
||||
options:
|
||||
method:
|
||||
description:
|
||||
- Type of Method used to configure the interface.
|
||||
- method depends on other settings such as the use of a static IP address, which will change the method to static.
|
||||
- loopback - Used to define the IPv4 loopback interface.
|
||||
- manual - Used to define interfaces for which no configuration is done by default.
|
||||
- dhcp - May be used to obtain an IP address via DHCP.
|
||||
- static - Used to define Ethernet interfaces with statically allocated IPv4 addresses.
|
||||
choices: ['loopback', 'manual', 'dhcp', 'static']
|
||||
required: true
|
||||
|
||||
ip_address_1g:
|
||||
description:
|
||||
- IP address for the 1G network.
|
||||
required: true
|
||||
|
||||
ip_address_10g:
|
||||
description:
|
||||
- IP address for the 10G network.
|
||||
required: true
|
||||
|
||||
subnet_1g:
|
||||
description:
|
||||
- 1GbE Subnet Mask.
|
||||
required: true
|
||||
|
||||
subnet_10g:
|
||||
description:
|
||||
- 10GbE Subnet Mask.
|
||||
required: true
|
||||
|
||||
gateway_address_1g:
|
||||
description:
|
||||
- Router network address to send packets out of the local network.
|
||||
required: true
|
||||
|
||||
gateway_address_10g:
|
||||
description:
|
||||
- Router network address to send packets out of the local network.
|
||||
required: true
|
||||
|
||||
mtu_1g:
|
||||
description:
|
||||
- Maximum Transmission Unit for 1GbE, Largest packet size that a network protocol can transmit.
|
||||
- Must be greater than or equal to 1500 bytes.
|
||||
default: '1500'
|
||||
|
||||
mtu_10g:
|
||||
description:
|
||||
- Maximum Transmission Unit for 10GbE, Largest packet size that a network protocol can transmit.
|
||||
- Must be greater than or equal to 1500 bytes.
|
||||
default: '1500'
|
||||
|
||||
dns_nameservers:
|
||||
description:
|
||||
- List of addresses for domain name servers.
|
||||
|
||||
dns_search_domains:
|
||||
description:
|
||||
- List of DNS search domains.
|
||||
|
||||
bond_mode_1g:
|
||||
description:
|
||||
- Bond mode for 1GbE configuration.
|
||||
choices: ['ActivePassive', 'ALB', 'LACP']
|
||||
default: 'ActivePassive'
|
||||
|
||||
bond_mode_10g:
|
||||
description:
|
||||
- Bond mode for 10GbE configuration.
|
||||
choices: ['ActivePassive', 'ALB', 'LACP']
|
||||
default: 'ActivePassive'
|
||||
|
||||
lacp_1g:
|
||||
description:
|
||||
- Link Aggregation Control Protocol useful only if LACP is selected as the Bond Mode.
|
||||
- Slow - Packets are transmitted at 30 second intervals.
|
||||
- Fast - Packets are transmitted in 1 second intervals.
|
||||
choices: ['Fast', 'Slow']
|
||||
default: 'Slow'
|
||||
|
||||
lacp_10g:
|
||||
description:
|
||||
- Link Aggregation Control Protocol useful only if LACP is selected as the Bond Mode.
|
||||
- Slow - Packets are transmitted at 30 second intervals.
|
||||
- Fast - Packets are transmitted in 1 second intervals.
|
||||
choices: ['Fast', 'Slow']
|
||||
default: 'Slow'
|
||||
|
||||
virtual_network_tag:
|
||||
description:
|
||||
- This is the primary network tag. All nodes in a cluster have the same VLAN tag.
|
||||
|
||||
'''
|
||||
|
||||
EXAMPLES = """
|
||||
|
||||
- name: Set Node network interfaces configuration for Bond 1G and 10G properties
|
||||
tags:
|
||||
- elementsw_network_interfaces
|
||||
na_elementsw_network_interfaces:
|
||||
hostname: "{{ elementsw_hostname }}"
|
||||
username: "{{ elementsw_username }}"
|
||||
password: "{{ elementsw_password }}"
|
||||
method: static
|
||||
ip_address_1g: 10.226.109.68
|
||||
ip_address_10g: 10.226.201.72
|
||||
subnet_1g: 255.255.255.0
|
||||
subnet_10g: 255.255.255.0
|
||||
gateway_address_1g: 10.193.139.1
|
||||
gateway_address_10g: 10.193.140.1
|
||||
mtu_1g: 1500
|
||||
mtu_10g: 9000
|
||||
bond_mode_1g: ActivePassive
|
||||
bond_mode_10g: LACP
|
||||
lacp_10g: Fast
|
||||
"""
|
||||
|
||||
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.module_utils.netapp as netapp_utils
|
||||
|
||||
HAS_SF_SDK = netapp_utils.has_sf_sdk()
|
||||
|
||||
try:
|
||||
from solidfire.models import Network, NetworkConfig
|
||||
HAS_SF_SDK = True
|
||||
except Exception:
|
||||
HAS_SF_SDK = False
|
||||
|
||||
|
||||
class ElementSWNetworkInterfaces(object):
|
||||
"""
|
||||
Element Software Network Interfaces - Bond 1G and 10G Network configuration
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
self.argument_spec = netapp_utils.ontap_sf_host_argument_spec()
|
||||
self.argument_spec.update(
|
||||
method=dict(type='str', required=True, choices=['loopback', 'manual', 'dhcp', 'static']),
|
||||
ip_address_1g=dict(type='str', required=True),
|
||||
ip_address_10g=dict(type='str', required=True),
|
||||
subnet_1g=dict(type='str', required=True),
|
||||
subnet_10g=dict(type='str', required=True),
|
||||
gateway_address_1g=dict(type='str', required=True),
|
||||
gateway_address_10g=dict(type='str', required=True),
|
||||
mtu_1g=dict(type='str', default='1500'),
|
||||
mtu_10g=dict(type='str', default='1500'),
|
||||
dns_nameservers=dict(type='list'),
|
||||
dns_search_domains=dict(type='list'),
|
||||
bond_mode_1g=dict(type='str', default='ActivePassive', choices=['ActivePassive', 'ALB', 'LACP']),
|
||||
bond_mode_10g=dict(type='str', default='ActivePassive', choices=['ActivePassive', 'ALB', 'LACP']),
|
||||
lacp_1g=dict(type='str', default='Slow', choices=['Fast', 'Slow']),
|
||||
lacp_10g=dict(type='str', default='Slow', choices=['Fast', 'Slow']),
|
||||
virtual_network_tag=dict(type='str'),
|
||||
)
|
||||
|
||||
self.module = AnsibleModule(
|
||||
argument_spec=self.argument_spec,
|
||||
supports_check_mode=True,
|
||||
)
|
||||
|
||||
input_params = self.module.params
|
||||
|
||||
self.method = input_params['method']
|
||||
self.ip_address_1g = input_params['ip_address_1g']
|
||||
self.ip_address_10g = input_params['ip_address_10g']
|
||||
self.subnet_1g = input_params['subnet_1g']
|
||||
self.subnet_10g = input_params['subnet_10g']
|
||||
self.gateway_address_1g = input_params['gateway_address_1g']
|
||||
self.gateway_address_10g = input_params['gateway_address_10g']
|
||||
self.mtu_1g = input_params['mtu_1g']
|
||||
self.mtu_10g = input_params['mtu_10g']
|
||||
self.dns_nameservers = input_params['dns_nameservers']
|
||||
self.dns_search_domains = input_params['dns_search_domains']
|
||||
self.bond_mode_1g = input_params['bond_mode_1g']
|
||||
self.bond_mode_10g = input_params['bond_mode_10g']
|
||||
self.lacp_1g = input_params['lacp_1g']
|
||||
self.lacp_10g = input_params['lacp_10g']
|
||||
self.virtual_network_tag = input_params['virtual_network_tag']
|
||||
|
||||
if HAS_SF_SDK is False:
|
||||
self.module.fail_json(msg="Unable to import the SolidFire Python SDK")
|
||||
else:
|
||||
self.sfe = netapp_utils.create_sf_connection(module=self.module, port=442)
|
||||
|
||||
def set_network_config(self):
|
||||
"""
|
||||
set network configuration
|
||||
"""
|
||||
try:
|
||||
self.sfe.set_network_config(network=self.network_object)
|
||||
except Exception as exception_object:
|
||||
self.module.fail_json(msg='Error network setting for node %s' % (to_native(exception_object)),
|
||||
exception=traceback.format_exc())
|
||||
|
||||
def get_network_params_object(self):
|
||||
"""
|
||||
Get Element SW Network object
|
||||
:description: get Network object
|
||||
|
||||
:return: NetworkConfig object
|
||||
:rtype: object(NetworkConfig object)
|
||||
"""
|
||||
try:
|
||||
bond_1g_network = NetworkConfig(method=self.method,
|
||||
address=self.ip_address_1g,
|
||||
netmask=self.subnet_1g,
|
||||
gateway=self.gateway_address_1g,
|
||||
mtu=self.mtu_1g,
|
||||
dns_nameservers=self.dns_nameservers,
|
||||
dns_search=self.dns_search_domains,
|
||||
bond_mode=self.bond_mode_1g,
|
||||
bond_lacp_rate=self.lacp_1g,
|
||||
virtual_network_tag=self.virtual_network_tag)
|
||||
bond_10g_network = NetworkConfig(method=self.method,
|
||||
address=self.ip_address_10g,
|
||||
netmask=self.subnet_10g,
|
||||
gateway=self.gateway_address_10g,
|
||||
mtu=self.mtu_10g,
|
||||
dns_nameservers=self.dns_nameservers,
|
||||
dns_search=self.dns_search_domains,
|
||||
bond_mode=self.bond_mode_10g,
|
||||
bond_lacp_rate=self.lacp_10g,
|
||||
virtual_network_tag=self.virtual_network_tag)
|
||||
network_object = Network(bond1_g=bond_1g_network,
|
||||
bond10_g=bond_10g_network)
|
||||
return network_object
|
||||
except Exception as e:
|
||||
self.module.fail_json(msg='Error with setting up network object for node 1G and 10G configuration : %s' % to_native(e),
|
||||
exception=to_native(e))
|
||||
|
||||
def apply(self):
|
||||
"""
|
||||
Check connection and initialize node with cluster ownership
|
||||
"""
|
||||
changed = False
|
||||
result_message = None
|
||||
self.network_object = self.get_network_params_object()
|
||||
if self.network_object is not None:
|
||||
self.set_network_config()
|
||||
changed = True
|
||||
else:
|
||||
result_message = "Skipping changes, No change requested"
|
||||
self.module.exit_json(changed=changed, msg=result_message)
|
||||
|
||||
|
||||
def main():
|
||||
"""
|
||||
Main function
|
||||
"""
|
||||
elementsw_network_interfaces = ElementSWNetworkInterfaces()
|
||||
elementsw_network_interfaces.apply()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
|
@ -1,239 +0,0 @@
|
|||
#!/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 Node Operation
|
||||
'''
|
||||
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_node
|
||||
|
||||
short_description: NetApp Element Software Node Operation
|
||||
extends_documentation_fragment:
|
||||
- netapp.solidfire
|
||||
version_added: '2.7'
|
||||
author: NetApp Ansible Team (@carchi8py) <ng-ansibleteam@netapp.com>
|
||||
description:
|
||||
- Add, remove cluster node on Element Software Cluster.
|
||||
|
||||
options:
|
||||
state:
|
||||
description:
|
||||
- Element Software Storage Node operation state.
|
||||
- present - To add pending node to participate in cluster data storage.
|
||||
- absent - To remove node from active cluster. A node cannot be removed if active drives are present.
|
||||
choices: ['present', 'absent']
|
||||
default: 'present'
|
||||
|
||||
node_id:
|
||||
description:
|
||||
- List of IDs or Names or IP Address of nodes from cluster used for operation.
|
||||
required: true
|
||||
|
||||
'''
|
||||
|
||||
EXAMPLES = """
|
||||
- name: Add node from pending to active cluster
|
||||
tags:
|
||||
- elementsw_add_node
|
||||
na_elementsw_node:
|
||||
hostname: "{{ elementsw_hostname }}"
|
||||
username: "{{ elementsw_username }}"
|
||||
password: "{{ elementsw_password }}"
|
||||
state: present
|
||||
node_id: sf4805-meg-03
|
||||
|
||||
- name: Remove active node from cluster
|
||||
tags:
|
||||
- elementsw_remove_node
|
||||
na_elementsw_node:
|
||||
hostname: "{{ elementsw_hostname }}"
|
||||
username: "{{ elementsw_username }}"
|
||||
password: "{{ elementsw_password }}"
|
||||
state: absent
|
||||
node_id: 13
|
||||
|
||||
- name: Add node from pending to active cluster using node IP
|
||||
tags:
|
||||
- elementsw_add_node_ip
|
||||
na_elementsw_node:
|
||||
hostname: "{{ elementsw_hostname }}"
|
||||
username: "{{ elementsw_username }}"
|
||||
password: "{{ elementsw_password }}"
|
||||
state: present
|
||||
node_id: 10.109.48.65
|
||||
"""
|
||||
|
||||
|
||||
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.module_utils.netapp as netapp_utils
|
||||
|
||||
|
||||
HAS_SF_SDK = netapp_utils.has_sf_sdk()
|
||||
|
||||
|
||||
class ElementSWNode(object):
|
||||
"""
|
||||
Element SW Storage Node operations
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
self.argument_spec = netapp_utils.ontap_sf_host_argument_spec()
|
||||
self.argument_spec.update(dict(
|
||||
state=dict(required=False, choices=['present', 'absent'], default='present'),
|
||||
node_id=dict(required=True, type='list'),
|
||||
))
|
||||
|
||||
self.module = AnsibleModule(
|
||||
argument_spec=self.argument_spec,
|
||||
supports_check_mode=True
|
||||
)
|
||||
|
||||
input_params = self.module.params
|
||||
|
||||
self.state = input_params['state']
|
||||
self.node_id = input_params['node_id']
|
||||
|
||||
if HAS_SF_SDK is False:
|
||||
self.module.fail_json(
|
||||
msg="Unable to import the SolidFire Python SDK")
|
||||
else:
|
||||
self.sfe = netapp_utils.create_sf_connection(module=self.module)
|
||||
|
||||
def check_node_has_active_drives(self, node_id=None):
|
||||
"""
|
||||
Check if node has active drives attached to cluster
|
||||
:description: Validate if node have active drives in cluster
|
||||
|
||||
:return: True or False
|
||||
:rtype: bool
|
||||
"""
|
||||
if node_id is not None:
|
||||
cluster_drives = self.sfe.list_drives()
|
||||
for drive in cluster_drives.drives:
|
||||
if drive.node_id == node_id and drive.status == "active":
|
||||
return True
|
||||
return False
|
||||
|
||||
def get_node_list(self):
|
||||
"""
|
||||
Get Node List
|
||||
:description: Find and retrieve node_id from the active cluster
|
||||
|
||||
:return: None
|
||||
:rtype: None
|
||||
"""
|
||||
if len(self.node_id) > 0:
|
||||
unprocessed_node_list = self.node_id
|
||||
list_nodes = []
|
||||
all_nodes = self.sfe.list_all_nodes()
|
||||
# For add operation lookup for nodes list with status pendingNodes list
|
||||
# else nodes will have to be traverse through active cluster
|
||||
if self.state == "present":
|
||||
list_nodes = all_nodes.pending_nodes
|
||||
else:
|
||||
list_nodes = all_nodes.nodes
|
||||
|
||||
for current_node in list_nodes:
|
||||
if self.state == "absent" and \
|
||||
(current_node.node_id in self.node_id or current_node.name in self.node_id or current_node.mip in self.node_id):
|
||||
if self.check_node_has_active_drives(current_node.node_id):
|
||||
self.module.fail_json(msg='Error deleting node %s: node has active drives' % current_node.name)
|
||||
else:
|
||||
self.action_nodes_list.append(current_node.node_id)
|
||||
if self.state == "present" and \
|
||||
(current_node.pending_node_id in self.node_id or current_node.name in self.node_id or current_node.mip in self.node_id):
|
||||
self.action_nodes_list.append(current_node.pending_node_id)
|
||||
|
||||
# report an error if state == present and node is unknown
|
||||
if self.state == "present":
|
||||
for current_node in all_nodes.nodes:
|
||||
if current_node.node_id in unprocessed_node_list:
|
||||
unprocessed_node_list.remove(current_node.node_id)
|
||||
elif current_node.name in unprocessed_node_list:
|
||||
unprocessed_node_list.remove(current_node.name)
|
||||
elif current_node.mip in unprocessed_node_list:
|
||||
unprocessed_node_list.remove(current_node.mip)
|
||||
for current_node in all_nodes.pending_nodes:
|
||||
if current_node.pending_node_id in unprocessed_node_list:
|
||||
unprocessed_node_list.remove(current_node.node_id)
|
||||
elif current_node.name in unprocessed_node_list:
|
||||
unprocessed_node_list.remove(current_node.name)
|
||||
elif current_node.mip in unprocessed_node_list:
|
||||
unprocessed_node_list.remove(current_node.mip)
|
||||
if len(unprocessed_node_list) > 0:
|
||||
self.module.fail_json(msg='Error adding node %s: node not in pending or active lists' % to_native(unprocessed_node_list))
|
||||
return None
|
||||
|
||||
def add_node(self, nodes_list=None):
|
||||
"""
|
||||
Add Node that are on PendingNodes list available on Cluster
|
||||
"""
|
||||
try:
|
||||
self.sfe.add_nodes(nodes_list,
|
||||
auto_install=True)
|
||||
except Exception as exception_object:
|
||||
self.module.fail_json(msg='Error add node to cluster %s' % (to_native(exception_object)),
|
||||
exception=traceback.format_exc())
|
||||
|
||||
def remove_node(self, nodes_list=None):
|
||||
"""
|
||||
Remove active node from Cluster
|
||||
"""
|
||||
try:
|
||||
self.sfe.remove_nodes(nodes_list)
|
||||
except Exception as exception_object:
|
||||
self.module.fail_json(msg='Error remove node from cluster %s' % (to_native(exception_object)),
|
||||
exception=traceback.format_exc())
|
||||
|
||||
def apply(self):
|
||||
"""
|
||||
Check, process and initiate Cluster Node operation
|
||||
"""
|
||||
changed = False
|
||||
self.action_nodes_list = []
|
||||
if self.module.check_mode is False:
|
||||
self.get_node_list()
|
||||
if self.state == "present" and len(self.action_nodes_list) > 0:
|
||||
self.add_node(self.action_nodes_list)
|
||||
changed = True
|
||||
elif self.state == "absent" and len(self.action_nodes_list) > 0:
|
||||
self.remove_node(self.action_nodes_list)
|
||||
changed = True
|
||||
result_message = 'List of nodes : %s - %s' % (to_native(self.action_nodes_list), to_native(self.node_id))
|
||||
self.module.exit_json(changed=changed, msg=result_message)
|
||||
|
||||
|
||||
def main():
|
||||
"""
|
||||
Main function
|
||||
"""
|
||||
|
||||
na_elementsw_node = ElementSWNode()
|
||||
na_elementsw_node.apply()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
|
@ -1,377 +0,0 @@
|
|||
#!/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 OS Software Snapshot Manager
|
||||
'''
|
||||
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_snapshot
|
||||
|
||||
short_description: NetApp Element Software Manage Snapshots
|
||||
extends_documentation_fragment:
|
||||
- netapp.solidfire
|
||||
version_added: '2.7'
|
||||
author: NetApp Ansible Team (@carchi8py) <ng-ansibleteam@netapp.com>
|
||||
description:
|
||||
- Create, Modify or Delete Snapshot on Element OS Cluster.
|
||||
|
||||
options:
|
||||
name:
|
||||
description:
|
||||
- Name of new snapshot create.
|
||||
- If unspecified, date and time when the snapshot was taken is used.
|
||||
|
||||
state:
|
||||
description:
|
||||
- Whether the specified snapshot should exist or not.
|
||||
choices: ['present', 'absent']
|
||||
default: 'present'
|
||||
|
||||
src_volume_id:
|
||||
description:
|
||||
- ID or Name of active volume.
|
||||
required: true
|
||||
|
||||
account_id:
|
||||
description:
|
||||
- Account ID or Name of Parent/Source Volume.
|
||||
required: true
|
||||
|
||||
retention:
|
||||
description:
|
||||
- Retention period for the snapshot.
|
||||
- Format is 'HH:mm:ss'.
|
||||
|
||||
src_snapshot_id:
|
||||
description:
|
||||
- ID or Name of an existing snapshot.
|
||||
- Required when C(state=present), to modify snapshot properties.
|
||||
- Required when C(state=present), to create snapshot from another snapshot in the volume.
|
||||
- Required when C(state=absent), to delete snapshot.
|
||||
|
||||
enable_remote_replication:
|
||||
description:
|
||||
- Flag, whether to replicate the snapshot created to a remote replication cluster.
|
||||
- To enable specify 'true' value.
|
||||
type: bool
|
||||
|
||||
snap_mirror_label:
|
||||
description:
|
||||
- Label used by SnapMirror software to specify snapshot retention policy on SnapMirror endpoint.
|
||||
|
||||
expiration_time:
|
||||
description:
|
||||
- The date and time (format ISO 8601 date string) at which this snapshot will expire.
|
||||
|
||||
password:
|
||||
description:
|
||||
- Element OS access account password
|
||||
aliases:
|
||||
- pass
|
||||
|
||||
username:
|
||||
description:
|
||||
- Element OS access account user-name
|
||||
aliases:
|
||||
- user
|
||||
|
||||
'''
|
||||
|
||||
EXAMPLES = """
|
||||
- name: Create snapshot
|
||||
tags:
|
||||
- elementsw_create_snapshot
|
||||
na_elementsw_snapshot:
|
||||
hostname: "{{ elementsw_hostname }}"
|
||||
username: "{{ elementsw_username }}"
|
||||
password: "{{ elementsw_password }}"
|
||||
state: present
|
||||
src_volume_id: 118
|
||||
account_id: sagarsh
|
||||
name: newsnapshot-1
|
||||
|
||||
- name: Modify Snapshot
|
||||
tags:
|
||||
- elementsw_modify_snapshot
|
||||
na_elementsw_snapshot:
|
||||
hostname: "{{ elementsw_hostname }}"
|
||||
username: "{{ elementsw_username }}"
|
||||
password: "{{ elementsw_password }}"
|
||||
state: present
|
||||
src_volume_id: sagarshansivolume
|
||||
src_snapshot_id: test1
|
||||
account_id: sagarsh
|
||||
expiration_time: '2018-06-16T12:24:56Z'
|
||||
enable_remote_replication: false
|
||||
|
||||
- name: Delete Snapshot
|
||||
tags:
|
||||
- elementsw_delete_snapshot
|
||||
na_elementsw_snapshot:
|
||||
hostname: "{{ elementsw_hostname }}"
|
||||
username: "{{ elementsw_username }}"
|
||||
password: "{{ elementsw_password }}"
|
||||
state: absent
|
||||
src_snapshot_id: deltest1
|
||||
account_id: sagarsh
|
||||
src_volume_id: sagarshansivolume
|
||||
"""
|
||||
|
||||
|
||||
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.module_utils.netapp as netapp_utils
|
||||
from ansible.module_utils.netapp_elementsw_module import NaElementSWModule
|
||||
|
||||
|
||||
HAS_SF_SDK = netapp_utils.has_sf_sdk()
|
||||
|
||||
|
||||
class ElementOSSnapshot(object):
|
||||
"""
|
||||
Element OS Snapshot Manager
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
self.argument_spec = netapp_utils.ontap_sf_host_argument_spec()
|
||||
self.argument_spec.update(dict(
|
||||
state=dict(required=False, choices=['present', 'absent'], default='present'),
|
||||
account_id=dict(required=True, type='str'),
|
||||
name=dict(required=False, type='str'),
|
||||
src_volume_id=dict(required=True, type='str'),
|
||||
retention=dict(required=False, type='str'),
|
||||
src_snapshot_id=dict(required=False, type='str'),
|
||||
enable_remote_replication=dict(required=False, type='bool'),
|
||||
expiration_time=dict(required=False, type='str'),
|
||||
snap_mirror_label=dict(required=False, type='str')
|
||||
))
|
||||
|
||||
self.module = AnsibleModule(
|
||||
argument_spec=self.argument_spec,
|
||||
supports_check_mode=True
|
||||
)
|
||||
|
||||
input_params = self.module.params
|
||||
|
||||
self.state = input_params['state']
|
||||
self.name = input_params['name']
|
||||
self.account_id = input_params['account_id']
|
||||
self.src_volume_id = input_params['src_volume_id']
|
||||
self.src_snapshot_id = input_params['src_snapshot_id']
|
||||
self.retention = input_params['retention']
|
||||
self.properties_provided = False
|
||||
|
||||
self.expiration_time = input_params['expiration_time']
|
||||
if input_params['expiration_time'] is not None:
|
||||
self.properties_provided = True
|
||||
|
||||
self.enable_remote_replication = input_params['enable_remote_replication']
|
||||
if input_params['enable_remote_replication'] is not None:
|
||||
self.properties_provided = True
|
||||
|
||||
self.snap_mirror_label = input_params['snap_mirror_label']
|
||||
if input_params['snap_mirror_label'] is not None:
|
||||
self.properties_provided = True
|
||||
|
||||
if self.state == 'absent' and self.src_snapshot_id is None:
|
||||
self.module.fail_json(
|
||||
msg="Please provide required parameter : snapshot_id")
|
||||
|
||||
if HAS_SF_SDK is False:
|
||||
self.module.fail_json(
|
||||
msg="Unable to import the SolidFire Python SDK")
|
||||
else:
|
||||
self.sfe = netapp_utils.create_sf_connection(module=self.module)
|
||||
|
||||
self.elementsw_helper = NaElementSWModule(self.sfe)
|
||||
|
||||
# add telemetry attributes
|
||||
self.attributes = self.elementsw_helper.set_element_attributes(source='na_elementsw_snapshot')
|
||||
|
||||
def get_account_id(self):
|
||||
"""
|
||||
Return account id if found
|
||||
"""
|
||||
try:
|
||||
# Update and return self.account_id
|
||||
self.account_id = self.elementsw_helper.account_exists(self.account_id)
|
||||
return self.account_id
|
||||
except Exception as err:
|
||||
self.module.fail_json(msg="Error: account_id %s does not exist" % self.account_id, exception=to_native(err))
|
||||
|
||||
def get_src_volume_id(self):
|
||||
"""
|
||||
Return volume id if found
|
||||
"""
|
||||
src_vol_id = self.elementsw_helper.volume_exists(self.src_volume_id, self.account_id)
|
||||
if src_vol_id is not None:
|
||||
# Update and return self.volume_id
|
||||
self.src_volume_id = src_vol_id
|
||||
# Return src_volume_id
|
||||
return self.src_volume_id
|
||||
return None
|
||||
|
||||
def get_snapshot(self, name=None):
|
||||
"""
|
||||
Return snapshot details if found
|
||||
"""
|
||||
src_snapshot = None
|
||||
if name is not None:
|
||||
src_snapshot = self.elementsw_helper.get_snapshot(name, self.src_volume_id)
|
||||
elif self.src_snapshot_id is not None:
|
||||
src_snapshot = self.elementsw_helper.get_snapshot(self.src_snapshot_id, self.src_volume_id)
|
||||
if src_snapshot is not None:
|
||||
# Update self.src_snapshot_id
|
||||
self.src_snapshot_id = src_snapshot.snapshot_id
|
||||
# Return src_snapshot
|
||||
return src_snapshot
|
||||
|
||||
def create_snapshot(self):
|
||||
"""
|
||||
Create Snapshot
|
||||
"""
|
||||
try:
|
||||
self.sfe.create_snapshot(volume_id=self.src_volume_id,
|
||||
snapshot_id=self.src_snapshot_id,
|
||||
name=self.name,
|
||||
enable_remote_replication=self.enable_remote_replication,
|
||||
retention=self.retention,
|
||||
snap_mirror_label=self.snap_mirror_label,
|
||||
attributes=self.attributes)
|
||||
except Exception as exception_object:
|
||||
self.module.fail_json(
|
||||
msg='Error creating snapshot %s' % (
|
||||
to_native(exception_object)),
|
||||
exception=traceback.format_exc())
|
||||
|
||||
def modify_snapshot(self):
|
||||
"""
|
||||
Modify Snapshot Properties
|
||||
"""
|
||||
try:
|
||||
self.sfe.modify_snapshot(snapshot_id=self.src_snapshot_id,
|
||||
expiration_time=self.expiration_time,
|
||||
enable_remote_replication=self.enable_remote_replication,
|
||||
snap_mirror_label=self.snap_mirror_label)
|
||||
except Exception as exception_object:
|
||||
self.module.fail_json(
|
||||
msg='Error modify snapshot %s' % (
|
||||
to_native(exception_object)),
|
||||
exception=traceback.format_exc())
|
||||
|
||||
def delete_snapshot(self):
|
||||
"""
|
||||
Delete Snapshot
|
||||
"""
|
||||
try:
|
||||
self.sfe.delete_snapshot(snapshot_id=self.src_snapshot_id)
|
||||
except Exception as exception_object:
|
||||
self.module.fail_json(
|
||||
msg='Error delete snapshot %s' % (
|
||||
to_native(exception_object)),
|
||||
exception=traceback.format_exc())
|
||||
|
||||
def apply(self):
|
||||
"""
|
||||
Check, process and initiate snapshot operation
|
||||
"""
|
||||
changed = False
|
||||
snapshot_delete = False
|
||||
snapshot_create = False
|
||||
snapshot_modify = False
|
||||
result_message = None
|
||||
self.get_account_id()
|
||||
|
||||
# Dont proceed if source volume is not found
|
||||
if self.get_src_volume_id() is None:
|
||||
self.module.fail_json(msg="Volume id not found %s" % self.src_volume_id)
|
||||
|
||||
# Get snapshot details using source volume
|
||||
snapshot_detail = self.get_snapshot()
|
||||
|
||||
if snapshot_detail:
|
||||
if self.properties_provided:
|
||||
if self.expiration_time != snapshot_detail.expiration_time:
|
||||
changed = True
|
||||
else: # To preserve value in case parameter expiration_time is not defined/provided.
|
||||
self.expiration_time = snapshot_detail.expiration_time
|
||||
|
||||
if self.enable_remote_replication != snapshot_detail.enable_remote_replication:
|
||||
changed = True
|
||||
else: # To preserve value in case parameter enable_remote_Replication is not defined/provided.
|
||||
self.enable_remote_replication = snapshot_detail.enable_remote_replication
|
||||
|
||||
if self.snap_mirror_label != snapshot_detail.snap_mirror_label:
|
||||
changed = True
|
||||
else: # To preserve value in case parameter snap_mirror_label is not defined/provided.
|
||||
self.snap_mirror_label = snapshot_detail.snap_mirror_label
|
||||
|
||||
if self.account_id is None or self.src_volume_id is None or self.module.check_mode:
|
||||
changed = False
|
||||
result_message = "Check mode, skipping changes"
|
||||
elif self.state == 'absent' and snapshot_detail is not None:
|
||||
self.delete_snapshot()
|
||||
changed = True
|
||||
elif self.state == 'present' and snapshot_detail is not None:
|
||||
if changed:
|
||||
self.modify_snapshot() # Modify Snapshot properties
|
||||
elif not self.properties_provided:
|
||||
if self.name is not None:
|
||||
snapshot = self.get_snapshot(self.name)
|
||||
# If snapshot with name already exists return without performing any action
|
||||
if snapshot is None:
|
||||
self.create_snapshot() # Create Snapshot using parent src_snapshot_id
|
||||
changed = True
|
||||
else:
|
||||
self.create_snapshot()
|
||||
changed = True
|
||||
elif self.state == 'present':
|
||||
if self.name is not None:
|
||||
snapshot = self.get_snapshot(self.name)
|
||||
# If snapshot with name already exists return without performing any action
|
||||
if snapshot is None:
|
||||
self.create_snapshot() # Create Snapshot using parent src_snapshot_id
|
||||
changed = True
|
||||
else:
|
||||
self.create_snapshot()
|
||||
changed = True
|
||||
else:
|
||||
changed = False
|
||||
result_message = "No changes requested, skipping changes"
|
||||
|
||||
self.module.exit_json(changed=changed, msg=result_message)
|
||||
|
||||
|
||||
def main():
|
||||
"""
|
||||
Main function
|
||||
"""
|
||||
|
||||
na_elementsw_snapshot = ElementOSSnapshot()
|
||||
na_elementsw_snapshot.apply()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
|
@ -1,201 +0,0 @@
|
|||
#!/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 Snapshot Restore
|
||||
"""
|
||||
|
||||
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_snapshot_restore
|
||||
|
||||
short_description: NetApp Element Software Restore Snapshot
|
||||
extends_documentation_fragment:
|
||||
- netapp.solidfire
|
||||
version_added: '2.7'
|
||||
author: NetApp Ansible Team (@carchi8py) <ng-ansibleteam@netapp.com>
|
||||
description:
|
||||
- Element OS Cluster restore snapshot to volume.
|
||||
|
||||
options:
|
||||
|
||||
src_volume_id:
|
||||
description:
|
||||
- ID or Name of source active volume.
|
||||
required: true
|
||||
|
||||
src_snapshot_id:
|
||||
description:
|
||||
- ID or Name of an existing snapshot.
|
||||
required: true
|
||||
|
||||
dest_volume_name:
|
||||
description:
|
||||
- New Name of destination for restoring the snapshot
|
||||
required: true
|
||||
|
||||
account_id:
|
||||
description:
|
||||
- Account ID or Name of Parent/Source Volume.
|
||||
required: true
|
||||
|
||||
'''
|
||||
|
||||
EXAMPLES = """
|
||||
- name: Restore snapshot to volume
|
||||
tags:
|
||||
- elementsw_create_snapshot_restore
|
||||
na_elementsw_snapshot_restore:
|
||||
hostname: "{{ elementsw_hostname }}"
|
||||
username: "{{ elementsw_username }}"
|
||||
password: "{{ elementsw_password }}"
|
||||
account_id: ansible-1
|
||||
src_snapshot_id: snapshot_20171021
|
||||
src_volume_id: volume-playarea
|
||||
dest_volume_name: dest-volume-area
|
||||
|
||||
"""
|
||||
|
||||
|
||||
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.module_utils.netapp as netapp_utils
|
||||
from ansible.module_utils.netapp_elementsw_module import NaElementSWModule
|
||||
|
||||
HAS_SF_SDK = netapp_utils.has_sf_sdk()
|
||||
|
||||
|
||||
class ElementOSSnapshotRestore(object):
|
||||
"""
|
||||
Element OS Restore from snapshot
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
self.argument_spec = netapp_utils.ontap_sf_host_argument_spec()
|
||||
self.argument_spec.update(dict(
|
||||
account_id=dict(required=True, type='str'),
|
||||
src_volume_id=dict(required=True, type='str'),
|
||||
dest_volume_name=dict(required=True, type='str'),
|
||||
src_snapshot_id=dict(required=True, type='str')
|
||||
))
|
||||
|
||||
self.module = AnsibleModule(
|
||||
argument_spec=self.argument_spec,
|
||||
supports_check_mode=True
|
||||
)
|
||||
|
||||
input_params = self.module.params
|
||||
|
||||
self.account_id = input_params['account_id']
|
||||
self.src_volume_id = input_params['src_volume_id']
|
||||
self.dest_volume_name = input_params['dest_volume_name']
|
||||
self.src_snapshot_id = input_params['src_snapshot_id']
|
||||
|
||||
if HAS_SF_SDK is False:
|
||||
self.module.fail_json(
|
||||
msg="Unable to import the SolidFire Python SDK")
|
||||
else:
|
||||
self.sfe = netapp_utils.create_sf_connection(module=self.module)
|
||||
|
||||
self.elementsw_helper = NaElementSWModule(self.sfe)
|
||||
|
||||
# add telemetry attributes
|
||||
self.attributes = self.elementsw_helper.set_element_attributes(source='na_elementsw_snapshot_restore')
|
||||
|
||||
def get_account_id(self):
|
||||
"""
|
||||
Get account id if found
|
||||
"""
|
||||
try:
|
||||
# Update and return self.account_id
|
||||
self.account_id = self.elementsw_helper.account_exists(self.account_id)
|
||||
return self.account_id
|
||||
except Exception as err:
|
||||
self.module.fail_json(msg="Error: account_id %s does not exist" % self.account_id, exception=to_native(err))
|
||||
|
||||
def get_snapshot_id(self):
|
||||
"""
|
||||
Return snapshot details if found
|
||||
"""
|
||||
src_snapshot = self.elementsw_helper.get_snapshot(self.src_snapshot_id, self.src_volume_id)
|
||||
# Update and return self.src_snapshot_id
|
||||
if src_snapshot:
|
||||
self.src_snapshot_id = src_snapshot.snapshot_id
|
||||
# Return self.src_snapshot_id
|
||||
return self.src_snapshot_id
|
||||
return None
|
||||
|
||||
def restore_snapshot(self):
|
||||
"""
|
||||
Restore Snapshot to Volume
|
||||
"""
|
||||
try:
|
||||
self.sfe.clone_volume(volume_id=self.src_volume_id,
|
||||
name=self.dest_volume_name,
|
||||
snapshot_id=self.src_snapshot_id,
|
||||
attributes=self.attributes)
|
||||
except Exception as exception_object:
|
||||
self.module.fail_json(
|
||||
msg='Error restore snapshot %s' % (to_native(exception_object)),
|
||||
exception=traceback.format_exc())
|
||||
|
||||
def apply(self):
|
||||
"""
|
||||
Check, process and initiate restore snapshot to volume operation
|
||||
"""
|
||||
changed = False
|
||||
result_message = None
|
||||
snapshot_detail = None
|
||||
self.get_account_id()
|
||||
src_vol_id = self.elementsw_helper.volume_exists(self.src_volume_id, self.account_id)
|
||||
|
||||
if src_vol_id is not None:
|
||||
# Update self.src_volume_id
|
||||
self.src_volume_id = src_vol_id
|
||||
if self.get_snapshot_id() is not None:
|
||||
# Addressing idempotency by comparing volume does not exist with same volume name
|
||||
if self.elementsw_helper.volume_exists(self.dest_volume_name, self.account_id) is None:
|
||||
self.restore_snapshot()
|
||||
changed = True
|
||||
else:
|
||||
result_message = "No changes requested, Skipping changes"
|
||||
else:
|
||||
self.module.fail_json(msg="Snapshot id not found %s" % self.src_snapshot_id)
|
||||
else:
|
||||
self.module.fail_json(msg="Volume id not found %s" % self.src_volume_id)
|
||||
|
||||
self.module.exit_json(changed=changed, msg=result_message)
|
||||
|
||||
|
||||
def main():
|
||||
"""
|
||||
Main function
|
||||
"""
|
||||
na_elementsw_snapshot_restore = ElementOSSnapshotRestore()
|
||||
na_elementsw_snapshot_restore.apply()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
|
@ -1,555 +0,0 @@
|
|||
#!/usr/bin/python
|
||||
# (c) 2017, NetApp, Inc
|
||||
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
|
||||
"""Element SW Software Snapshot Schedule"""
|
||||
|
||||
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_snapshot_schedule
|
||||
|
||||
short_description: NetApp Element Software Snapshot Schedules
|
||||
extends_documentation_fragment:
|
||||
- netapp.solidfire
|
||||
version_added: '2.7'
|
||||
author: NetApp Ansible Team (@carchi8py) <ng-ansibleteam@netapp.com>
|
||||
description:
|
||||
- Create, destroy, or update accounts on ElementSW
|
||||
|
||||
options:
|
||||
|
||||
state:
|
||||
description:
|
||||
- Whether the specified schedule should exist or not.
|
||||
required: true
|
||||
choices: ['present', 'absent']
|
||||
|
||||
paused:
|
||||
description:
|
||||
- Pause / Resume a schedule.
|
||||
type: bool
|
||||
|
||||
recurring:
|
||||
description:
|
||||
- Should the schedule recur?
|
||||
type: bool
|
||||
|
||||
schedule_type:
|
||||
description:
|
||||
- Schedule type for creating schedule.
|
||||
choices: ['DaysOfWeekFrequency','DaysOfMonthFrequency','TimeIntervalFrequency']
|
||||
|
||||
time_interval_days:
|
||||
description: Time interval in days.
|
||||
default: 1
|
||||
|
||||
time_interval_hours:
|
||||
description: Time interval in hours.
|
||||
default: 0
|
||||
|
||||
time_interval_minutes:
|
||||
description: Time interval in minutes.
|
||||
default: 0
|
||||
|
||||
days_of_week_weekdays:
|
||||
description: List of days of the week (Sunday to Saturday)
|
||||
|
||||
days_of_week_hours:
|
||||
description: Time specified in hours
|
||||
default: 0
|
||||
|
||||
days_of_week_minutes:
|
||||
description: Time specified in minutes.
|
||||
default: 0
|
||||
|
||||
days_of_month_monthdays:
|
||||
description: List of days of the month (1-31)
|
||||
|
||||
days_of_month_hours:
|
||||
description: Time specified in hours
|
||||
default: 0
|
||||
|
||||
days_of_month_minutes:
|
||||
description: Time specified in minutes.
|
||||
default: 0
|
||||
|
||||
name:
|
||||
description:
|
||||
- Name for the snapshot schedule.
|
||||
- It accepts either schedule_id or schedule_name
|
||||
- if name is digit, it will consider as schedule_id
|
||||
- If name is string, it will consider as schedule_name
|
||||
|
||||
snapshot_name:
|
||||
description:
|
||||
- Name for the created snapshots.
|
||||
|
||||
volumes:
|
||||
description:
|
||||
- Volume IDs that you want to set the snapshot schedule for.
|
||||
- It accepts both volume_name and volume_id
|
||||
|
||||
account_id:
|
||||
description:
|
||||
- Account ID for the owner of this volume.
|
||||
- It accepts either account_name or account_id
|
||||
- if account_id is digit, it will consider as account_id
|
||||
- If account_id is string, it will consider as account_name
|
||||
|
||||
retention:
|
||||
description:
|
||||
- Retention period for the snapshot.
|
||||
- Format is 'HH:mm:ss'.
|
||||
|
||||
starting_date:
|
||||
description:
|
||||
- Starting date for the schedule.
|
||||
- Required when C(state=present).
|
||||
- "Format: C(2016-12-01T00:00:00Z)"
|
||||
|
||||
|
||||
password:
|
||||
description:
|
||||
- Element SW access account password
|
||||
aliases:
|
||||
- pass
|
||||
|
||||
username:
|
||||
description:
|
||||
- Element SW access account user-name
|
||||
aliases:
|
||||
- user
|
||||
'''
|
||||
|
||||
EXAMPLES = """
|
||||
- name: Create Snapshot schedule
|
||||
na_elementsw_snapshot_schedule:
|
||||
hostname: "{{ elementsw_hostname }}"
|
||||
username: "{{ elementsw_username }}"
|
||||
password: "{{ elementsw_password }}"
|
||||
state: present
|
||||
name: Schedule_A
|
||||
schedule_type: TimeIntervalFrequency
|
||||
time_interval_days: 1
|
||||
starting_date: '2016-12-01T00:00:00Z'
|
||||
volumes:
|
||||
- 7
|
||||
- test
|
||||
account_id: 1
|
||||
|
||||
- name: Update Snapshot schedule
|
||||
na_elementsw_snapshot_schedule:
|
||||
hostname: "{{ elementsw_hostname }}"
|
||||
username: "{{ elementsw_username }}"
|
||||
password: "{{ elementsw_password }}"
|
||||
state: present
|
||||
name: Schedule_A
|
||||
schedule_type: TimeIntervalFrequency
|
||||
time_interval_days: 1
|
||||
starting_date: '2016-12-01T00:00:00Z'
|
||||
volumes:
|
||||
- 8
|
||||
- test1
|
||||
account_id: 1
|
||||
|
||||
- name: Delete Snapshot schedule
|
||||
na_elementsw_snapshot_schedule:
|
||||
hostname: "{{ elementsw_hostname }}"
|
||||
username: "{{ elementsw_username }}"
|
||||
password: "{{ elementsw_password }}"
|
||||
state: absent
|
||||
name: 6
|
||||
"""
|
||||
|
||||
RETURN = """
|
||||
|
||||
schedule_id:
|
||||
description: Schedule ID of the newly created schedule
|
||||
returned: success
|
||||
type: str
|
||||
"""
|
||||
import traceback
|
||||
from ansible.module_utils.basic import AnsibleModule
|
||||
from ansible.module_utils._text import to_native
|
||||
import ansible.module_utils.netapp as netapp_utils
|
||||
from ansible.module_utils.netapp_elementsw_module import NaElementSWModule
|
||||
|
||||
HAS_SF_SDK = netapp_utils.has_sf_sdk()
|
||||
try:
|
||||
from solidfire.custom.models import DaysOfWeekFrequency, Weekday, DaysOfMonthFrequency
|
||||
from solidfire.common import ApiServerError
|
||||
except Exception:
|
||||
HAS_SF_SDK = False
|
||||
|
||||
|
||||
class ElementSWSnapShotSchedule(object):
|
||||
"""
|
||||
Contains methods to parse arguments,
|
||||
derive details of ElementSW objects
|
||||
and send requests to ElementSW via
|
||||
the ElementSW SDK
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
"""
|
||||
Parse arguments, setup state variables,
|
||||
check parameters and ensure SDK is installed
|
||||
"""
|
||||
self.argument_spec = netapp_utils.ontap_sf_host_argument_spec()
|
||||
self.argument_spec.update(dict(
|
||||
state=dict(required=True, choices=['present', 'absent']),
|
||||
name=dict(required=True, type='str'),
|
||||
schedule_type=dict(required=False, choices=['DaysOfWeekFrequency', 'DaysOfMonthFrequency', 'TimeIntervalFrequency']),
|
||||
|
||||
time_interval_days=dict(required=False, type='int', default=1),
|
||||
time_interval_hours=dict(required=False, type='int', default=0),
|
||||
time_interval_minutes=dict(required=False, type='int', default=0),
|
||||
|
||||
days_of_week_weekdays=dict(required=False, type='list'),
|
||||
days_of_week_hours=dict(required=False, type='int', default=0),
|
||||
days_of_week_minutes=dict(required=False, type='int', default=0),
|
||||
|
||||
days_of_month_monthdays=dict(required=False, type='list'),
|
||||
days_of_month_hours=dict(required=False, type='int', default=0),
|
||||
days_of_month_minutes=dict(required=False, type='int', default=0),
|
||||
|
||||
paused=dict(required=False, type='bool'),
|
||||
recurring=dict(required=False, type='bool'),
|
||||
|
||||
starting_date=dict(required=False, type='str'),
|
||||
|
||||
snapshot_name=dict(required=False, type='str'),
|
||||
volumes=dict(required=False, type='list'),
|
||||
account_id=dict(required=False, type='str'),
|
||||
retention=dict(required=False, type='str'),
|
||||
))
|
||||
|
||||
self.module = AnsibleModule(
|
||||
argument_spec=self.argument_spec,
|
||||
required_if=[
|
||||
('state', 'present', ['account_id', 'volumes', 'schedule_type']),
|
||||
('schedule_type', 'DaysOfMonthFrequency', ['days_of_month_monthdays']),
|
||||
('schedule_type', 'DaysOfWeekFrequency', ['days_of_week_weekdays'])
|
||||
|
||||
],
|
||||
supports_check_mode=True
|
||||
)
|
||||
|
||||
param = self.module.params
|
||||
|
||||
# set up state variables
|
||||
self.state = param['state']
|
||||
self.name = param['name']
|
||||
self.schedule_type = param['schedule_type']
|
||||
self.days_of_week_weekdays = param['days_of_week_weekdays']
|
||||
self.days_of_week_hours = param['days_of_week_hours']
|
||||
self.days_of_week_minutes = param['days_of_week_minutes']
|
||||
self.days_of_month_monthdays = param['days_of_month_monthdays']
|
||||
self.days_of_month_hours = param['days_of_month_hours']
|
||||
self.days_of_month_minutes = param['days_of_month_minutes']
|
||||
self.time_interval_days = param['time_interval_days']
|
||||
self.time_interval_hours = param['time_interval_hours']
|
||||
self.time_interval_minutes = param['time_interval_minutes']
|
||||
self.paused = param['paused']
|
||||
self.recurring = param['recurring']
|
||||
if self.schedule_type == 'DaysOfWeekFrequency':
|
||||
# Create self.weekday list if self.schedule_type is days_of_week
|
||||
if self.days_of_week_weekdays is not None:
|
||||
# Create self.weekday list if self.schedule_type is days_of_week
|
||||
self.weekdays = []
|
||||
for day in self.days_of_week_weekdays:
|
||||
if str(day).isdigit():
|
||||
# If id specified, return appropriate day
|
||||
self.weekdays.append(Weekday.from_id(int(day)))
|
||||
else:
|
||||
# If name specified, return appropriate day
|
||||
self.weekdays.append(Weekday.from_name(day.capitalize()))
|
||||
|
||||
if self.state == 'present' and self.schedule_type is None:
|
||||
# Mandate schedule_type for create operation
|
||||
self.module.fail_json(
|
||||
msg="Please provide required parameter: schedule_type")
|
||||
|
||||
# Mandate schedule name for delete operation
|
||||
if self.state == 'absent' and self.name is None:
|
||||
self.module.fail_json(
|
||||
msg="Please provide required parameter: name")
|
||||
|
||||
self.starting_date = param['starting_date']
|
||||
self.snapshot_name = param['snapshot_name']
|
||||
self.volumes = param['volumes']
|
||||
self.account_id = param['account_id']
|
||||
self.retention = param['retention']
|
||||
self.create_schedule_result = None
|
||||
|
||||
if HAS_SF_SDK is False:
|
||||
# Create ElementSW connection
|
||||
self.module.fail_json(msg="Unable to import the ElementSW Python SDK")
|
||||
else:
|
||||
self.sfe = netapp_utils.create_sf_connection(module=self.module)
|
||||
self.elementsw_helper = NaElementSWModule(self.sfe)
|
||||
|
||||
def get_schedule(self):
|
||||
# Checking whether schedule id is exist or not
|
||||
# Return schedule details if found, None otherwise
|
||||
# If exist set variable self.name
|
||||
try:
|
||||
schedule_list = self.sfe.list_schedules()
|
||||
except ApiServerError:
|
||||
return None
|
||||
|
||||
for schedule in schedule_list.schedules:
|
||||
if str(schedule.schedule_id) == self.name:
|
||||
self.name = schedule.name
|
||||
return schedule
|
||||
elif schedule.name == self.name:
|
||||
return schedule
|
||||
return None
|
||||
|
||||
def get_account_id(self):
|
||||
# Validate account id
|
||||
# Return account_id if found, None otherwise
|
||||
try:
|
||||
account_id = self.elementsw_helper.account_exists(self.account_id)
|
||||
return account_id
|
||||
except ApiServerError:
|
||||
return None
|
||||
|
||||
def get_volume_id(self):
|
||||
# Validate volume_ids
|
||||
# Return volume ids if found, fail if not found
|
||||
volume_ids = []
|
||||
for volume in self.volumes:
|
||||
volume_id = self.elementsw_helper.volume_exists(volume.strip(), self.account_id)
|
||||
if volume_id:
|
||||
volume_ids.append(volume_id)
|
||||
else:
|
||||
self.module.fail_json(msg='Specified volume %s does not exist' % volume)
|
||||
return volume_ids
|
||||
|
||||
def get_frequency(self):
|
||||
# Configuring frequency depends on self.schedule_type
|
||||
frequency = None
|
||||
if self.schedule_type is not None and self.schedule_type == 'DaysOfWeekFrequency':
|
||||
if self.weekdays is not None:
|
||||
frequency = DaysOfWeekFrequency(weekdays=self.weekdays,
|
||||
hours=self.days_of_week_hours,
|
||||
minutes=self.days_of_week_minutes)
|
||||
elif self.schedule_type is not None and self.schedule_type == 'DaysOfMonthFrequency':
|
||||
if self.days_of_month_monthdays is not None:
|
||||
frequency = DaysOfMonthFrequency(monthdays=self.days_of_month_monthdays,
|
||||
hours=self.days_of_month_hours,
|
||||
minutes=self.days_of_month_minutes)
|
||||
elif self.schedule_type is not None and self.schedule_type == 'TimeIntervalFrequency':
|
||||
frequency = netapp_utils.TimeIntervalFrequency(days=self.time_interval_days,
|
||||
hours=self.time_interval_hours,
|
||||
minutes=self.time_interval_minutes)
|
||||
return frequency
|
||||
|
||||
def is_same_schedule_type(self, schedule_detail):
|
||||
# To check schedule type is same or not
|
||||
if str(schedule_detail.frequency).split('(')[0] == self.schedule_type:
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
def create_schedule(self):
|
||||
# Create schedule
|
||||
try:
|
||||
frequency = self.get_frequency()
|
||||
if frequency is None:
|
||||
self.module.fail_json(msg='Failed to create schedule frequency object - type %s parameters' % self.schedule_type)
|
||||
|
||||
# Create schedule
|
||||
name = self.name
|
||||
schedule_info = netapp_utils.ScheduleInfo(
|
||||
volume_ids=self.volumes,
|
||||
snapshot_name=self.snapshot_name,
|
||||
retention=self.retention
|
||||
)
|
||||
|
||||
sched = netapp_utils.Schedule(schedule_info, name, frequency)
|
||||
sched.paused = self.paused
|
||||
sched.recurring = self.recurring
|
||||
sched.starting_date = self.starting_date
|
||||
|
||||
self.create_schedule_result = self.sfe.create_schedule(sched)
|
||||
|
||||
except Exception as e:
|
||||
self.module.fail_json(msg='Error creating schedule %s: %s' % (self.name, to_native(e.message)),
|
||||
exception=traceback.format_exc())
|
||||
|
||||
def delete_schedule(self, schedule_id):
|
||||
# delete schedule
|
||||
try:
|
||||
get_schedule_result = self.sfe.get_schedule(schedule_id=schedule_id)
|
||||
sched = get_schedule_result.schedule
|
||||
sched.to_be_deleted = True
|
||||
self.sfe.modify_schedule(schedule=sched)
|
||||
|
||||
except Exception as e:
|
||||
self.module.fail_json(msg='Error deleting schedule %s: %s' % (self.name, to_native(e.message)),
|
||||
exception=traceback.format_exc())
|
||||
|
||||
def update_schedule(self, schedule_id):
|
||||
# Update schedule
|
||||
try:
|
||||
get_schedule_result = self.sfe.get_schedule(schedule_id=schedule_id)
|
||||
sched = get_schedule_result.schedule
|
||||
# Update schedule properties
|
||||
sched.frequency = self.get_frequency()
|
||||
if sched.frequency is None:
|
||||
self.module.fail_json(msg='Failed to create schedule frequency object - type %s parameters' % self.schedule_type)
|
||||
|
||||
if self.volumes is not None and len(self.volumes) > 0:
|
||||
sched.schedule_info.volume_ids = self.volumes
|
||||
if self.retention is not None:
|
||||
sched.schedule_info.retention = self.retention
|
||||
if self.snapshot_name is not None:
|
||||
sched.schedule_info.snapshot_name = self.snapshot_name
|
||||
if self.paused is not None:
|
||||
sched.paused = self.paused
|
||||
if self.recurring is not None:
|
||||
sched.recurring = self.recurring
|
||||
if self.starting_date is not None:
|
||||
sched.starting_date = self.starting_date
|
||||
|
||||
# Make API call
|
||||
self.sfe.modify_schedule(schedule=sched)
|
||||
|
||||
except Exception as e:
|
||||
self.module.fail_json(msg='Error updating schedule %s: %s' % (self.name, to_native(e.message)),
|
||||
exception=traceback.format_exc())
|
||||
|
||||
def apply(self):
|
||||
# Perform pre-checks, call functions and exit
|
||||
|
||||
changed = False
|
||||
update_schedule = False
|
||||
|
||||
if self.account_id is not None:
|
||||
self.account_id = self.get_account_id()
|
||||
|
||||
if self.state == 'present' and self.volumes is not None:
|
||||
if self.account_id:
|
||||
self.volumes = self.get_volume_id()
|
||||
else:
|
||||
self.module.fail_json(msg='Specified account id does not exist')
|
||||
|
||||
# Getting the schedule details
|
||||
schedule_detail = self.get_schedule()
|
||||
|
||||
if schedule_detail is None and self.state == 'present':
|
||||
if len(self.volumes) > 0:
|
||||
changed = True
|
||||
else:
|
||||
self.module.fail_json(msg='Specified volumes not on cluster')
|
||||
elif schedule_detail is not None:
|
||||
# Getting the schedule id
|
||||
if self.state == 'absent':
|
||||
changed = True
|
||||
else:
|
||||
# Check if we need to update the account
|
||||
if self.retention is not None and schedule_detail.schedule_info.retention != self.retention:
|
||||
update_schedule = True
|
||||
changed = True
|
||||
elif self.snapshot_name is not None and schedule_detail.schedule_info.snapshot_name != self.snapshot_name:
|
||||
update_schedule = True
|
||||
changed = True
|
||||
elif self.paused is not None and schedule_detail.paused != self.paused:
|
||||
update_schedule = True
|
||||
changed = True
|
||||
elif self.recurring is not None and schedule_detail.recurring != self.recurring:
|
||||
update_schedule = True
|
||||
changed = True
|
||||
elif self.starting_date is not None and schedule_detail.starting_date != self.starting_date:
|
||||
update_schedule = True
|
||||
changed = True
|
||||
elif self.volumes is not None and len(self.volumes) > 0:
|
||||
for volumeID in schedule_detail.schedule_info.volume_ids:
|
||||
if volumeID not in self.volumes:
|
||||
update_schedule = True
|
||||
changed = True
|
||||
|
||||
temp_frequency = self.get_frequency()
|
||||
if temp_frequency is not None:
|
||||
# Checking schedule_type changes
|
||||
if self.is_same_schedule_type(schedule_detail):
|
||||
# If same schedule type
|
||||
if self.schedule_type == "TimeIntervalFrequency":
|
||||
# Check if there is any change in schedule.frequency, If schedule_type is time_interval
|
||||
if schedule_detail.frequency.days != temp_frequency.days or \
|
||||
schedule_detail.frequency.hours != temp_frequency.hours or \
|
||||
schedule_detail.frequency.minutes != temp_frequency.minutes:
|
||||
update_schedule = True
|
||||
changed = True
|
||||
elif self.schedule_type == "DaysOfMonthFrequency":
|
||||
# Check if there is any change in schedule.frequency, If schedule_type is days_of_month
|
||||
if len(schedule_detail.frequency.monthdays) != len(temp_frequency.monthdays) or \
|
||||
schedule_detail.frequency.hours != temp_frequency.hours or \
|
||||
schedule_detail.frequency.minutes != temp_frequency.minutes:
|
||||
update_schedule = True
|
||||
changed = True
|
||||
elif len(schedule_detail.frequency.monthdays) == len(temp_frequency.monthdays):
|
||||
actual_frequency_monthday = schedule_detail.frequency.monthdays
|
||||
temp_frequency_monthday = temp_frequency.monthdays
|
||||
for monthday in actual_frequency_monthday:
|
||||
if monthday not in temp_frequency_monthday:
|
||||
update_schedule = True
|
||||
changed = True
|
||||
elif self.schedule_type == "DaysOfWeekFrequency":
|
||||
# Check if there is any change in schedule.frequency, If schedule_type is days_of_week
|
||||
if len(schedule_detail.frequency.weekdays) != len(temp_frequency.weekdays) or \
|
||||
schedule_detail.frequency.hours != temp_frequency.hours or \
|
||||
schedule_detail.frequency.minutes != temp_frequency.minutes:
|
||||
update_schedule = True
|
||||
changed = True
|
||||
elif len(schedule_detail.frequency.weekdays) == len(temp_frequency.weekdays):
|
||||
actual_frequency_weekdays = schedule_detail.frequency.weekdays
|
||||
temp_frequency_weekdays = temp_frequency.weekdays
|
||||
if len([actual_weekday for actual_weekday, temp_weekday in
|
||||
zip(actual_frequency_weekdays, temp_frequency_weekdays) if actual_weekday != temp_weekday]) != 0:
|
||||
update_schedule = True
|
||||
changed = True
|
||||
else:
|
||||
update_schedule = True
|
||||
changed = True
|
||||
else:
|
||||
self.module.fail_json(msg='Failed to create schedule frequency object - type %s parameters' % self.schedule_type)
|
||||
|
||||
result_message = " "
|
||||
if changed:
|
||||
if self.module.check_mode:
|
||||
# Skip changes
|
||||
result_message = "Check mode, skipping changes"
|
||||
else:
|
||||
if self.state == 'present':
|
||||
if update_schedule:
|
||||
self.update_schedule(schedule_detail.schedule_id)
|
||||
result_message = "Snapshot Schedule modified"
|
||||
else:
|
||||
self.create_schedule()
|
||||
result_message = "Snapshot Schedule created"
|
||||
elif self.state == 'absent':
|
||||
self.delete_schedule(schedule_detail.schedule_id)
|
||||
result_message = "Snapshot Schedule deleted"
|
||||
|
||||
self.module.exit_json(changed=changed, msg=result_message)
|
||||
|
||||
|
||||
def main():
|
||||
v = ElementSWSnapShotSchedule()
|
||||
v.apply()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
|
@ -1,261 +0,0 @@
|
|||
#!/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)
|
||||
|
||||
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_vlan
|
||||
|
||||
short_description: NetApp Element Software Manage VLAN
|
||||
extends_documentation_fragment:
|
||||
- netapp.solidfire
|
||||
version_added: '2.7'
|
||||
author: NetApp Ansible Team (@carchi8py) <ng-ansibleteam@netapp.com>
|
||||
description:
|
||||
- Create, delete, modify VLAN
|
||||
|
||||
options:
|
||||
|
||||
state:
|
||||
description:
|
||||
- Whether the specified vlan should exist or not.
|
||||
choices: ['present', 'absent']
|
||||
default: present
|
||||
|
||||
vlan_tag:
|
||||
description:
|
||||
- Virtual Network Tag
|
||||
required: true
|
||||
|
||||
name:
|
||||
description:
|
||||
- User defined name for the new VLAN
|
||||
- Name of the vlan is unique
|
||||
- Required for create
|
||||
|
||||
svip:
|
||||
description:
|
||||
- Storage virtual IP which is unique
|
||||
- Required for create
|
||||
|
||||
address_blocks:
|
||||
description:
|
||||
- List of address blocks for the VLAN
|
||||
- Each address block contains the starting IP address and size for the block
|
||||
- Required for create
|
||||
|
||||
netmask:
|
||||
description:
|
||||
- Netmask for the VLAN
|
||||
- Required for create
|
||||
|
||||
gateway:
|
||||
description:
|
||||
- Gateway for the VLAN
|
||||
|
||||
namespace:
|
||||
description:
|
||||
- Enable or disable namespaces
|
||||
type: bool
|
||||
|
||||
attributes:
|
||||
description:
|
||||
- Dictionary of attributes with name and value for each attribute
|
||||
|
||||
'''
|
||||
|
||||
EXAMPLES = """
|
||||
- name: Create vlan
|
||||
na_elementsw_vlan:
|
||||
state: present
|
||||
name: test
|
||||
vlan_tag: 1
|
||||
svip: "{{ ip address }}"
|
||||
netmask: "{{ netmask }}"
|
||||
address_blocks:
|
||||
- start: "{{ starting ip_address }}"
|
||||
size: 5
|
||||
- start: "{{ starting ip_address }}"
|
||||
size: 5
|
||||
hostname: "{{ netapp_hostname }}"
|
||||
username: "{{ netapp_username }}"
|
||||
password: "{{ netapp_password }}"
|
||||
|
||||
- name: Delete Lun
|
||||
na_elementsw_vlan:
|
||||
state: absent
|
||||
vlan_tag: 1
|
||||
hostname: "{{ netapp_hostname }}"
|
||||
username: "{{ netapp_username }}"
|
||||
password: "{{ netapp_password }}"
|
||||
"""
|
||||
|
||||
RETURN = """
|
||||
|
||||
"""
|
||||
|
||||
from ansible.module_utils.basic import AnsibleModule
|
||||
from ansible.module_utils._text import to_native
|
||||
import ansible.module_utils.netapp as netapp_utils
|
||||
from ansible.module_utils.netapp_module import NetAppModule
|
||||
from ansible.module_utils.netapp_elementsw_module import NaElementSWModule
|
||||
|
||||
HAS_SF_SDK = netapp_utils.has_sf_sdk()
|
||||
try:
|
||||
import solidfire.common
|
||||
except ImportError:
|
||||
HAS_SF_SDK = False
|
||||
|
||||
|
||||
class ElementSWVlan(object):
|
||||
""" class to handle VLAN operations """
|
||||
|
||||
def __init__(self):
|
||||
"""
|
||||
Setup Ansible parameters and ElementSW connection
|
||||
"""
|
||||
self.argument_spec = netapp_utils.ontap_sf_host_argument_spec()
|
||||
self.argument_spec.update(dict(
|
||||
state=dict(required=False, choices=['present', 'absent'],
|
||||
default='present'),
|
||||
name=dict(required=False, type='str'),
|
||||
vlan_tag=dict(required=True, type='str'),
|
||||
svip=dict(required=False, type='str'),
|
||||
netmask=dict(required=False, type='str'),
|
||||
gateway=dict(required=False, type='str'),
|
||||
namespace=dict(required=False, type='bool'),
|
||||
attributes=dict(required=False, type='dict'),
|
||||
address_blocks=dict(required=False, type='list')
|
||||
))
|
||||
|
||||
self.module = AnsibleModule(
|
||||
argument_spec=self.argument_spec,
|
||||
supports_check_mode=True
|
||||
)
|
||||
|
||||
if HAS_SF_SDK is False:
|
||||
self.module.fail_json(msg="Unable to import the SolidFire Python SDK")
|
||||
else:
|
||||
self.elem = netapp_utils.create_sf_connection(module=self.module)
|
||||
|
||||
self.na_helper = NetAppModule()
|
||||
self.parameters = self.na_helper.set_parameters(self.module.params)
|
||||
|
||||
self.elementsw_helper = NaElementSWModule(self.elem)
|
||||
|
||||
# add telemetry attributes
|
||||
if self.parameters.get('attributes') is not None:
|
||||
self.parameters['attributes'].update(self.elementsw_helper.set_element_attributes(source='na_elementsw_vlan'))
|
||||
else:
|
||||
self.parameters['attributes'] = self.elementsw_helper.set_element_attributes(source='na_elementsw_vlan')
|
||||
|
||||
def validate_keys(self):
|
||||
"""
|
||||
Validate if all required keys are present before creating
|
||||
"""
|
||||
required_keys = ['address_blocks', 'svip', 'netmask', 'name']
|
||||
if all(item in self.parameters.keys() for item in required_keys) is False:
|
||||
self.module.fail_json(msg="One or more required fields %s for creating VLAN is missing"
|
||||
% required_keys)
|
||||
addr_blk_fields = ['start', 'size']
|
||||
for address in self.parameters['address_blocks']:
|
||||
if 'start' not in address or 'size' not in address:
|
||||
self.module.fail_json(msg="One or more required fields %s for address blocks is missing"
|
||||
% addr_blk_fields)
|
||||
|
||||
def create_network(self):
|
||||
"""
|
||||
Add VLAN
|
||||
"""
|
||||
try:
|
||||
self.validate_keys()
|
||||
create_params = self.parameters.copy()
|
||||
for key in ['username', 'hostname', 'password', 'state', 'vlan_tag']:
|
||||
del create_params[key]
|
||||
self.elem.add_virtual_network(virtual_network_tag=self.parameters['vlan_tag'], **create_params)
|
||||
except solidfire.common.ApiServerError as err:
|
||||
self.module.fail_json(msg="Error creating VLAN %s"
|
||||
% self.parameters['vlan_tag'],
|
||||
exception=to_native(err))
|
||||
|
||||
def delete_network(self):
|
||||
"""
|
||||
Remove VLAN
|
||||
"""
|
||||
try:
|
||||
self.elem.remove_virtual_network(virtual_network_tag=self.parameters['vlan_tag'])
|
||||
except solidfire.common.ApiServerError as err:
|
||||
self.module.fail_json(msg="Error deleting VLAN %s"
|
||||
% self.parameters['vlan_tag'],
|
||||
exception=to_native(err))
|
||||
|
||||
def modify_network(self, modify):
|
||||
"""
|
||||
Modify the VLAN
|
||||
"""
|
||||
try:
|
||||
self.elem.modify_virtual_network(virtual_network_tag=self.parameters['vlan_tag'], **modify)
|
||||
except solidfire.common.ApiServerError as err:
|
||||
self.module.fail_json(msg="Error modifying VLAN %s"
|
||||
% self.parameters['vlan_tag'],
|
||||
exception=to_native(err))
|
||||
|
||||
def get_network_details(self):
|
||||
"""
|
||||
Check existing VLANs
|
||||
:return: vlan details if found, None otherwise
|
||||
:type: dict
|
||||
"""
|
||||
vlans = self.elem.list_virtual_networks(virtual_network_tag=self.parameters['vlan_tag'])
|
||||
vlan_details = dict()
|
||||
for vlan in vlans.virtual_networks:
|
||||
if vlan is not None:
|
||||
vlan_details['name'] = vlan.name
|
||||
vlan_details['address_blocks'] = list()
|
||||
for address in vlan.address_blocks:
|
||||
vlan_details['address_blocks'].append({
|
||||
'start': address.start,
|
||||
'size': address.size
|
||||
})
|
||||
vlan_details['svip'] = vlan.svip
|
||||
vlan_details['gateway'] = vlan.gateway
|
||||
vlan_details['netmask'] = vlan.netmask
|
||||
vlan_details['namespace'] = vlan.namespace
|
||||
vlan_details['attributes'] = vlan.attributes
|
||||
return vlan_details
|
||||
return None
|
||||
|
||||
def apply(self):
|
||||
"""
|
||||
Call create / delete / modify vlan methods
|
||||
"""
|
||||
network = self.get_network_details()
|
||||
# calling helper to determine action
|
||||
cd_action = self.na_helper.get_cd_action(network, self.parameters)
|
||||
modify = self.na_helper.get_modified_attributes(network, self.parameters)
|
||||
if cd_action == "create":
|
||||
self.create_network()
|
||||
elif cd_action == "delete":
|
||||
self.delete_network()
|
||||
elif modify:
|
||||
self.modify_network(modify)
|
||||
self.module.exit_json(changed=self.na_helper.changed)
|
||||
|
||||
|
||||
def main():
|
||||
""" Apply vlan actions """
|
||||
network_obj = ElementSWVlan()
|
||||
network_obj.apply()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
|
@ -1,396 +0,0 @@
|
|||
#!/usr/bin/python
|
||||
|
||||
# (c) 2017, NetApp, Inc
|
||||
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
|
||||
"""Element OS Software Volume Manager"""
|
||||
|
||||
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_volume
|
||||
|
||||
short_description: NetApp Element Software Manage Volumes
|
||||
extends_documentation_fragment:
|
||||
- netapp.solidfire
|
||||
version_added: '2.7'
|
||||
author: NetApp Ansible Team (@carchi8py) <ng-ansibleteam@netapp.com>
|
||||
description:
|
||||
- Create, destroy, or update volumes on ElementSW
|
||||
|
||||
options:
|
||||
|
||||
state:
|
||||
description:
|
||||
- Whether the specified volume should exist or not.
|
||||
required: true
|
||||
choices: ['present', 'absent']
|
||||
|
||||
name:
|
||||
description:
|
||||
- The name of the volume to manage.
|
||||
- It accepts volume_name or volume_id
|
||||
required: true
|
||||
|
||||
account_id:
|
||||
description:
|
||||
- Account ID for the owner of this volume.
|
||||
- It accepts Account_id or Account_name
|
||||
required: true
|
||||
|
||||
enable512e:
|
||||
description:
|
||||
- Required when C(state=present)
|
||||
- Should the volume provide 512-byte sector emulation?
|
||||
type: bool
|
||||
aliases:
|
||||
- 512emulation
|
||||
|
||||
qos:
|
||||
description: Initial quality of service settings for this volume. Configure as dict in playbooks.
|
||||
|
||||
attributes:
|
||||
description: A YAML dictionary of attributes that you would like to apply on this volume.
|
||||
|
||||
size:
|
||||
description:
|
||||
- The size of the volume in (size_unit).
|
||||
- Required when C(state = present).
|
||||
|
||||
size_unit:
|
||||
description:
|
||||
- The unit used to interpret the size parameter.
|
||||
choices: ['bytes', 'b', 'kb', 'mb', 'gb', 'tb', 'pb', 'eb', 'zb', 'yb']
|
||||
default: 'gb'
|
||||
|
||||
access:
|
||||
description:
|
||||
- Access allowed for the volume.
|
||||
- readOnly Only read operations are allowed.
|
||||
- readWrite Reads and writes are allowed.
|
||||
- locked No reads or writes are allowed.
|
||||
- replicationTarget Identify a volume as the target volume for a paired set of volumes.
|
||||
- If the volume is not paired, the access status is locked.
|
||||
- If unspecified, the access settings of the clone will be the same as the source.
|
||||
choices: ['readOnly', 'readWrite', 'locked', 'replicationTarget']
|
||||
|
||||
password:
|
||||
description:
|
||||
- ElementSW access account password
|
||||
aliases:
|
||||
- pass
|
||||
|
||||
username:
|
||||
description:
|
||||
- ElementSW access account user-name
|
||||
aliases:
|
||||
- user
|
||||
|
||||
'''
|
||||
|
||||
EXAMPLES = """
|
||||
- name: Create Volume
|
||||
na_elementsw_volume:
|
||||
hostname: "{{ elementsw_hostname }}"
|
||||
username: "{{ elementsw_username }}"
|
||||
password: "{{ elementsw_password }}"
|
||||
state: present
|
||||
name: AnsibleVol
|
||||
qos: {minIOPS: 1000, maxIOPS: 20000, burstIOPS: 50000}
|
||||
account_id: 3
|
||||
enable512e: False
|
||||
size: 1
|
||||
size_unit: gb
|
||||
|
||||
- name: Update Volume
|
||||
na_elementsw_volume:
|
||||
hostname: "{{ elementsw_hostname }}"
|
||||
username: "{{ elementsw_username }}"
|
||||
password: "{{ elementsw_password }}"
|
||||
state: present
|
||||
name: AnsibleVol
|
||||
account_id: 3
|
||||
access: readWrite
|
||||
|
||||
- name: Delete Volume
|
||||
na_elementsw_volume:
|
||||
hostname: "{{ elementsw_hostname }}"
|
||||
username: "{{ elementsw_username }}"
|
||||
password: "{{ elementsw_password }}"
|
||||
state: absent
|
||||
name: AnsibleVol
|
||||
account_id: 2
|
||||
"""
|
||||
|
||||
RETURN = """
|
||||
|
||||
msg:
|
||||
description: Success message
|
||||
returned: success
|
||||
type: str
|
||||
|
||||
"""
|
||||
|
||||
from ansible.module_utils.basic import AnsibleModule
|
||||
from ansible.module_utils._text import to_native
|
||||
import ansible.module_utils.netapp as netapp_utils
|
||||
from ansible.module_utils.netapp_elementsw_module import NaElementSWModule
|
||||
|
||||
HAS_SF_SDK = netapp_utils.has_sf_sdk()
|
||||
try:
|
||||
import solidfire.common
|
||||
except ImportError:
|
||||
HAS_SF_SDK = False
|
||||
|
||||
|
||||
class ElementOSVolume(object):
|
||||
"""
|
||||
Contains methods to parse arguments,
|
||||
derive details of ElementSW objects
|
||||
and send requests to ElementOS via
|
||||
the ElementSW SDK
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
"""
|
||||
Parse arguments, setup state variables,
|
||||
check parameters and ensure SDK is installed
|
||||
"""
|
||||
self._size_unit_map = netapp_utils.SF_BYTE_MAP
|
||||
|
||||
self.argument_spec = netapp_utils.ontap_sf_host_argument_spec()
|
||||
self.argument_spec.update(dict(
|
||||
state=dict(required=True, choices=['present', 'absent']),
|
||||
name=dict(required=True, type='str'),
|
||||
account_id=dict(required=True),
|
||||
enable512e=dict(type='bool', aliases=['512emulation']),
|
||||
qos=dict(required=False, type='dict', default=None),
|
||||
attributes=dict(required=False, type='dict', default=None),
|
||||
size=dict(type='int'),
|
||||
size_unit=dict(default='gb',
|
||||
choices=['bytes', 'b', 'kb', 'mb', 'gb', 'tb',
|
||||
'pb', 'eb', 'zb', 'yb'], type='str'),
|
||||
|
||||
access=dict(required=False, type='str', default=None, choices=['readOnly', 'readWrite',
|
||||
'locked', 'replicationTarget']),
|
||||
|
||||
))
|
||||
|
||||
self.module = AnsibleModule(
|
||||
argument_spec=self.argument_spec,
|
||||
required_if=[
|
||||
('state', 'present', ['size', 'enable512e'])
|
||||
],
|
||||
supports_check_mode=True
|
||||
)
|
||||
|
||||
param = self.module.params
|
||||
|
||||
# set up state variables
|
||||
self.state = param['state']
|
||||
self.name = param['name']
|
||||
self.account_id = param['account_id']
|
||||
self.enable512e = param['enable512e']
|
||||
self.qos = param['qos']
|
||||
self.attributes = param['attributes']
|
||||
self.access = param['access']
|
||||
self.size_unit = param['size_unit']
|
||||
if param['size'] is not None:
|
||||
self.size = param['size'] * self._size_unit_map[self.size_unit]
|
||||
else:
|
||||
self.size = None
|
||||
|
||||
if HAS_SF_SDK is False:
|
||||
self.module.fail_json(msg="Unable to import the ElementSW Python SDK")
|
||||
else:
|
||||
try:
|
||||
self.sfe = netapp_utils.create_sf_connection(module=self.module)
|
||||
except solidfire.common.ApiServerError:
|
||||
self.module.fail_json(msg="Unable to create the connection")
|
||||
|
||||
self.elementsw_helper = NaElementSWModule(self.sfe)
|
||||
|
||||
# add telemetry attributes
|
||||
if self.attributes is not None:
|
||||
self.attributes.update(self.elementsw_helper.set_element_attributes(source='na_elementsw_volume'))
|
||||
else:
|
||||
self.attributes = self.elementsw_helper.set_element_attributes(source='na_elementsw_volume')
|
||||
|
||||
def get_account_id(self):
|
||||
"""
|
||||
Return account id if found
|
||||
"""
|
||||
try:
|
||||
# Update and return self.account_id
|
||||
self.account_id = self.elementsw_helper.account_exists(self.account_id)
|
||||
return self.account_id
|
||||
except Exception as err:
|
||||
self.module.fail_json(msg="Error: account_id %s does not exist" % self.account_id, exception=to_native(err))
|
||||
|
||||
def get_volume(self):
|
||||
"""
|
||||
Return volume details if found
|
||||
"""
|
||||
# Get volume details
|
||||
volume_id = self.elementsw_helper.volume_exists(self.name, self.account_id)
|
||||
|
||||
if volume_id is not None:
|
||||
# Return volume_details
|
||||
volume_details = self.elementsw_helper.get_volume(volume_id)
|
||||
if volume_details is not None:
|
||||
return volume_details
|
||||
return None
|
||||
|
||||
def create_volume(self):
|
||||
"""
|
||||
Create Volume
|
||||
|
||||
:return: True if created, False if fails
|
||||
"""
|
||||
try:
|
||||
self.sfe.create_volume(name=self.name,
|
||||
account_id=self.account_id,
|
||||
total_size=self.size,
|
||||
enable512e=self.enable512e,
|
||||
qos=self.qos,
|
||||
attributes=self.attributes)
|
||||
|
||||
except Exception as err:
|
||||
self.module.fail_json(msg="Error provisioning volume %s of size %s" % (self.name, self.size),
|
||||
exception=to_native(err))
|
||||
|
||||
def delete_volume(self, volume_id):
|
||||
"""
|
||||
Delete and purge the volume using volume id
|
||||
|
||||
:return: Success : True , Failed : False
|
||||
"""
|
||||
try:
|
||||
self.sfe.delete_volume(volume_id=volume_id)
|
||||
self.sfe.purge_deleted_volume(volume_id=volume_id)
|
||||
# Delete method will delete and also purge the volume instead of moving the volume state to inactive.
|
||||
|
||||
except Exception as err:
|
||||
# Throwing the exact error message instead of generic error message
|
||||
self.module.fail_json(msg=err.message,
|
||||
exception=to_native(err))
|
||||
|
||||
def update_volume(self, volume_id):
|
||||
"""
|
||||
|
||||
Update the volume with the specified param
|
||||
|
||||
:return: Success : True, Failed : False
|
||||
"""
|
||||
try:
|
||||
self.sfe.modify_volume(volume_id,
|
||||
account_id=self.account_id,
|
||||
access=self.access,
|
||||
qos=self.qos,
|
||||
total_size=self.size,
|
||||
attributes=self.attributes)
|
||||
|
||||
except Exception as err:
|
||||
# Throwing the exact error message instead of generic error message
|
||||
self.module.fail_json(msg=err.message,
|
||||
exception=to_native(err))
|
||||
|
||||
def apply(self):
|
||||
# Perform pre-checks, call functions and exit
|
||||
changed = False
|
||||
volume_exists = False
|
||||
update_volume = False
|
||||
|
||||
self.get_account_id()
|
||||
volume_detail = self.get_volume()
|
||||
|
||||
if volume_detail:
|
||||
volume_exists = True
|
||||
volume_id = volume_detail.volume_id
|
||||
if self.state == 'absent':
|
||||
# Checking for state change(s) here, and applying it later in the code allows us to support
|
||||
# check_mode
|
||||
|
||||
changed = True
|
||||
|
||||
elif self.state == 'present':
|
||||
# Checking all the params for update operation
|
||||
if volume_detail.access is not None and self.access is not None and volume_detail.access != self.access:
|
||||
update_volume = True
|
||||
changed = True
|
||||
|
||||
elif volume_detail.account_id is not None and self.account_id is not None \
|
||||
and volume_detail.account_id != self.account_id:
|
||||
update_volume = True
|
||||
changed = True
|
||||
|
||||
elif volume_detail.qos is not None and self.qos is not None:
|
||||
"""
|
||||
Actual volume_detail.qos has ['burst_iops', 'burst_time', 'curve', 'max_iops', 'min_iops'] keys.
|
||||
As only minOPS, maxOPS, burstOPS is important to consider, checking only these values.
|
||||
"""
|
||||
volume_qos = volume_detail.qos.__dict__
|
||||
if volume_qos['min_iops'] != self.qos['minIOPS'] or volume_qos['max_iops'] != self.qos['maxIOPS'] \
|
||||
or volume_qos['burst_iops'] != self.qos['burstIOPS']:
|
||||
update_volume = True
|
||||
changed = True
|
||||
else:
|
||||
# If check fails, do nothing
|
||||
pass
|
||||
|
||||
if volume_detail.total_size is not None and volume_detail.total_size != self.size:
|
||||
size_difference = abs(float(volume_detail.total_size - self.size))
|
||||
# Change size only if difference is bigger than 0.001
|
||||
if size_difference / self.size > 0.001:
|
||||
update_volume = True
|
||||
changed = True
|
||||
|
||||
else:
|
||||
# If check fails, do nothing
|
||||
pass
|
||||
|
||||
if volume_detail.attributes is not None and self.attributes is not None and \
|
||||
volume_detail.attributes != self.attributes:
|
||||
update_volume = True
|
||||
changed = True
|
||||
else:
|
||||
if self.state == 'present':
|
||||
changed = True
|
||||
|
||||
result_message = ""
|
||||
|
||||
if changed:
|
||||
if self.module.check_mode:
|
||||
result_message = "Check mode, skipping changes"
|
||||
else:
|
||||
if self.state == 'present':
|
||||
if not volume_exists:
|
||||
self.create_volume()
|
||||
result_message = "Volume created"
|
||||
elif update_volume:
|
||||
self.update_volume(volume_id)
|
||||
result_message = "Volume updated"
|
||||
|
||||
elif self.state == 'absent':
|
||||
self.delete_volume(volume_id)
|
||||
result_message = "Volume deleted"
|
||||
|
||||
self.module.exit_json(changed=changed, msg=result_message)
|
||||
|
||||
|
||||
def main():
|
||||
# Create object and call apply
|
||||
na_elementsw_volume = ElementOSVolume()
|
||||
na_elementsw_volume.apply()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
|
@ -1,271 +0,0 @@
|
|||
#!/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 volume clone"""
|
||||
|
||||
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_volume_clone
|
||||
|
||||
short_description: NetApp Element Software Create Volume Clone
|
||||
extends_documentation_fragment:
|
||||
- netapp.solidfire
|
||||
version_added: '2.7'
|
||||
author: NetApp Ansible Team (@carchi8py) <ng-ansibleteam@netapp.com>
|
||||
description:
|
||||
- Create volume clones on Element OS
|
||||
|
||||
options:
|
||||
|
||||
name:
|
||||
description:
|
||||
- The name of the clone.
|
||||
required: true
|
||||
|
||||
src_volume_id:
|
||||
description:
|
||||
- The id of the src volume to clone. id may be a numeric identifier or a volume name.
|
||||
required: true
|
||||
|
||||
src_snapshot_id:
|
||||
description:
|
||||
- The id of the snapshot to clone. id may be a numeric identifier or a snapshot name.
|
||||
|
||||
account_id:
|
||||
description:
|
||||
- Account ID for the owner of this cloned volume. id may be a numeric identifier or an account name.
|
||||
required: true
|
||||
|
||||
attributes:
|
||||
description: A YAML dictionary of attributes that you would like to apply on this cloned volume.
|
||||
|
||||
size:
|
||||
description:
|
||||
- The size of the cloned volume in (size_unit).
|
||||
|
||||
size_unit:
|
||||
description:
|
||||
- The unit used to interpret the size parameter.
|
||||
choices: ['bytes', 'b', 'kb', 'mb', 'gb', 'tb', 'pb', 'eb', 'zb', 'yb']
|
||||
default: 'gb'
|
||||
|
||||
access:
|
||||
choices: ['readOnly', 'readWrite', 'locked', 'replicationTarget']
|
||||
description:
|
||||
- Access allowed for the volume.
|
||||
- If unspecified, the access settings of the clone will be the same as the source.
|
||||
- readOnly - Only read operations are allowed.
|
||||
- readWrite - Reads and writes are allowed.
|
||||
- locked - No reads or writes are allowed.
|
||||
- replicationTarget - Identify a volume as the target volume for a paired set of volumes. If the volume is not paired, the access status is locked.
|
||||
|
||||
'''
|
||||
|
||||
EXAMPLES = """
|
||||
- name: Clone Volume
|
||||
na_elementsw_volume_clone:
|
||||
hostname: "{{ elementsw_hostname }}"
|
||||
username: "{{ elementsw_username }}"
|
||||
password: "{{ elementsw_password }}"
|
||||
name: CloneAnsibleVol
|
||||
src_volume_id: 123
|
||||
src_snapshot_id: 41
|
||||
account_id: 3
|
||||
size: 1
|
||||
size_unit: gb
|
||||
access: readWrite
|
||||
attributes: {"virtual_network_id": 12345}
|
||||
|
||||
"""
|
||||
|
||||
RETURN = """
|
||||
|
||||
msg:
|
||||
description: Success message
|
||||
returned: success
|
||||
type: str
|
||||
|
||||
"""
|
||||
|
||||
from ansible.module_utils.basic import AnsibleModule
|
||||
from ansible.module_utils._text import to_native
|
||||
import ansible.module_utils.netapp as netapp_utils
|
||||
from ansible.module_utils.netapp_elementsw_module import NaElementSWModule
|
||||
|
||||
HAS_SF_SDK = netapp_utils.has_sf_sdk()
|
||||
|
||||
|
||||
class ElementOSVolumeClone(object):
|
||||
"""
|
||||
Contains methods to parse arguments,
|
||||
derive details of Element Software objects
|
||||
and send requests to Element OS via
|
||||
the Solidfire SDK
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
"""
|
||||
Parse arguments, setup state variables,
|
||||
check parameters and ensure SDK is installed
|
||||
"""
|
||||
self._size_unit_map = netapp_utils.SF_BYTE_MAP
|
||||
|
||||
self.argument_spec = netapp_utils.ontap_sf_host_argument_spec()
|
||||
self.argument_spec.update(dict(
|
||||
name=dict(required=True),
|
||||
src_volume_id=dict(required=True),
|
||||
src_snapshot_id=dict(),
|
||||
account_id=dict(required=True),
|
||||
|
||||
attributes=dict(type='dict', default=None),
|
||||
|
||||
size=dict(type='int'),
|
||||
size_unit=dict(default='gb',
|
||||
choices=['bytes', 'b', 'kb', 'mb', 'gb', 'tb',
|
||||
'pb', 'eb', 'zb', 'yb'], type='str'),
|
||||
|
||||
access=dict(type='str',
|
||||
default=None, choices=['readOnly', 'readWrite',
|
||||
'locked', 'replicationTarget']),
|
||||
|
||||
))
|
||||
|
||||
self.module = AnsibleModule(
|
||||
argument_spec=self.argument_spec,
|
||||
supports_check_mode=True
|
||||
)
|
||||
|
||||
parameters = self.module.params
|
||||
|
||||
# set up state variables
|
||||
self.name = parameters['name']
|
||||
self.src_volume_id = parameters['src_volume_id']
|
||||
self.src_snapshot_id = parameters['src_snapshot_id']
|
||||
self.account_id = parameters['account_id']
|
||||
self.attributes = parameters['attributes']
|
||||
|
||||
self.size_unit = parameters['size_unit']
|
||||
if parameters['size'] is not None:
|
||||
self.size = parameters['size'] * \
|
||||
self._size_unit_map[self.size_unit]
|
||||
else:
|
||||
self.size = None
|
||||
self.access = parameters['access']
|
||||
|
||||
if HAS_SF_SDK is False:
|
||||
self.module.fail_json(
|
||||
msg="Unable to import the SolidFire Python SDK")
|
||||
else:
|
||||
self.sfe = netapp_utils.create_sf_connection(module=self.module)
|
||||
|
||||
self.elementsw_helper = NaElementSWModule(self.sfe)
|
||||
|
||||
# add telemetry attributes
|
||||
if self.attributes is not None:
|
||||
self.attributes.update(self.elementsw_helper.set_element_attributes(source='na_elementsw_volume_clone'))
|
||||
else:
|
||||
self.attributes = self.elementsw_helper.set_element_attributes(source='na_elementsw_volume_clone')
|
||||
|
||||
def get_account_id(self):
|
||||
"""
|
||||
Return account id if found
|
||||
"""
|
||||
try:
|
||||
# Update and return self.account_id
|
||||
self.account_id = self.elementsw_helper.account_exists(self.account_id)
|
||||
return self.account_id
|
||||
except Exception as err:
|
||||
self.module.fail_json(msg="Error: account_id %s does not exist" % self.account_id, exception=to_native(err))
|
||||
|
||||
def get_snapshot_id(self):
|
||||
"""
|
||||
Return snapshot details if found
|
||||
"""
|
||||
src_snapshot = self.elementsw_helper.get_snapshot(self.src_snapshot_id, self.src_volume_id)
|
||||
# Update and return self.src_snapshot_id
|
||||
if src_snapshot is not None:
|
||||
self.src_snapshot_id = src_snapshot.snapshot_id
|
||||
# Return src_snapshot
|
||||
return self.src_snapshot_id
|
||||
return None
|
||||
|
||||
def get_src_volume_id(self):
|
||||
"""
|
||||
Return volume id if found
|
||||
"""
|
||||
src_vol_id = self.elementsw_helper.volume_exists(self.src_volume_id, self.account_id)
|
||||
if src_vol_id is not None:
|
||||
# Update and return self.volume_id
|
||||
self.src_volume_id = src_vol_id
|
||||
# Return src_volume_id
|
||||
return self.src_volume_id
|
||||
return None
|
||||
|
||||
def clone_volume(self):
|
||||
"""Clone Volume from source"""
|
||||
try:
|
||||
self.sfe.clone_volume(volume_id=self.src_volume_id,
|
||||
name=self.name,
|
||||
new_account_id=self.account_id,
|
||||
new_size=self.size,
|
||||
access=self.access,
|
||||
snapshot_id=self.src_snapshot_id,
|
||||
attributes=self.attributes)
|
||||
|
||||
except Exception as err:
|
||||
self.module.fail_json(msg="Error creating clone %s of size %s" % (self.name, self.size), exception=to_native(err))
|
||||
|
||||
def apply(self):
|
||||
"""Perform pre-checks, call functions and exit"""
|
||||
changed = False
|
||||
result_message = ""
|
||||
|
||||
if self.get_account_id() is None:
|
||||
self.module.fail_json(msg="Account id not found: %s" % (self.account_id))
|
||||
|
||||
# there is only one state. other operations
|
||||
# are part of the volume module
|
||||
|
||||
# ensure that a volume with the clone name
|
||||
# isn't already present
|
||||
if self.elementsw_helper.volume_exists(self.name, self.account_id) is None:
|
||||
# check for the source volume
|
||||
if self.get_src_volume_id() is not None:
|
||||
# check for a valid snapshot
|
||||
if self.src_snapshot_id and not self.get_snapshot_id():
|
||||
self.module.fail_json(msg="Snapshot id not found: %s" % (self.src_snapshot_id))
|
||||
# change required
|
||||
changed = True
|
||||
else:
|
||||
self.module.fail_json(msg="Volume id not found %s" % (self.src_volume_id))
|
||||
|
||||
if changed:
|
||||
if self.module.check_mode:
|
||||
result_message = "Check mode, skipping changes"
|
||||
else:
|
||||
self.clone_volume()
|
||||
result_message = "Volume cloned"
|
||||
|
||||
self.module.exit_json(changed=changed, msg=result_message)
|
||||
|
||||
|
||||
def main():
|
||||
"""Create object and call apply"""
|
||||
volume_clone = ElementOSVolumeClone()
|
||||
volume_clone.apply()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
|
@ -1,284 +0,0 @@
|
|||
#!/usr/bin/python
|
||||
# (c) 2017, NetApp, Inc
|
||||
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
|
||||
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_volume_pair
|
||||
|
||||
short_description: NetApp Element Software Volume Pair
|
||||
extends_documentation_fragment:
|
||||
- netapp.solidfire
|
||||
version_added: '2.7'
|
||||
author: NetApp Ansible Team (@carchi8py) <ng-ansibleteam@netapp.com>
|
||||
description:
|
||||
- Create, delete volume pair
|
||||
|
||||
options:
|
||||
|
||||
state:
|
||||
description:
|
||||
- Whether the specified volume pair should exist or not.
|
||||
choices: ['present', 'absent']
|
||||
default: present
|
||||
|
||||
src_volume:
|
||||
description:
|
||||
- Source volume name or volume ID
|
||||
required: true
|
||||
|
||||
src_account:
|
||||
description:
|
||||
- Source account name or ID
|
||||
required: true
|
||||
|
||||
dest_volume:
|
||||
description:
|
||||
- Destination volume name or volume ID
|
||||
required: true
|
||||
|
||||
dest_account:
|
||||
description:
|
||||
- Destination account name or ID
|
||||
required: true
|
||||
|
||||
mode:
|
||||
description:
|
||||
- Mode to start the volume pairing
|
||||
choices: ['async', 'sync', 'snapshotsonly']
|
||||
default: async
|
||||
|
||||
dest_mvip:
|
||||
description:
|
||||
- Destination IP address of the paired cluster.
|
||||
required: true
|
||||
|
||||
dest_username:
|
||||
description:
|
||||
- Destination username for the paired cluster
|
||||
- Optional if this is same as source cluster username.
|
||||
|
||||
dest_password:
|
||||
description:
|
||||
- Destination password for the paired cluster
|
||||
- Optional if this is same as source cluster password.
|
||||
|
||||
'''
|
||||
|
||||
EXAMPLES = """
|
||||
- name: Create volume pair
|
||||
na_elementsw_volume_pair:
|
||||
hostname: "{{ src_cluster_hostname }}"
|
||||
username: "{{ src_cluster_username }}"
|
||||
password: "{{ src_cluster_password }}"
|
||||
state: present
|
||||
src_volume: test1
|
||||
src_account: test2
|
||||
dest_volume: test3
|
||||
dest_account: test4
|
||||
mode: sync
|
||||
dest_mvip: "{{ dest_cluster_hostname }}"
|
||||
|
||||
- name: Delete volume pair
|
||||
na_elementsw_volume_pair:
|
||||
hostname: "{{ src_cluster_hostname }}"
|
||||
username: "{{ src_cluster_username }}"
|
||||
password: "{{ src_cluster_password }}"
|
||||
state: absent
|
||||
src_volume: 3
|
||||
src_account: 1
|
||||
dest_volume: 2
|
||||
dest_account: 1
|
||||
dest_mvip: "{{ dest_cluster_hostname }}"
|
||||
dest_username: "{{ dest_cluster_username }}"
|
||||
dest_password: "{{ dest_cluster_password }}"
|
||||
|
||||
"""
|
||||
|
||||
RETURN = """
|
||||
|
||||
"""
|
||||
|
||||
from ansible.module_utils.basic import AnsibleModule
|
||||
from ansible.module_utils._text import to_native
|
||||
import ansible.module_utils.netapp as netapp_utils
|
||||
from ansible.module_utils.netapp_elementsw_module import NaElementSWModule
|
||||
from ansible.module_utils.netapp_module import NetAppModule
|
||||
|
||||
HAS_SF_SDK = netapp_utils.has_sf_sdk()
|
||||
try:
|
||||
import solidfire.common
|
||||
except ImportError:
|
||||
HAS_SF_SDK = False
|
||||
|
||||
|
||||
class ElementSWVolumePair(object):
|
||||
''' class to handle volume pairing operations '''
|
||||
|
||||
def __init__(self):
|
||||
"""
|
||||
Setup Ansible parameters and SolidFire connection
|
||||
"""
|
||||
self.argument_spec = netapp_utils.ontap_sf_host_argument_spec()
|
||||
self.argument_spec.update(dict(
|
||||
state=dict(required=False, choices=['present', 'absent'],
|
||||
default='present'),
|
||||
src_volume=dict(required=True, type='str'),
|
||||
src_account=dict(required=True, type='str'),
|
||||
dest_volume=dict(required=True, type='str'),
|
||||
dest_account=dict(required=True, type='str'),
|
||||
mode=dict(required=False, type='str',
|
||||
choices=['async', 'sync', 'snapshotsonly'],
|
||||
default='async'),
|
||||
dest_mvip=dict(required=True, type='str'),
|
||||
dest_username=dict(required=False, type='str'),
|
||||
dest_password=dict(required=False, type='str', no_log=True)
|
||||
))
|
||||
|
||||
self.module = AnsibleModule(
|
||||
argument_spec=self.argument_spec,
|
||||
supports_check_mode=True
|
||||
)
|
||||
|
||||
if HAS_SF_SDK is False:
|
||||
self.module.fail_json(msg="Unable to import the SolidFire Python SDK")
|
||||
else:
|
||||
self.elem = netapp_utils.create_sf_connection(module=self.module)
|
||||
|
||||
self.elementsw_helper = NaElementSWModule(self.elem)
|
||||
self.na_helper = NetAppModule()
|
||||
self.parameters = self.na_helper.set_parameters(self.module.params)
|
||||
# get element_sw_connection for destination cluster
|
||||
# overwrite existing source host, user and password with destination credentials
|
||||
self.module.params['hostname'] = self.parameters['dest_mvip']
|
||||
# username and password is same as source,
|
||||
# if dest_username and dest_password aren't specified
|
||||
if self.parameters.get('dest_username'):
|
||||
self.module.params['username'] = self.parameters['dest_username']
|
||||
if self.parameters.get('dest_password'):
|
||||
self.module.params['password'] = self.parameters['dest_password']
|
||||
self.dest_elem = netapp_utils.create_sf_connection(module=self.module)
|
||||
self.dest_elementsw_helper = NaElementSWModule(self.dest_elem)
|
||||
|
||||
def check_if_already_paired(self, vol_id):
|
||||
"""
|
||||
Check for idempotency
|
||||
A volume can have only one pair
|
||||
Return paired-volume-id if volume is paired already
|
||||
None if volume is not paired
|
||||
"""
|
||||
paired_volumes = self.elem.list_volumes(volume_ids=[vol_id],
|
||||
is_paired=True)
|
||||
for vol in paired_volumes.volumes:
|
||||
for pair in vol.volume_pairs:
|
||||
if pair is not None:
|
||||
return pair.remote_volume_id
|
||||
return None
|
||||
|
||||
def pair_volumes(self):
|
||||
"""
|
||||
Start volume pairing on source, and complete on target volume
|
||||
"""
|
||||
try:
|
||||
pair_key = self.elem.start_volume_pairing(
|
||||
volume_id=self.parameters['src_vol_id'],
|
||||
mode=self.parameters['mode'])
|
||||
self.dest_elem.complete_volume_pairing(
|
||||
volume_pairing_key=pair_key.volume_pairing_key,
|
||||
volume_id=self.parameters['dest_vol_id'])
|
||||
except solidfire.common.ApiServerError as err:
|
||||
self.module.fail_json(msg="Error pairing volume id %s"
|
||||
% (self.parameters['src_vol_id']),
|
||||
exception=to_native(err))
|
||||
|
||||
def pairing_exists(self, src_id, dest_id):
|
||||
src_paired = self.check_if_already_paired(self.parameters['src_vol_id'])
|
||||
dest_paired = self.check_if_already_paired(self.parameters['dest_vol_id'])
|
||||
if src_paired is not None or dest_paired is not None:
|
||||
return True
|
||||
return None
|
||||
|
||||
def unpair_volumes(self):
|
||||
"""
|
||||
Delete volume pair
|
||||
"""
|
||||
try:
|
||||
self.elem.remove_volume_pair(volume_id=self.parameters['src_vol_id'])
|
||||
self.dest_elem.remove_volume_pair(volume_id=self.parameters['dest_vol_id'])
|
||||
except solidfire.common.ApiServerError as err:
|
||||
self.module.fail_json(msg="Error unpairing volume ids %s and %s"
|
||||
% (self.parameters['src_vol_id'],
|
||||
self.parameters['dest_vol_id']),
|
||||
exception=to_native(err))
|
||||
|
||||
def get_account_id(self, account, type):
|
||||
"""
|
||||
Get source and destination account IDs
|
||||
"""
|
||||
try:
|
||||
if type == 'src':
|
||||
self.parameters['src_account_id'] = self.elementsw_helper.account_exists(account)
|
||||
elif type == 'dest':
|
||||
self.parameters['dest_account_id'] = self.dest_elementsw_helper.account_exists(account)
|
||||
except solidfire.common.ApiServerError as err:
|
||||
self.module.fail_json(msg="Error: either account %s or %s does not exist"
|
||||
% (self.parameters['src_account'],
|
||||
self.parameters['dest_account']),
|
||||
exception=to_native(err))
|
||||
|
||||
def get_volume_id(self, volume, type):
|
||||
"""
|
||||
Get source and destination volume IDs
|
||||
"""
|
||||
if type == 'src':
|
||||
self.parameters['src_vol_id'] = self.elementsw_helper.volume_exists(volume, self.parameters['src_account_id'])
|
||||
if self.parameters['src_vol_id'] is None:
|
||||
self.module.fail_json(msg="Error: source volume %s does not exist"
|
||||
% (self.parameters['src_volume']))
|
||||
elif type == 'dest':
|
||||
self.parameters['dest_vol_id'] = self.dest_elementsw_helper.volume_exists(volume, self.parameters['dest_account_id'])
|
||||
if self.parameters['dest_vol_id'] is None:
|
||||
self.module.fail_json(msg="Error: destination volume %s does not exist"
|
||||
% (self.parameters['dest_volume']))
|
||||
|
||||
def get_ids(self):
|
||||
"""
|
||||
Get IDs for volumes and accounts
|
||||
"""
|
||||
self.get_account_id(self.parameters['src_account'], 'src')
|
||||
self.get_account_id(self.parameters['dest_account'], 'dest')
|
||||
self.get_volume_id(self.parameters['src_volume'], 'src')
|
||||
self.get_volume_id(self.parameters['dest_volume'], 'dest')
|
||||
|
||||
def apply(self):
|
||||
"""
|
||||
Call create / delete volume pair methods
|
||||
"""
|
||||
self.get_ids()
|
||||
paired = self.pairing_exists(self.parameters['src_vol_id'],
|
||||
self.parameters['dest_vol_id'])
|
||||
# calling helper to determine action
|
||||
cd_action = self.na_helper.get_cd_action(paired, self.parameters)
|
||||
if cd_action == "create":
|
||||
self.pair_volumes()
|
||||
elif cd_action == "delete":
|
||||
self.unpair_volumes()
|
||||
self.module.exit_json(changed=self.na_helper.changed)
|
||||
|
||||
|
||||
def main():
|
||||
""" Apply volume pair actions """
|
||||
vol_obj = ElementSWVolumePair()
|
||||
vol_obj.apply()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
|
@ -3324,53 +3324,6 @@ lib/ansible/modules/source_control/git.py validate-modules:parameter-type-not-in
|
|||
lib/ansible/modules/source_control/subversion.py validate-modules:doc-required-mismatch
|
||||
lib/ansible/modules/source_control/subversion.py validate-modules:parameter-type-not-in-doc
|
||||
lib/ansible/modules/source_control/subversion.py validate-modules:undocumented-parameter
|
||||
lib/ansible/modules/storage/netapp/na_elementsw_access_group.py validate-modules:doc-missing-type
|
||||
lib/ansible/modules/storage/netapp/na_elementsw_access_group.py validate-modules:parameter-list-no-elements
|
||||
lib/ansible/modules/storage/netapp/na_elementsw_access_group.py validate-modules:parameter-type-not-in-doc
|
||||
lib/ansible/modules/storage/netapp/na_elementsw_account.py validate-modules:doc-missing-type
|
||||
lib/ansible/modules/storage/netapp/na_elementsw_account.py validate-modules:parameter-type-not-in-doc
|
||||
lib/ansible/modules/storage/netapp/na_elementsw_admin_users.py validate-modules:doc-missing-type
|
||||
lib/ansible/modules/storage/netapp/na_elementsw_admin_users.py validate-modules:parameter-list-no-elements
|
||||
lib/ansible/modules/storage/netapp/na_elementsw_admin_users.py validate-modules:parameter-type-not-in-doc
|
||||
lib/ansible/modules/storage/netapp/na_elementsw_backup.py validate-modules:doc-missing-type
|
||||
lib/ansible/modules/storage/netapp/na_elementsw_backup.py validate-modules:parameter-type-not-in-doc
|
||||
lib/ansible/modules/storage/netapp/na_elementsw_check_connections.py validate-modules:parameter-type-not-in-doc
|
||||
lib/ansible/modules/storage/netapp/na_elementsw_cluster.py validate-modules:parameter-type-not-in-doc
|
||||
lib/ansible/modules/storage/netapp/na_elementsw_cluster_config.py validate-modules:parameter-list-no-elements
|
||||
lib/ansible/modules/storage/netapp/na_elementsw_cluster_config.py validate-modules:parameter-type-not-in-doc
|
||||
lib/ansible/modules/storage/netapp/na_elementsw_cluster_pair.py validate-modules:doc-missing-type
|
||||
lib/ansible/modules/storage/netapp/na_elementsw_cluster_pair.py validate-modules:parameter-type-not-in-doc
|
||||
lib/ansible/modules/storage/netapp/na_elementsw_cluster_snmp.py validate-modules:parameter-type-not-in-doc
|
||||
lib/ansible/modules/storage/netapp/na_elementsw_drive.py validate-modules:doc-missing-type
|
||||
lib/ansible/modules/storage/netapp/na_elementsw_drive.py validate-modules:parameter-type-not-in-doc
|
||||
lib/ansible/modules/storage/netapp/na_elementsw_initiators.py validate-modules:doc-missing-type
|
||||
lib/ansible/modules/storage/netapp/na_elementsw_initiators.py validate-modules:doc-required-mismatch
|
||||
lib/ansible/modules/storage/netapp/na_elementsw_initiators.py validate-modules:parameter-list-no-elements
|
||||
lib/ansible/modules/storage/netapp/na_elementsw_initiators.py validate-modules:parameter-type-not-in-doc
|
||||
lib/ansible/modules/storage/netapp/na_elementsw_initiators.py validate-modules:undocumented-parameter
|
||||
lib/ansible/modules/storage/netapp/na_elementsw_ldap.py validate-modules:parameter-type-not-in-doc
|
||||
lib/ansible/modules/storage/netapp/na_elementsw_network_interfaces.py validate-modules:parameter-list-no-elements
|
||||
lib/ansible/modules/storage/netapp/na_elementsw_network_interfaces.py validate-modules:parameter-type-not-in-doc
|
||||
lib/ansible/modules/storage/netapp/na_elementsw_node.py validate-modules:doc-missing-type
|
||||
lib/ansible/modules/storage/netapp/na_elementsw_node.py validate-modules:parameter-list-no-elements
|
||||
lib/ansible/modules/storage/netapp/na_elementsw_node.py validate-modules:parameter-type-not-in-doc
|
||||
lib/ansible/modules/storage/netapp/na_elementsw_snapshot.py validate-modules:doc-missing-type
|
||||
lib/ansible/modules/storage/netapp/na_elementsw_snapshot.py validate-modules:parameter-type-not-in-doc
|
||||
lib/ansible/modules/storage/netapp/na_elementsw_snapshot_restore.py validate-modules:parameter-type-not-in-doc
|
||||
lib/ansible/modules/storage/netapp/na_elementsw_snapshot_schedule.py validate-modules:doc-missing-type
|
||||
lib/ansible/modules/storage/netapp/na_elementsw_snapshot_schedule.py validate-modules:doc-required-mismatch
|
||||
lib/ansible/modules/storage/netapp/na_elementsw_snapshot_schedule.py validate-modules:parameter-list-no-elements
|
||||
lib/ansible/modules/storage/netapp/na_elementsw_snapshot_schedule.py validate-modules:parameter-type-not-in-doc
|
||||
lib/ansible/modules/storage/netapp/na_elementsw_vlan.py validate-modules:doc-missing-type
|
||||
lib/ansible/modules/storage/netapp/na_elementsw_vlan.py validate-modules:parameter-list-no-elements
|
||||
lib/ansible/modules/storage/netapp/na_elementsw_vlan.py validate-modules:parameter-type-not-in-doc
|
||||
lib/ansible/modules/storage/netapp/na_elementsw_volume.py validate-modules:doc-missing-type
|
||||
lib/ansible/modules/storage/netapp/na_elementsw_volume.py validate-modules:parameter-invalid
|
||||
lib/ansible/modules/storage/netapp/na_elementsw_volume.py validate-modules:parameter-type-not-in-doc
|
||||
lib/ansible/modules/storage/netapp/na_elementsw_volume_clone.py validate-modules:doc-missing-type
|
||||
lib/ansible/modules/storage/netapp/na_elementsw_volume_clone.py validate-modules:parameter-type-not-in-doc
|
||||
lib/ansible/modules/storage/netapp/na_elementsw_volume_pair.py validate-modules:doc-missing-type
|
||||
lib/ansible/modules/storage/netapp/na_elementsw_volume_pair.py validate-modules:parameter-type-not-in-doc
|
||||
lib/ansible/modules/storage/netapp/na_ontap_aggregate.py validate-modules:doc-missing-type
|
||||
lib/ansible/modules/storage/netapp/na_ontap_aggregate.py validate-modules:parameter-list-no-elements
|
||||
lib/ansible/modules/storage/netapp/na_ontap_aggregate.py validate-modules:parameter-type-not-in-doc
|
||||
|
@ -3995,12 +3948,6 @@ test/units/modules/packaging/os/test_yum.py future-import-boilerplate
|
|||
test/units/modules/packaging/os/test_yum.py metaclass-boilerplate
|
||||
test/units/modules/remote_management/oneview/conftest.py future-import-boilerplate
|
||||
test/units/modules/remote_management/oneview/conftest.py metaclass-boilerplate
|
||||
test/units/modules/storage/netapp/test_na_elementsw_cluster_config.py future-import-boilerplate
|
||||
test/units/modules/storage/netapp/test_na_elementsw_cluster_config.py metaclass-boilerplate
|
||||
test/units/modules/storage/netapp/test_na_elementsw_cluster_snmp.py future-import-boilerplate
|
||||
test/units/modules/storage/netapp/test_na_elementsw_cluster_snmp.py metaclass-boilerplate
|
||||
test/units/modules/storage/netapp/test_na_elementsw_initiators.py future-import-boilerplate
|
||||
test/units/modules/storage/netapp/test_na_elementsw_initiators.py metaclass-boilerplate
|
||||
test/units/modules/storage/netapp/test_na_ontap_aggregate.py future-import-boilerplate
|
||||
test/units/modules/storage/netapp/test_na_ontap_aggregate.py metaclass-boilerplate
|
||||
test/units/modules/storage/netapp/test_na_ontap_autosupport.py future-import-boilerplate
|
||||
|
|
|
@ -1,175 +0,0 @@
|
|||
''' unit test for Ansible module: na_elementsw_account.py '''
|
||||
|
||||
from __future__ import absolute_import, division, print_function
|
||||
__metaclass__ = type
|
||||
|
||||
import json
|
||||
import pytest
|
||||
|
||||
from units.compat import unittest
|
||||
from units.compat.mock import patch
|
||||
from ansible.module_utils import basic
|
||||
from ansible.module_utils._text import to_bytes
|
||||
import ansible.module_utils.netapp as netapp_utils
|
||||
|
||||
if not netapp_utils.has_sf_sdk():
|
||||
pytestmark = pytest.mark.skip('skipping as missing required SolidFire Python SDK')
|
||||
|
||||
from ansible.modules.storage.netapp.na_elementsw_access_group \
|
||||
import ElementSWAccessGroup as my_module # module under test
|
||||
|
||||
|
||||
def set_module_args(args):
|
||||
"""prepare arguments so that they will be picked up during module creation"""
|
||||
args = json.dumps({'ANSIBLE_MODULE_ARGS': args})
|
||||
basic._ANSIBLE_ARGS = to_bytes(args) # pylint: disable=protected-access
|
||||
|
||||
|
||||
class AnsibleExitJson(Exception):
|
||||
"""Exception class to be raised by module.exit_json and caught by the test case"""
|
||||
pass
|
||||
|
||||
|
||||
class AnsibleFailJson(Exception):
|
||||
"""Exception class to be raised by module.fail_json and caught by the test case"""
|
||||
pass
|
||||
|
||||
|
||||
def exit_json(*args, **kwargs): # pylint: disable=unused-argument
|
||||
"""function to patch over exit_json; package return data into an exception"""
|
||||
if 'changed' not in kwargs:
|
||||
kwargs['changed'] = False
|
||||
raise AnsibleExitJson(kwargs)
|
||||
|
||||
|
||||
def fail_json(*args, **kwargs): # pylint: disable=unused-argument
|
||||
"""function to patch over fail_json; package return data into an exception"""
|
||||
kwargs['failed'] = True
|
||||
raise AnsibleFailJson(kwargs)
|
||||
|
||||
|
||||
ADD_ERROR = 'some_error_in_add_access_group'
|
||||
|
||||
|
||||
class MockSFConnection(object):
|
||||
''' mock connection to ElementSW host '''
|
||||
|
||||
class Bunch(object): # pylint: disable=too-few-public-methods
|
||||
''' create object with arbitrary attributes '''
|
||||
def __init__(self, **kw):
|
||||
''' called with (k1=v1, k2=v2), creates obj.k1, obj.k2 with values v1, v2 '''
|
||||
setattr(self, '__dict__', kw)
|
||||
|
||||
def __init__(self, force_error=False, where=None):
|
||||
''' save arguments '''
|
||||
self.force_error = force_error
|
||||
self.where = where
|
||||
|
||||
def list_volume_access_groups(self, *args, **kwargs): # pylint: disable=unused-argument
|
||||
''' build access_group list: access_groups.name, access_groups.account_id '''
|
||||
access_groups = list()
|
||||
access_group_list = self.Bunch(volume_access_groups=access_groups)
|
||||
return access_group_list
|
||||
|
||||
def create_volume_access_group(self, *args, **kwargs): # pylint: disable=unused-argument
|
||||
''' We don't check the return code, but could force an exception '''
|
||||
if self.force_error and 'add' in self.where:
|
||||
# The module does not check for a specific exception :(
|
||||
raise OSError(ADD_ERROR)
|
||||
|
||||
def get_account_by_name(self, *args, **kwargs): # pylint: disable=unused-argument
|
||||
''' returns account_id '''
|
||||
if self.force_error and 'account_id' in self.where:
|
||||
account_id = None
|
||||
else:
|
||||
account_id = 1
|
||||
print('account_id', account_id)
|
||||
account = self.Bunch(account_id=account_id)
|
||||
result = self.Bunch(account=account)
|
||||
return result
|
||||
|
||||
|
||||
class TestMyModule(unittest.TestCase):
|
||||
''' a group of related Unit Tests '''
|
||||
|
||||
def setUp(self):
|
||||
self.mock_module_helper = patch.multiple(basic.AnsibleModule,
|
||||
exit_json=exit_json,
|
||||
fail_json=fail_json)
|
||||
self.mock_module_helper.start()
|
||||
self.addCleanup(self.mock_module_helper.stop)
|
||||
|
||||
def test_module_fail_when_required_args_missing(self):
|
||||
''' required arguments are reported as errors '''
|
||||
with pytest.raises(AnsibleFailJson) as exc:
|
||||
set_module_args({})
|
||||
my_module()
|
||||
print('Info: %s' % exc.value.args[0]['msg'])
|
||||
|
||||
@patch('ansible.module_utils.netapp.create_sf_connection')
|
||||
def test_ensure_command_called(self, mock_create_sf_connection):
|
||||
''' a more interesting test '''
|
||||
set_module_args({
|
||||
'state': 'present',
|
||||
'name': 'element_groupname',
|
||||
'account_id': 'element_account_id',
|
||||
'hostname': 'hostname',
|
||||
'username': 'username',
|
||||
'password': 'password',
|
||||
})
|
||||
# my_obj.sfe will be assigned a MockSFConnection object:
|
||||
mock_create_sf_connection.return_value = MockSFConnection()
|
||||
my_obj = my_module()
|
||||
with pytest.raises(AnsibleExitJson) as exc:
|
||||
# It may not be a good idea to start with apply
|
||||
# More atomic methods can be easier to mock
|
||||
my_obj.apply()
|
||||
print(exc.value.args[0])
|
||||
assert exc.value.args[0]['changed']
|
||||
|
||||
@patch('ansible.module_utils.netapp.create_sf_connection')
|
||||
def test_check_error_reporting_on_add_exception(self, mock_create_sf_connection):
|
||||
''' a more interesting test '''
|
||||
set_module_args({
|
||||
'state': 'present',
|
||||
'name': 'element_groupname',
|
||||
'account_id': 'element_account_id',
|
||||
'hostname': 'hostname',
|
||||
'username': 'username',
|
||||
'password': 'password',
|
||||
})
|
||||
# my_obj.sfe will be assigned a MockSFConnection object:
|
||||
mock_create_sf_connection.return_value = MockSFConnection(force_error=True, where=['add'])
|
||||
my_obj = my_module()
|
||||
with pytest.raises(AnsibleFailJson) as exc:
|
||||
# It may not be a good idea to start with apply
|
||||
# More atomic methods can be easier to mock
|
||||
# apply() is calling list_accounts() and add_account()
|
||||
my_obj.apply()
|
||||
print(exc.value.args[0])
|
||||
message = 'Error creating volume access group element_groupname: %s' % ADD_ERROR
|
||||
assert exc.value.args[0]['msg'] == message
|
||||
|
||||
@patch('ansible.module_utils.netapp.create_sf_connection')
|
||||
def test_check_error_reporting_on_invalid_account_id(self, mock_create_sf_connection):
|
||||
''' a more interesting test '''
|
||||
set_module_args({
|
||||
'state': 'present',
|
||||
'name': 'element_groupname',
|
||||
'account_id': 'element_account_id',
|
||||
'volumes': ['volume1'],
|
||||
'hostname': 'hostname',
|
||||
'username': 'username',
|
||||
'password': 'password',
|
||||
})
|
||||
# my_obj.sfe will be assigned a MockSFConnection object:
|
||||
mock_create_sf_connection.return_value = MockSFConnection(force_error=True, where=['account_id'])
|
||||
my_obj = my_module()
|
||||
with pytest.raises(AnsibleFailJson) as exc:
|
||||
# It may not be a good idea to start with apply
|
||||
# More atomic methods can be easier to mock
|
||||
# apply() is calling list_accounts() and add_account()
|
||||
my_obj.apply()
|
||||
print(exc.value.args[0])
|
||||
message = 'Error: Specified account id "%s" does not exist.' % 'element_account_id'
|
||||
assert exc.value.args[0]['msg'] == message
|
|
@ -1,159 +0,0 @@
|
|||
# (c) 2019, NetApp, Inc
|
||||
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
|
||||
''' unit test for Ansible module: na_elementsw_cluster_config.py '''
|
||||
|
||||
from __future__ import print_function
|
||||
import json
|
||||
import pytest
|
||||
|
||||
from units.compat import unittest
|
||||
from units.compat.mock import patch
|
||||
from ansible.module_utils import basic
|
||||
from ansible.module_utils._text import to_bytes
|
||||
import ansible.module_utils.netapp as netapp_utils
|
||||
|
||||
if not netapp_utils.has_sf_sdk():
|
||||
pytestmark = pytest.mark.skip('skipping as missing required SolidFire Python SDK')
|
||||
|
||||
from ansible.modules.storage.netapp.na_elementsw_cluster_config \
|
||||
import ElementSWClusterConfig as my_module # module under test
|
||||
|
||||
|
||||
def set_module_args(args):
|
||||
"""prepare arguments so that they will be picked up during module creation"""
|
||||
args = json.dumps({'ANSIBLE_MODULE_ARGS': args})
|
||||
basic._ANSIBLE_ARGS = to_bytes(args) # pylint: disable=protected-access
|
||||
|
||||
|
||||
class AnsibleExitJson(Exception):
|
||||
"""Exception class to be raised by module.exit_json and caught by the test case"""
|
||||
pass
|
||||
|
||||
|
||||
class AnsibleFailJson(Exception):
|
||||
"""Exception class to be raised by module.fail_json and caught by the test case"""
|
||||
pass
|
||||
|
||||
|
||||
def exit_json(*args, **kwargs): # pylint: disable=unused-argument
|
||||
"""function to patch over exit_json; package return data into an exception"""
|
||||
if 'changed' not in kwargs:
|
||||
kwargs['changed'] = False
|
||||
raise AnsibleExitJson(kwargs)
|
||||
|
||||
|
||||
def fail_json(*args, **kwargs): # pylint: disable=unused-argument
|
||||
"""function to patch over fail_json; package return data into an exception"""
|
||||
kwargs['failed'] = True
|
||||
raise AnsibleFailJson(kwargs)
|
||||
|
||||
|
||||
GET_ERROR = 'some_error_in_get_ntp_info'
|
||||
|
||||
|
||||
class MockSFConnection(object):
|
||||
''' mock connection to ElementSW host '''
|
||||
|
||||
class Bunch(object): # pylint: disable=too-few-public-methods
|
||||
''' create object with arbitrary attributes '''
|
||||
|
||||
def __init__(self, **kw):
|
||||
''' called with (k1=v1, k2=v2), creates obj.k1, obj.k2 with values v1, v2 '''
|
||||
setattr(self, '__dict__', kw)
|
||||
|
||||
def __init__(self, force_error=False, where=None):
|
||||
''' save arguments '''
|
||||
self.force_error = force_error
|
||||
self.where = where
|
||||
|
||||
|
||||
class TestMyModule(unittest.TestCase):
|
||||
''' a group of related Unit Tests '''
|
||||
|
||||
def setUp(self):
|
||||
self.mock_module_helper = patch.multiple(basic.AnsibleModule,
|
||||
exit_json=exit_json,
|
||||
fail_json=fail_json)
|
||||
self.mock_module_helper.start()
|
||||
self.addCleanup(self.mock_module_helper.stop)
|
||||
|
||||
def set_default_args(self):
|
||||
return dict({
|
||||
'hostname': '10.253.168.129',
|
||||
'username': 'namburu',
|
||||
'password': 'SFlab1234',
|
||||
})
|
||||
|
||||
@patch('ansible.module_utils.netapp.create_sf_connection')
|
||||
def test_module_fail_when_required_args_missing(self, mock_create_sf_connection):
|
||||
''' required arguments are reported as errors '''
|
||||
with pytest.raises(AnsibleFailJson) as exc:
|
||||
set_module_args({})
|
||||
my_module()
|
||||
print('Info: %s' % exc.value.args[0]['msg'])
|
||||
|
||||
@patch('ansible.module_utils.netapp.create_sf_connection')
|
||||
def test_ensure_setup_ntp_info_called(self, mock_create_sf_connection):
|
||||
''' test if setup_ntp_info is called '''
|
||||
module_args = {}
|
||||
module_args.update(self.set_default_args())
|
||||
ntp_dict = {'set_ntp_info': {'broadcastclient': None,
|
||||
'ntp_servers': ['1.1.1.1']}}
|
||||
module_args.update(ntp_dict)
|
||||
set_module_args(module_args)
|
||||
my_obj = my_module()
|
||||
with pytest.raises(AnsibleExitJson) as exc:
|
||||
my_obj.apply()
|
||||
print('Info: test_setup_ntp_info: %s' % repr(exc.value))
|
||||
assert exc.value.args[0]['changed']
|
||||
|
||||
@patch('ansible.module_utils.netapp.create_sf_connection')
|
||||
def test_ensure_set_encryption_at_rest_called(self, mock_create_sf_connection):
|
||||
''' test if set_encryption_at_rest is called '''
|
||||
module_args = {}
|
||||
module_args.update(self.set_default_args())
|
||||
module_args.update({'encryption_at_rest': 'present'})
|
||||
set_module_args(module_args)
|
||||
my_obj = my_module()
|
||||
with pytest.raises(AnsibleExitJson) as exc:
|
||||
my_obj.apply()
|
||||
print('Info: test_set_encryption_at_rest enable: %s' % repr(exc.value))
|
||||
assert not exc.value.args[0]['changed']
|
||||
module_args.update({'encryption_at_rest': 'absent'})
|
||||
set_module_args(module_args)
|
||||
my_obj = my_module()
|
||||
with pytest.raises(AnsibleExitJson) as exc:
|
||||
my_obj.apply()
|
||||
print('Info: test_set_encryption_at_rest disable: %s' % repr(exc.value))
|
||||
assert not exc.value.args[0]['changed']
|
||||
|
||||
@patch('ansible.module_utils.netapp.create_sf_connection')
|
||||
def test_ensure_enable_feature_called(self, mock_create_sf_connection):
|
||||
''' test if enable_feature for vvols is called '''
|
||||
module_args = {}
|
||||
module_args.update(self.set_default_args())
|
||||
module_args.update({'enable_virtual_volumes': True})
|
||||
set_module_args(module_args)
|
||||
my_obj = my_module()
|
||||
with pytest.raises(AnsibleExitJson) as exc:
|
||||
my_obj.apply()
|
||||
print('Info: test_enable_feature: %s' % repr(exc.value))
|
||||
assert not exc.value.args[0]['changed']
|
||||
|
||||
@patch('ansible.module_utils.netapp.create_sf_connection')
|
||||
def test_ensure_set_cluster_full_threshold_called(self, mock_create_sf_connection):
|
||||
''' test if set_cluster_full threshold is called '''
|
||||
module_args = {}
|
||||
module_args.update(self.set_default_args())
|
||||
cluster_mod_dict = \
|
||||
{'modify_cluster_full_threshold': {'stage2_aware_threshold': 2,
|
||||
'stage3_block_threshold_percent': 2,
|
||||
'max_metadata_over_provision_factor': 2}}
|
||||
module_args.update(cluster_mod_dict)
|
||||
set_module_args(module_args)
|
||||
my_obj = my_module()
|
||||
with pytest.raises(AnsibleExitJson) as exc:
|
||||
my_obj.apply()
|
||||
print('Info: test_set_cluster_full_threshold: %s' % repr(exc.value))
|
||||
assert exc.value.args[0]['changed']
|
|
@ -1,178 +0,0 @@
|
|||
# (c) 2019, NetApp, Inc
|
||||
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
|
||||
''' unit test for Ansible module: na_elementsw_cluster_snmp.py '''
|
||||
|
||||
from __future__ import print_function
|
||||
import json
|
||||
import pytest
|
||||
|
||||
from units.compat import unittest
|
||||
from units.compat.mock import patch
|
||||
from ansible.module_utils import basic
|
||||
from ansible.module_utils._text import to_bytes
|
||||
import ansible.module_utils.netapp as netapp_utils
|
||||
|
||||
if not netapp_utils.has_sf_sdk():
|
||||
pytestmark = pytest.mark.skip('skipping as missing required SolidFire Python SDK')
|
||||
|
||||
from ansible.modules.storage.netapp.na_elementsw_cluster_snmp \
|
||||
import ElementSWClusterSnmp as my_module # module under test
|
||||
|
||||
|
||||
def set_module_args(args):
|
||||
"""prepare arguments so that they will be picked up during module creation"""
|
||||
args = json.dumps({'ANSIBLE_MODULE_ARGS': args})
|
||||
basic._ANSIBLE_ARGS = to_bytes(args) # pylint: disable=protected-access
|
||||
|
||||
|
||||
class AnsibleExitJson(Exception):
|
||||
"""Exception class to be raised by module.exit_json and caught by the test case"""
|
||||
pass
|
||||
|
||||
|
||||
class AnsibleFailJson(Exception):
|
||||
"""Exception class to be raised by module.fail_json and caught by the test case"""
|
||||
pass
|
||||
|
||||
|
||||
def exit_json(*args, **kwargs): # pylint: disable=unused-argument
|
||||
"""function to patch over exit_json; package return data into an exception"""
|
||||
if 'changed' not in kwargs:
|
||||
kwargs['changed'] = False
|
||||
raise AnsibleExitJson(kwargs)
|
||||
|
||||
|
||||
def fail_json(*args, **kwargs): # pylint: disable=unused-argument
|
||||
"""function to patch over fail_json; package return data into an exception"""
|
||||
kwargs['failed'] = True
|
||||
raise AnsibleFailJson(kwargs)
|
||||
|
||||
|
||||
GET_ERROR = 'some_error_in_get_snmp_info'
|
||||
|
||||
|
||||
class MockSFConnection(object):
|
||||
''' mock connection to ElementSW host '''
|
||||
|
||||
class Bunch(object): # pylint: disable=too-few-public-methods
|
||||
''' create object with arbitrary attributes '''
|
||||
def __init__(self, **kw):
|
||||
''' called with (k1=v1, k2=v2), creates obj.k1, obj.k2 with values v1, v2 '''
|
||||
setattr(self, '__dict__', kw)
|
||||
|
||||
def __init__(self, force_error=False, where=None):
|
||||
''' save arguments '''
|
||||
self.force_error = force_error
|
||||
self.where = where
|
||||
|
||||
|
||||
class TestMyModule(unittest.TestCase):
|
||||
''' a group of related Unit Tests '''
|
||||
|
||||
def setUp(self):
|
||||
self.mock_module_helper = patch.multiple(basic.AnsibleModule,
|
||||
exit_json=exit_json,
|
||||
fail_json=fail_json)
|
||||
self.mock_module_helper.start()
|
||||
self.addCleanup(self.mock_module_helper.stop)
|
||||
|
||||
def set_default_args(self):
|
||||
return dict({
|
||||
'hostname': '10.117.78.131',
|
||||
'username': 'admin',
|
||||
'password': 'netapp1!',
|
||||
})
|
||||
|
||||
@patch('ansible.module_utils.netapp.create_sf_connection')
|
||||
def test_module_fail_when_required_args_missing(self, mock_create_sf_connection):
|
||||
''' required arguments are reported as errors '''
|
||||
with pytest.raises(AnsibleFailJson) as exc:
|
||||
set_module_args({})
|
||||
my_module()
|
||||
print('Info: %s' % exc.value)
|
||||
|
||||
@patch('ansible.module_utils.netapp.create_sf_connection')
|
||||
def test_ensure_enable_snmp_called(self, mock_create_sf_connection):
|
||||
''' test if enable_snmp is called '''
|
||||
module_args = {}
|
||||
module_args.update(self.set_default_args())
|
||||
module_args.update({'snmp_v3_enabled': True,
|
||||
'state': 'present'})
|
||||
module_args.update({'usm_users': {'access': 'rouser',
|
||||
'name': 'TestUser',
|
||||
'password': 'ChangeMe@123',
|
||||
'passphrase': 'ChangeMe@123',
|
||||
'secLevel': 'auth', }})
|
||||
|
||||
module_args.update({'networks': {'access': 'ro',
|
||||
'cidr': 24,
|
||||
'community': 'TestNetwork',
|
||||
'network': '192.168.0.1', }})
|
||||
set_module_args(module_args)
|
||||
my_obj = my_module()
|
||||
with pytest.raises(AnsibleExitJson) as exc:
|
||||
my_obj.apply()
|
||||
print('Info: test_if_enable_snmp_called: %s' % repr(exc.value))
|
||||
assert exc.value
|
||||
|
||||
@patch('ansible.module_utils.netapp.create_sf_connection')
|
||||
def test_ensure_configure_snmp_from_version_3_TO_version_2_called(self, mock_create_sf_connection):
|
||||
''' test if configure snmp from version_3 to version_2'''
|
||||
module_args = {}
|
||||
module_args.update(self.set_default_args())
|
||||
module_args.update({'snmp_v3_enabled': False,
|
||||
'state': 'present'})
|
||||
module_args.update({'usm_users': {'access': 'rouser',
|
||||
'name': 'TestUser',
|
||||
'password': 'ChangeMe@123',
|
||||
'passphrase': 'ChangeMe@123',
|
||||
'secLevel': 'auth', }})
|
||||
|
||||
module_args.update({'networks': {'access': 'ro',
|
||||
'cidr': 24,
|
||||
'community': 'TestNetwork',
|
||||
'network': '192.168.0.1', }})
|
||||
set_module_args(module_args)
|
||||
my_obj = my_module()
|
||||
with pytest.raises(AnsibleExitJson) as exc:
|
||||
my_obj.apply()
|
||||
print('Info: test_ensure_configure_snmp_from_version_3_TO_version_2_called: %s' % repr(exc.value))
|
||||
assert exc.value
|
||||
|
||||
@patch('ansible.module_utils.netapp.create_sf_connection')
|
||||
def test_ensure_configure_snmp_from_version_2_TO_version_3_called(self, mock_create_sf_connection):
|
||||
''' test if configure snmp from version_2 to version_3'''
|
||||
module_args = {}
|
||||
module_args.update(self.set_default_args())
|
||||
module_args.update({'snmp_v3_enabled': True,
|
||||
'state': 'present'})
|
||||
module_args.update({'usm_users': {'access': 'rouser',
|
||||
'name': 'TestUser_sample',
|
||||
'password': 'ChangeMe@123',
|
||||
'passphrase': 'ChangeMe@123',
|
||||
'secLevel': 'auth', }})
|
||||
|
||||
module_args.update({'networks': {'access': 'ro',
|
||||
'cidr': 24,
|
||||
'community': 'TestNetwork',
|
||||
'network': '192.168.0.1', }})
|
||||
set_module_args(module_args)
|
||||
my_obj = my_module()
|
||||
with pytest.raises(AnsibleExitJson) as exc:
|
||||
my_obj.apply()
|
||||
print('Info: test_ensure_configure_snmp_from_version_2_TO_version_3_called: %s' % repr(exc.value))
|
||||
assert exc.value
|
||||
|
||||
@patch('ansible.module_utils.netapp.create_sf_connection')
|
||||
def test_ensure_disable_snmp_called(self, mock_create_sf_connection):
|
||||
''' test if disable_snmp is called '''
|
||||
module_args = {}
|
||||
module_args.update(self.set_default_args())
|
||||
module_args.update({'state': 'absent'})
|
||||
set_module_args(module_args)
|
||||
my_obj = my_module()
|
||||
with pytest.raises(AnsibleExitJson) as exc:
|
||||
my_obj.apply()
|
||||
print('Info: test_if_disable_snmp_called: %s' % repr(exc.value))
|
||||
assert exc.value
|
|
@ -1,176 +0,0 @@
|
|||
# (c) 2019, NetApp, Inc
|
||||
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
''' unit test for Ansible module: na_elementsw_initiators.py '''
|
||||
|
||||
from __future__ import print_function
|
||||
import json
|
||||
import pytest
|
||||
|
||||
from units.compat import unittest
|
||||
from units.compat.mock import patch
|
||||
from ansible.module_utils import basic
|
||||
from ansible.module_utils._text import to_bytes
|
||||
import ansible.module_utils.netapp as netapp_utils
|
||||
|
||||
if not netapp_utils.has_sf_sdk():
|
||||
pytestmark = pytest.mark.skip('skipping as missing required SolidFire Python SDK')
|
||||
|
||||
from ansible.modules.storage.netapp.na_elementsw_initiators \
|
||||
import ElementSWInitiators as my_module # module under test
|
||||
|
||||
|
||||
def set_module_args(args):
|
||||
"""prepare arguments so that they will be picked up during module creation"""
|
||||
args = json.dumps({'ANSIBLE_MODULE_ARGS': args})
|
||||
basic._ANSIBLE_ARGS = to_bytes(args) # pylint: disable=protected-access
|
||||
|
||||
|
||||
class AnsibleExitJson(Exception):
|
||||
"""Exception class to be raised by module.exit_json and caught by the test case"""
|
||||
pass
|
||||
|
||||
|
||||
class AnsibleFailJson(Exception):
|
||||
"""Exception class to be raised by module.fail_json and caught by the test case"""
|
||||
pass
|
||||
|
||||
|
||||
def exit_json(*args, **kwargs): # pylint: disable=unused-argument
|
||||
"""function to patch over exit_json; package return data into an exception"""
|
||||
if 'changed' not in kwargs:
|
||||
kwargs['changed'] = False
|
||||
raise AnsibleExitJson(kwargs)
|
||||
|
||||
|
||||
def fail_json(*args, **kwargs): # pylint: disable=unused-argument
|
||||
"""function to patch over fail_json; package return data into an exception"""
|
||||
kwargs['failed'] = True
|
||||
raise AnsibleFailJson(kwargs)
|
||||
|
||||
|
||||
class MockSFConnection(object):
|
||||
''' mock connection to ElementSW host '''
|
||||
|
||||
class Bunch(object): # pylint: disable=too-few-public-methods
|
||||
''' create object with arbitrary attributes '''
|
||||
def __init__(self, **kw):
|
||||
''' called with (k1=v1, k2=v2), creates obj.k1, obj.k2 with values v1, v2 '''
|
||||
setattr(self, '__dict__', kw)
|
||||
|
||||
class Initiator(object):
|
||||
def __init__(self, entries):
|
||||
self.__dict__.update(entries)
|
||||
|
||||
def list_initiators(self):
|
||||
''' build initiator Obj '''
|
||||
all_initiators = {
|
||||
"initiators": [{
|
||||
"initiator_name": "a",
|
||||
"initiator_id": 13,
|
||||
"alias": "a2",
|
||||
"attributes": {"key": "value"}
|
||||
}]
|
||||
}
|
||||
return json.loads(json.dumps(all_initiators), object_hook=self.Initiator)
|
||||
|
||||
def create_initiators(self, *args, **kwargs): # pylint: disable=unused-argument
|
||||
''' mock method '''
|
||||
pass
|
||||
|
||||
def delete_initiators(self, *args, **kwargs): # pylint: disable=unused-argument
|
||||
''' mock method '''
|
||||
pass
|
||||
|
||||
def modify_initiators(self, *args, **kwargs): # pylint: disable=unused-argument
|
||||
''' mock method '''
|
||||
pass
|
||||
|
||||
|
||||
class TestMyModule(unittest.TestCase):
|
||||
''' a group of related Unit Tests '''
|
||||
|
||||
def setUp(self):
|
||||
self.mock_module_helper = patch.multiple(basic.AnsibleModule,
|
||||
exit_json=exit_json,
|
||||
fail_json=fail_json)
|
||||
self.mock_module_helper.start()
|
||||
self.addCleanup(self.mock_module_helper.stop)
|
||||
|
||||
def set_default_args(self):
|
||||
return dict({
|
||||
'hostname': '10.253.168.129',
|
||||
'username': 'namburu',
|
||||
'password': 'SFlab1234',
|
||||
})
|
||||
|
||||
@patch('ansible.module_utils.netapp.create_sf_connection')
|
||||
def test_module_fail_when_required_args_missing(self, mock_create_sf_connection):
|
||||
''' required arguments are reported as errors '''
|
||||
with pytest.raises(AnsibleFailJson) as exc:
|
||||
set_module_args({})
|
||||
my_module()
|
||||
print('Info: %s' % exc.value.args[0]['msg'])
|
||||
|
||||
@patch('ansible.module_utils.netapp.create_sf_connection')
|
||||
def test_create_initiators(self, mock_create_sf_connection):
|
||||
''' test if create initiator is called '''
|
||||
module_args = {}
|
||||
module_args.update(self.set_default_args())
|
||||
initiator_dict = {
|
||||
"state": "present",
|
||||
"initiators": [{
|
||||
"name": "newinitiator1",
|
||||
"alias": "newinitiator1alias",
|
||||
"attributes": {"key1": "value1"}
|
||||
}]
|
||||
}
|
||||
module_args.update(initiator_dict)
|
||||
set_module_args(module_args)
|
||||
mock_create_sf_connection.return_value = MockSFConnection()
|
||||
my_obj = my_module()
|
||||
with pytest.raises(AnsibleExitJson) as exc:
|
||||
my_obj.apply()
|
||||
print('Info: test_create_initiators: %s' % repr(exc.value))
|
||||
assert exc.value.args[0]['changed']
|
||||
|
||||
@patch('ansible.module_utils.netapp.create_sf_connection')
|
||||
def test_delete_initiators(self, mock_create_sf_connection):
|
||||
''' test if delete initiator is called '''
|
||||
module_args = {}
|
||||
module_args.update(self.set_default_args())
|
||||
initiator_dict = {
|
||||
"state": "absent",
|
||||
"initiators": [{
|
||||
"name": "a"
|
||||
}]
|
||||
}
|
||||
module_args.update(initiator_dict)
|
||||
set_module_args(module_args)
|
||||
mock_create_sf_connection.return_value = MockSFConnection()
|
||||
my_obj = my_module()
|
||||
with pytest.raises(AnsibleExitJson) as exc:
|
||||
my_obj.apply()
|
||||
print('Info: test_delete_initiators: %s' % repr(exc.value))
|
||||
assert exc.value.args[0]['changed']
|
||||
|
||||
@patch('ansible.module_utils.netapp.create_sf_connection')
|
||||
def test_modify_initiators(self, mock_create_sf_connection):
|
||||
''' test if modify initiator is called '''
|
||||
module_args = {}
|
||||
module_args.update(self.set_default_args())
|
||||
initiator_dict = {
|
||||
"state": "present",
|
||||
"initiators": [{
|
||||
"initiator_name": "a",
|
||||
"alias": "a3",
|
||||
"attributes": {"key": "value"}
|
||||
}]
|
||||
}
|
||||
module_args.update(initiator_dict)
|
||||
set_module_args(module_args)
|
||||
mock_create_sf_connection.return_value = MockSFConnection()
|
||||
my_obj = my_module()
|
||||
with pytest.raises(AnsibleExitJson) as exc:
|
||||
my_obj.apply()
|
||||
print('Info: test_modify_initiators: %s' % repr(exc.value))
|
||||
assert exc.value.args[0]['changed']
|
Loading…
Reference in a new issue