Init: mediaserver

This commit is contained in:
2023-02-08 12:13:28 +01:00
parent 848bc9739c
commit f7c23d4ba9
31914 changed files with 6175775 additions and 0 deletions

View File

@@ -0,0 +1,53 @@
# Copyright: (c) 2020, Dell Technologies.
# Apache License version 2.0 (see MODULE-LICENSE or http://www.apache.org/licenses/LICENSE-2.0.txt)
from __future__ import absolute_import, division, print_function
__metaclass__ = type
class ModuleDocFragment(object):
# Documentation fragment for Unity (unity)
DOCUMENTATION = r'''
options:
unispherehost:
required: true
description:
- IP or FQDN of the Unity management server.
type: str
username:
type: str
required: true
description:
- The username of the Unity management server.
password:
type: str
required: true
description:
- The password of the Unity management server.
validate_certs:
type: bool
default: true
aliases:
- verifycert
description:
- Boolean variable to specify whether or not to validate SSL
certificate.
- C(true) - Indicates that the SSL certificate should be verified.
- C(false) - Indicates that the SSL certificate should not be
verified.
port:
description:
- Port number through which communication happens with Unity
management server.
type: int
default: 443
requirements:
- A Dell Unity Storage device version 5.1 or later.
- Ansible-core 2.12 or later.
- Python 3.9, 3.10 or 3.11.
- Storops Python SDK 1.2.11.
notes:
- The modules present in this collection named as 'dellemc.unity'
are built to support the Dell Unity storage platform.
'''

View File

@@ -0,0 +1,25 @@
# Copyright: (c) 2022, Dell Technologies
# Apache License version 2.0 (see MODULE-LICENSE or http://www.apache.org/licenses/LICENSE-2.0.txt)
"""Custom rotating file handler for Unity"""
from __future__ import (absolute_import, division, print_function)
__metaclass__ = type
from datetime import datetime
from logging.handlers import RotatingFileHandler
class CustomRotatingFileHandler(RotatingFileHandler):
def rotation_filename(self, default_name):
"""
Modify the filename of a log file when rotating.
:param default_name: The default name of the log file.
"""
src_file_name = default_name.split('.')
dest_file_name = "{0}_{1}.{2}.{3}".format(
src_file_name[0], '{0:%Y%m%d}'.format(datetime.now()),
src_file_name[1], src_file_name[2]
)
return dest_file_name

View File

@@ -0,0 +1,254 @@
# Copyright: (c) 2020, Dell Technologies
# Apache License version 2.0 (see MODULE-LICENSE or http://www.apache.org/licenses/LICENSE-2.0.txt)
from __future__ import absolute_import, division, print_function
__metaclass__ = type
import logging
from decimal import Decimal
import re
import traceback
import math
from ansible_collections.dellemc.unity.plugins.module_utils.storage.dell.logging_handler \
import CustomRotatingFileHandler
from ansible.module_utils.basic import missing_required_lib
try:
import urllib3
urllib3.disable_warnings()
HAS_URLLIB3, URLLIB3_IMP_ERR = True, None
except ImportError:
HAS_URLLIB3, URLLIB3_IMP_ERR = False, traceback.format_exc()
try:
from storops import UnitySystem
from storops.unity.client import UnityClient
from storops.unity.resource import host, cg, snap_schedule, snap, \
cifs_share, nas_server
from storops.unity.resource.lun import UnityLun
from storops.unity.resource.pool import UnityPool, UnityPoolList, RaidGroupParameter
from storops.unity.resource.filesystem import UnityFileSystem, \
UnityFileSystemList
from storops.unity.resource.nas_server import UnityNasServer
from storops.unity.resource.nfs_share import UnityNfsShare, \
UnityNfsShareList
from storops.unity.resource.snap_schedule import UnitySnapScheduleList, \
UnitySnapSchedule
from storops.unity.resource.replication_session import UnityReplicationSession
from storops.unity.enums import HostInitiatorTypeEnum, \
TieringPolicyEnum, ScheduleTypeEnum, DayOfWeekEnum, NodeEnum, \
HostLUNAccessEnum, HostTypeEnum, AccessPolicyEnum, \
FilesystemTypeEnum, FSSupportedProtocolEnum, FSFormatEnum, \
NFSTypeEnum, NFSShareDefaultAccessEnum, NFSShareSecurityEnum, \
FilesystemSnapAccessTypeEnum, FSLockingPolicyEnum, \
CifsShareOfflineAvailabilityEnum, NasServerUnixDirectoryServiceEnum, \
KdcTypeEnum, NodeEnum, FileInterfaceRoleEnum
from storops.exception import UnityResourceNotFoundError, \
StoropsConnectTimeoutError, UnityNfsShareNameExistedError
from storops.connection.exceptions import HttpError, HTTPClientError
from storops.unity.resource.user_quota import UnityUserQuota, \
UnityUserQuotaList
from storops.unity.resource.tree_quota import UnityTreeQuota, \
UnityTreeQuotaList
from storops.unity.resource.quota_config import UnityQuotaConfig, \
UnityQuotaConfigList
from storops.unity.resource.storage_resource import UnityStorageResource
from storops.unity.enums import QuotaPolicyEnum, RaidTypeEnum, \
RaidStripeWidthEnum, StoragePoolTypeEnum
from storops.unity.resource.disk import UnityDisk, \
UnityDiskList, UnityDiskGroup, UnityDiskGroupList
from storops.unity.resource.cifs_server import UnityCifsServer
from storops.unity.resource.nfs_server import UnityNfsServer
from storops.unity.resource.interface import UnityFileInterface
HAS_UNITY_SDK, STOROPS_IMP_ERR = True, None
except ImportError:
HAS_UNITY_SDK, STOROPS_IMP_ERR = False, traceback.format_exc()
try:
from pkg_resources import parse_version
import pkg_resources
HAS_PKG_RESOURCE, PKG_RESOURCE_IMP_ERR = True, None
except ImportError:
HAS_PKG_RESOURCE, PKG_RESOURCE_IMP_ERR = False, traceback.format_exc()
def ensure_required_libs(module):
"""Check required libraries"""
if not HAS_UNITY_SDK:
module.fail_json(msg=missing_required_lib("storops"),
exception=STOROPS_IMP_ERR)
if not HAS_PKG_RESOURCE:
module.fail_json(msg=missing_required_lib("pkg_resources"),
exception=PKG_RESOURCE_IMP_ERR)
if not HAS_URLLIB3:
module.fail_json(msg=missing_required_lib("urllib3"),
exception=URLLIB3_IMP_ERR)
min_ver = '1.2.11'
try:
curr_version = pkg_resources.require("storops")[0].version
except Exception as err:
module.fail_json(msg="Failed to get Storops SDK version - "
"{0}".format(str(err)))
if parse_version(curr_version) < parse_version(min_ver):
module.fail_json(msg="Storops {0} is not supported. "
"Required minimum version is "
"{1}".format(curr_version, min_ver))
def get_unity_management_host_parameters():
"""Provides common access parameters required for the
ansible modules on Unity StorageSystem"""
return dict(
unispherehost=dict(type='str', required=True, no_log=True),
username=dict(type='str', required=True),
password=dict(type='str', required=True, no_log=True),
validate_certs=dict(type='bool', required=False,
aliases=['verifycert'], default=True),
port=dict(type='int', required=False, default=443, no_log=True)
)
def get_unity_unisphere_connection(module_params, application_type=None):
"""Establishes connection with Unity array using storops SDK"""
if HAS_UNITY_SDK:
conn = UnitySystem(host=module_params['unispherehost'],
port=module_params['port'],
verify=module_params['validate_certs'],
username=module_params['username'],
password=module_params['password'],
application_type=application_type)
return conn
def get_logger(module_name, log_file_name='ansible_unity.log',
log_devel=logging.INFO):
"""Intializes and returns the logger object
:param module_name: Name of module to be part of log message
:param log_file_name: Name of file in which the log messages get appended
:param log_devel: Log level
"""
FORMAT = '%(asctime)-15s %(filename)s %(levelname)s : %(message)s'
max_bytes = 5 * 1024 * 1024
logging.basicConfig(filename=log_file_name, format=FORMAT)
LOG = logging.getLogger(module_name)
LOG.setLevel(log_devel)
handler = CustomRotatingFileHandler(log_file_name,
maxBytes=max_bytes,
backupCount=5)
formatter = logging.Formatter(FORMAT)
handler.setFormatter(formatter)
LOG.addHandler(handler)
LOG.propagate = False
return LOG
KB_IN_BYTES = 1024
MB_IN_BYTES = 1024 * 1024
GB_IN_BYTES = 1024 * 1024 * 1024
TB_IN_BYTES = 1024 * 1024 * 1024 * 1024
def get_size_bytes(size, cap_units):
"""Convert the given size to bytes"""
if size is not None and size > 0:
if cap_units in ('kb', 'KB'):
return size * KB_IN_BYTES
elif cap_units in ('mb', 'MB'):
return size * MB_IN_BYTES
elif cap_units in ('gb', 'GB'):
return size * GB_IN_BYTES
elif cap_units in ('tb', 'TB'):
return size * TB_IN_BYTES
else:
return size
else:
return 0
def convert_size_with_unit(size_bytes):
"""Convert size in byte with actual unit like KB,MB,GB,TB,PB etc."""
if not isinstance(size_bytes, int):
raise ValueError('This method takes Integer type argument only')
if size_bytes == 0:
return "0B"
size_name = ("B", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB")
i = int(math.floor(math.log(size_bytes, 1024)))
p = math.pow(1024, i)
s = round(size_bytes / p, 2)
return "%s %s" % (s, size_name[i])
def get_size_in_gb(size, cap_units):
"""Convert the given size to size in GB, size is restricted to 2 decimal places"""
size_in_bytes = get_size_bytes(size, cap_units)
size = Decimal(size_in_bytes / GB_IN_BYTES)
size_in_gb = round(size)
return size_in_gb
def is_input_empty(item):
"""Check whether input string is empty"""
if item == "" or item.isspace():
return True
else:
return False
def is_size_negative(size):
"""Check whether size is negative"""
if size and size < 0:
return True
else:
return False
def has_special_char(value):
"""Check whether the string has any special character.
It allows '_' character"""
regex = re.compile(r'[@!#$%^&*()<>?/\|}{~:]')
if regex.search(value) is None:
return False
else:
return True
def is_initiator_valid(value):
"""Validate format of the FC or iSCSI initiator"""
if value.startswith('iqn') or re.match(r"([A-Fa-f0-9]{2}:){15}[A-Fa-f0-9]{2}", value, re.I) is not None:
return True
else:
return False
def is_valid_netmask(netmask):
"""Validates if ip is valid subnet mask"""
if netmask:
regexp = re.compile(r'^((128|192|224|240|248|252|254)\.0\.0\.0)|'
r'(255\.(((0|128|192|224|240|248|252|254)\.0\.0)|'
r'(255\.(((0|128|192|224|240|248|252|254)\.0)|'
r'255\.(0|128|192|224|240|248|252|254)))))$')
if not regexp.search(netmask):
return False
return True

View File

@@ -0,0 +1,629 @@
#!/usr/bin/python
# Copyright: (c) 2022, Dell Technologies
# Apache License version 2.0 (see MODULE-LICENSE or http://www.apache.org/licenses/LICENSE-2.0.txt)
"""Ansible module for managing CIFS server on Unity"""
from __future__ import absolute_import, division, print_function
__metaclass__ = type
DOCUMENTATION = r'''
module: cifsserver
version_added: '1.4.0'
short_description: Manage CIFS server on Unity storage system
description:
- Managing the CIFS server on the Unity storage system includes creating CIFS server, getting CIFS server details
and deleting CIFS server.
extends_documentation_fragment:
- dellemc.unity.unity
author:
- Akash Shendge (@shenda1) <ansible.team@dell.com>
options:
nas_server_name:
description:
- Name of the NAS server on which CIFS server will be hosted.
type: str
nas_server_id:
description:
- ID of the NAS server on which CIFS server will be hosted.
type: str
netbios_name:
description:
- The computer name of the SMB server in Windows network.
type: str
workgroup:
description:
- Standalone SMB server workgroup.
type: str
local_password:
description:
- Standalone SMB server administrator password.
type: str
domain:
description:
- The domain name where the SMB server is registered in Active Directory.
type: str
domain_username:
description:
- Active Directory domain user name.
type: str
domain_password:
description:
- Active Directory domain password.
type: str
cifs_server_name:
description:
- The name of the CIFS server.
type: str
cifs_server_id:
description:
- The ID of the CIFS server.
type: str
interfaces:
description:
- List of file IP interfaces that service CIFS protocol of SMB server.
type: list
elements: str
unjoin_cifs_server_account:
description:
- Keep SMB server account unjoined in Active Directory after deletion.
- C(false) specifies keep SMB server account joined after deletion.
- C(true) specifies unjoin SMB server account from Active Directory before deletion.
type: bool
state:
description:
- Define whether the CIFS server should exist or not.
choices: [absent, present]
required: true
type: str
notes:
- The I(check_mode) is supported.
'''
EXAMPLES = r'''
- name: Create CIFS server belonging to Active Directory
dellemc.unity.cifsserver:
unispherehost: "{{unispherehost}}"
username: "{{username}}"
password: "{{password}}"
validate_certs: "{{validate_certs}}"
nas_server_name: "test_nas1"
cifs_server_name: "test_cifs"
domain: "ad_domain"
domain_username: "domain_username"
domain_password: "domain_password"
state: "present"
- name: Get CIFS server details using CIFS server ID
dellemc.unity.cifsserver:
unispherehost: "{{unispherehost}}"
username: "{{username}}"
password: "{{password}}"
validate_certs: "{{validate_certs}}"
cifs_server_id: "cifs_37"
state: "present"
- name: Get CIFS server details using NAS server name
dellemc.unity.cifsserver:
unispherehost: "{{unispherehost}}"
username: "{{username}}"
password: "{{password}}"
validate_certs: "{{validate_certs}}"
nas_server_name: "test_nas1"
state: "present"
- name: Delete CIFS server
dellemc.unity.cifsserver:
unispherehost: "{{unispherehost}}"
username: "{{username}}"
password: "{{password}}"
validate_certs: "{{validate_certs}}"
cifs_server_id: "cifs_37"
unjoin_cifs_server_account: True
domain_username: "domain_username"
domain_password: "domain_password"
state: "absent"
- name: Create standalone CIFS server
dellemc.unity.cifsserver:
unispherehost: "{{unispherehost}}"
username: "{{username}}"
password: "{{password}}"
validate_certs: "{{validate_certs}}"
netbios_name: "ANSIBLE_CIFS"
workgroup: "ansible"
local_password: "Password123!"
nas_server_name: "test_nas1"
state: "present"
- name: Get CIFS server details using netbios name
dellemc.unity.cifsserver:
unispherehost: "{{unispherehost}}"
username: "{{username}}"
password: "{{password}}"
validate_certs: "{{validate_certs}}"
netbios_name: "ANSIBLE_CIFS"
state: "present"
- name: Delete standalone CIFS server
dellemc.unity.cifsserver:
unispherehost: "{{unispherehost}}"
username: "{{username}}"
password: "{{password}}"
validate_certs: "{{validate_certs}}"
cifs_server_id: "cifs_40"
state: "absent"
'''
RETURN = r'''
changed:
description: Whether or not the resource has changed.
returned: always
type: bool
sample: true
cifs_server_details:
description: Details of the CIFS server.
returned: When CIFS server exists
type: dict
contains:
id:
description: Unique identifier of the CIFS server instance.
type: str
name:
description: User-specified name for the SMB server.
type: str
netbios_name:
description: Computer Name of the SMB server in windows network.
type: str
description:
description: Description of the SMB server.
type: str
domain:
description: Domain name where SMB server is registered in Active Directory.
type: str
workgroup:
description: Windows network workgroup for the SMB server.
type: str
is_standalone:
description: Indicates whether the SMB server is standalone.
type: bool
nasServer:
description: Information about the NAS server in the storage system.
type: dict
contains:
UnityNasServer:
description: Information about the NAS server in the storage system.
type: dict
contains:
id:
description: Unique identifier of the NAS server instance.
type: str
file_interfaces:
description: The file interfaces associated with the NAS server.
type: dict
contains:
UnityFileInterfaceList:
description: List of file interfaces associated with the NAS server.
type: list
contains:
UnityFileInterface:
description: Details of file interface associated with the NAS server.
type: dict
contains:
id:
description: Unique identifier of the file interface.
type: str
smb_multi_channel_supported:
description: Indicates whether the SMB 3.0+ multichannel feature is supported.
type: bool
smb_protocol_versions:
description: Supported SMB protocols, such as 1.0, 2.0, 2.1, 3.0, and so on.
type: list
smbca_supported:
description: Indicates whether the SMB server supports continuous availability.
type: bool
sample: {
"description": null,
"domain": "xxx.xxx.xxx.com",
"existed": true,
"file_interfaces": {
"UnityFileInterfaceList": [
{
"UnityFileInterface": {
"hash": -9223363258905013637,
"id": "if_43"
}
}
]
},
"hash": -9223363258905010379,
"health": {
"UnityHealth": {
"hash": 8777949765559
}
},
"id": "cifs_40",
"is_standalone": false,
"last_used_organizational_unit": "ou=Computers,ou=Dell NAS servers",
"name": "ansible_cifs",
"nas_server": {
"UnityNasServer": {
"hash": 8777949765531,
"id": "nas_18"
}
},
"netbios_name": "ANSIBLE_CIFS",
"smb_multi_channel_supported": true,
"smb_protocol_versions": [
"1.0",
"2.0",
"2.1",
"3.0"
],
"smbca_supported": true,
"workgroup": null
}
'''
from ansible.module_utils.basic import AnsibleModule
from ansible_collections.dellemc.unity.plugins.module_utils.storage.dell import utils
LOG = utils.get_logger('cifsserver')
application_type = "Ansible/1.5.0"
class CIFSServer(object):
"""Class with CIFS server operations"""
def __init__(self):
"""Define all parameters required by this module"""
self.module_params = utils.get_unity_management_host_parameters()
self.module_params.update(get_cifs_server_parameters())
mutually_exclusive = [['nas_server_name', 'nas_server_id'], ['cifs_server_id', 'cifs_server_name'],
['cifs_server_id', 'netbios_name']]
required_one_of = [['cifs_server_id', 'cifs_server_name', 'netbios_name', 'nas_server_name', 'nas_server_id']]
# initialize the Ansible module
self.module = AnsibleModule(
argument_spec=self.module_params,
supports_check_mode=True,
mutually_exclusive=mutually_exclusive,
required_one_of=required_one_of
)
utils.ensure_required_libs(self.module)
self.unity_conn = utils.get_unity_unisphere_connection(
self.module.params, application_type)
LOG.info('Check Mode Flag %s', self.module.check_mode)
def get_details(self, cifs_server_id=None, cifs_server_name=None, netbios_name=None, nas_server_id=None):
"""Get CIFS server details.
:param: cifs_server_id: The ID of the CIFS server
:param: cifs_server_name: The name of the CIFS server
:param: netbios_name: Name of the SMB server in windows network
:param: nas_server_id: The ID of the NAS server
:return: Dict containing CIFS server details if exists
"""
LOG.info("Getting CIFS server details")
id_or_name = get_id_name(cifs_server_id, cifs_server_name, netbios_name, nas_server_id)
try:
if cifs_server_id:
cifs_server_details = self.unity_conn.get_cifs_server(_id=cifs_server_id)
return process_response(cifs_server_details)
if cifs_server_name:
cifs_server_details = self.unity_conn.get_cifs_server(name=cifs_server_name)
return process_response(cifs_server_details)
if netbios_name:
cifs_server_details = self.unity_conn.get_cifs_server(netbios_name=netbios_name)
if len(cifs_server_details) > 0:
return process_dict(cifs_server_details._get_properties())
if nas_server_id:
cifs_server_details = self.unity_conn.get_cifs_server(nas_server=nas_server_id)
if len(cifs_server_details) > 0:
return process_dict(cifs_server_details._get_properties())
return None
except utils.HttpError as e:
if e.http_status == 401:
msg = "Failed to get CIFS server: %s due to incorrect " \
"username/password error: %s" % (id_or_name, str(e))
else:
msg = "Failed to get CIFS server: %s with error: %s" % (id_or_name, str(e))
except utils.UnityResourceNotFoundError:
msg = "CIFS server with ID %s not found" % cifs_server_id
LOG.info(msg)
return None
except utils.StoropsConnectTimeoutError as e:
msg = "Failed to get CIFS server: %s with error: %s. Please check unispherehost IP: %s" % (
id_or_name, str(e), self.module.params['unispherehost'])
except Exception as e:
msg = "Failed to get details of CIFS server: %s with error: %s" % (id_or_name, str(e))
LOG.error(msg)
self.module.fail_json(msg=msg)
def get_cifs_server_instance(self, cifs_server_id):
"""Get CIFS server instance.
:param: cifs_server_id: The ID of the CIFS server
:return: Return CIFS server instance if exists
"""
try:
cifs_server_obj = utils.UnityCifsServer.get(cli=self.unity_conn._cli, _id=cifs_server_id)
return cifs_server_obj
except Exception as e:
error_msg = "Failed to get the CIFS server %s instance" \
" with error %s" % (cifs_server_id, str(e))
LOG.error(error_msg)
self.module.fail_json(msg=error_msg)
def delete_cifs_server(self, cifs_server_id, skip_unjoin=None, domain_username=None, domain_password=None):
"""Delete CIFS server.
:param: cifs_server_id: The ID of the CIFS server
:param: skip_unjoin: Flag indicating whether to unjoin SMB server account from AD before deletion
:param: domain_username: The domain username
:param: domain_password: The domain password
:return: Return True if CIFS server is deleted
"""
LOG.info("Deleting CIFS server")
try:
if not self.module.check_mode:
cifs_obj = self.get_cifs_server_instance(cifs_server_id=cifs_server_id)
cifs_obj.delete(skip_domain_unjoin=skip_unjoin, username=domain_username, password=domain_password)
return True
except Exception as e:
msg = "Failed to delete CIFS server: %s with error: %s" % (cifs_server_id, str(e))
LOG.error(msg)
self.module.fail_json(msg=msg)
def get_nas_server_id(self, nas_server_name):
"""Get NAS server ID.
:param: nas_server_name: The name of NAS server
:return: Return NAS server ID if exists
"""
LOG.info("Getting NAS server ID")
try:
obj_nas = self.unity_conn.get_nas_server(name=nas_server_name)
return obj_nas.get_id()
except Exception as e:
msg = "Failed to get details of NAS server: %s with error: %s" % (nas_server_name, str(e))
LOG.error(msg)
self.module.fail_json(msg=msg)
def is_modify_interfaces(self, cifs_server_details):
"""Check if modification is required in existing interfaces
:param: cifs_server_details: CIFS server details
:return: Flag indicating if modification is required
"""
existing_interfaces = []
if cifs_server_details['file_interfaces']['UnityFileInterfaceList']:
for interface in cifs_server_details['file_interfaces']['UnityFileInterfaceList']:
existing_interfaces.append(interface['UnityFileInterface']['id'])
for interface in self.module.params['interfaces']:
if interface not in existing_interfaces:
return True
return False
def is_modification_required(self, cifs_server_details):
"""Check if modification is required in existing CIFS server
:param: cifs_server_details: CIFS server details
:return: Flag indicating if modification is required
"""
LOG.info("Checking if any modification is required")
param_list = ['netbios_name', 'workgroup']
for param in param_list:
if self.module.params[param] is not None and cifs_server_details[param] is not None and \
self.module.params[param].upper() != cifs_server_details[param]:
return True
# Check for domain
if self.module.params['domain'] is not None and cifs_server_details['domain'] is not None and \
self.module.params['domain'] != cifs_server_details['domain']:
return True
# Check file interfaces
if self.module.params['interfaces'] is not None:
return self.is_modify_interfaces(cifs_server_details)
return False
def create_cifs_server(self, nas_server_id, interfaces=None, netbios_name=None, cifs_server_name=None, domain=None,
domain_username=None, domain_password=None, workgroup=None, local_password=None):
"""Create CIFS server.
:param: nas_server_id: The ID of NAS server
:param: interfaces: List of file interfaces
:param: netbios_name: Name of the SMB server in windows network
:param: cifs_server_name: Name of the CIFS server
:param: domain: The domain name where the SMB server is registered in Active Directory
:param: domain_username: The domain username
:param: domain_password: The domain password
:param: workgroup: Standalone SMB server workgroup
:param: local_password: Standalone SMB server admin password
:return: Return True if CIFS server is created
"""
LOG.info("Creating CIFS server")
try:
if not self.module.check_mode:
utils.UnityCifsServer.create(cli=self.unity_conn._cli, nas_server=nas_server_id, interfaces=interfaces,
netbios_name=netbios_name, name=cifs_server_name, domain=domain,
domain_username=domain_username, domain_password=domain_password,
workgroup=workgroup, local_password=local_password)
return True
except Exception as e:
msg = "Failed to create CIFS server with error: %s" % (str(e))
LOG.error(msg)
self.module.fail_json(msg=msg)
def validate_params(self):
"""Validate the parameters
"""
param_list = ['nas_server_id', 'nas_server_name', 'domain', 'cifs_server_id', 'cifs_server_name',
'local_password', 'netbios_name', 'workgroup', 'domain_username', 'domain_password']
msg = "Please provide valid {0}"
for param in param_list:
if self.module.params[param] is not None and len(self.module.params[param].strip()) == 0:
errmsg = msg.format(param)
self.module.fail_json(msg=errmsg)
def perform_module_operation(self):
"""
Perform different actions on CIFS server module based on parameters
passed in the playbook
"""
cifs_server_id = self.module.params['cifs_server_id']
cifs_server_name = self.module.params['cifs_server_name']
nas_server_id = self.module.params['nas_server_id']
nas_server_name = self.module.params['nas_server_name']
netbios_name = self.module.params['netbios_name']
workgroup = self.module.params['workgroup']
local_password = self.module.params['local_password']
domain = self.module.params['domain']
domain_username = self.module.params['domain_username']
domain_password = self.module.params['domain_password']
interfaces = self.module.params['interfaces']
unjoin_cifs_server_account = self.module.params['unjoin_cifs_server_account']
state = self.module.params['state']
# result is a dictionary that contains changed status and CIFS server details
result = dict(
changed=False,
cifs_server_details={}
)
# Validate the parameters
self.validate_params()
if nas_server_name is not None:
nas_server_id = self.get_nas_server_id(nas_server_name)
cifs_server_details = self.get_details(cifs_server_id=cifs_server_id, cifs_server_name=cifs_server_name,
netbios_name=netbios_name, nas_server_id=nas_server_id)
# Check if modification is required
if cifs_server_details:
if cifs_server_id is None:
cifs_server_id = cifs_server_details['id']
modify_flag = self.is_modification_required(cifs_server_details)
if modify_flag:
self.module.fail_json(msg="Modification is not supported through Ansible module")
if not cifs_server_details and state == 'present':
if not nas_server_id:
self.module.fail_json(msg="Please provide nas server id/name to create CIFS server.")
if any([netbios_name, workgroup, local_password]):
if not all([netbios_name, workgroup, local_password]):
msg = "netbios_name, workgroup and local_password" \
" are required to create standalone CIFS server."
LOG.error(msg)
self.module.fail_json(msg=msg)
result['changed'] = self.create_cifs_server(nas_server_id, interfaces, netbios_name,
cifs_server_name, domain, domain_username, domain_password,
workgroup, local_password)
if state == 'absent' and cifs_server_details:
skip_unjoin = None
if unjoin_cifs_server_account is not None:
skip_unjoin = not unjoin_cifs_server_account
result['changed'] = self.delete_cifs_server(cifs_server_id, skip_unjoin, domain_username,
domain_password)
if state == 'present':
result['cifs_server_details'] = self.get_details(cifs_server_id=cifs_server_id,
cifs_server_name=cifs_server_name,
netbios_name=netbios_name,
nas_server_id=nas_server_id)
LOG.info("Process Dict: %s", result['cifs_server_details'])
self.module.exit_json(**result)
def get_id_name(cifs_server_id=None, cifs_server_name=None, netbios_name=None, nas_server_id=None):
"""Get the id_or_name.
:param: cifs_server_id: The ID of CIFS server
:param: cifs_server_name: The name of CIFS server
:param: netbios_name: Name of the SMB server in windows network
:param: nas_server_id: The ID of NAS server
:return: Return id_or_name
"""
if cifs_server_id:
id_or_name = cifs_server_id
elif cifs_server_name:
id_or_name = cifs_server_name
elif netbios_name:
id_or_name = netbios_name
else:
id_or_name = nas_server_id
return id_or_name
def process_response(cifs_server_details):
"""Process CIFS server details.
:param: cifs_server_details: Dict containing CIFS server details
:return: Processed dict containing CIFS server details
"""
if cifs_server_details.existed:
return cifs_server_details._get_properties()
def process_dict(cifs_server_details):
"""Process CIFS server details.
:param: cifs_server_details: Dict containing CIFS server details
:return: Processed dict containing CIFS server details
"""
param_list = ['description', 'domain', 'file_interfaces', 'health', 'id', 'is_standalone', 'name', 'nas_server'
'netbios_name', 'smb_multi_channel_supported', 'smb_protocol_versions', 'smbca_supported',
'workgroup', 'netbios_name']
for param in param_list:
if param in cifs_server_details:
cifs_server_details[param] = cifs_server_details[param][0]
return cifs_server_details
def get_cifs_server_parameters():
"""This method provide parameters required for the ansible
CIFS server module on Unity"""
return dict(
cifs_server_id=dict(), cifs_server_name=dict(),
netbios_name=dict(), workgroup=dict(),
local_password=dict(no_log=True), domain=dict(),
domain_username=dict(), domain_password=dict(no_log=True),
nas_server_name=dict(), nas_server_id=dict(),
interfaces=dict(type='list', elements='str'),
unjoin_cifs_server_account=dict(type='bool'),
state=dict(required=True, type='str', choices=['present', 'absent']),
)
def main():
"""Create Unity CIFS server object and perform action on it
based on user input from playbook"""
obj = CIFSServer()
obj.perform_module_operation()
if __name__ == '__main__':
main()

View File

@@ -0,0 +1,772 @@
#!/usr/bin/python
# Copyright: (c) 2020, Dell Technologies
# Apache License version 2.0 (see MODULE-LICENSE or http://www.apache.org/licenses/LICENSE-2.0.txt)
""" Ansible module for managing Filesystem Snapshots on Unity"""
from __future__ import (absolute_import, division, print_function)
__metaclass__ = type
DOCUMENTATION = r'''
---
module: filesystem_snapshot
short_description: Manage filesystem snapshot on the Unity storage system
description:
- Managing Filesystem Snapshot on the Unity storage system includes
create filesystem snapshot, get filesystem snapshot, modify filesystem
snapshot and delete filesystem snapshot.
version_added: '1.1.0'
extends_documentation_fragment:
- dellemc.unity.unity
author:
- Rajshree Khare (@kharer5) <ansible.team@dell.com>
options:
snapshot_name:
description:
- The name of the filesystem snapshot.
- Mandatory parameter for creating a filesystem snapshot.
- For all other operations either I(snapshot_name) or I(snapshot_id)
is required.
type: str
snapshot_id:
description:
- During creation snapshot_id is auto generated.
- For all other operations either I(snapshot_id) or I(snapshot_name)
is required.
type: str
filesystem_name:
description:
- The name of the Filesystem for which snapshot is created.
- For creation of filesystem snapshot either I(filesystem_name) or
I(filesystem_id) is required.
- Not required for other operations.
type: str
filesystem_id:
description:
- The ID of the Filesystem for which snapshot is created.
- For creation of filesystem snapshot either I(filesystem_id) or
I(filesystem_name) is required.
- Not required for other operations.
type: str
nas_server_name:
description:
- The name of the NAS server in which the Filesystem is created.
- For creation of filesystem snapshot either I(nas_server_name) or
I(nas_server_id) is required.
- Not required for other operations.
type: str
nas_server_id:
description:
- The ID of the NAS server in which the Filesystem is created.
- For creation of filesystem snapshot either I(filesystem_id) or
I(filesystem_name) is required.
- Not required for other operations.
type: str
auto_delete:
description:
- This option specifies whether or not the filesystem snapshot will be
automatically deleted.
- If set to C(true), the filesystem snapshot will expire based on the pool
auto deletion policy.
- If set to C(false), the filesystem snapshot will not be auto deleted
based on the pool auto deletion policy.
- Option I(auto_delete) can not be set to C(true), if I(expiry_time) is specified.
- If during creation neither I(auto_delete) nor I(expiry_time) is mentioned
then the filesystem snapshot will be created keeping I(auto_delete) as
C(true).
- Once the I(expiry_time) is set, then the filesystem snapshot cannot be
assigned to the auto delete policy.
type: bool
expiry_time:
description:
- This option is for specifying the date and time after which the
filesystem snapshot will expire.
- The time is to be mentioned in UTC timezone.
- The format is "MM/DD/YYYY HH:MM". Year must be in 4 digits.
type: str
description:
description:
- The additional information about the filesystem snapshot can be
provided using this option.
- The description can be removed by passing an empty string.
type: str
fs_access_type:
description:
- Access type of the filesystem snapshot.
- Required only during creation of filesystem snapshot.
- If not given, snapshot's access type will be C(Checkpoint).
type: str
choices: ['Checkpoint' , 'Protocol']
state:
description:
- The state option is used to mention the existence of the filesystem
snapshot.
type: str
required: true
choices: ['absent', 'present']
notes:
- Filesystem snapshot cannot be deleted, if it has nfs or smb share.
- The I(check_mode) is not supported.
'''
EXAMPLES = r'''
- name: Create Filesystem Snapshot
dellemc.unity.filesystem_snapshot:
unispherehost: "{{unispherehost}}"
username: "{{username}}"
password: "{{password}}"
validate_certs: "{{validate_certs}}"
snapshot_name: "ansible_test_FS_snap"
filesystem_name: "ansible_test_FS"
nas_server_name: "lglad069"
description: "Created using playbook"
auto_delete: True
fs_access_type: "Protocol"
state: "present"
- name: Create Filesystem Snapshot with expiry time
dellemc.unity.filesystem_snapshot:
unispherehost: "{{unispherehost}}"
username: "{{username}}"
password: "{{password}}"
validate_certs: "{{validate_certs}}"
snapshot_name: "ansible_test_FS_snap_1"
filesystem_name: "ansible_test_FS_1"
nas_server_name: "lglad069"
description: "Created using playbook"
expiry_time: "04/15/2021 2:30"
fs_access_type: "Protocol"
state: "present"
- name: Get Filesystem Snapshot Details using Name
dellemc.unity.filesystem_snapshot:
unispherehost: "{{unispherehost}}"
username: "{{username}}"
password: "{{password}}"
validate_certs: "{{validate_certs}}"
snapshot_name: "ansible_test_FS_snap"
state: "present"
- name: Get Filesystem Snapshot Details using ID
dellemc.unity.filesystem_snapshot:
unispherehost: "{{unispherehost}}"
username: "{{username}}"
password: "{{password}}"
validate_certs: "{{validate_certs}}"
snapshot_id: "10008000403"
state: "present"
- name: Update Filesystem Snapshot attributes
dellemc.unity.filesystem_snapshot:
unispherehost: "{{unispherehost}}"
username: "{{username}}"
password: "{{password}}"
validate_certs: "{{validate_certs}}"
snapshot_name: "ansible_test_FS_snap"
description: "Description updated"
auto_delete: False
expiry_time: "04/15/2021 5:30"
state: "present"
- name: Update Filesystem Snapshot attributes using ID
dellemc.unity.filesystem_snapshot:
unispherehost: "{{unispherehost}}"
username: "{{username}}"
password: "{{password}}"
validate_certs: "{{validate_certs}}"
snapshot_id: "10008000403"
expiry_time: "04/18/2021 8:30"
state: "present"
- name: Delete Filesystem Snapshot using Name
dellemc.unity.filesystem_snapshot:
unispherehost: "{{unispherehost}}"
username: "{{username}}"
password: "{{password}}"
validate_certs: "{{validate_certs}}"
snapshot_name: "ansible_test_FS_snap"
state: "absent"
- name: Delete Filesystem Snapshot using ID
dellemc.unity.filesystem_snapshot:
unispherehost: "{{unispherehost}}"
username: "{{username}}"
password: "{{password}}"
validate_certs: "{{validate_certs}}"
snapshot_id: "10008000403"
state: "absent"
'''
RETURN = r'''
changed:
description: Whether or not the resource has changed.
returned: always
type: bool
sample: true
filesystem_snapshot_details:
description: Details of the filesystem snapshot.
returned: When filesystem snapshot exists
type: dict
contains:
access_type:
description: Access type of filesystem snapshot.
type: str
attached_wwn:
description: Attached WWN details.
type: str
creation_time:
description: Creation time of filesystem snapshot.
type: str
creator_schedule:
description: Creator schedule of filesystem snapshot.
type: str
creator_type:
description: Creator type for filesystem snapshot.
type: str
creator_user:
description: Creator user for filesystem snapshot.
type: str
description:
description: Description of the filesystem snapshot.
type: str
expiration_time:
description: Date and time after which the filesystem snapshot
will expire.
type: str
is_auto_delete:
description: Is the filesystem snapshot is auto deleted or not.
type: bool
id:
description: Unique identifier of the filesystem snapshot
instance.
type: str
name:
description: The name of the filesystem snapshot.
type: str
size:
description: Size of the filesystem snapshot.
type: int
filesystem_name:
description: Name of the filesystem for which the snapshot exists.
type: str
filesystem_id:
description: Id of the filesystem for which the snapshot exists.
type: str
nas_server_name:
description: Name of the NAS server on which filesystem exists.
type: str
nas_server_id:
description: Id of the NAS server on which filesystem exists.
type: str
sample: {
"access_type": "FilesystemSnapAccessTypeEnum.CHECKPOINT",
"attached_wwn": null,
"creation_time": "2022-10-21 04:42:53.951000+00:00",
"creator_schedule": null,
"creator_type": "SnapCreatorTypeEnum.USER_CUSTOM",
"creator_user": {
"id": "user_admin"
},
"description": "Created using playbook",
"existed": true,
"expiration_time": null,
"filesystem_id": "fs_137",
"filesystem_name": "test",
"hash": 8739894572587,
"host_access": null,
"id": "171798721695",
"io_limit_policy": null,
"is_auto_delete": true,
"is_modifiable": false,
"is_modified": false,
"is_read_only": true,
"is_system_snap": false,
"last_writable_time": null,
"lun": null,
"name": "test_FS_snap_1",
"nas_server_id": "nas_1",
"nas_server_name": "lglad072",
"parent_snap": null,
"size": 107374182400,
"snap_group": null,
"state": "SnapStateEnum.READY"
}
'''
from ansible.module_utils.basic import AnsibleModule
from ansible_collections.dellemc.unity.plugins.module_utils.storage.dell \
import utils
from datetime import datetime
LOG = utils.get_logger('filesystem_snapshot')
application_type = "Ansible/1.5.0"
class FilesystemSnapshot(object):
"""Class with Filesystem Snapshot operations"""
def __init__(self):
""" Define all parameters required by this module"""
self.module_params = utils.get_unity_management_host_parameters()
self.module_params.update(get_snapshot_parameters())
mutually_exclusive = [['snapshot_name', 'snapshot_id'],
['filesystem_name', 'filesystem_id'],
['nas_server_name', 'nas_server_id']]
required_one_of = [['snapshot_name', 'snapshot_id']]
# initialize the ansible module
self.module = AnsibleModule(argument_spec=self.module_params,
supports_check_mode=False,
mutually_exclusive=mutually_exclusive,
required_one_of=required_one_of)
utils.ensure_required_libs(self.module)
# result is a dictionary that contains changed status and
# filesystem snapshot details
self.result = {"changed": False,
'filesystem_snapshot_details': {}}
self.unity_conn = utils.get_unity_unisphere_connection(
self.module.params, application_type)
self.snap_obj = utils.snap.UnitySnap(self.unity_conn)
LOG.info('Connection established with the Unity Array')
def validate_expiry_time(self, expiry_time):
"""Validates the specified expiry_time"""
try:
datetime.strptime(expiry_time, '%m/%d/%Y %H:%M')
except ValueError:
error_msg = ("expiry_time: %s, not in MM/DD/YYYY HH:MM format." %
expiry_time)
LOG.error(error_msg)
self.module.fail_json(msg=error_msg)
def to_update(self, fs_snapshot, description=None, auto_del=None,
expiry_time=None, fs_access_type=None):
"""Determines whether to update the snapshot or not"""
snap_modify_dict = dict()
if fs_access_type and fs_access_type != fs_snapshot.access_type:
error_message = "Modification of access type is not allowed."
LOG.error(error_message)
self.module.fail_json(msg=error_message)
if expiry_time:
# If the snapshot has is_auto_delete True,
# Check if auto_delete in the input is either None or True
if fs_snapshot.is_auto_delete and (auto_del is None or auto_del):
self.module.fail_json(msg="expiry_time can be assigned when"
" auto delete is False.")
if auto_del is not None:
if fs_snapshot.expiration_time:
error_msg = "expiry_time for filesystem snapshot is set." \
" Once it is set then snapshot cannot" \
" be assigned to auto_delete policy."
self.module.fail_json(msg=error_msg)
if auto_del != fs_snapshot.is_auto_delete:
snap_modify_dict['is_auto_delete'] = auto_del
if description is not None and description != fs_snapshot.description:
snap_modify_dict['description'] = description
if to_update_expiry_time(fs_snapshot, expiry_time):
snap_modify_dict['expiry_time'] = expiry_time
LOG.info("Snapshot modification details: %s", snap_modify_dict)
return snap_modify_dict
def update_filesystem_snapshot(self, fs_snapshot, snap_modify_dict):
try:
duration = None
if 'expiry_time' in snap_modify_dict \
and snap_modify_dict['expiry_time']:
duration = convert_timestamp_to_sec(
snap_modify_dict['expiry_time'],
self.unity_conn.system_time)
if duration and duration <= 0:
self.module.fail_json(msg="expiry_time should be after"
" the current system time.")
if 'is_auto_delete' in snap_modify_dict \
and snap_modify_dict['is_auto_delete'] is not None:
auto_delete = snap_modify_dict['is_auto_delete']
else:
auto_delete = None
if 'description' in snap_modify_dict \
and (snap_modify_dict['description']
or len(snap_modify_dict['description']) == 0):
description = snap_modify_dict['description']
else:
description = None
fs_snapshot.modify(retentionDuration=duration,
isAutoDelete=auto_delete,
description=description)
fs_snapshot.update()
except Exception as e:
error_msg = "Failed to modify filesystem snapshot" \
" [name: %s , id: %s] with error %s."\
% (fs_snapshot.name, fs_snapshot.id, str(e))
LOG.error(error_msg)
self.module.fail_json(msg=error_msg)
def create_filesystem_snapshot(self, snap_name, storage_id,
description=None, auto_del=None,
expiry_time=None, fs_access_type=None):
try:
duration = None
if expiry_time:
duration = convert_timestamp_to_sec(
expiry_time, self.unity_conn.system_time)
if duration <= 0:
self.module.fail_json(msg="expiry_time should be after"
" the current system time.")
fs_snapshot = self.snap_obj.create(
cli=self.unity_conn._cli, storage_resource=storage_id,
name=snap_name, description=description,
is_auto_delete=auto_del, retention_duration=duration,
fs_access_type=fs_access_type)
return fs_snapshot
except Exception as e:
error_msg = "Failed to create filesystem snapshot" \
" %s with error %s" % (snap_name, str(e))
LOG.error(error_msg)
self.module.fail_json(msg=error_msg)
def is_snap_has_share(self, fs_snap):
try:
obj = self.unity_conn.get_nfs_share(snap=fs_snap) or \
self.unity_conn.get_cifs_share(snap=fs_snap)
if len(obj) > 0:
LOG.info("Snapshot has %s nfs/smb share/s", len(obj))
return True
except Exception as e:
msg = "Failed to get nfs/smb share from filesystem snapshot. " \
"error: %s" % str(e)
LOG.error(msg)
self.module.fail_json(msg=msg)
return False
def delete_fs_snapshot(self, fs_snapshot):
try:
# Checking whether nfs/smb share created from fs_snapshot
if self.is_snap_has_share(fs_snapshot):
msg = "Filesystem snapshot cannot be deleted because it has " \
"nfs/smb share"
LOG.error(msg)
self.module.fail_json(msg=msg)
fs_snapshot.delete()
return None
except Exception as e:
error_msg = "Failed to delete filesystem snapshot" \
" [name: %s, id: %s] with error %s." \
% (fs_snapshot.name, fs_snapshot.id, str(e))
LOG.error(error_msg)
self.module.fail_json(msg=error_msg)
def get_fs_snapshot_obj(self, name=None, id=None):
fs_snapshot = id if id else name
msg = "Failed to get details of filesystem snapshot %s with error %s."
try:
fs_snap_obj = self.unity_conn.get_snap(name=name, _id=id)
if fs_snap_obj and fs_snap_obj.existed:
LOG.info("Successfully got the filesystem snapshot object "
"%s.", fs_snap_obj)
else:
fs_snap_obj = None
return fs_snap_obj
except utils.HttpError as e:
if e.http_status == 401:
cred_err = ("Incorrect username or password , %s" % e.message)
self.module.fail_json(msg=cred_err)
else:
err_msg = msg % (fs_snapshot, str(e))
LOG.error(err_msg)
self.module.fail_json(msg=err_msg)
except utils.UnityResourceNotFoundError as e:
err_msg = msg % (fs_snapshot, str(e))
LOG.error(err_msg)
return None
except Exception as e:
err_msg = msg % (fs_snapshot, str(e))
LOG.error(err_msg)
self.module.fail_json(msg=err_msg)
def get_filesystem_obj(self, nas_server=None, name=None, id=None):
filesystem = id if id else name
try:
obj_fs = None
if name:
if not nas_server:
err_msg = "NAS Server is required to get the FileSystem."
LOG.error(err_msg)
self.module.fail_json(msg=err_msg)
obj_fs = self.unity_conn.get_filesystem(name=name,
nas_server=nas_server)
if obj_fs and obj_fs.existed:
LOG.info("Successfully got the filesystem object %s.",
obj_fs)
return obj_fs
if id:
if nas_server:
obj_fs = self.unity_conn\
.get_filesystem(id=id, nas_server=nas_server)
else:
obj_fs = self.unity_conn.get_filesystem(id=id)
if obj_fs and obj_fs.existed:
LOG.info("Successfully got the filesystem object %s.",
obj_fs)
return obj_fs
except Exception as e:
error_msg = "Failed to get filesystem %s with error %s."\
% (filesystem, str(e))
LOG.error(error_msg)
self.module.fail_json(msg=error_msg)
def get_nas_server_obj(self, name=None, id=None):
nas_server = id if id else name
error_msg = ("Failed to get NAS server %s." % nas_server)
try:
obj_nas = self.unity_conn.get_nas_server(_id=id, name=name)
if name and obj_nas.existed:
LOG.info("Successfully got the NAS server object %s.",
obj_nas)
return obj_nas
elif id and obj_nas.existed:
LOG.info("Successfully got the NAS server object %s.",
obj_nas)
return obj_nas
else:
LOG.error(error_msg)
self.module.fail_json(msg=error_msg)
except Exception as e:
error_msg = "Failed to get NAS server %s with error %s."\
% (nas_server, str(e))
LOG.error(error_msg)
self.module.fail_json(msg=error_msg)
def create_fs_snapshot_details_dict(self, fs_snapshot):
""" Add name and id of storage resource to filesystem snapshot
details """
snapshot_dict = fs_snapshot._get_properties()
del snapshot_dict['storage_resource']
snapshot_dict['filesystem_name'] = fs_snapshot.storage_resource.name
snapshot_dict['filesystem_id'] = fs_snapshot.storage_resource.filesystem.id
obj_fs = self.unity_conn.\
get_filesystem(id=fs_snapshot.storage_resource.filesystem.id)
if obj_fs and obj_fs.existed:
snapshot_dict['nas_server_name'] = obj_fs.nas_server[0].name
snapshot_dict['nas_server_id'] = obj_fs.nas_server[0].id
return snapshot_dict
def perform_module_operation(self):
"""
Perform different actions on snapshot module based on parameters
chosen in playbook
"""
snapshot_name = self.module.params['snapshot_name']
snapshot_id = self.module.params['snapshot_id']
filesystem_name = self.module.params['filesystem_name']
filesystem_id = self.module.params['filesystem_id']
nas_server_name = self.module.params['nas_server_name']
nas_server_id = self.module.params['nas_server_id']
auto_delete = self.module.params['auto_delete']
expiry_time = self.module.params['expiry_time']
description = self.module.params['description']
fs_access_type = self.module.params['fs_access_type']
state = self.module.params['state']
nas_server_resource = None
filesystem_resource = None
changed = False
LOG.info("Getting Filesystem Snapshot details.")
fs_snapshot = self.get_fs_snapshot_obj(name=snapshot_name,
id=snapshot_id)
msg = "Filesystem Snapshot details: %s." % str(fs_snapshot)
LOG.info(msg)
# Get NAS server Object
if nas_server_name is not None:
if nas_server_name == "" or nas_server_name.isspace():
self.module.fail_json(msg="Invalid nas_server_name given,"
" Please provide a valid name.")
nas_server_resource = self\
.get_nas_server_obj(name=nas_server_name)
elif nas_server_id is not None:
if nas_server_id == "" or nas_server_id.isspace():
self.module.fail_json(msg="Invalid nas_server_id given,"
" Please provide a valid ID.")
nas_server_resource = self.get_nas_server_obj(id=nas_server_id)
# Get Filesystem Object
if filesystem_name is not None:
if filesystem_name == "" or filesystem_name.isspace():
self.module.fail_json(msg="Invalid filesystem_name given,"
" Please provide a valid name.")
filesystem_resource = self\
.get_filesystem_obj(nas_server=nas_server_resource,
name=filesystem_name)
fs_res_id = filesystem_resource.storage_resource.id
elif filesystem_id is not None:
if filesystem_id == "" or filesystem_id.isspace():
self.module.fail_json(msg="Invalid filesystem_id given,"
" Please provide a valid ID.")
filesystem_resource = self\
.get_filesystem_obj(id=filesystem_id)
fs_res_id = filesystem_resource[0].storage_resource.id
# Check for error, if user tries to create a filesystem snapshot
# with the same name.
if fs_snapshot and filesystem_resource and \
(fs_snapshot.storage_resource.id
!= fs_res_id):
self.module.fail_json(
msg="Snapshot %s is of %s storage resource. Cannot create new"
" snapshot with same name for %s storage resource."
% (fs_snapshot.name, fs_snapshot.storage_resource.name,
filesystem_resource.storage_resource.name))
# check for valid expiry_time
if expiry_time is not None and \
(expiry_time == "" or expiry_time.isspace()):
self.module.fail_json(msg="Please provide valid expiry_time,"
" empty expiry_time given.")
if expiry_time:
self.validate_expiry_time(expiry_time)
# Check if in input auto_delete is True and expiry_time is not None
if expiry_time and auto_delete:
error_msg = "Cannot set expiry_time if auto_delete given as True."
LOG.info(error_msg)
self.module.fail_json(msg=error_msg)
# check for fs_access_type
if fs_access_type is not None:
if (fs_access_type == "" or fs_access_type.isspace()):
self.module.fail_json(msg="Please provide valid "
"fs_access_type, empty "
"fs_access_type given.")
if fs_access_type == "Checkpoint":
fs_access_type = utils.FilesystemSnapAccessTypeEnum.CHECKPOINT
elif fs_access_type == "Protocol":
fs_access_type = utils.FilesystemSnapAccessTypeEnum.PROTOCOL
# Check whether to modify the filesystem snapshot or not
fs_snap_modify_dict = dict()
if state == 'present' and fs_snapshot:
fs_snap_modify_dict = self\
.to_update(fs_snapshot, description=description,
auto_del=auto_delete, expiry_time=expiry_time,
fs_access_type=fs_access_type)
# Create Filesystem Snapshot
if not fs_snapshot and state == "present":
LOG.info("Creating the filesystem snapshot.")
if snapshot_id:
self.module.fail_json(msg="Creation of Filesystem Snapshot is"
" allowed using snapshot_name only,"
" snapshot_id given.")
if snapshot_name == "" or snapshot_name.isspace():
self.module.fail_json(msg="snapshot_name is required for"
" creation of the filesystem"
" snapshot, empty snapshot_name"
" given.")
if not filesystem_resource:
self.module.fail_json(msg="filesystem_name or filesystem_id"
" required to create a snapshot.")
fs_snapshot = self.create_filesystem_snapshot(
snapshot_name,
fs_res_id,
description,
auto_delete,
expiry_time,
fs_access_type)
changed = True
# Update the Snapshot
if fs_snapshot and state == "present" and fs_snap_modify_dict:
LOG.info("Updating the Filesystem Snapshot.")
self.update_filesystem_snapshot(fs_snapshot, fs_snap_modify_dict)
changed = True
# Delete the Filesystem Snapshot
if state == "absent" and fs_snapshot:
fs_snapshot = self.delete_fs_snapshot(fs_snapshot)
changed = True
# Add filesystem snapshot details to the result.
if fs_snapshot:
fs_snapshot.update()
self.result["filesystem_snapshot_details"] = \
self.create_fs_snapshot_details_dict(fs_snapshot)
else:
self.result["filesystem_snapshot_details"] = {}
self.result["changed"] = changed
self.module.exit_json(**self.result)
def to_update_expiry_time(fs_snapshot, expiry_time=None):
""" Check whether to update expiry_time or not"""
if not expiry_time:
return False
if fs_snapshot.expiration_time is None:
return True
if convert_timestamp_to_sec(expiry_time, fs_snapshot.expiration_time)\
!= 0:
return True
return False
def convert_timestamp_to_sec(expiry_time, snap_time):
"""Converts the time difference to seconds"""
snap_time_str = snap_time.strftime('%m/%d/%Y %H:%M')
snap_timestamp = datetime.strptime(snap_time_str, '%m/%d/%Y %H:%M')
expiry_timestamp = datetime.strptime(expiry_time, "%m/%d/%Y %H:%M")
return int((expiry_timestamp - snap_timestamp).total_seconds())
def get_snapshot_parameters():
"""This method provide parameter required for the ansible filesystem
snapshot module on Unity"""
return dict(
snapshot_name=dict(required=False, type='str'),
snapshot_id=dict(required=False, type='str'),
filesystem_name=dict(required=False, type='str'),
filesystem_id=dict(required=False, type='str'),
nas_server_name=dict(required=False, type='str'),
nas_server_id=dict(required=False, type='str'),
auto_delete=dict(required=False, type='bool'),
expiry_time=dict(required=False, type='str'),
description=dict(required=False, type='str'),
fs_access_type=dict(required=False, type='str',
choices=['Checkpoint', 'Protocol']),
state=dict(required=True, type='str', choices=['present', 'absent'])
)
def main():
""" Create Unity Filesystem Snapshot object and perform actions on it
based on user input from playbook"""
obj = FilesystemSnapshot()
obj.perform_module_operation()
if __name__ == '__main__':
main()

View File

@@ -0,0 +1,521 @@
#!/usr/bin/python
# Copyright: (c) 2022, Dell Technologies
# Apache License version 2.0 (see MODULE-LICENSE or http://www.apache.org/licenses/LICENSE-2.0.txt)
"""Ansible module for managing Interfaces on Unity"""
from __future__ import absolute_import, division, print_function
__metaclass__ = type
DOCUMENTATION = r'''
module: interface
version_added: '1.4.0'
short_description: Manage Interfaces on Unity storage system
description:
- Managing the Interfaces on the Unity storage system includes adding Interfaces to NAS Server, getting
details of interface and deleting configured interfaces.
extends_documentation_fragment:
- dellemc.unity.unity
author:
- Meenakshi Dembi (@dembim) <ansible.team@dell.com>
options:
nas_server_name:
description:
- Name of the NAS server for which interface will be configured.
type: str
nas_server_id:
description:
- ID of the NAS server for which interface will be configured.
type: str
ethernet_port_name:
description:
- Name of the ethernet port.
type: str
ethernet_port_id:
description:
- ID of the ethernet port.
type: str
role:
description:
- Indicates whether interface is configured as production or backup.
choices: [PRODUCTION, BACKUP]
type: str
interface_ip:
description:
- IP of network interface.
required: true
type: str
netmask:
description:
- Netmask of network interface.
type: str
prefix_length:
description:
- Prefix length is mutually exclusive with I(netmask).
type: int
gateway:
description:
- Gateway of network interface.
type: str
vlan_id:
description:
- Vlan id of the interface.
type: int
state:
description:
- Define whether the interface should exist or not.
choices: [present, absent]
required: true
type: str
notes:
- The I(check_mode) is supported.
- Modify operation for interface is not supported.
'''
EXAMPLES = r'''
- name: Add Interface as Backup to NAS Server
dellemc.unity.interface:
unispherehost: "{{unispherehost}}"
username: "{{username}}"
password: "{{password}}"
validate_certs: "{{validate_certs}}"
nas_server_name: "dummy_nas"
ethernet_port_name: "SP A 4-Port Card Ethernet Port 0"
role: "BACKUP"
interface_ip: "xx.xx.xx.xx"
netmask: "xx.xx.xx.xx"
gateway: "xx.xx.xx.xx"
vlan_id: 324
state: "present"
- name: Add Interface as Production to NAS Server
dellemc.unity.interface:
unispherehost: "{{unispherehost}}"
username: "{{username}}"
password: "{{password}}"
validate_certs: "{{validate_certs}}"
nas_server_name: "dummy_nas"
ethernet_port_name: "SP A 4-Port Card Ethernet Port 0"
role: "PRODUCTION"
interface_ip: "xx.xx.xx.xx"
netmask: "xx.xx.xx.xx"
gateway: "xx.xx.xx.xx"
vlan_id: 324
state: "present"
- name: Get interface details
dellemc.unity.interface:
unispherehost: "{{unispherehost}}"
username: "{{username}}"
password: "{{password}}"
validate_certs: "{{validate_certs}}"
nas_server_name: "dummy_nas"
interface_ip: "xx.xx.xx.xx"
state: "present"
- name: Delete Interface
dellemc.unity.interface:
unispherehost: "{{unispherehost}}"
username: "{{username}}"
password: "{{password}}"
validate_certs: "{{validate_certs}}"
nas_server_name: "dummy_nas"
interface_ip: "xx.xx.xx.xx"
state: "absent"
'''
RETURN = r'''
changed:
description: Whether or not the resource has changed.
returned: always
type: bool
sample: true
interface_details:
description: Details of the interface.
returned: When interface is configured for NAS Server.
type: dict
contains:
existed:
description: Indicates if interface exists.
type: bool
gateway:
description: Gateway of network interface.
type: str
id:
description: Unique identifier interface.
type: str
ip_address:
description: IP address of interface.
type: str
ip_port:
description: Port on which network interface is configured.
type: dict
contains:
id:
description: ID of ip_port.
type: str
ip_protocol_version:
description: IP protocol version.
type: str
is_disabled:
description: Indicates whether interface is disabled.
type: bool
is_preferred:
description: Indicates whether interface is preferred.
type: bool
mac_address:
description: Mac address of ip_port.
type: bool
name:
description: System configured name of interface.
type: bool
nas_server:
description: Details of NAS server where interface is configured.
type: dict
contains:
id:
description: ID of NAS Server.
type: str
sample: {
"existed": true,
"gateway": "xx.xx.xx.xx",
"hash": 8785300560421,
"health": {
"UnityHealth": {
"hash": 8785300565468
}
},
"id": "if_69",
"ip_address": "10.10.10.10",
"ip_port": {
"UnityIpPort": {
"hash": 8785300565300,
"id": "spb_ocp_0_eth0"
}
},
"ip_protocol_version": "IpProtocolVersionEnum.IPv4",
"is_disabled": false,
"is_preferred": true,
"mac_address": "0C:48:C6:9F:57:BF",
"name": "36_APM00213404194",
"nas_server": {
"UnityNasServer": {
"hash": 8785300565417,
"id": "nas_10"
}
},
"netmask": "10.10.10.10",
"replication_policy": null,
"role": "FileInterfaceRoleEnum.PRODUCTION",
"source_parameters": null,
"v6_prefix_length": null,
"vlan_id": 324
}
'''
from ansible.module_utils.basic import AnsibleModule
from ansible_collections.dellemc.unity.plugins.module_utils.storage.dell \
import utils
import ipaddress
from ipaddress import ip_network
LOG = utils.get_logger('interface')
application_type = "Ansible/1.5.0"
class Interface(object):
"""Class with Interface operations"""
def __init__(self):
"""Define all parameters required by this module"""
self.module_params = utils.get_unity_management_host_parameters()
self.module_params.update(get_interface_parameters())
mutually_exclusive = [['nas_server_name', 'nas_server_id'], ['ethernet_port_id', 'ethernet_port_name'], ['netmask', 'prefix_length']]
required_one_of = [['nas_server_name', 'nas_server_id']]
# initialize the Ansible module
self.module = AnsibleModule(
argument_spec=self.module_params,
supports_check_mode=True,
mutually_exclusive=mutually_exclusive,
required_one_of=required_one_of
)
utils.ensure_required_libs(self.module)
self.unity_conn = utils.get_unity_unisphere_connection(
self.module.params, application_type)
LOG.info('Check Mode Flag %s', self.module.check_mode)
def get_interface_details(self, nas_server_obj):
"""Get interface details.
:param: nas_server_obj: NAS server object.
:return: Returns interface details configured on NAS server.
"""
try:
nas_server_obj_properties = nas_server_obj._get_properties()
if nas_server_obj_properties['file_interface']:
for item in nas_server_obj_properties['file_interface']['UnityFileInterfaceList']:
interface_id = self.unity_conn.get_file_interface(_id=item['UnityFileInterface']['id'])
if interface_id.ip_address == self.module.params['interface_ip']:
return interface_id
return None
except Exception as e:
error_msg = "Getting Interface details failed" \
" with error %s" % (str(e))
LOG.error(error_msg)
self.module.fail_json(msg=error_msg)
def get_nas_server_obj(self, nas_server_name, nas_server_id):
"""Get NAS server ID.
:param: nas_server_name: The name of NAS server
:param: nas_server_id: ID of NAS server
:return: Return NAS server object if exists
"""
LOG.info("Getting NAS server object")
try:
if nas_server_name:
obj_nas = self.unity_conn.get_nas_server(name=nas_server_name)
return obj_nas
elif nas_server_id:
obj_nas = self.unity_conn.get_nas_server(_id=nas_server_id)
if obj_nas._get_properties()['existed']:
return obj_nas
else:
msg = "NAS server with id %s does not exist" % (nas_server_id)
LOG.error(msg)
self.module.fail_json(msg=msg)
except Exception as e:
msg = "Failed to get details of NAS server with error: %s" % (str(e))
LOG.error(msg)
self.module.fail_json(msg=msg)
def add_interface(self, nas_server_obj, ethernet_port_id=None, ethernet_port_name=None, role=None, interface_ip=None,
netmask=None, prefix_length=None, gateway=None, vlan_id=None):
"""Adding interface to NAS server.
:param: nas_server_obj: The NAS server object.
:param: ethernet_port_id: ID of ethernet port.
:param: ethernet_port_name: Name of ethernet port.
:param: role: Role of the interface.
:param: interface_ip: IP of interface.
:param: netmask: Netmask for interface.
:param: prefix_length: Prefix length.
:param: gateway: Gateway for interface.
:param: vlan_id: vlan_id for interface.
:return: Return True if interface is configured successfully.
"""
LOG.info("Adding interface to NAS Server")
try:
nas_server_obj_properties = nas_server_obj._get_properties()
if nas_server_obj_properties['file_interface']:
for item in nas_server_obj_properties['file_interface']['UnityFileInterfaceList']:
interface_id = self.unity_conn.get_file_interface(_id=item['UnityFileInterface']['id'])
if interface_id._get_properties()['ip_address'] == self.module.params['interface_ip']:
return False
if role:
role_value = get_role_enum(role)
if ethernet_port_name:
ethernet_port_info = self.unity_conn.get_ethernet_port(name=ethernet_port_name)
ethernet_port_id = ethernet_port_info.id
if not self.module.check_mode:
utils.UnityFileInterface.create(cli=self.unity_conn._cli, nas_server=nas_server_obj.get_id(), ip_port=ethernet_port_id,
role=role_value, ip=interface_ip, netmask=netmask, v6_prefix_length=prefix_length,
gateway=gateway, vlan_id=vlan_id)
return True
except Exception as e:
msg = "Failed to add interface to NAS Server with error: %s" % (str(e))
LOG.error(msg)
self.module.fail_json(msg=msg)
def is_modification_required(self, interface_details):
"""Check if modification is required in existing interface/s configured for NAS Server
:param: interface_details: Existing interface details
:return: True if modification is required
"""
key_list = ['vlan_id', 'gateway', 'netmask']
for item in key_list:
if self.module.params[item] and self.module.params[item] != interface_details[item]:
return True
return False
def delete_interface(self, interface_obj):
"""Delete NFS server.
:param: interface_obj: Interface object.
:return: Return True if interface is deleted.
"""
LOG.info("Deleting interface")
try:
if not self.module.check_mode:
interface_obj.delete()
return True
except Exception as e:
msg = "Failed to delete interface with error: %s" % (str(e))
LOG.error(msg)
self.module.fail_json(msg=msg)
def validate_input_params(self):
"""Validates input parameters"""
param_list = ["nas_server_id", "nas_server_name", "ethernet_port_name", "ethernet_port_id", "role", "interface_ip",
"netmask", "gateway"]
for param in param_list:
msg = "Please provide valid value for: %s" % param
if self.module.params[param] is not None and len(self.module.params[param].strip()) == 0:
errmsg = msg.format(param)
self.module.fail_json(msg=errmsg)
if self.module.params['vlan_id'] is not None:
if self.module.params['vlan_id'] <= 3 or self.module.params['vlan_id'] >= 4094:
self.module.fail_json(msg='vlan_id should be in the range of 3 to 4094')
if self.module.params['interface_ip'] and \
not is_valid_ip(self.module.params['interface_ip']):
self.module.fail_json(msg='The value for interface ip is invalid')
if self.module.params['gateway'] and \
not is_valid_ip(self.module.params['gateway']):
self.module.fail_json(msg='The value for gateway is invalid')
if self.module.params['netmask'] and not \
utils.is_valid_netmask(self.module.params['netmask']):
self.module.fail_json(msg='Invalid IPV4 address specified for netmask')
if self.module.params['interface_ip'] and (get_ip_version(self.module.params['interface_ip']) == 6):
self.module.fail_json(msg='IPv6 format is not supported')
def validate_create_params(self):
"""Validates input parameters for adding interface"""
if self.module.params['role'] is None:
self.module.fail_json(msg='Role is a mandatory parameter for adding interface to NAS Server.')
if self.module.params['ethernet_port_name'] is None and self.module.params['ethernet_port_id'] is None:
self.module.fail_json(msg='ethernet_port_name/ethernet_port_id is mandatory parameter for adding interface to NAS Server.')
def perform_module_operation(self):
"""
Perform different actions on Interface module based on parameters
passed in the playbook
"""
nas_server_id = self.module.params['nas_server_id']
nas_server_name = self.module.params['nas_server_name']
ethernet_port_name = self.module.params['ethernet_port_name']
ethernet_port_id = self.module.params['ethernet_port_id']
role = self.module.params['role']
interface_ip = self.module.params['interface_ip']
netmask = self.module.params['netmask']
prefix_length = self.module.params['prefix_length']
gateway = self.module.params['gateway']
vlan_id = self.module.params['vlan_id']
state = self.module.params['state']
# result is a dictionary that contains changed status and Interface details
result = dict(
changed=False,
interface_details={}
)
modify_flag = False
self.validate_input_params()
interface_details = None
nas_server_obj = self.get_nas_server_obj(nas_server_name, nas_server_id)
interface_obj = self.get_interface_details(nas_server_obj)
if interface_obj and state == 'present':
interface_details = interface_obj._get_properties()
modify_flag = self.is_modification_required(interface_details)
if modify_flag:
self.module.fail_json(msg="Modification of Interfaces for NAS server is not supported through Ansible module")
if not interface_obj and state == 'present':
self.validate_create_params()
result['changed'] = self.add_interface(nas_server_obj, ethernet_port_id, ethernet_port_name, role,
interface_ip, netmask, prefix_length, gateway, vlan_id)
if interface_obj and state == 'absent':
result['changed'] = self.delete_interface(interface_obj)
if result['changed']:
nas_server_obj = self.get_nas_server_obj(nas_server_name, nas_server_id)
interface_obj = self.get_interface_details(nas_server_obj)
if interface_obj:
interface_details = interface_obj._get_properties()
result['interface_details'] = interface_details
self.module.exit_json(**result)
def get_interface_parameters():
"""This method provide parameters required for the ansible
Interface module on Unity"""
return dict(
nas_server_id=dict(type='str'),
nas_server_name=dict(type='str'),
ethernet_port_name=dict(type='str'),
ethernet_port_id=dict(type='str'),
role=dict(type='str', choices=['PRODUCTION', 'BACKUP']),
interface_ip=dict(required=True, type='str'),
netmask=dict(type='str'),
prefix_length=dict(type='int'),
gateway=dict(type='str'),
vlan_id=dict(type='int'),
state=dict(required=True, type='str', choices=['present', 'absent'])
)
def get_role_enum(role):
"""Getting correct enum values for role
:param: role: Indicates role of interface.
:return: enum value for role.
"""
if utils.FileInterfaceRoleEnum[role]:
role = utils.FileInterfaceRoleEnum[role]
return role
def is_valid_ip(address):
"""Validating IP address format
:param: address: IP address to be validated for format.
"""
try:
ipaddress.ip_address(address)
return True
except ValueError:
return False
def get_ip_version(val):
"""Returns IP address version
:param: val: IP address to be validated for version.
"""
try:
val = u'{0}'.format(val)
ip = ip_network(val, strict=False)
return ip.version
except ValueError:
return 0
def main():
"""Create Unity Interface object and perform action on it
based on user input from playbook"""
obj = Interface()
obj.perform_module_operation()
if __name__ == '__main__':
main()

View File

@@ -0,0 +1,494 @@
#!/usr/bin/python
# Copyright: (c) 2022, Dell Technologies
# Apache License version 2.0 (see MODULE-LICENSE or http://www.apache.org/licenses/LICENSE-2.0.txt)
"""Ansible module for managing NFS server on Unity"""
from __future__ import absolute_import, division, print_function
__metaclass__ = type
DOCUMENTATION = r'''
module: nfsserver
version_added: '1.4.0'
short_description: Manage NFS server on Unity storage system
description:
- Managing the NFS server on the Unity storage system includes creating NFS server, getting NFS server details
and deleting NFS server attributes.
extends_documentation_fragment:
- dellemc.unity.unity
author:
- Meenakshi Dembi (@dembim) <ansible.team@dell.com>
options:
nas_server_name:
description:
- Name of the NAS server on which NFS server will be hosted.
type: str
nas_server_id:
description:
- ID of the NAS server on which NFS server will be hosted.
type: str
nfs_server_id:
description:
- ID of the NFS server.
type: str
host_name:
description:
- Host name of the NFS server.
type: str
nfs_v4_enabled:
description:
- Indicates whether the NFSv4 is enabled on the NAS server.
type: bool
is_secure_enabled:
description:
- Indicates whether the secure NFS is enabled.
type: bool
kerberos_domain_controller_type:
description:
- Type of Kerberos Domain Controller used for secure NFS service.
choices: [CUSTOM, UNIX, WINDOWS]
type: str
kerberos_domain_controller_username:
description:
- Kerberos Domain Controller administrator username.
type: str
kerberos_domain_controller_password:
description:
- Kerberos Domain Controller administrator password.
type: str
is_extended_credentials_enabled:
description:
- Indicates whether support for more than 16 unix groups in a Unix credential.
type: bool
remove_spn_from_kerberos:
description:
- Indicates whether to remove the SPN from Kerberos Domain Controller.
default: true
type: bool
state:
description:
- Define whether the NFS server should exist or not.
choices: [absent, present]
required: true
type: str
notes:
- The I(check_mode) is supported.
- Modify operation for NFS Server is not supported.
- When I(kerberos_domain_controller_type) is C(UNIX), I(kdc_type) in I(nfs_server_details) output is displayed as C(null).
'''
EXAMPLES = r'''
- name: Create NFS server with kdctype as Windows
dellemc.unity.nfsserver:
unispherehost: "{{unispherehost}}"
username: "{{username}}"
password: "{{password}}"
validate_certs: "{{validate_certs}}"
nas_server_name: "dummy_nas"
host_name: "dummy_nas23"
is_secure_enabled: True
kerberos_domain_controller_type: "WINDOWS"
kerberos_domain_controller_username: "administrator"
kerberos_domain_controller_password: "Password123!"
is_extended_credentials_enabled: True
nfs_v4_enabled: True
state: "present"
- name: Create NFS server with kdctype as Unix
dellemc.unity.nfsserver:
unispherehost: "{{unispherehost}}"
username: "{{username}}"
password: "{{password}}"
validate_certs: "{{validate_certs}}"
nas_server_name: "dummy_nas"
host_name: "dummy_nas23"
is_secure_enabled: True
kerberos_domain_controller_type: "UNIX"
is_extended_credentials_enabled: True
nfs_v4_enabled: True
state: "present"
- name: Get NFS server details
dellemc.unity.nfsserver:
unispherehost: "{{unispherehost}}"
username: "{{username}}"
password: "{{password}}"
validate_certs: "{{validate_certs}}"
nas_server_name: "dummy_nas"
state: "present"
- name: Delete NFS server
dellemc.unity.nfsserver:
unispherehost: "{{unispherehost}}"
username: "{{username}}"
password: "{{password}}"
validate_certs: "{{validate_certs}}"
nas_server_name: "dummy_nas"
kerberos_domain_controller_username: "administrator"
kerberos_domain_controller_password: "Password123!"
unjoin_server_account: False
state: "absent"
'''
RETURN = r'''
changed:
description: Whether or not the resource has changed.
returned: always
type: bool
sample: true
nfs_server_details:
description: Details of the NFS server.
returned: When NFS server exists
type: dict
contains:
credentials_cache_ttl:
description: Credential cache refresh timeout. Resolution is in minutes. Default value is 15 minutes.
type: str
existed:
description: Indicates if NFS Server exists.
type: bool
host_name:
description: Host name of the NFS server.
type: str
id:
description: Unique identifier of the NFS Server instance.
type: str
is_extended_credentials_enabled:
description: Indicates whether the NFS server supports more than 16 Unix groups in a Unix credential.
type: bool
is_secure_enabled:
description: Indicates whether secure NFS is enabled on the NFS server.
type: bool
kdc_type:
description: Type of Kerberos Domain Controller used for secure NFS service.
type: str
nfs_v4_enabled:
description: Indicates whether NFSv4 is enabled on the NAS server.
type: bool
servicee_principal_name:
description: The Service Principal Name (SPN) for the NFS Server.
type: str
sample: {
"credentials_cache_ttl": "0:15:00",
"existed": true,
"file_interfaces": {
"UnityFileInterfaceList": [
{
"UnityFileInterface": {
"hash": 8778980109421,
"id": "if_37"
}
}
]
},
"hash": 8778980109388,
"host_name": "dummy_nas23.pie.lab.emc.com",
"id": "nfs_51",
"is_extended_credentials_enabled": true,
"is_secure_enabled": true,
"kdc_type": "KdcTypeEnum.WINDOWS",
"nas_server": {
"UnityNasServer": {
"hash": 8778980109412
}
},
"nfs_v4_enabled": true,
"servicee_principal_name": null
}
'''
from ansible.module_utils.basic import AnsibleModule
from ansible_collections.dellemc.unity.plugins.module_utils.storage.dell \
import utils
LOG = utils.get_logger('nfsserver')
application_type = "Ansible/1.5.0"
class NFSServer(object):
"""Class with NFS server operations"""
def __init__(self):
"""Define all parameters required by this module"""
self.module_params = utils.get_unity_management_host_parameters()
self.module_params.update(get_nfs_server_parameters())
mutually_exclusive = [['nas_server_name', 'nas_server_id']]
required_one_of = [['nfs_server_id', 'nas_server_name', 'nas_server_id']]
# initialize the Ansible module
self.module = AnsibleModule(
argument_spec=self.module_params,
supports_check_mode=True,
mutually_exclusive=mutually_exclusive,
required_one_of=required_one_of
)
utils.ensure_required_libs(self.module)
self.unity_conn = utils.get_unity_unisphere_connection(
self.module.params, application_type)
LOG.info('Check Mode Flag %s', self.module.check_mode)
def get_nfs_server_details(self, nfs_server_id=None, nas_server_id=None):
"""Get NFS server details.
:param: nfs_server_id: The ID of the NFS server
:param: nas_server_id: The name of the NAS server
:return: Dict containing NFS server details if exists
"""
LOG.info("Getting NFS server details")
try:
if nfs_server_id:
nfs_server_details = self.unity_conn.get_nfs_server(_id=nfs_server_id)
return nfs_server_details._get_properties()
elif nas_server_id:
nfs_server_details = self.unity_conn.get_nfs_server(nas_server=nas_server_id)
if len(nfs_server_details) > 0:
return process_dict(nfs_server_details._get_properties())
return None
except utils.HttpError as e:
if e.http_status == 401:
msg = 'Incorrect username or password provided.'
LOG.error(msg)
self.module.fail_json(msg=msg)
else:
err_msg = "Failed to get details of NFS Server" \
" with error {0}".format(str(e))
LOG.error(err_msg)
self.module.fail_json(msg=err_msg)
except utils.UnityResourceNotFoundError as e:
err_msg = "Failed to get details of NFS Server" \
" with error {0}".format(str(e))
LOG.error(err_msg)
return None
def get_nfs_server_instance(self, nfs_server_id):
"""Get NFS server instance.
:param: nfs_server_id: The ID of the NFS server
:return: Return NFS server instance if exists
"""
try:
nfs_server_obj = self.unity_conn.get_nfs_server(_id=nfs_server_id)
return nfs_server_obj
except Exception as e:
error_msg = "Failed to get the NFS server %s instance" \
" with error %s" % (nfs_server_id, str(e))
LOG.error(error_msg)
self.module.fail_json(msg=error_msg)
def delete_nfs_server(self, nfs_server_id, skip_unjoin=None, domain_username=None, domain_password=None):
"""Delete NFS server.
:param: nfs_server_id: The ID of the NFS server
:param: skip_unjoin: Flag indicating whether to unjoin SMB server account from AD before deletion
:param: domain_username: The domain username
:param: domain_password: The domain password
:return: Return True if NFS server is deleted
"""
LOG.info("Deleting NFS server")
try:
if not self.module.check_mode:
nfs_obj = self.get_nfs_server_instance(nfs_server_id=nfs_server_id)
nfs_obj.delete(skip_kdc_unjoin=skip_unjoin, username=domain_username, password=domain_password)
return True
except Exception as e:
msg = "Failed to delete NFS server: %s with error: %s" % (nfs_server_id, str(e))
LOG.error(msg)
self.module.fail_json(msg=msg)
def get_nas_server_id(self, nas_server_name):
"""Get NAS server ID.
:param: nas_server_name: The name of NAS server
:return: Return NAS server ID if exists
"""
LOG.info("Getting NAS server ID")
try:
obj_nas = self.unity_conn.get_nas_server(name=nas_server_name)
return obj_nas.get_id()
except Exception as e:
msg = "Failed to get details of NAS server: %s with error: %s" % (nas_server_name, str(e))
LOG.error(msg)
self.module.fail_json(msg=msg)
def is_modification_required(self, is_extended_credentials_enabled, nfs_server_details):
"""Check if modification is required in existing NFS server
:param: is_extended_credentials_enabled: Indicates whether the NFS server supports more than 16 Unix groups in a Unix credential.
:param: nfs_server_details: NFS server details
:return: True if modification is required
"""
LOG.info("Checking if any modification is required")
# Check for Extend Credential
if is_extended_credentials_enabled is not None and \
is_extended_credentials_enabled != nfs_server_details['is_extended_credentials_enabled']:
return True
def create_nfs_server(self, nas_server_id, host_name=None, nfs_v4_enabled=None, is_secure_enabled=None,
kerberos_domain_controller_type=None, kerberos_domain_controller_username=None,
kerberos_domain_controller_password=None, is_extended_credentials_enabled=None):
"""Create NFS server.
:param: nas_server_id: The ID of NAS server.
:param: host_name: Name of NFS Server.
:param: nfs_v4_enabled: Indicates whether the NFSv4 is enabled on the NAS server.
:param: is_secure_enabled: Indicates whether the secure NFS is enabled.
:param: kerberos_domain_controller_type: Type of Kerberos Domain Controller used for secure NFS service.
:param: kerberos_domain_controller_username: Kerberos Domain Controller administrator username.
:param: kerberos_domain_controller_password: Kerberos Domain Controller administrator password.
:param: is_extended_credentials_enabled: Indicates whether support for more than 16 unix groups in a Unix credential.
"""
LOG.info("Creating NFS server")
try:
if not self.module.check_mode:
kdc_enum_type = get_enum_kdctype(kerberos_domain_controller_type)
if kerberos_domain_controller_type == "UNIX":
is_extended_credentials_enabled = None
is_secure_enabled = None
utils.UnityNfsServer.create(cli=self.unity_conn._cli, nas_server=nas_server_id, host_name=host_name,
nfs_v4_enabled=nfs_v4_enabled,
is_secure_enabled=is_secure_enabled, kdc_type=kdc_enum_type,
kdc_username=kerberos_domain_controller_username,
kdc_password=kerberos_domain_controller_password,
is_extended_credentials_enabled=is_extended_credentials_enabled)
return True
except Exception as e:
msg = "Failed to create NFS server with on NAS Server %s with error: %s" % (nas_server_id, str(e))
LOG.error(msg)
self.module.fail_json(msg=msg)
def validate_input_params(self):
param_list = ["nfs_server_id", "nas_server_id", "nas_server_name", "host_name", "kerberos_domain_controller_username",
"kerberos_domain_controller_password"]
for param in param_list:
msg = "Please provide valid value for: %s" % param
if self.module.params[param] is not None and len(self.module.params[param].strip()) == 0:
errmsg = msg.format(param)
self.module.fail_json(msg=errmsg)
def perform_module_operation(self):
"""
Perform different actions on NFS server module based on parameters
passed in the playbook
"""
nfs_server_id = self.module.params['nfs_server_id']
nas_server_id = self.module.params['nas_server_id']
nas_server_name = self.module.params['nas_server_name']
host_name = self.module.params['host_name']
nfs_v4_enabled = self.module.params['nfs_v4_enabled']
is_secure_enabled = self.module.params['is_secure_enabled']
kerberos_domain_controller_type = self.module.params['kerberos_domain_controller_type']
kerberos_domain_controller_username = self.module.params['kerberos_domain_controller_username']
kerberos_domain_controller_password = self.module.params['kerberos_domain_controller_password']
is_extended_credentials_enabled = self.module.params['is_extended_credentials_enabled']
remove_spn_from_kerberos = self.module.params['remove_spn_from_kerberos']
state = self.module.params['state']
# result is a dictionary that contains changed status and NFS server details
result = dict(
changed=False,
nfs_server_details={}
)
modify_flag = False
self.validate_input_params()
if nas_server_name:
nas_server_id = self.get_nas_server_id(nas_server_name)
nfs_server_details = self.get_nfs_server_details(nfs_server_id=nfs_server_id,
nas_server_id=nas_server_id)
# Check if modification is required
if nfs_server_details and state == 'present':
modify_flag = self.is_modification_required(is_extended_credentials_enabled, nfs_server_details)
if modify_flag:
self.module.fail_json(msg="Modification of NFS Server parameters is not supported through Ansible module")
if not nfs_server_details and state == 'present':
if not nas_server_id:
self.module.fail_json(msg="Please provide nas server id/name to create NFS server.")
result['changed'] = self.create_nfs_server(nas_server_id, host_name, nfs_v4_enabled,
is_secure_enabled, kerberos_domain_controller_type,
kerberos_domain_controller_username,
kerberos_domain_controller_password,
is_extended_credentials_enabled)
if state == 'absent' and nfs_server_details:
skip_unjoin = not remove_spn_from_kerberos
result['changed'] = self.delete_nfs_server(nfs_server_details["id"], skip_unjoin,
kerberos_domain_controller_username,
kerberos_domain_controller_password)
if state == 'present':
result['nfs_server_details'] = self.get_nfs_server_details(nfs_server_id=nfs_server_id,
nas_server_id=nas_server_id)
self.module.exit_json(**result)
def get_nfs_server_parameters():
"""This method provide parameters required for the ansible
NFS server module on Unity"""
return dict(
nfs_server_id=dict(type='str'),
host_name=dict(type='str'),
nfs_v4_enabled=dict(type='bool'),
is_secure_enabled=dict(type='bool'),
kerberos_domain_controller_type=dict(type='str', choices=['UNIX', 'WINDOWS', 'CUSTOM']),
kerberos_domain_controller_username=dict(type='str'),
kerberos_domain_controller_password=dict(type='str', no_log=True),
nas_server_name=dict(type='str'),
nas_server_id=dict(type='str'),
is_extended_credentials_enabled=dict(type='bool'),
remove_spn_from_kerberos=dict(default=True, type='bool'),
state=dict(required=True, type='str', choices=['present', 'absent']),
)
def get_enum_kdctype(kerberos_domain_controller_type):
"""Getting correct enum values for kerberos_domain_controller_type
:param: kerberos_domain_controller_type: Type of Kerberos Domain Controller used for secure NFS service.
:return: enum value for kerberos_domain_controller_type.
"""
if utils.KdcTypeEnum[kerberos_domain_controller_type]:
kerberos_domain_controller_type = utils.KdcTypeEnum[kerberos_domain_controller_type]
return kerberos_domain_controller_type
def process_dict(nfs_server_details):
"""Process NFS server details.
:param: nfs_server_details: Dict containing NFS server details
:return: Processed dict containing NFS server details
"""
param_list = ['credentials_cache_ttl', 'file_interfaces', 'host_name', 'id', 'kdc_type', 'nas_server', 'is_secure_enabled',
'is_extended_credentials_enabled', 'nfs_v4_enabled', 'servicee_principal_name']
for param in param_list:
if param in nfs_server_details and param == 'credentials_cache_ttl':
nfs_server_details[param] = str(nfs_server_details[param][0])
else:
nfs_server_details[param] = nfs_server_details[param][0]
return nfs_server_details
def main():
"""Create Unity NFS server object and perform action on it
based on user input from playbook"""
obj = NFSServer()
obj.perform_module_operation()
if __name__ == '__main__':
main()

View File

@@ -0,0 +1,877 @@
#!/usr/bin/python
# Copyright: (c) 2020, Dell Technologies
# Apache License version 2.0 (see MODULE-LICENSE or http://www.apache.org/licenses/LICENSE-2.0.txt)
from __future__ import (absolute_import, division, print_function)
__metaclass__ = type
DOCUMENTATION = r'''
---
module: smbshare
version_added: '1.1.0'
short_description: Manage SMB shares on Unity storage system
extends_documentation_fragment:
- dellemc.unity.unity
author:
- P Srinivas Rao (@srinivas-rao5) <ansible.team@dell.com>
description:
- Managing SMB Shares on Unity storage system includes create, get,
modify, and delete the smb shares.
options:
share_name:
description:
- Name of the SMB share.
- Required during creation of the SMB share.
- For all other operations either I(share_name) or I(share_id) is required.
type: str
share_id:
description:
- ID of the SMB share.
- Should not be specified during creation. Id is auto generated.
- For all other operations either I(share_name) or I(share_id) is required.
- If I(share_id) is used then no need to pass nas_server/filesystem/snapshot/path.
type: str
path:
description:
- Local path to the file system/Snapshot or any existing sub-folder of
the file system/Snapshot that is shared over the network.
- Path is relative to the root of the filesystem.
- Required for creation of the SMB share.
type: str
filesystem_id:
description:
- The ID of the File System.
- Either I(filesystem_name) or I(filesystem_id) is required for creation of the SMB share for filesystem.
- If I(filesystem_name) is specified, then I(nas_server_name)/I(nas_server_id) is required to
uniquely identify the filesystem.
- Options I(filesystem_name) and I(filesystem_id) are mutually exclusive parameters.
type: str
snapshot_id:
description:
- The ID of the Filesystem Snapshot.
- Either I(snapshot_name) or I(snapshot_id) is required for creation of the SMB share for a snapshot.
- If I(snapshot_name) is specified, then I(nas_server_name)/I(nas_server_id) is required to
uniquely identify the snapshot.
- Options I(snapshot_name) and I(snapshot_id) are mutually exclusive parameters.
type: str
nas_server_id:
description:
- The ID of the NAS Server.
- It is not required if I(share_id) is used.
type: str
filesystem_name:
description:
- The Name of the File System.
- Either I(filesystem_name) or I(filesystem_id) is required for creation of the SMB share for filesystem.
- If I(filesystem_name) is specified, then I(nas_server_name)/I(nas_server_id) is required to
uniquely identify the filesystem.
- Options I(filesystem_name) and I(filesytem_id) are mutually exclusive parameters.
type: str
snapshot_name:
description:
- The Name of the Filesystem Snapshot.
- Either I(snapshot_name) or I(snapshot_id) is required for creation of the SMB share for a snapshot.
- If I(snapshot_name) is specified, then I(nas_server_name)/I(nas_server_id) is required to
uniquely identify the snapshot.
- Options I(snapshot_name) and I(snapshot_id) are mutually exclusive parameters.
type: str
nas_server_name:
description:
- The Name of the NAS Server.
- It is not required if I(share_id) is used.
- Options I(nas_server_name) and I(nas_server_id) are mutually exclusive parameters.
type: str
description:
description:
- Description for the SMB share.
- Optional parameter when creating a share.
- To modify, pass the new value in description field.
type: str
is_abe_enabled:
description:
- Indicates whether Access-based Enumeration (ABE) for SMB share is enabled.
- During creation, if not mentioned then default is C(false).
type: bool
is_branch_cache_enabled:
description:
- Indicates whether Branch Cache optimization for SMB share is enabled.
- During creation, if not mentioned then default is C(false).
type: bool
is_continuous_availability_enabled:
description:
- Indicates whether continuous availability for SMB 3.0 is enabled.
- During creation, if not mentioned then default is C(false).
type: bool
is_encryption_enabled:
description:
- Indicates whether encryption for SMB 3.0 is enabled at the shared folder level.
- During creation, if not mentioned then default is C(false).
type: bool
offline_availability:
description:
- Defines valid states of Offline Availability.
- C(MANUAL)- Only specified files will be available offline.
- C(DOCUMENTS)- All files that users open will be available offline.
- C(PROGRAMS)- Program will preferably run from the offline cache even when
connected to the network. All files that users open will be available offline.
- C(NONE)- Prevents clients from storing documents and programs in offline cache.
type: str
choices: ["MANUAL","DOCUMENTS","PROGRAMS","NONE"]
umask:
description:
- The default UNIX umask for new files created on the SMB Share.
type: str
state:
description:
- Define whether the SMB share should exist or not.
- Value C(present) indicates that the share should exist on the system.
- Value C(absent) indicates that the share should not exist on the system.
type: str
required: true
choices: ['absent', 'present']
notes:
- When ID/Name of the filesystem/snapshot is passed then I(nas_server) is not required.
If passed, then filesystem/snapshot should exist for the mentioned I(nas_server),
else the task will fail.
- The I(check_mode) is not supported.
'''
EXAMPLES = r'''
- name: Create SMB share for a filesystem
dellemc.unity.smbshare:
unispherehost: "{{unispherehost}}"
username: "{{username}}"
password: "{{password}}"
validate_certs: "{{validate_certs}}"
share_name: "sample_smb_share"
filesystem_name: "sample_fs"
nas_server_id: "NAS_11"
path: "/sample_fs"
description: "Sample SMB share created"
is_abe_enabled: True
is_branch_cache_enabled: True
offline_availability: "DOCUMENTS"
is_continuous_availability_enabled: True
is_encryption_enabled: True
umask: "777"
state: "present"
- name: Modify Attributes of SMB share for a filesystem
dellemc.unity.smbshare:
unispherehost: "{{unispherehost}}"
username: "{{username}}"
password: "{{password}}"
validate_certs: "{{validate_certs}}"
share_name: "sample_smb_share"
nas_server_name: "sample_nas_server"
description: "Sample SMB share attributes updated"
is_abe_enabled: False
is_branch_cache_enabled: False
offline_availability: "MANUAL"
is_continuous_availability_enabled: "False"
is_encryption_enabled: "False"
umask: "022"
state: "present"
- name: Create SMB share for a snapshot
dellemc.unity.smbshare:
unispherehost: "{{unispherehost}}"
username: "{{username}}"
password: "{{password}}"
validate_certs: "{{validate_certs}}"
share_name: "sample_snap_smb_share"
snapshot_name: "sample_snapshot"
nas_server_id: "NAS_11"
path: "/sample_snapshot"
description: "Sample SMB share created for snapshot"
is_abe_enabled: True
is_branch_cache_enabled: True
is_continuous_availability_enabled: True
is_encryption_enabled: True
umask: "777"
state: "present"
- name: Modify Attributes of SMB share for a snapshot
dellemc.unity.smbshare:
unispherehost: "{{unispherehost}}"
username: "{{username}}"
password: "{{password}}"
validate_certs: "{{validate_certs}}"
share_name: "sample_snap_smb_share"
snapshot_name: "sample_snapshot"
description: "Sample SMB share attributes updated for snapshot"
is_abe_enabled: False
is_branch_cache_enabled: False
offline_availability: "MANUAL"
is_continuous_availability_enabled: "False"
is_encryption_enabled: "False"
umask: "022"
state: "present"
- name: Get details of SMB share
dellemc.unity.smbshare:
unispherehost: "{{unispherehost}}"
username: "{{username}}"
password: "{{password}}"
validate_certs: "{{validate_certs}}"
share_id: "{{smb_share_id}}"
state: "present"
- name: Delete SMB share
dellemc.unity.smbshare:
unispherehost: "{{unispherehost}}"
username: "{{username}}"
password: "{{password}}"
validate_certs: "{{validate_certs}}"
share_id: "{{smb_share_id}}"
state: "absent"
'''
RETURN = r'''
changed:
description: Whether or not the resource has changed.
returned: always
type: bool
sample: True
smb_share_details:
description: The SMB share details.
type: dict
returned: When share exists.
contains:
id:
description: The ID of the SMB share.
type: str
name:
description: Name of the SMB share.
type: str
sample: "sample_smb_share"
filesystem_id:
description: The ID of the Filesystem.
type: str
filesystem_name:
description: The Name of the filesystem
type: str
snapshot_id:
description: The ID of the Snapshot.
type: str
snapshot_name:
description: The Name of the Snapshot.
type: str
nas_server_id:
description: The ID of the nas_server.
type: str
nas_server_name:
description: The Name of the nas_server.
type: str
description:
description: Additional information about the share.
type: str
sample: This share is created for demo purpose only.
is_abe_enabled:
description: Whether Access Based enumeration is enforced or not.
type: bool
sample: false
is_branch_cache_enabled:
description: Whether branch cache is enabled or not.
type: bool
sample: false
is_continuous_availability_enabled:
description: Whether the share will be available continuously or not.
type: bool
sample: false
is_encryption_enabled:
description: Whether encryption is enabled or not.
type: bool
sample: false
umask:
description: Unix mask for the SMB share.
type: str
sample: {
"creation_time": "2022-03-17 11:56:54.867000+00:00",
"description": "",
"existed": true,
"export_paths": [
"\\\\multi-prot-pie.extreme1.com\\multi-prot-hui",
"\\\\10.230.24.26\\multi-prot-hui"
],
"filesystem": {
"UnityFileSystem": {
"hash": 8748426746492
}
},
"filesystem_id": "fs_140",
"filesystem_name": "multi-prot-hui",
"hash": 8748426746588,
"id": "SMBShare_20",
"is_abe_enabled": false,
"is_ace_enabled": false,
"is_branch_cache_enabled": false,
"is_continuous_availability_enabled": false,
"is_dfs_enabled": false,
"is_encryption_enabled": false,
"is_read_only": null,
"modified_time": "2022-03-17 11:56:54.867000+00:00",
"name": "multi-prot-hui",
"nas_server_id": "nas_5",
"nas_server_name": "multi-prot",
"offline_availability": "CifsShareOfflineAvailabilityEnum.NONE",
"path": "/",
"snap": null,
"type": "CIFSTypeEnum.CIFS_SHARE",
"umask": "022"
}
'''
from ansible.module_utils.basic import AnsibleModule
from ansible_collections.dellemc.unity.plugins.module_utils.storage.dell \
import utils
LOG = utils.get_logger('smbshare')
application_type = "Ansible/1.5.0"
class SMBShare(object):
"""Class with SMB Share operations"""
def __init__(self):
""" Define all parameters required by this module"""
self.module_params = utils.get_unity_management_host_parameters()
self.module_params.update(get_smb_share_parameters())
# initialize the ansible module
mut_ex_args = [['share_name', 'share_id'],
['nas_server_name', 'nas_server_id'],
['filesystem_name', 'snapshot_name',
'filesystem_id', 'snapshot_id'],
['share_id', 'nas_server_name'],
['share_id', 'nas_server_id'],
['share_id', 'filesystem_name'],
['share_id', 'filesystem_id'],
['share_id', 'path'],
['share_id', 'snapshot_name'],
['share_id', 'snapshot_id']]
required_one_of = [['share_id', 'share_name']]
self.module = AnsibleModule(
argument_spec=self.module_params,
supports_check_mode=False,
mutually_exclusive=mut_ex_args,
required_one_of=required_one_of
)
utils.ensure_required_libs(self.module)
# result is a dictionary that contains changed status and
# snapshot details
self.result = {"changed": False,
'smb_share_details': {}}
self.unity_conn = utils.get_unity_unisphere_connection(
self.module.params, application_type)
self.smb_share_conn_obj = utils.cifs_share.UnityCifsShare(
self.unity_conn)
LOG.info('Connection established with the Unity Array')
def get_offline_availability_enum(self, offline_availability):
"""
Get the enum of the Offline Availability parameter.
:param offline_availability: The offline_availability string
:return: offline_availability enum
"""
if offline_availability in \
utils.CifsShareOfflineAvailabilityEnum.__members__:
return utils.CifsShareOfflineAvailabilityEnum[
offline_availability]
else:
error_msg = "Invalid value {0} for offline availability" \
" provided".format(offline_availability)
LOG.error(error_msg)
self.module.fail_json(msg=error_msg)
def get_smb_share_obj(self, share_id=None, share_name=None,
filesystem_obj=None, snap_obj=None, nas_obj=None):
"""Get SMB share details"""
msg = "Failed to get details of SMB Share {0} with error {1} "
smb_share = share_name if share_name else share_id
try:
if share_id:
obj_smb = self.unity_conn.get_cifs_share(_id=share_id)
if obj_smb and obj_smb.existed:
LOG.info("Successfully got the SMB share "
"object %s ", obj_smb)
return obj_smb
elif share_name is not None and filesystem_obj:
# There might be a case where SMB share with same name exists
# for different nas server. Hence, filesystem_obj is passed
# along with share name to get a unique resource.
return self.unity_conn.get_cifs_share(
name=share_name, filesystem=filesystem_obj)
elif share_name is not None and snap_obj:
# There might be a case where SMB share with same name exists
# for different nas server. Hence, snap_obj is passed
# along with share name to get a unique resource.
return self.unity_conn.get_cifs_share(
name=share_name, snap=snap_obj)
# This elif is addressing scenario where nas server details is
# passed and neither filesystem nor snapshot details are passed.
elif share_name is not None and nas_obj:
# Multiple smb shares can be received, as only name is passed
smb_share_obj = self.unity_conn.get_cifs_share(
name=share_name)
# Checking if instance or list of instance is returned.
if isinstance(smb_share_obj,
utils.cifs_share.UnityCifsShareList):
LOG.info("Multiple SMB share with same name found.")
smb_share_obj_list = smb_share_obj
for smb_share in smb_share_obj_list:
if smb_share.filesystem.nas_server == nas_obj:
return smb_share
msg = "No SMB share found with the given NAS Server." \
" Please provide correct share name and" \
" nas server details."
return None
# Below statements will execute when there is only single
# smb share returned.
if smb_share_obj.filesystem.nas_server == nas_obj:
return smb_share_obj
msg = "No SMB share found with the given NAS Server." \
" Please provide correct share name and" \
" nas server details."
return None
else:
self.module.fail_json(
msg="Share Name is Passed. Please enter Filesystem/"
"Snapshot/NAS Server Resource along with share_name"
" to get the details of the SMB share")
except utils.HttpError as e:
if e.http_status == 401:
cred_err = "Incorrect username or password , {0}".format(
e.message)
self.module.fail_json(msg=cred_err)
else:
err_msg = msg.format(smb_share, str(e))
LOG.error(err_msg)
self.module.fail_json(msg=err_msg)
except utils.UnityResourceNotFoundError as e:
err_msg = msg.format(smb_share, str(e))
LOG.error(err_msg)
return None
except Exception as e:
err_msg = msg.format(smb_share, str(e))
LOG.error(err_msg)
self.module.fail_json(msg=err_msg)
def create_smb_share(self, share_name, path, filesystem_obj=None,
snapshot_obj=None, description=None,
is_abe_enabled=None, is_branch_cache_enabled=None,
is_continuous_availability_enabled=None,
is_encryption_enabled=None,
offline_availability=None, umask=None):
"""
Create SMB Share
:return: SMB Share Object if successful, else error.
"""
if path is None or path == "":
self.module.fail_json(msg="Please enter a valid path."
" Empty string or None provided.")
if not filesystem_obj and not snapshot_obj:
self.module.fail_json(msg="Either Filesystem or Snapshot "
"Resource's Name/ID is required to"
" Create a SMB share")
try:
if filesystem_obj:
return self.smb_share_conn_obj.create(
cli=self.unity_conn._cli, name=share_name,
fs=filesystem_obj, path=path,
is_encryption_enabled=is_encryption_enabled,
is_con_avail_enabled=is_continuous_availability_enabled,
is_abe_enabled=is_abe_enabled,
is_branch_cache_enabled=is_branch_cache_enabled,
umask=umask, description=description,
offline_availability=offline_availability)
else:
return self.smb_share_conn_obj.create_from_snap(
cli=self.unity_conn._cli, name=share_name,
snap=snapshot_obj, path=path,
is_encryption_enabled=is_encryption_enabled,
is_con_avail_enabled=is_continuous_availability_enabled,
is_abe_enabled=is_abe_enabled,
is_branch_cache_enabled=is_branch_cache_enabled,
umask=umask, description=description,
offline_availability=offline_availability)
except Exception as e:
error_msg = "Failed to create SMB share" \
" %s with error %s" % (share_name, str(e))
LOG.error(error_msg)
self.module.fail_json(msg=error_msg)
def get_filesystem(self, filesystem_id=None, filesystem_name=None,
nas_server_obj=None):
"""
Get the Filesystem Object.
:param filesystem_id: ID of the Filesystem.
:param filesystem_name: Name of the filesystem.
:param nas_server_obj: NAS Server object.
:return: Object of the filesystem.
"""
try:
if filesystem_id:
obj_fs = self.unity_conn.get_filesystem(_id=filesystem_id)
if obj_fs and obj_fs.existed:
LOG.info("Successfully got the filesystem "
"object %s ", obj_fs)
return obj_fs
else:
return self.unity_conn.get_filesystem(
name=filesystem_name, nas_server=nas_server_obj)
return None
except Exception as e:
filesystem = filesystem_name if filesystem_name \
else filesystem_id
err_msg = "Failed to get filesystem details {0} with" \
" error {1}".format(filesystem, str(e))
LOG.error(err_msg)
self.module.fail_json(msg=err_msg)
def get_snapshot(self, snapshot_name, snapshot_id):
"""
Get the Snapshot Object.
:param snapshot_id: ID of the Snapshot.
:param snapshot_name: Name of the Snapshot
:return: Object of the filesystem.
"""
try:
obj_snap = self.unity_conn.get_snap(_id=snapshot_id,
name=snapshot_name)
if snapshot_id and obj_snap and not obj_snap.existed:
LOG.info("Snapshot object does not exist %s ", obj_snap)
return None
return obj_snap
except Exception as e:
snapshot = snapshot_name if snapshot_name else snapshot_id
err_msg = "Failed to get filesystem snapshots details {0} with" \
" error {1}".format(snapshot, str(e))
LOG.error(err_msg)
self.module.fail_json(msg=err_msg)
def get_nas_server(self, nas_server_name, nas_server_id):
"""
Get the NAS Server Object using NAME/ID of the NAS Server.
:param nas_server_name: Name of the NAS Server
:param nas_server_id: ID of the NAS Server
:return: NAS Server object.
"""
nas_server = nas_server_name if nas_server_name else nas_server_id
try:
obj_nas = self.unity_conn.get_nas_server(_id=nas_server_id,
name=nas_server_name)
if nas_server_id and obj_nas and not obj_nas.existed:
LOG.info("NAS Server object does not exist %s ", obj_nas)
return None
return obj_nas
except utils.HttpError as e:
if e.http_status == 401:
cred_err = "Incorrect username or password , {0}".format(
e.message)
self.module.fail_json(msg=cred_err)
else:
err_msg = "Failed to get details of NAS Server" \
" {0} with error {1}".format(nas_server, str(e))
LOG.error(err_msg)
self.module.fail_json(msg=err_msg)
except Exception as e:
nas_server = nas_server_name if nas_server_name \
else nas_server_id
err_msg = "Failed to get nas server details {0} with" \
" error {1}".format(nas_server, str(e))
LOG.error(err_msg)
self.module.fail_json(msg=err_msg)
def delete_smb_share(self, smb_share_obj):
"""
Delete SMB share if exists, else thrown error.
"""
try:
smb_share_obj.delete()
except Exception as e:
error_msg = "Failed to Delete SMB share" \
" %s with error %s" % (smb_share_obj.name, str(e))
LOG.error(error_msg)
self.module.fail_json(msg=error_msg)
def to_update(self, smb_share_obj):
LOG.info("Checking Whether the parameters are modified or not.")
offline_availability = self.module.params['offline_availability']
# Get the enum for the corresponding offline_availability
if offline_availability:
offline_availability = \
self.get_offline_availability_enum(offline_availability)
if offline_availability is not None and \
offline_availability != smb_share_obj.offline_availability:
return True
smb_share_dict = smb_share_obj._get_properties()
params_list = ['is_abe_enabled', 'is_branch_cache_enabled',
'is_continuous_availability_enabled',
'is_encryption_enabled', 'description', 'umask']
for param in params_list:
if self.module.params[param] is not None and \
self.module.params[param] != smb_share_dict[param]:
return True
return False
def update_smb_share(self, smb_share_obj, is_encryption_enabled=None,
is_continuous_availability_enabled=None,
is_abe_enabled=None,
is_branch_cache_enabled=None,
umask=None, description=None,
offline_availability=None):
"""
The Details of the SMB share will be updated in the function.
"""
try:
smb_share_obj.modify(
is_encryption_enabled=is_encryption_enabled,
is_con_avail_enabled=is_continuous_availability_enabled,
is_abe_enabled=is_abe_enabled,
is_branch_cache_enabled=is_branch_cache_enabled,
umask=umask, description=description,
offline_availability=offline_availability)
except Exception as e:
error_msg = "Failed to Update parameters of SMB share" \
" %s with error %s" % (smb_share_obj.name, str(e))
LOG.error(error_msg)
self.module.fail_json(msg=error_msg)
def perform_module_operation(self):
"""
Perform different actions on SMB share based on user parameters
chosen in playbook
"""
state = self.module.params['state']
share_name = self.module.params['share_name']
filesystem_name = self.module.params['filesystem_name']
snapshot_name = self.module.params['snapshot_name']
nas_server_name = self.module.params['nas_server_name']
share_id = self.module.params['share_id']
filesystem_id = self.module.params['filesystem_id']
snapshot_id = self.module.params['snapshot_id']
nas_server_id = self.module.params['nas_server_id']
path = self.module.params['path']
description = self.module.params['description']
is_branch_cache_enabled = \
self.module.params['is_branch_cache_enabled']
is_continuous_availability_enabled = \
self.module.params['is_continuous_availability_enabled']
is_encryption_enabled = self.module.params['is_encryption_enabled']
is_abe_enabled = self.module.params['is_abe_enabled']
umask = self.module.params['umask']
offline_availability = self.module.params['offline_availability']
# Get the enum for the corresponding offline_availability
if offline_availability:
offline_availability = \
self.get_offline_availability_enum(offline_availability)
changed = False
'''
Validate parameters.
'''
if share_id is not None and \
(share_id == "" or len(share_id.split()) == 0):
self.module.fail_json(msg="Invalid share id provided."
" Please enter a valid share ID.")
'''
Get details of NAS Server, if entered.
'''
nas_server_obj = None
if nas_server_name or nas_server_id:
nas_server_obj = self.get_nas_server(nas_server_name,
nas_server_id)
if nas_server_obj:
msg = "NAS Server Object:" \
" {0}".format(nas_server_obj._get_properties())
LOG.info(msg)
else:
msg = "NAS Server Resource not fetched."
LOG.info(msg)
'''
Get details of Filesystem, if entered.
'''
filesystem_obj = None
if filesystem_id:
filesystem_obj = self.get_filesystem(filesystem_id)
if filesystem_name:
# nas_server_obj is required to uniquely identify filesystem
# resource. If neither nas_server_name nor nas_server_id
# is passed along with filesystem_name then error is thrown.
if not nas_server_obj:
self.module.fail_json(msg="nas_server_id/nas_server_name is "
"required when filesystem_name is "
"passed")
filesystem_obj = self.get_filesystem(
None, filesystem_name, nas_server_obj)
if filesystem_obj:
msg = "Filesystem Object:" \
" {0}".format(filesystem_obj._get_properties())
LOG.info(msg)
# Checking if filesystem supports SMB protocol or not.
if filesystem_obj and \
filesystem_obj.supported_protocols.name == "NFS":
self.module.fail_json(msg="Cannot perform SMB share operations "
"as file system supports only NFS "
"protocol. Please enter a valid "
"Filesystem having supported protocol"
" as SMB or Multiprotocol.")
'''
Get details of Snapshot, if entered.
'''
snapshot_obj = None
if snapshot_id or snapshot_name:
# Snapshot Name and Snapshot ID both are unique across array.
# Hence no need to mention nas server details
snapshot_obj = self.get_snapshot(snapshot_name, snapshot_id)
if snapshot_obj:
msg = "Snapshot Object:" \
" {0}".format(snapshot_obj._get_properties())
LOG.info(msg)
else:
msg = "Snapshot Resource not fetched."
LOG.info(msg)
'''
Get the Details of the SMB Share
'''
smb_share_obj = self.get_smb_share_obj(
share_id, share_name, filesystem_obj, snapshot_obj,
nas_server_obj)
if smb_share_obj:
msg = "SMB Share Object:" \
" {0}".format(smb_share_obj._get_properties())
LOG.info(msg)
elif state == 'present' and share_id:
msg = "Unable to fetch SMB Share Resource. " \
"Incorrect SMB share id provided. " \
"Please enter a correct share id."
LOG.error(msg)
self.module.fail_json(msg=msg)
'''
Creation of SMB Share
'''
if state == "present" and not smb_share_obj:
smb_share_obj = self.create_smb_share(
share_name, path, filesystem_obj, snapshot_obj, description,
is_abe_enabled, is_branch_cache_enabled,
is_continuous_availability_enabled, is_encryption_enabled,
offline_availability, umask)
changed = True
'''
Update the SMB share details
'''
if state == "present" and smb_share_obj:
LOG.info("Modify the details of the SMB share.")
update_flag = self.to_update(smb_share_obj)
msg = "Update Flag: {0}".format(str(update_flag))
LOG.info(msg)
if update_flag:
self.update_smb_share(smb_share_obj, is_encryption_enabled,
is_continuous_availability_enabled,
is_abe_enabled,
is_branch_cache_enabled,
umask, description,
offline_availability)
changed = True
'''
Delete the SMB share details
'''
if state == "absent" and smb_share_obj:
self.delete_smb_share(smb_share_obj)
changed = True
'''
Update the changed state and SMB share details
'''
self.result["changed"] = changed
smb_details = self.prepare_output_dict(state, share_id, share_name,
filesystem_obj, snapshot_obj,
nas_server_obj)
self.result["smb_share_details"] = smb_details
self.module.exit_json(**self.result)
def prepare_output_dict(self, state, share_id, share_name,
filesystem_obj, snapshot_obj, nas_server_obj):
smb_share_details = None
smb_share_obj = None
if state == 'present':
smb_share_obj = self.get_smb_share_obj(
share_id, share_name, filesystem_obj,
snapshot_obj, nas_server_obj)
smb_share_details = smb_share_obj._get_properties()
if smb_share_details:
# Get Snapshot NAME and ID if SMB share exists for Snapshot
if smb_share_obj.type.name == "CIFS_SNAPSHOT":
smb_share_details['snapshot_name'] = smb_share_obj.snap.name
smb_share_details['snapshot_id'] = smb_share_obj.snap.id
# Get Filesystem NAME and ID
smb_share_details['filesystem_name'] = \
smb_share_obj.filesystem.name
smb_share_details['filesystem_id'] = smb_share_obj.filesystem.id
# Get NAS server NAME and ID
smb_share_details['nas_server_name'] = \
smb_share_obj.filesystem.nas_server.name
smb_share_details['nas_server_id'] = \
smb_share_obj.filesystem.nas_server.id
return smb_share_details
def get_smb_share_parameters():
"""
This method provides parameters required for the ansible smb share
modules on Unity
"""
return dict(
share_name=dict(), share_id=dict(),
filesystem_name=dict(), filesystem_id=dict(),
snapshot_name=dict(), snapshot_id=dict(),
nas_server_name=dict(), nas_server_id=dict(),
path=dict(no_log=True), umask=dict(), description=dict(),
offline_availability=dict(
choices=["MANUAL", "DOCUMENTS", "PROGRAMS", "NONE"]),
is_abe_enabled=dict(type='bool'),
is_branch_cache_enabled=dict(type='bool'),
is_continuous_availability_enabled=dict(type='bool'),
is_encryption_enabled=dict(type='bool'),
state=dict(required=True, choices=['present', 'absent'], type='str')
)
def main():
""" Create Unity SMB share object and perform action on it
based on user input from playbook"""
obj = SMBShare()
obj.perform_module_operation()
if __name__ == '__main__':
main()

View File

@@ -0,0 +1,751 @@
#!/usr/bin/python
# Copyright: (c) 2020, Dell Technologies
# Apache License version 2.0 (see MODULE-LICENSE or http://www.apache.org/licenses/LICENSE-2.0.txt)
""" Ansible module for managing Snapshots on Unity"""
from __future__ import (absolute_import, division, print_function)
__metaclass__ = type
DOCUMENTATION = r'''
---
module: snapshot
short_description: Manage snapshots on the Unity storage system
description:
- Managing snapshots on the Unity storage system includes create snapshot,
delete snapshot, update snapshot, get snapshot, map host and unmap host.
version_added: '1.1.0'
extends_documentation_fragment:
- dellemc.unity.unity
author:
- P Srinivas Rao (@srinivas-rao5) <ansible.team@dell.com>
options:
snapshot_name:
description:
- The name of the snapshot.
- Mandatory parameter for creating a snapshot.
- For all other operations either I(snapshot_name) or I(snapshot_id) is
required.
type: str
vol_name:
description:
- The name of the volume for which snapshot is created.
- For creation of a snapshot either I(vol_name) or I(cg_name) is required.
- Not required for other operations.
type: str
cg_name:
description:
- The name of the Consistency Group for which snapshot is created.
- For creation of a snapshot either I(vol_name) or I(cg_name) is required.
- Not required for other operations.
type: str
snapshot_id:
description:
- The id of the snapshot.
- For all operations other than creation either I(snapshot_name) or
I(snapshot_id) is required.
type: str
auto_delete:
description:
- This option specifies whether the snapshot is auto deleted or not.
- If set to C(true), snapshot will expire based on the pool auto deletion
policy.
- If set to (false), snapshot will not be auto deleted
based on the pool auto deletion policy.
- Option I(auto_delete) can not be set to C(true), if I(expiry_time) is specified.
- If during creation neither I(auto_delete) nor I(expiry_time) is mentioned
then snapshot will be created keeping I(auto_delete) as C(true).
- Once the I(expiry_time) is set then snapshot cannot be assigned
to the auto delete policy.
type: bool
expiry_time:
description:
- This option is for specifying the date and time after which the
snapshot will expire.
- The time is to be mentioned in UTC timezone.
- The format is "MM/DD/YYYY HH:MM". Year must be in 4 digits.
type: str
description:
description:
- The additional information about the snapshot can be provided using
this option.
type: str
new_snapshot_name:
description:
- New name for the snapshot.
type: str
state:
description:
- The I(state) option is used to mention the existence of
the snapshot.
type: str
required: true
choices: [ 'absent', 'present' ]
host_name:
description:
- The name of the host.
- Either I(host_name) or I(host_id) is required to map or unmap a snapshot from
a host.
- Snapshot can be attached to multiple hosts.
type: str
host_id:
description:
- The id of the host.
- Either I(host_name) or I(host_id) is required to map or unmap a snapshot from
a host.
- Snapshot can be attached to multiple hosts.
type: str
host_state:
description:
- The I(host_state) option is used to mention the existence of the host
for snapshot.
- It is required when a snapshot is mapped or unmapped from host.
type: str
choices: ['mapped', 'unmapped']
notes:
- The I(check_mode) is not supported.
'''
EXAMPLES = r'''
- name: Create a Snapshot for a CG
dellemc.unity.snapshot:
unispherehost: "{{unispherehost}}"
username: "{{username}}"
password: "{{password}}"
validate_certs: "{{validate_certs}}"
port: "{{port}}"
cg_name: "{{cg_name}}"
snapshot_name: "{{cg_snapshot_name}}"
description: "{{description}}"
auto_delete: False
state: "present"
- name: Create a Snapshot for a volume with Host attached
dellemc.unity.snapshot:
unispherehost: "{{unispherehost}}"
username: "{{username}}"
password: "{{password}}"
validate_certs: "{{validate_certs}}"
port: "{{port}}"
vol_name: "{{vol_name}}"
snapshot_name: "{{vol_snapshot_name}}"
description: "{{description}}"
expiry_time: "04/15/2025 16:30"
host_name: "{{host_name}}"
host_state: "mapped"
state: "present"
- name: Unmap a host for a Snapshot
dellemc.unity.snapshot:
unispherehost: "{{unispherehost}}"
username: "{{username}}"
password: "{{password}}"
validate_certs: "{{validate_certs}}"
port: "{{port}}"
snapshot_name: "{{vol_snapshot_name}}"
host_name: "{{host_name}}"
host_state: "unmapped"
state: "present"
- name: Map snapshot to a host
dellemc.unity.snapshot:
unispherehost: "{{unispherehost}}"
username: "{{username}}"
password: "{{password}}"
validate_certs: "{{validate_certs}}"
port: "{{port}}"
snapshot_name: "{{vol_snapshot_name}}"
host_name: "{{host_name}}"
host_state: "mapped"
state: "present"
- name: Update attributes of a Snapshot for a volume
dellemc.unity.snapshot:
unispherehost: "{{unispherehost}}"
username: "{{username}}"
password: "{{password}}"
validate_certs: "{{validate_certs}}"
snapshot_name: "{{vol_snapshot_name}}"
new_snapshot_name: "{{new_snapshot_name}}"
description: "{{new_description}}"
host_name: "{{host_name}}"
host_state: "unmapped"
state: "present"
- name: Delete Snapshot of CG
dellemc.unity.snapshot:
unispherehost: "{{unispherehost}}"
username: "{{username}}"
password: "{{password}}"
validate_certs: "{{validate_certs}}"
snapshot_name: "{{cg_snapshot_name}}"
state: "absent"
'''
RETURN = r'''
changed:
description: Whether or not the resource has changed.
returned: always
type: bool
sample: True
snapshot_details:
description: Details of the snapshot.
returned: When snapshot exists
type: dict
contains:
is_auto_delete:
description: Additional information mentioned for snapshot.
type: str
expiration_time:
description: Date and time after which the snapshot
will expire.
type: str
hosts_list:
description: Contains the name and id of the associated
hosts.
type: dict
id:
description: Unique identifier of the snapshot instance.
type: str
name:
description: The name of the snapshot.
type: str
storage_resource_name:
description: Name of the storage resource for which the
snapshot exists.
type: str
storage_resource_id:
description: Id of the storage resource for which the snapshot
exists.
type: str
sample: {
"access_type": null,
"attached_wwn": null,
"creation_time": "2022-10-21 08:20:25.803000+00:00",
"creator_schedule": null,
"creator_type": "SnapCreatorTypeEnum.USER_CUSTOM",
"creator_user": {
"id": "user_admin"
},
"description": "Test snap creation",
"existed": true,
"expiration_time": null,
"hash": 8756689457056,
"hosts_list": [],
"id": "85899355291",
"io_limit_policy": null,
"is_auto_delete": true,
"is_modifiable": false,
"is_modified": false,
"is_read_only": true,
"is_system_snap": false,
"last_writable_time": null,
"lun": null,
"name": "ansible_snap_cg_1_1",
"parent_snap": null,
"size": null,
"snap_group": null,
"state": "SnapStateEnum.READY",
"storage_resource_id": "res_95",
"storage_resource_name": "CG_ansible_test_2_new"
}
'''
import logging
from ansible.module_utils.basic import AnsibleModule
from ansible_collections.dellemc.unity.plugins.module_utils.storage.dell \
import utils
from datetime import datetime
LOG = utils.get_logger('snapshot')
application_type = "Ansible/1.5.0"
class Snapshot(object):
"""Class with Snapshot operations"""
def __init__(self):
""" Define all parameters required by this module"""
self.module_params = utils.get_unity_management_host_parameters()
self.module_params.update(get_snapshot_parameters())
mutually_exclusive = [['snapshot_name', 'snapshot_id'],
['vol_name', 'cg_name'],
['host_name', 'host_id']]
required_one_of = [['snapshot_name', 'snapshot_id']]
# initialize the ansible module
self.module = AnsibleModule(argument_spec=self.module_params,
supports_check_mode=False,
mutually_exclusive=mutually_exclusive,
required_one_of=required_one_of)
utils.ensure_required_libs(self.module)
# result is a dictionary that contains changed status and
# snapshot details
self.result = {"changed": False,
'snapshot_details': {}}
self.unity_conn = utils.get_unity_unisphere_connection(
self.module.params, application_type)
self.snap_obj = utils.snap.UnitySnap(self.unity_conn)
LOG.info('Connection established with the Unity Array')
def validate_expiry_time(self, expiry_time):
"""Validates the specified expiry_time"""
try:
datetime.strptime(expiry_time, '%m/%d/%Y %H:%M')
except ValueError:
error_msg = "expiry_time not in MM/DD/YYYY HH:MM format"
LOG.error(error_msg)
self.module.fail_json(msg=error_msg)
def to_update(self, snapshot, new_name=None, description=None,
auto_del=None, expiry_time=None, host=None,
host_state=None):
"""Determines whether to update the snapshot or not"""
if expiry_time:
# If the snapshot has is_auto_delete True,
# Check if auto_delete in the input is either None or True
if snapshot.is_auto_delete and (auto_del is None or auto_del):
self.module.fail_json(msg="expiry_time can be assigned when"
" auto delete is False")
if auto_del and snapshot.expiration_time:
error_msg = "expiry_time for snapshot is set." \
" Once it is set then snapshot cannot" \
" be assigned to auto_delete policy"
self.module.fail_json(msg=error_msg)
if new_name and new_name != snapshot.name:
return True
if description and description != snapshot.description:
return True
if auto_del and auto_del != snapshot.is_auto_delete:
return True
if to_update_expiry_time(snapshot, expiry_time):
return True
if host and to_update_host_list(snapshot, host, host_state):
return True
return False
def update_snapshot(self, snapshot, new_name=None,
description=None, auto_del=None, expiry_time=None,
host_access_list=None):
try:
duration = None
if expiry_time:
duration = convert_timestamp_to_sec(
expiry_time, self.unity_conn.system_time)
if duration and duration <= 0:
self.module.fail_json(msg="expiry_time should be after"
" the current system time")
snapshot.modify(name=new_name, retentionDuration=duration,
isAutoDelete=auto_del, description=description,
hostAccess=host_access_list)
snapshot.update()
except Exception as e:
error_msg = "Failed to modify snapshot" \
" [name: %s , id: %s] with error %s"\
% (snapshot.name, snapshot.id, str(e))
LOG.error(error_msg)
self.module.fail_json(msg=error_msg)
def create_snapshot(self, snap_name, storage_id, description=None,
auto_del=None, expiry_time=None):
try:
duration = None
if expiry_time:
duration = convert_timestamp_to_sec(
expiry_time, self.unity_conn.system_time)
if duration <= 0:
self.module.fail_json(msg="expiry_time should be after"
" the current system time")
snapshot = self.snap_obj.create(
cli=self.unity_conn._cli, storage_resource=storage_id,
name=snap_name, description=description,
is_auto_delete=auto_del, retention_duration=duration)
return snapshot
except Exception as e:
error_msg = "Failed to create snapshot" \
" %s with error %s" % (snap_name, str(e))
LOG.error(error_msg)
self.module.fail_json(msg=error_msg)
def delete_snapshot(self, snapshot):
try:
if not bool(get_hosts_dict(snapshot)):
snapshot.detach_from(None)
snapshot.delete()
else:
snapshot.delete()
return None
except Exception as e:
error_msg = "Failed to delete snapshot" \
" [name: %s, id: %s] with error %s" \
% (snapshot.name, snapshot.id, str(e))
LOG.error(error_msg)
self.module.fail_json(msg=error_msg)
def get_snapshot_obj(self, name=None, id=None):
snapshot = id if id else name
msg = "Failed to get details of snapshot %s with error %s "
try:
return self.unity_conn.get_snap(name=name, _id=id)
except utils.HttpError as e:
if e.http_status == 401:
cred_err = "Incorrect username or password , {0}".format(
e.message)
self.module.fail_json(msg=cred_err)
else:
err_msg = msg % (snapshot, str(e))
LOG.error(err_msg)
self.module.fail_json(msg=err_msg)
except utils.UnityResourceNotFoundError as e:
err_msg = msg % (snapshot, str(e))
LOG.error(err_msg)
return None
except Exception as e:
err_msg = msg % (snapshot, str(e))
LOG.error(err_msg)
self.module.fail_json(msg=err_msg)
def get_volume_obj(self, name):
try:
return self.unity_conn.get_lun(name=name)
except Exception as e:
error_msg = "Failed to get volume %s with error %s"\
% (name, str(e))
LOG.error(error_msg)
self.module.fail_json(msg=error_msg)
def get_cg_obj(self, name):
try:
return self.unity_conn.get_cg(name=name)
except Exception as e:
error_msg = "Failed to get cg %s with error %s" % (name, str(e))
LOG.error(error_msg)
self.module.fail_json(msg=error_msg)
def get_host_obj(self, name=None, id=None):
""" Get the Host object"""
try:
return self.unity_conn.get_host(name=name, _id=id)
except Exception as e:
host = id if id else name
error_msg = "Failed to get host %s with error %s"\
% (host, str(e))
LOG.error(error_msg)
self.module.fail_json(msg=error_msg)
def attach_to_snap(self, snapshot, host):
""" Attach snapshot to a host """
try:
if not get_hosts_dict(snapshot):
snapshot.detach_from(None)
snapshot.attach_to(host)
snapshot.update()
except Exception as e:
error_msg = "Failed to attach snapshot [name: %s, id: %s]" \
" to host [%s, %s] with error %s"\
% (snapshot.name, snapshot.id,
host.name, host.id, str(e))
LOG.error(error_msg)
self.module.fail_json(msg=error_msg)
def perform_module_operation(self):
"""
Perform different actions on snapshot module based on parameters
chosen in playbook
"""
snapshot_name = self.module.params['snapshot_name']
snapshot_id = self.module.params['snapshot_id']
vol_name = self.module.params['vol_name']
cg_name = self.module.params['cg_name']
auto_delete = self.module.params['auto_delete']
expiry_time = self.module.params['expiry_time']
description = self.module.params['description']
new_snapshot_name = self.module.params['new_snapshot_name']
host_name = self.module.params['host_name']
host_id = self.module.params['host_id']
host_state = self.module.params['host_state']
state = self.module.params['state']
host = None
storage_resource = None
changed = False
LOG.info("Getting Snapshot details")
snapshot = self.get_snapshot_obj(name=snapshot_name, id=snapshot_id)
if snapshot and not snapshot.existed:
snapshot = None
msg = "snapshot details: %s" % str(snapshot)
LOG.info(msg)
# Get Volume Object
if vol_name is not None:
if vol_name == "" or vol_name.isspace():
self.module.fail_json(msg="Invalid vol_name given, Please"
" provide a valid vol_name")
storage_resource = self.get_volume_obj(name=vol_name)
# Get Consistency Group Object
if cg_name is not None:
if cg_name == "" or cg_name.isspace():
self.module.fail_json(msg="Invalid cg_name given, Please"
" provide a valid cg_name")
storage_resource = self.get_cg_obj(name=cg_name)
# Get host object for volume snapshots
if host_id or host_name:
if cg_name:
self.module.fail_json(msg="Mapping CG snapshot to host"
" is not supported.")
host = self.get_host_obj(name=host_name, id=host_id)
# Check whether host_name or host_id is given in input
# along with host_state
if (host and not host_state) or (not host and host_state):
self.module.fail_json(
msg="Either host_name or host_id along with host_state "
"is required to map or unmap a snapshot from a host")
# Check for error, if user tries to create a snapshot with the
# same name for other storage resource.
if snapshot and storage_resource and\
(snapshot.storage_resource.id != storage_resource.id):
self.module.fail_json(
msg="Snapshot %s is of %s storage resource. Cannot create new"
" snapshot with same name for %s storage resource"
% (snapshot.name, snapshot.storage_resource.name,
storage_resource.name))
# check for valid expiry_time
if expiry_time is not None and \
(expiry_time == "" or expiry_time.isspace()):
self.module.fail_json(msg="Please provide valid expiry_time,"
" empty expiry_time given")
# Check if in input auto_delete is True and expiry_time is not None
if expiry_time and auto_delete:
error_msg = "Cannot set expiry_time if auto_delete given as True"
LOG.info(error_msg)
self.module.fail_json(msg=error_msg)
# Check whether to modify the snapshot or not
update_flag = False
if snapshot:
update_flag = self.to_update(snapshot,
new_name=new_snapshot_name,
description=description,
auto_del=auto_delete,
expiry_time=expiry_time,
host=host, host_state=host_state)
msg = "update_flag for snapshot %s" % str(update_flag)
LOG.info(msg)
# Create a Snapshot
if not snapshot and state == "present":
LOG.info("Creating a snapshot")
if snapshot_id:
self.module.fail_json(msg="Creation of Snapshot is allowed"
" using snapshot_name only, "
"snapshot_id given")
if snapshot_name == "" or snapshot_name.isspace():
self.module.fail_json(msg="snapshot_name is required for"
" creation of a snapshot,"
" empty snapshot_name given")
if not storage_resource:
self.module.fail_json(msg="vol_name or cg_name required to"
" create a snapshot")
if new_snapshot_name:
self.module.fail_json(
msg="new_snapshot_name can not be assigned"
" during creation of a snapshot")
snapshot = self.create_snapshot(snapshot_name,
storage_resource.id,
description, auto_delete,
expiry_time)
if host and host_state == "mapped":
self.attach_to_snap(snapshot, host)
changed = True
# Update the Snapshot
if snapshot and state == "present" and update_flag:
LOG.info("Updating the Snapshot details")
if host_state == 'mapped':
self.attach_to_snap(snapshot, host)
self.update_snapshot(
snapshot, new_name=new_snapshot_name,
description=description, auto_del=auto_delete,
expiry_time=expiry_time)
elif host_state == 'unmapped':
host_access_list = create_host_access_list(snapshot,
host,
host_state)
self.update_snapshot(
snapshot, new_name=new_snapshot_name,
description=description, auto_del=auto_delete,
expiry_time=expiry_time,
host_access_list=host_access_list)
else:
self.update_snapshot(
snapshot, new_name=new_snapshot_name,
description=description, auto_del=auto_delete,
expiry_time=expiry_time)
changed = True
# Delete the Snapshot
if state == "absent" and snapshot:
snapshot = self.delete_snapshot(snapshot)
changed = True
# Add snapshot details to the result.
if snapshot:
snapshot.update()
self.result["snapshot_details"] = \
create_snapshot_details_dict(snapshot)
else:
self.result["snapshot_details"] = {}
self.result["changed"] = changed
self.module.exit_json(**self.result)
def create_snapshot_details_dict(snapshot):
""" Add name and id of storage resource and hosts to snapshot details """
snapshot_dict = snapshot._get_properties()
del snapshot_dict['storage_resource']
del snapshot_dict['host_access']
snapshot_dict['hosts_list'] = get_hosts_list(
get_hosts_dict(snapshot))
snapshot_dict['storage_resource_name'] = \
snapshot.storage_resource.name
snapshot_dict['storage_resource_id'] = \
snapshot.storage_resource.id
return snapshot_dict
def get_hosts_list(hosts_dict):
""" Get the host name and host id of all the associated hosts """
hosts_list = []
if not hosts_dict:
return hosts_list
for host in list(hosts_dict.keys()):
hosts_list.append(
{
"host_name": host.name,
"host_id": host.id
}
)
return hosts_list
def create_host_access_list(snapshot, host, host_state):
""" This method creates a List of dictionaries which will be used
to modify the list of hosts mapped to a snapshot """
host_access_list = []
hosts_dict = get_hosts_dict(snapshot)
# If snapshot is not attached to any host.
if not hosts_dict:
return None
if to_update_host_list(snapshot, host, host_state):
if host_state == "mapped":
return None
for snap_host in list(hosts_dict.keys()):
if snap_host != host:
access_dict = {'host': snap_host,
'allowedAccess': hosts_dict[snap_host]}
host_access_list.append(access_dict)
return host_access_list
def get_hosts_dict(snapshot):
""" This method creates a dictionary, with host as key and
allowed access as value """
hosts_dict = {}
LOG.info("Inside get_hosts_dict")
if not snapshot.host_access:
return hosts_dict
for host_access_obj in snapshot.host_access:
hosts_dict[host_access_obj.host] = \
host_access_obj.allowed_access
return hosts_dict
def to_update_host_list(snapshot, host, host_state):
""" Determines whether to update hosts list or not"""
hosts_dict = get_hosts_dict(snapshot)
if (not hosts_dict or host not in list(hosts_dict.keys()))\
and host_state == "mapped":
return True
if (hosts_dict and host in list(hosts_dict.keys())) \
and host_state == "unmapped":
return True
return False
def to_update_expiry_time(snapshot, expiry_time=None):
""" Check whether to update expiry_time or not"""
if not expiry_time:
return False
if snapshot.expiration_time is None:
return True
if convert_timestamp_to_sec(expiry_time, snapshot.expiration_time) != 0:
return True
return False
def convert_timestamp_to_sec(expiry_time, snap_time):
"""Converts the time difference to seconds"""
snap_time_str = snap_time.strftime('%m/%d/%Y %H:%M')
snap_timestamp = datetime.strptime(snap_time_str, '%m/%d/%Y %H:%M')
expiry_timestamp = datetime.strptime(expiry_time, "%m/%d/%Y %H:%M")
return int((expiry_timestamp - snap_timestamp).total_seconds())
def get_snapshot_parameters():
"""This method provide parameter required for the ansible snapshot
module on Unity"""
return dict(
snapshot_name=dict(required=False, type='str'),
snapshot_id=dict(required=False, type='str'),
vol_name=dict(required=False, type='str'),
cg_name=dict(required=False, type='str'),
auto_delete=dict(required=False, type='bool'),
expiry_time=dict(required=False, type='str'),
description=dict(required=False, type='str'),
new_snapshot_name=dict(required=False, type='str'),
host_name=dict(required=False, type='str'),
host_id=dict(required=False, type='str'),
host_state=dict(required=False, type='str',
choices=['mapped', 'unmapped']),
state=dict(required=True, type='str', choices=['present', 'absent'])
)
def main():
""" Create Unity Snapshot object and perform actions on it
based on user input from playbook"""
obj = Snapshot()
obj.perform_module_operation()
if __name__ == '__main__':
main()

View File

@@ -0,0 +1,879 @@
#!/usr/bin/python
# Copyright: (c) 2020, Dell Technologies
# Apache License version 2.0 (see MODULE-LICENSE or http://www.apache.org/licenses/LICENSE-2.0.txt)
"""Ansible module for managing storage pool on Unity"""
from __future__ import absolute_import, division, print_function
__metaclass__ = type
DOCUMENTATION = r'''
module: storagepool
version_added: '1.1.0'
short_description: Manage storage pool on Unity
description:
- Managing storage pool on Unity storage system contains the operations
Get details of storage pool,
Create a storage pool,
Modify storage pool.
extends_documentation_fragment:
- dellemc.unity.unity
author:
- Ambuj Dubey (@AmbujDube) <ansible.team@dell.com>
options:
pool_name:
description:
- Name of the storage pool, unique in the storage system.
type: str
pool_id:
description:
- Unique identifier of the pool instance.
type: str
new_pool_name:
description:
- New name of the storage pool, unique in the storage system.
type: str
pool_description:
description:
- The description of the storage pool.
type: str
fast_cache:
description:
- Indicates whether the fast cache is enabled for the storage pool.
- C(Enabled) - FAST Cache is enabled for the pool.
- C(Disabled) - FAST Cache is disabled for the pool.
choices: [enabled, disabled]
type: str
fast_vp:
description:
- Indicates whether to enable scheduled data relocations for the pool.
- C(Enabled) - Enabled scheduled data relocations for the pool.
- C(Disabled) - Disabled scheduled data relocations for the pool.
choices: [enabled, disabled]
type: str
raid_groups:
description:
- Parameters to create RAID group from the disks and add it to the pool.
type: dict
suboptions:
disk_group_id:
description:
- Id of the disk group.
type: str
disk_num:
description:
- Number of disks.
type: int
raid_type:
description:
- RAID group types or RAID levels.
choices: [None, RAID5, RAID0, RAID1, RAID3, RAID10, RAID6, Mixed, Automatic]
type: str
stripe_width :
description:
- RAID group stripe widths, including parity or mirror disks.
choices: ['BEST_FIT', '2', '4', '5', '6', '8', '9', '10', '12', '13', '14', '16']
type: str
alert_threshold:
description:
- Threshold at which the system will generate alerts about the free space in the pool, specified as a percentage.
- Minimum threshold limit is 50.
- Maximum threshold limit is 84.
type: int
is_harvest_enabled:
description:
- Enable/Disable automatic deletion of snapshots based on pool space usage.
type: bool
pool_harvest_high_threshold:
description:
- Max threshold for space used in pool beyond which the system automatically starts deleting snapshots in the pool.
- Applies when the automatic deletion of snapshots based on pool space usage is enabled for the system and pool.
- Minimum pool harvest high threshold value is 1.
- Maximum pool harvest high threshold value is 99.
type: float
pool_harvest_low_threshold:
description:
- Min threshold for space used in pool below which the system automatically stops deletion of snapshots in the pool.
- Applies when the automatic deletion of snapshots based on pool space usage is enabled for the system and pool.
- Minimum pool harvest low threshold value is 0.
- Maximum pool harvest low threshold value is 98.
type: float
is_snap_harvest_enabled:
description:
- Enable/Disable automatic deletion of snapshots based on pool space usage.
type: bool
snap_harvest_high_threshold:
description:
- Max threshold for space used in snapshot beyond which the system automatically starts deleting snapshots in the pool.
- Applies when the automatic deletion of snapshots based on pool space usage is enabled for the pool.
- Minimum snap harvest high threshold value is 1.
- Maximum snap harvest high threshold value is 99.
type: float
snap_harvest_low_threshold:
description:
- Min threshold for space used in snapshot below which the system will stop automatically deleting snapshots in the pool.
- Applies when the automatic deletion of snapshots based on pool space usage is enabled for the pool.
- Minimum snap harvest low threshold value is 0.
- Maximum snap harvest low threshold value is 98.
type: float
pool_type:
description:
- Indicates storage pool type.
choices: [TRADITIONAL, DYNAMIC]
type: str
state:
description:
- Define whether the storage pool should exist or not.
- C(Present) - indicates that the storage pool should exist on the system.
- C(Absent) - indicates that the storage pool should not exist on the system.
choices: [absent, present]
type: str
required: true
notes:
- Deletion of storage pool is not allowed through Ansible module.
- The I(check_mode) is not supported.
'''
EXAMPLES = r'''
- name: Get Storage pool details using pool_name
dellemc.unity.storagepool:
unispherehost: "{{unispherehost}}"
username: "{{username}}"
password: "{{password}}"
validate_certs: "{{validate_certs}}"
pool_name: "{{pool_name}}"
state: "present"
- name: Get Storage pool details using pool_id
dellemc.unity.storagepool:
unispherehost: "{{unispherehost}}"
username: "{{username}}"
password: "{{password}}"
validate_certs: "{{validate_certs}}"
pool_id: "{{pool_id}}"
state: "present"
- name: Modify Storage pool attributes using pool_name
dellemc.unity.storagepool:
unispherehost: "{{unispherehost}}"
username: "{{username}}"
password: "{{password}}"
validate_certs: "{{validate_certs}}"
pool_name: "{{pool_name}}"
new_pool_name: "{{new_pool_name}}"
pool_description: "{{pool_description}}"
fast_cache: "{{fast_cache_enabled}}"
fast_vp: "{{fast_vp_enabled}}"
state: "present"
- name: Modify Storage pool attributes using pool_id
dellemc.unity.storagepool:
unispherehost: "{{unispherehost}}"
username: "{{username}}"
password: "{{password}}"
validate_certs: "{{validate_certs}}"
pool_id: "{{pool_id}}"
new_pool_name: "{{new_pool_name}}"
pool_description: "{{pool_description}}"
fast_cache: "{{fast_cache_enabled}}"
fast_vp: "{{fast_vp_enabled}}"
state: "present"
- name: Create a StoragePool
dellemc.unity.storagepool:
unispherehost: "{{unispherehost}}"
username: "{{username}}"
password: "{{password}}"
validate_certs: "{{validate_certs}}"
pool_name: "Test"
pool_description: "test pool"
raid_groups:
disk_group_id : "dg_16"
disk_num : 2
raid_type : "RAID10"
stripe_width : "BEST_FIT"
alert_threshold : 50
is_harvest_enabled : True
pool_harvest_high_threshold : 60
pool_harvest_low_threshold : 40
is_snap_harvest_enabled : True
snap_harvest_high_threshold : 70
snap_harvest_low_threshold : 50
fast_vp: "enabled"
fast_cache: "enabled"
pool_type : "DYNAMIC"
state: "present"
'''
RETURN = r'''
changed:
description: Whether or not the storage pool has changed.
returned: always
type: bool
sample: True
storage_pool_details:
description: The storage pool details.
returned: When storage pool exists.
type: dict
contains:
id:
description: Pool id, unique identifier of the pool.
type: str
name:
description: Pool name, unique in the storage system.
type: str
is_fast_cache_enabled:
description: Indicates whether the fast cache is enabled for the storage
pool.
true - FAST Cache is enabled for the pool.
false - FAST Cache is disabled for the pool.
type: bool
is_fast_vp_enabled:
description: Indicates whether to enable scheduled data relocations
for the storage pool.
true - Enabled scheduled data relocations for the pool.
false - Disabled scheduled data relocations for the pool.
type: bool
size_free_with_unit:
description: Indicates size_free with its appropriate unit
in human readable form.
type: str
size_subscribed_with_unit:
description: Indicates size_subscribed with its appropriate unit in
human readable form.
type: str
size_total_with_unit:
description: Indicates size_total with its appropriate unit in human
readable form.
type: str
size_used_with_unit:
description: Indicates size_used with its appropriate unit in human
readable form.
type: str
snap_size_subscribed_with_unit:
description: Indicates snap_size_subscribed with its
appropriate unit in human readable form.
type: str
snap_size_used_with_unit:
description: Indicates snap_size_used with its
appropriate unit in human readable form.
type: str
drives:
description: Indicates information about the drives
associated with the storage pool.
type: list
contains:
id:
description: Unique identifier of the drive.
type: str
name:
description: Indicates name of the drive.
type: str
size:
description: Indicates size of the drive.
type: str
disk_technology:
description: Indicates disk technology of the drive.
type: str
tier_type:
description: Indicates tier type of the drive.
type: str
sample: {
"alert_threshold": 50,
"creation_time": "2022-03-08 14:05:32+00:00",
"description": "",
"drives": [
{
"disk_technology": "SAS",
"id": "dpe_disk_22",
"name": "DPE Drive 22",
"size": 590860984320,
"tier_type": "PERFORMANCE"
},
{
"disk_technology": "SAS",
"id": "dpe_disk_23",
"name": "DPE Drive 23",
"size": 590860984320,
"tier_type": "PERFORMANCE"
},
{
"disk_technology": "SAS",
"id": "dpe_disk_24",
"name": "DPE Drive 24",
"size": 590860984320,
"tier_type": "PERFORMANCE"
}
],
"existed": true,
"harvest_state": "UsageHarvestStateEnum.IDLE",
"hash": 8744642897210,
"health": {
"UnityHealth": {
"hash": 8744642799842
}
},
"id": "pool_280",
"is_all_flash": false,
"is_empty": false,
"is_fast_cache_enabled": false,
"is_fast_vp_enabled": false,
"is_harvest_enabled": true,
"is_snap_harvest_enabled": true,
"metadata_size_subscribed": 105763569664,
"metadata_size_used": 57176752128,
"name": "test_pool",
"object_id": 12884902146,
"pool_fast_vp": {
"UnityPoolFastVp": {
"hash": 8744647518980
}
},
"pool_space_harvest_high_threshold": 59.0,
"pool_space_harvest_low_threshold": 40.0,
"pool_type": "StoragePoolTypeEnum.DYNAMIC",
"raid_type": "RaidTypeEnum.RAID10",
"rebalance_progress": null,
"size_free": 470030483456,
"size_free_with_unit": "437.75 GB",
"size_subscribed": 447215820800,
"size_subscribed_with_unit": "416.5 GB",
"size_total": 574720311296,
"size_total_with_unit": "535.25 GB",
"size_used": 76838068224,
"size_used_with_unit": "71.56 GB",
"snap_size_subscribed": 128851369984,
"snap_size_subscribed_with_unit": "120.0 GB",
"snap_size_used": 2351104,
"snap_size_used_with_unit": "2.24 MB",
"snap_space_harvest_high_threshold": 80.0,
"snap_space_harvest_low_threshold": 60.0,
"tiers": {
"UnityPoolTierList": [
{
"disk_count": [
0,
3,
0
],
"existed": true,
"hash": 8744643017382,
"name": [
"Extreme Performance",
"Performance",
"Capacity"
],
"pool_units": [
null,
{
"UnityPoolUnitList": [
{
"UnityPoolUnit": {
"hash": 8744642786759,
"id": "rg_4"
}
},
{
"UnityPoolUnit": {
"hash": 8744642786795,
"id": "rg_5"
}
}
]
},
null
],
"raid_type": [
"RaidTypeEnum.NONE",
"RaidTypeEnum.RAID10",
"RaidTypeEnum.NONE"
],
"size_free": [
0,
470030483456,
0
],
"size_moving_down": [
0,
0,
0
],
"size_moving_up": [
0,
0,
0
],
"size_moving_within": [
0,
0,
0
],
"size_total": [
0,
574720311296,
0
],
"size_used": [
0,
104689827840,
0
],
"stripe_width": [
null,
"RaidStripeWidthEnum._2",
null
],
"tier_type": [
"TierTypeEnum.EXTREME_PERFORMANCE",
"TierTypeEnum.PERFORMANCE",
"TierTypeEnum.CAPACITY"
]
}
]
}
}
'''
from ansible.module_utils.basic import AnsibleModule
from ansible_collections.dellemc.unity.plugins.module_utils.storage.dell \
import utils
import logging
LOG = utils.get_logger('storagepool')
application_type = "Ansible/1.5.0"
class StoragePool(object):
"""Class with storage pool operations"""
def __init__(self):
""" Define all parameters required by this module"""
self.module_params = utils.get_unity_management_host_parameters()
self.module_params.update(get_storagepool_parameters())
mutually_exclusive = [['pool_name', 'pool_id']]
required_one_of = [['pool_name', 'pool_id']]
# initialize the Ansible module
self.module = AnsibleModule(argument_spec=self.module_params,
supports_check_mode=False,
mutually_exclusive=mutually_exclusive,
required_one_of=required_one_of)
utils.ensure_required_libs(self.module)
self.conn = utils.\
get_unity_unisphere_connection(self.module.params, application_type)
def get_details(self, pool_id=None, pool_name=None):
""" Get storage pool details"""
try:
api_response = self.conn.get_pool(_id=pool_id, name=pool_name)
details = api_response._get_properties()
is_fast_vp_enabled = api_response._get_property_from_raw(
'pool_fast_vp').is_schedule_enabled
details['is_fast_vp_enabled'] = is_fast_vp_enabled
details['size_free_with_unit'] = utils.\
convert_size_with_unit(int(details['size_free']))
details['size_subscribed_with_unit'] = utils.\
convert_size_with_unit(int(details['size_subscribed']))
details['size_total_with_unit'] = utils.\
convert_size_with_unit(int(details['size_total']))
details['size_used_with_unit'] = utils.\
convert_size_with_unit(int(details['size_used']))
details['snap_size_subscribed_with_unit'] = utils.\
convert_size_with_unit(int(details['snap_size_subscribed']))
details['snap_size_used_with_unit'] = utils.\
convert_size_with_unit(int(details['snap_size_used']))
pool_instance = utils.UnityPool.get(self.conn._cli, details['id'])
pool_tier_list = []
pool_tier_list.append((pool_instance.tiers)._get_properties())
pool_tier_dict = {}
pool_tier_dict['UnityPoolTierList'] = pool_tier_list
details['tiers'] = pool_tier_dict
return details
except Exception as e:
error = str(e)
check_list = ['not found', 'no attribute']
if any(ele in error for ele in check_list):
error_message = "pool details are not found"
LOG.info(error_message)
return None
error_message = 'Get details of storage pool failed with ' \
'error: {0}'.format(str(e))
LOG.error(error_message)
self.module.fail_json(msg=error_message)
def is_pool_modification_required(self, storage_pool_details):
""" Check if attributes of storage pool needs to be modified
"""
try:
if self.module.params['new_pool_name'] and \
self.module.params['new_pool_name'] != \
storage_pool_details['name']:
return True
if self.module.params['pool_description'] is not None and \
self.module.params['pool_description'] != \
storage_pool_details['description']:
return True
if self.module.params['fast_cache']:
if (self.module.params['fast_cache'] == "enabled" and
not storage_pool_details['is_fast_cache_enabled']) or\
(self.module.params['fast_cache'] == "disabled" and storage_pool_details['is_fast_cache_enabled']):
return True
if self.module.params['fast_vp']:
if (self.module.params['fast_vp'] == "enabled" and
not storage_pool_details['is_fast_vp_enabled']) or \
(self.module.params['fast_vp'] == "disabled" and
storage_pool_details['is_fast_vp_enabled']):
return True
LOG.info("modify not required")
return False
except Exception as e:
error_message = 'Failed to determine if any modification'\
'required for pool attributes with error: {0}'.format(str(e))
LOG.error(error_message)
self.module.fail_json(msg=error_message)
def pool_modify(self, id, new_pool_name,
pool_description, fast_cache, fast_vp):
""" Modify attributes of storage pool """
pool_obj = utils.UnityPool.get(self.conn._cli, id)
try:
pool_obj.modify(name=new_pool_name, description=pool_description,
is_fast_cache_enabled=fast_cache,
is_fastvp_enabled=fast_vp)
new_storage_pool_details = self.get_details(pool_id=id,
pool_name=None)
LOG.info("Modification Successful")
return new_storage_pool_details
except Exception as e:
if self.module.params['pool_id']:
pool_identifier = self.module.params['pool_id']
else:
pool_identifier = self.module.params['pool_name']
error_message = 'Modify attributes of storage pool {0} ' \
'failed with error: {1}'.format(pool_identifier, str(e))
LOG.error(error_message)
self.module.fail_json(msg=error_message)
def get_pool_drives(self, pool_id=None, pool_name=None):
""" Get pool drives attached to pool"""
pool_identifier = pool_id or pool_name
pool_drives_list = []
try:
drive_instances = utils.UnityDiskList.get(self.conn._cli)
if drive_instances:
for drive in drive_instances:
if drive.pool and (drive.pool.id == pool_identifier or drive.pool.name == pool_identifier):
pool_drive = {"id": drive.id, "name": drive.name, "size": drive.size,
"disk_technology": drive.disk_technology.name,
"tier_type": drive.tier_type.name}
pool_drives_list.append(pool_drive)
LOG.info("Successfully retrieved pool drive details")
return pool_drives_list
except Exception as e:
error_message = 'Get details of pool drives failed with ' \
'error: {0}'.format(str(e))
LOG.error(error_message)
self.module.fail_json(msg=error_message)
def get_raid_type_enum(self, raid_type):
""" Get raid_type_enum.
:param raid_type: The raid_type
:return: raid_type enum
"""
if raid_type in utils.RaidTypeEnum.__members__:
return utils.RaidTypeEnum[raid_type]
else:
errormsg = "Invalid choice %s for Raid Type" % raid_type
LOG.error(errormsg)
self.module.fail_json(msg=errormsg)
def get_raid_stripe_width_enum(self, stripe_width):
""" Get raid_stripe_width enum.
:param stripe_width: The raid_stripe_width
:return: raid_stripe_width enum
"""
if stripe_width != "BEST_FIT":
stripe_width = "_" + stripe_width
if stripe_width in utils.RaidStripeWidthEnum.__members__:
return utils.RaidStripeWidthEnum[stripe_width]
else:
errormsg = "Invalid choice %s for stripe width" % stripe_width
LOG.error(errormsg)
self.module.fail_json(msg=errormsg)
def get_pool_type_enum(self, pool_type):
""" Get the storage pool_type enum.
:param pool_type: The pool_type
:return: pool_type enum
"""
if pool_type == "TRADITIONAL":
return 1
elif pool_type == "DYNAMIC":
return 2
else:
errormsg = "Invalid choice %s for Storage Pool Type" % pool_type
LOG.error(errormsg)
self.module.fail_json(msg=errormsg)
def get_raid_groups(self, raid_groups):
""" Get the raid groups for creating pool"""
try:
disk_obj = utils.UnityDiskGroup.get(self.conn._cli, _id=raid_groups['disk_group_id'])
disk_num = raid_groups['disk_num']
raid_type = raid_groups['raid_type']
raid_type = self.get_raid_type_enum(raid_type) \
if raid_type else None
stripe_width = raid_groups['stripe_width']
stripe_width = self.get_raid_stripe_width_enum(stripe_width) \
if stripe_width else None
raid_group = utils.RaidGroupParameter(disk_group=disk_obj,
disk_num=disk_num, raid_type=raid_type,
stripe_width=stripe_width)
raid_groups = [raid_group]
return raid_groups
except Exception as e:
error_message = 'Failed to create storage pool with error: %s' % str(e)
LOG.error(error_message)
self.module.fail_json(msg=error_message)
def validate_create_pool_params(self, alert_threshold=None,
pool_harvest_high_threshold=None,
pool_harvest_low_threshold=None,
snap_harvest_high_threshold=None,
snap_harvest_low_threshold=None):
""" Validates params for creating pool"""
if alert_threshold and (alert_threshold < 50 or alert_threshold > 84):
errormsg = "Alert threshold is not in the allowed value range of 50 - 84"
LOG.error(errormsg)
self.module.fail_json(msg=errormsg)
if pool_harvest_high_threshold and (pool_harvest_high_threshold < 1 or pool_harvest_high_threshold > 99):
errormsg = "Pool harvest high threshold is not in the allowed value range of 1 - 99"
LOG.error(errormsg)
self.module.fail_json(msg=errormsg)
if pool_harvest_low_threshold and (pool_harvest_low_threshold < 0 or pool_harvest_low_threshold > 98):
errormsg = "Pool harvest low threshold is not in the allowed value range of 0 - 98"
LOG.error(errormsg)
self.module.fail_json(msg=errormsg)
if snap_harvest_high_threshold and (snap_harvest_high_threshold < 1 or snap_harvest_high_threshold > 99):
errormsg = "Snap harvest high threshold is not in the allowed value range of 1 - 99"
LOG.error(errormsg)
self.module.fail_json(msg=errormsg)
if snap_harvest_low_threshold and (snap_harvest_low_threshold < 0 or snap_harvest_low_threshold > 98):
errormsg = "Snap harvest low threshold is not in the allowed value range of 0 - 98"
LOG.error(errormsg)
self.module.fail_json(msg=errormsg)
def create_pool(self, name, raid_groups):
""" Creates a StoragePool"""
try:
pool_obj = utils.UnityPool.get(self.conn._cli)
pool_description = self.module.params['pool_description']
raid_groups = self.get_raid_groups(raid_groups) \
if raid_groups else None
alert_threshold = self.module.params['alert_threshold']
pool_harvest_high_threshold = None
pool_harvest_low_threshold = None
snap_harvest_high_threshold = None
snap_harvest_low_threshold = None
is_harvest_enabled = self.module.params['is_harvest_enabled']
if is_harvest_enabled:
pool_harvest_high_threshold = self.module.params['pool_harvest_high_threshold']
pool_harvest_low_threshold = self.module.params['pool_harvest_low_threshold']
is_snap_harvest_enabled = self.module.params['is_snap_harvest_enabled']
if is_snap_harvest_enabled:
snap_harvest_high_threshold = self.module.params['snap_harvest_high_threshold']
snap_harvest_low_threshold = self.module.params['snap_harvest_low_threshold']
self.validate_create_pool_params(alert_threshold=alert_threshold,
pool_harvest_high_threshold=pool_harvest_high_threshold,
pool_harvest_low_threshold=pool_harvest_low_threshold,
snap_harvest_high_threshold=snap_harvest_high_threshold,
snap_harvest_low_threshold=snap_harvest_low_threshold)
pool_type = self.module.params['pool_type']
pool_type = self.get_pool_type_enum(pool_type) \
if pool_type else None
fast_vp = self.module.params['fast_vp']
if fast_vp:
if fast_vp == "enabled":
fast_vp = True
else:
fast_vp = False
pool_obj.create(self.conn._cli, name=name, description=pool_description, raid_groups=raid_groups,
alert_threshold=alert_threshold,
is_harvest_enabled=is_harvest_enabled,
is_snap_harvest_enabled=is_snap_harvest_enabled,
pool_harvest_high_threshold=pool_harvest_high_threshold,
pool_harvest_low_threshold=pool_harvest_low_threshold,
snap_harvest_high_threshold=snap_harvest_high_threshold,
snap_harvest_low_threshold=snap_harvest_low_threshold,
is_fastvp_enabled=fast_vp,
pool_type=pool_type)
LOG.info("Creation of storage pool successful")
storage_pool_details = self.get_details(pool_name=name)
changed = True
return changed, storage_pool_details
except Exception as e:
error_message = 'Failed to create storage pool with error: %s' % str(e)
LOG.error(error_message)
self.module.fail_json(msg=error_message)
def perform_module_operation(self):
"""
Perform different actions on storage pool module based on parameters
chosen in playbook
"""
pool_name = self.module.params['pool_name']
pool_id = self.module.params['pool_id']
new_pool_name = self.module.params['new_pool_name']
pool_description = self.module.params['pool_description']
fast_cache = self.module.params['fast_cache']
fast_vp = self.module.params['fast_vp']
state = self.module.params['state']
raid_groups = self.module.params['raid_groups']
if fast_cache:
if fast_cache == "enabled":
fast_cache = True
else:
fast_cache = False
if fast_vp:
if fast_vp == "enabled":
fast_vp = True
else:
fast_vp = False
# result is a dictionary that contains changed status and storage pool details
result = dict(
changed=False,
storage_pool_details={}
)
storage_pool_details = self.get_details(pool_id, pool_name)
result['storage_pool_details'] = storage_pool_details
if state == 'absent' and storage_pool_details:
error_message = 'Deletion of storage pool is not allowed through'\
' Ansible module'
LOG.error(error_message)
self.module.fail_json(msg=error_message)
# Create storage pool
if state == 'present' and not storage_pool_details:
if pool_name is not None and len(pool_name) != 0:
result['changed'], storage_pool_details \
= self.create_pool(name=pool_name, raid_groups=raid_groups)
result['storage_pool_details'] = storage_pool_details
else:
error_message = 'The parameter pool_name length is 0. It'\
' is too short. The min length is 1'
LOG.error(error_message)
self.module.fail_json(msg=error_message)
# Get pool drive details
if result['storage_pool_details']:
result['storage_pool_details']['drives'] = self.get_pool_drives(pool_id=pool_id, pool_name=pool_name)
if state == 'present' and storage_pool_details:
if new_pool_name is not None and len(new_pool_name) == 0:
error_message = 'The parameter new_pool_name length is 0. It'\
' is too short. The min length is 1'
LOG.error(error_message)
self.module.fail_json(msg=error_message)
pool_modify_flag = self.\
is_pool_modification_required(storage_pool_details)
LOG.info("Storage pool modification flag %s",
str(pool_modify_flag))
if pool_modify_flag:
result['storage_pool_details'] = \
self.pool_modify(storage_pool_details['id'], new_pool_name,
pool_description, fast_cache, fast_vp)
result['changed'] = True
self.module.exit_json(**result)
def get_storagepool_parameters():
"""This method provides parameters required for the ansible storage pool
module on Unity"""
return dict(
pool_name=dict(required=False, type='str'),
pool_id=dict(required=False, type='str'),
new_pool_name=dict(required=False, type='str'),
pool_description=dict(required=False, type='str'),
fast_cache=dict(required=False, type='str', choices=['enabled',
'disabled']),
fast_vp=dict(required=False, type='str', choices=['enabled',
'disabled']),
state=dict(required=True, type='str', choices=['present', 'absent']),
raid_groups=dict(required=False, type='dict', options=dict(
disk_group_id=dict(required=False, type='str'),
disk_num=dict(required=False, type='int'),
raid_type=dict(required=False, type='str', choices=['None', 'RAID5', 'RAID0', 'RAID1', 'RAID3', 'RAID10',
'RAID6', 'Mixed', 'Automatic']),
stripe_width=dict(required=False, type='str', choices=['BEST_FIT', '2', '4', '5',
'6', '8', '9', '10', '12', '13', '14', '16']))),
alert_threshold=dict(required=False, type='int'),
is_harvest_enabled=dict(required=False, type='bool'),
pool_harvest_high_threshold=dict(required=False, type='float'),
pool_harvest_low_threshold=dict(required=False, type='float'),
is_snap_harvest_enabled=dict(required=False, type='bool'),
snap_harvest_high_threshold=dict(required=False, type='float'),
snap_harvest_low_threshold=dict(required=False, type='float'),
pool_type=dict(required=False, type='str', choices=['TRADITIONAL', 'DYNAMIC'])
)
def main():
""" Create Unity storage pool object and perform action on it
based on user input from playbook"""
obj = StoragePool()
obj.perform_module_operation()
if __name__ == '__main__':
main()

View File

@@ -0,0 +1,708 @@
#!/usr/bin/python
# Copyright: (c) 2021, Dell Technologies
# Apache License version 2.0 (see MODULE-LICENSE or http://www.apache.org/licenses/LICENSE-2.0.txt)
"""Ansible module for managing quota tree on Unity"""
from __future__ import absolute_import, division, print_function
__metaclass__ = type
DOCUMENTATION = r'''
---
module: tree_quota
short_description: Manage quota tree on the Unity storage system
description:
- Managing Quota tree on the Unity storage system includes
Create quota tree,
Get quota tree,
Modify quota tree and
Delete quota tree.
version_added: '1.2.0'
extends_documentation_fragment:
- dellemc.unity.unity
author:
- Spandita Panigrahi (@panigs7) <ansible.team@dell.com>
options:
filesystem_name:
description:
- The name of the filesystem for which quota tree is created.
- For creation or modification of a quota tree either I(filesystem_name) or
I(filesystem_id) is required.
type: str
filesystem_id:
description:
- The ID of the filesystem for which the quota tree is created.
- For creation of a quota tree either I(filesystem_id) or
I(filesystem_name) is required.
type: str
nas_server_name:
description:
- The name of the NAS server in which the filesystem is created.
- For creation of a quota tree either I(nas_server_name) or
I(nas_server_id) is required.
type: str
nas_server_id:
description:
- The ID of the NAS server in which the filesystem is created.
- For creation of a quota tree either I(filesystem_id) or
I(filesystem_name) is required.
type: str
tree_quota_id:
description:
- The ID of the quota tree.
- Either I(tree_quota_id) or I(path) to quota tree is required to
view/modify/delete quota tree.
type: str
path:
description:
- The path to the quota tree.
- Either I(tree_quota_id) or I(path) to quota tree is required to
create/view/modify/delete a quota tree.
- Path must start with a forward slash '/'.
type: str
hard_limit:
description:
- Hard limitation for a quota tree on the total space available. If exceeded,
users in quota tree cannot write data.
- Value C(0) implies no limit.
- One of the values of I(soft_limit) and I(hard_limit) can be C(0), however, both cannot be both C(0)
during creation of a quota tree.
type: int
soft_limit:
description:
- Soft limitation for a quota tree on the total space available. If exceeded,
notification will be sent to users in the quota tree for the grace period mentioned, beyond
which users cannot use space.
- Value C(0) implies no limit.
- Both I(soft_limit) and I(hard_limit) cannot be C(0) during creation of quota tree.
type: int
cap_unit:
description:
- Unit of I(soft_limit) and I(hard_limit) size.
- It defaults to C(GB) if not specified.
choices: ['MB', 'GB', 'TB']
type: str
description:
description:
- Description of a quota tree.
type: str
state:
description:
- The state option is used to mention the existence of the filesystem
quota tree.
type: str
required: true
choices: ['absent', 'present']
notes:
- The I(check_mode) is not supported.
'''
EXAMPLES = r'''
- name: Get quota tree details by quota tree id
dellemc.unity.tree_quota:
unispherehost: "{{unispherehost}}"
username: "{{username}}"
password: "{{password}}"
validate_certs: "{{validate_certs}}"
tree_quota_id: "treequota_171798700679_10"
state: "present"
- name: Get quota tree details by quota tree path
dellemc.unity.tree_quota:
unispherehost: "{{unispherehost}}"
username: "{{username}}"
password: "{{password}}"
validate_certs: "{{validate_certs}}"
filesystem_name: "fs_2171"
nas_server_id: "nas_21"
path: "/test"
state: "present"
- name: Create quota tree for a filesystem with filesystem id
dellemc.unity.tree_quota:
unispherehost: "{{unispherehost}}"
username: "{{username}}"
password: "{{password}}"
validate_certs: "{{validate_certs}}"
filesystem_id: "fs_2171"
hard_limit: 6
cap_unit: "TB"
soft_limit: 5
path: "/test_new"
state: "present"
- name: Create quota tree for a filesystem with filesystem name
dellemc.unity.tree_quota:
unispherehost: "{{unispherehost}}"
username: "{{username}}"
password: "{{password}}"
validate_certs: "{{validate_certs}}"
filesystem_name: "Test_filesystem"
nas_server_name: "lglad068"
hard_limit: 6
cap_unit: "TB"
soft_limit: 5
path: "/test_new"
state: "present"
- name: Modify quota tree limit usage by quota tree path
dellemc.unity.tree_quota:
unispherehost: "{{unispherehost}}"
username: "{{username}}"
password: "{{password}}"
validate_certs: "{{validate_certs}}"
path: "/test_new"
hard_limit: 10
cap_unit: "TB"
soft_limit: 8
state: "present"
- name: Modify quota tree by quota tree id
dellemc.unity.tree_quota:
unispherehost: "{{unispherehost}}"
username: "{{username}}"
password: "{{password}}"
validate_certs: "{{validate_certs}}"
filesystem_id: "fs_2171"
tree_quota_id: "treequota_171798700679_10"
hard_limit: 12
cap_unit: "TB"
soft_limit: 10
state: "present"
- name: Delete quota tree by quota tree id
dellemc.unity.tree_quota:
unispherehost: "{{unispherehost}}"
username: "{{username}}"
password: "{{password}}"
validate_certs: "{{validate_certs}}"
filesystem_id: "fs_2171"
tree_quota_id: "treequota_171798700679_10"
state: "absent"
- name: Delete quota tree by path
dellemc.unity.tree_quota:
unispherehost: "{{unispherehost}}"
username: "{{username}}"
password: "{{password}}"
validate_certs: "{{validate_certs}}"
filesystem_id: "fs_2171"
path: "/test_new"
state: "absent"
'''
RETURN = r'''
changed:
description: Whether or not the resource has changed.
returned: always
type: bool
sample: True
get_tree_quota_details:
description: Details of the quota tree.
returned: When quota tree exists
type: dict
contains:
filesystem:
description: Filesystem details for which the quota
tree is created.
type: dict
contains:
UnityFileSystem:
description: Filesystem details for which the
quota tree is created.
type: dict
contains:
id:
description: ID of the filesystem for
which the quota tree is create.
type: str
description:
description: Description of the quota tree.
type: str
path:
description: Path to quota tree.
A valid path must start with a forward slash '/'.
It is mandatory while creating a quota tree.
type: str
hard_limit:
description: Hard limit of quota tree.
If the quota tree's space usage exceeds
the hard limit, users in quota tree cannot write data.
type: int
soft_limit:
description: Soft limit of the quota tree.
If the quota tree's space usage exceeds the soft limit,
the storage system starts to count down based
on the specified grace period.
type: int
id:
description: Quota tree ID.
type: str
size_used:
description: Size of used space in the filesystem by the user files.
type: int
gp_left:
description: The grace period left after the
soft limit for the user quota is exceeded.
type: int
state:
description: State of the quota tree.
type: int
sample: {
"description": "",
"existed": true,
"filesystem": {
"UnityFileSystem": {
"hash": 8788549469862,
"id": "fs_137",
"name": "test",
"nas_server": {
"id": "nas_1",
"name": "lglad072"
}
}
},
"gp_left": null,
"hard_limit": "6.0 TB",
"hash": 8788549497558,
"id": "treequota_171798694897_1",
"path": "VALUE_SPECIFIED_IN_NO_LOG_PARAMETER",
"size_used": 0,
"soft_limit": "5.0 TB",
"state": 0
}
'''
from ansible.module_utils.basic import AnsibleModule
from ansible_collections.dellemc.unity.plugins.module_utils.storage.dell \
import utils
LOG = utils.get_logger('tree_quota')
application_type = "Ansible/1.5.0"
class QuotaTree(object):
"""Class with Quota Tree operations"""
def __init__(self):
"""Define all parameters required by this module"""
self.module_params = utils.get_unity_management_host_parameters()
self.module_params.update(get_quota_tree_parameters())
mutually_exclusive = [['filesystem_name', 'filesystem_id'],
['nas_server_name', 'nas_server_id']]
# initialize the Ansible module
self.module = AnsibleModule(
argument_spec=self.module_params,
supports_check_mode=False,
mutually_exclusive=mutually_exclusive)
utils.ensure_required_libs(self.module)
self.unity_conn = utils.get_unity_unisphere_connection(
self.module.params, application_type)
def check_quota_tree_is_present(self, fs_id, path, tree_quota_id):
"""
Check if quota tree is present in filesystem.
:param fs_id: ID of filesystem where quota tree is searched.
:param path: Path to the quota tree
:param tree_quota_id: ID of the quota tree
:return: ID of quota tree if it exists else None.
"""
if tree_quota_id is None and path is None:
return None
all_tree_quota = self.unity_conn.get_tree_quota(filesystem=fs_id,
id=tree_quota_id,
path=path)
if tree_quota_id and len(all_tree_quota) == 0 \
and self.module.params['state'] == "present":
errormsg = "Tree quota %s does not exist." % tree_quota_id
LOG.error(errormsg)
self.module.fail_json(msg=errormsg)
if len(all_tree_quota) > 0:
msg = "Quota tree with id %s is present in filesystem %s" % (all_tree_quota[0].id,
fs_id)
LOG.info(msg)
return all_tree_quota[0].id
else:
return None
def create_quota_tree(self, fs_id, soft_limit, hard_limit, unit, path, description):
"""
Create quota tree of a filesystem.
:param fs_id: ID of filesystem where quota tree is to be created.
:param soft_limit: Soft limit
:param hard_limit: Hard limit
:param unit: Unit of soft limit and hard limit
:param path: Path to quota tree
:param description: Description for quota tree
:return: Dict containing new quota tree details.
"""
if soft_limit is None and hard_limit is None:
errormsg = "Both soft limit and hard limit cannot be empty. " \
"Please provide atleast one to create quota tree."
LOG.error(errormsg)
self.module.fail_json(msg=errormsg)
soft_limit_in_bytes = utils.get_size_bytes(soft_limit, unit)
hard_limit_in_bytes = utils.get_size_bytes(hard_limit, unit)
try:
obj_tree_quota = self.unity_conn.create_tree_quota(filesystem_id=fs_id, hard_limit=hard_limit_in_bytes,
soft_limit=soft_limit_in_bytes, path=path,
description=description)
LOG.info("Successfully created quota tree")
if obj_tree_quota:
return obj_tree_quota
else:
return None
except Exception as e:
errormsg = "Create quota tree operation at path {0} failed in filesystem {1}" \
" with error {2}".format(path, fs_id, str(e))
LOG.error(errormsg)
self.module.fail_json(msg=errormsg)
def get_filesystem_tree_quota_display_attributes(self, tree_quota_id):
"""Display quota tree attributes
:param tree_quota_id: Quota tree ID
:return: Quota tree dict to display
"""
try:
tree_quota_obj = self.unity_conn.get_tree_quota(_id=tree_quota_id)
tree_quota_details = tree_quota_obj._get_properties()
if tree_quota_obj and tree_quota_obj.existed:
tree_quota_details['soft_limit'] = utils. \
convert_size_with_unit(int(tree_quota_details['soft_limit']))
tree_quota_details['hard_limit'] = utils. \
convert_size_with_unit(int(tree_quota_details['hard_limit']))
tree_quota_details['filesystem']['UnityFileSystem']['name'] = \
tree_quota_obj.filesystem.name
tree_quota_details['filesystem']['UnityFileSystem'].update(
{'nas_server': {'name': tree_quota_obj.filesystem.nas_server.name,
'id': tree_quota_obj.filesystem.nas_server.id}})
return tree_quota_details
except Exception as e:
errormsg = "Failed to display quota tree details {0} with " \
"error {1}".format(tree_quota_obj.id, str(e))
LOG.error(errormsg)
self.module.fail_json(msg=errormsg)
def get_filesystem(self, nas_server=None, name=None, id=None):
"""
Get filesystem details.
:param nas_server: Nas server object.
:param name: Name of filesystem.
:param id: ID of filesystem.
:return: Dict containing filesystem details if it exists.
"""
id_or_name = id if id else name
try:
obj_fs = None
if name:
if not nas_server:
err_msg = "NAS Server is required to get the FileSystem."
LOG.error(err_msg)
self.module.fail_json(msg=err_msg)
obj_fs = self.unity_conn.get_filesystem(name=name,
nas_server=nas_server)
if obj_fs and obj_fs.existed:
LOG.info("Successfully got the filesystem object %s.",
obj_fs)
return obj_fs
if id:
if nas_server:
obj_fs = self.unity_conn \
.get_filesystem(id=id, nas_server=nas_server)
else:
obj_fs = self.unity_conn.get_filesystem(id=id)
if obj_fs and obj_fs.existed:
LOG.info("Successfully got the filesystem object %s.",
obj_fs)
return obj_fs
except Exception as e:
error_msg = "Failed to get filesystem %s with error %s." \
% (id_or_name, str(e))
LOG.error(error_msg)
self.module.fail_json(msg=error_msg)
def get_nas_server_obj(self, name=None, id=None):
"""
Get nas server details.
:param name: Nas server name.
:param id: Nas server ID.
:return: Dict containing nas server details if it exists.
"""
nas_server = id if id else name
error_msg = ("Failed to get NAS server %s." % nas_server)
try:
obj_nas = self.unity_conn.get_nas_server(_id=id, name=name)
if name and obj_nas.existed:
LOG.info("Successfully got the NAS server object %s.",
obj_nas)
return obj_nas
elif id and obj_nas.existed:
LOG.info("Successfully got the NAS server object %s.",
obj_nas)
return obj_nas
else:
LOG.error(error_msg)
self.module.fail_json(msg=error_msg)
except Exception as e:
error_msg = "Failed to get NAS server %s with error %s." \
% (nas_server, str(e))
LOG.error(error_msg)
self.module.fail_json(msg=error_msg)
def modify_tree_quota(self, tree_quota_id, soft_limit, hard_limit, unit, description):
"""
Modify quota tree of filesystem.
:param tree_quota_id: ID of the quota tree
:param soft_limit: Soft limit
:param hard_limit: Hard limit
:param unit: Unit of soft limit and hard limit
:param description: Description of quota tree
:return: Boolean value whether modify quota tree operation is successful.
"""
try:
if soft_limit is None and hard_limit is None:
return False
tree_quota_obj = self.unity_conn.get_tree_quota(tree_quota_id)._get_properties()
if soft_limit is None:
soft_limit_in_bytes = tree_quota_obj['soft_limit']
else:
soft_limit_in_bytes = utils.get_size_bytes(soft_limit, unit)
if hard_limit is None:
hard_limit_in_bytes = tree_quota_obj['hard_limit']
else:
hard_limit_in_bytes = utils.get_size_bytes(hard_limit, unit)
if description is None:
description = tree_quota_obj['description']
if tree_quota_obj:
if tree_quota_obj['soft_limit'] == soft_limit_in_bytes and \
tree_quota_obj['hard_limit'] == hard_limit_in_bytes and \
tree_quota_obj['description'] == description:
return False
else:
modify_tree_quota = self.unity_conn.modify_tree_quota(tree_quota_id=tree_quota_id,
hard_limit=hard_limit_in_bytes,
soft_limit=soft_limit_in_bytes,
description=description)
LOG.info("Successfully modified quota tree")
if modify_tree_quota:
return True
except Exception as e:
errormsg = "Modify quota tree operation {0} failed" \
" with error {1}".format(tree_quota_id, str(e))
LOG.error(errormsg)
self.module.fail_json(msg=errormsg)
def delete_tree_quota(self, tree_quota_id):
"""
Delete quota tree of a filesystem.
:param tree_quota_id: ID of quota tree
:return: Boolean whether quota tree is deleted
"""
try:
delete_tree_quota_obj = self.unity_conn.delete_tree_quota(tree_quota_id=tree_quota_id)
if delete_tree_quota_obj:
return True
except Exception as e:
errormsg = "Delete operation of quota tree id:{0} " \
"failed with error {1}".format(tree_quota_id,
str(e))
LOG.error(errormsg)
self.module.fail_json(msg=errormsg)
def perform_module_operation(self):
"""
Perform different actions on quota tree module based on parameters
passed in the playbook
"""
filesystem_id = self.module.params['filesystem_id']
filesystem_name = self.module.params['filesystem_name']
nas_server_name = self.module.params['nas_server_name']
nas_server_id = self.module.params['nas_server_id']
cap_unit = self.module.params['cap_unit']
state = self.module.params['state']
hard_limit = self.module.params['hard_limit']
soft_limit = self.module.params['soft_limit']
path = self.module.params['path']
description = self.module.params['description']
tree_quota_id = self.module.params['tree_quota_id']
create_tree_quota_obj = None
nas_server_resource = None
get_unity_quota_tree_details = None
fs_id = None
changed = False
'''
result is a dictionary to contain end state and quota tree details
'''
result = dict(
changed=False,
create_tree_quota=False,
modify_tree_quota=False,
get_tree_quota_details={},
delete_tree_quota=False
)
if (soft_limit or hard_limit) and cap_unit is None:
cap_unit = 'GB'
if soft_limit and utils.is_size_negative(soft_limit):
error_message = "Invalid soft_limit provided, " \
"must be greater than or equal to 0"
LOG.error(error_message)
self.module.fail_json(msg=error_message)
if hard_limit and utils.is_size_negative(hard_limit):
error_message = "Invalid hard_limit provided, " \
"must be greater than or equal to 0"
LOG.error(error_message)
self.module.fail_json(msg=error_message)
'''
Get NAS server Object
'''
if nas_server_name is not None:
if utils.is_input_empty(nas_server_name):
self.module.fail_json(msg="Invalid nas_server_name given,"
" Please provide a valid name.")
nas_server_resource = self \
.get_nas_server_obj(name=nas_server_name)
elif nas_server_id is not None:
if utils.is_input_empty(nas_server_id):
self.module.fail_json(msg="Invalid nas_server_id given,"
" Please provide a valid ID.")
nas_server_resource = self.get_nas_server_obj(id=nas_server_id)
'''
Get filesystem Object
'''
if filesystem_name is not None:
if utils.is_input_empty(filesystem_name):
self.module.fail_json(msg="Invalid filesystem_name given,"
" Please provide a valid name.")
filesystem_obj = self \
.get_filesystem(nas_server=nas_server_resource,
name=filesystem_name)
fs_id = filesystem_obj.id
elif filesystem_id is not None:
if utils.is_input_empty(filesystem_id):
self.module.fail_json(msg="Invalid filesystem_id given,"
" Please provide a valid ID.")
filesystem_obj = self \
.get_filesystem(id=filesystem_id)
if filesystem_obj:
fs_id = filesystem_obj[0].id
else:
self.module.fail_json(msg="Filesystem does not exist.")
'''
Validate path to quota tree
'''
if path is not None:
if utils.is_input_empty(path):
self.module.fail_json(msg=" Please provide a valid path.")
elif not path.startswith('/'):
self.module.fail_json(msg="The path is relative to the root of the file system "
"and must start with a forward slash '/'.")
if filesystem_id is None and filesystem_name is None:
self.module.fail_json(msg="Please provide either filesystem_name or fileystem_id.")
quota_tree_id_present = self.check_quota_tree_is_present(fs_id, path, tree_quota_id)
tree_quota_id = quota_tree_id_present
'''
Create quota tree
'''
if (filesystem_id or filesystem_name) and path is not None and state == "present":
if not tree_quota_id:
LOG.info("Creating quota tree")
create_tree_quota_obj = self.create_quota_tree(fs_id, soft_limit, hard_limit,
cap_unit, path, description)
if create_tree_quota_obj:
tree_quota_id = create_tree_quota_obj.id
result['create_tree_quota'] = True
'''
Modify quota tree
'''
if tree_quota_id and state == "present":
LOG.info("Modifying quota tree")
result['modify_tree_quota'] = self.modify_tree_quota(tree_quota_id, soft_limit, hard_limit, cap_unit,
description)
'''
Delete quota tree
'''
if tree_quota_id is not None and state == "absent":
LOG.info("Deleting quota tree")
result['delete_tree_quota'] = self.delete_tree_quota(tree_quota_id)
'''
Get quota tree details
'''
if state == "present" and tree_quota_id is not None:
result['get_tree_quota_details'] = self.get_filesystem_tree_quota_display_attributes(tree_quota_id)
else:
result['get_tree_quota_details'] = {}
if result['create_tree_quota'] or result['modify_tree_quota'] or result['delete_tree_quota']:
result['changed'] = True
self.module.exit_json(**result)
def get_quota_tree_parameters():
"""This method provide parameters required for the ansible
quota tree module on Unity"""
return dict(
filesystem_id=dict(required=False, type='str'),
filesystem_name=dict(required=False, type='str'),
state=dict(required=True, type='str', choices=['present', 'absent']),
hard_limit=dict(required=False, type='int'),
soft_limit=dict(required=False, type='int'),
cap_unit=dict(required=False, type='str', choices=['MB', 'GB', 'TB']),
tree_quota_id=dict(required=False, type='str'),
nas_server_name=dict(required=False, type='str'),
nas_server_id=dict(required=False, type='str'),
path=dict(required=False, type='str', no_log=True),
description=dict(required=False, type='str')
)
def main():
""" Create Unity quota tree object and perform action on it
based on user input from playbook"""
obj = QuotaTree()
obj.perform_module_operation()
if __name__ == '__main__':
main()