File: //usr/lib/python3.9/site-packages/ansible_collections/netapp/ontap/plugins/modules/na_ontap_lun.py
# (c) 2017-2022, NetApp, Inc
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
from __future__ import absolute_import, division, print_function
__metaclass__ = type
DOCUMENTATION = '''
module: na_ontap_lun
short_description: NetApp ONTAP manage LUNs
extends_documentation_fragment:
- netapp.ontap.netapp.na_ontap
version_added: 2.6.0
author: NetApp Ansible Team (@carchi8py) <[email protected]>
description:
- Create, destroy, resize LUNs on NetApp ONTAP.
options:
state:
description:
- Whether the specified LUN should exist or not.
choices: ['present', 'absent']
type: str
default: present
name:
description:
- The name of the LUN to manage.
- Or LUN group name (volume name) when san_application_template is used.
required: true
type: str
from_name:
description:
- The name of the LUN to be renamed.
type: str
version_added: 20.12.0
flexvol_name:
description:
- The name of the FlexVol the LUN should exist on.
- Required if san_application_template is not present.
- Not allowed if san_application_template is present.
type: str
size:
description:
- The size of the LUN in C(size_unit).
- Required when creating a single LUN if application template is not used.
type: int
size_unit:
description:
- The unit used to interpret the size parameter.
choices: ['bytes', 'b', 'kb', 'mb', 'gb', 'tb', 'pb', 'eb', 'zb', 'yb']
default: 'gb'
type: str
comment:
description:
- Optional descriptive comment for the LUN.
type: str
version_added: 21.2.0
force_resize:
description:
- Forcibly reduce the size. This is required for reducing the size of the LUN to avoid accidentally
reducing the LUN size.
type: bool
force_remove:
description:
- If "true", override checks that prevent a LUN from being destroyed if it is online and mapped.
- If "false", destroying an online and mapped LUN will fail.
type: bool
default: false
force_remove_fenced:
description:
- If "true", override checks that prevent a LUN from being destroyed while it is fenced.
- If "false", attempting to destroy a fenced LUN will fail.
- The default if not specified is "false". This field is available in Data ONTAP 8.2 and later.
type: bool
vserver:
required: true
description:
- The name of the vserver to use.
type: str
os_type:
description:
- The os type for the LUN.
type: str
aliases: ['ostype']
qos_policy_group:
description:
- The QoS policy group to be set on the LUN.
- With REST, qos_policy_group and qos_adaptive_policy_group are handled as QOS policy.
type: str
version_added: 20.12.0
qos_adaptive_policy_group:
description:
- The adaptive QoS policy group to be set on the LUN.
- Defines measurable service level objectives (SLOs) and service level agreements (SLAs) that adjust based on the LUN's allocated space or used space.
- Requires ONTAP 9.4 or later.
- With REST, qos_policy_group and qos_adaptive_policy_group are handled as QOS policy.
type: str
version_added: 21.2.0
space_reserve:
description:
- This can be set to "false" which will create a LUN without any space being reserved.
type: bool
default: true
space_allocation:
description:
- This enables support for the SCSI Thin Provisioning features. If the Host and file system do
not support this do not enable it.
type: bool
version_added: 2.7.0
use_exact_size:
description:
- This can be set to "false" which will round the LUN >= 450g.
type: bool
default: true
version_added: 20.11.0
san_application_template:
description:
- additional options when using the application/applications REST API to create LUNs.
- the module is using ZAPI by default, and switches to REST if san_application_template is present.
- create one or more LUNs (and the associated volume as needed).
- operations at the LUN level are supported, they require to know the LUN short name.
- this requires ONTAP 9.8 or higher.
- The module partially supports ONTAP 9.7 for create and delete operations, but not for modify (API limitations).
type: dict
version_added: 20.12.0
suboptions:
name:
description: name of the SAN application.
type: str
required: true
igroup_name:
description: name of the initiator group through which the contents of this application will be accessed.
type: str
lun_count:
description: number of LUNs in the application component (1 to 32).
type: int
protection_type:
description:
- The snasphot policy for the volume supporting the LUNs.
type: dict
suboptions:
local_policy:
description:
- The snapshot copy policy for the volume.
type: str
storage_service:
description:
- The performance service level (PSL) for this volume
type: str
choices: ['value', 'performance', 'extreme']
tiering:
description:
- Cloud tiering policy.
type: dict
suboptions:
control:
description: Storage tiering placement rules for the container.
choices: ['required', 'best_effort', 'disallowed']
type: str
policy:
description:
- Cloud tiering policy.
choices: ['all', 'auto', 'none', 'snapshot-only']
type: str
object_stores:
description: list of object store names for tiering.
type: list
elements: str
total_size:
description:
- The total size of the application component, split across the member LUNs in C(total_size_unit).
- Recommended when C(lun_count) is present.
- Required when C(lun_count) is present and greater than 1.
- Note - if lun_count is equal to 1, and total_size is not present, size is used to maintain backward compatibility.
type: int
version_added: 21.1.0
total_size_unit:
description:
- The unit used to interpret the total_size parameter.
- Defaults to size_unit if not present.
choices: ['bytes', 'b', 'kb', 'mb', 'gb', 'tb', 'pb', 'eb', 'zb', 'yb']
type: str
version_added: 21.1.0
use_san_application:
description:
- Whether to use the application/applications REST/API to create LUNs.
- This will default to true if any other suboption is present.
type: bool
default: true
scope:
description:
- whether the top level name identifies a single LUN or a LUN group (application).
- By default, the module will try to make the right choice, but can report extra warnings.
- Setting scope to 'application' is required to convert an existing volume to a smart container.
- The module reports an error when 'lun' or 'application' is used and the desired action cannot be completed.
- The module issues warnings when the default 'auto' is used, and there is ambiguity regarding the desired actions.
type: str
choices: ['application', 'auto', 'lun']
default: auto
version_added: 21.2.0
exclude_aggregates:
description:
- The list of aggregate names to exclude when creating a volume.
- Requires ONTAP 9.9.1 GA or better.
type: list
elements: str
version_added: 21.7.0
'''
EXAMPLES = """
- name: Create LUN
netapp.ontap.na_ontap_lun:
state: present
name: ansibleLUN
flexvol_name: ansibleVolume
vserver: ansibleVServer
size: 5
size_unit: mb
os_type: linux
space_reserve: true
hostname: "{{ netapp_hostname }}"
username: "{{ netapp_username }}"
password: "{{ netapp_password }}"
- name: Resize LUN
netapp.ontap.na_ontap_lun:
state: present
name: ansibleLUN
force_resize: true
flexvol_name: ansibleVolume
vserver: ansibleVServer
size: 5
size_unit: gb
hostname: "{{ netapp_hostname }}"
username: "{{ netapp_username }}"
password: "{{ netapp_password }}"
- name: Create LUNs using SAN application
tags: create
netapp.ontap.na_ontap_lun:
state: present
name: ansibleLUN
size: 15
size_unit: mb
os_type: linux
space_reserve: false
san_application_template:
name: san-ansibleLUN
igroup_name: testme_igroup
lun_count: 3
protection_type:
local_policy: default
exclude_aggregates: aggr0
hostname: "{{ netapp_hostname }}"
username: "{{ netapp_username }}"
password: "{{ netapp_password }}"
- name: Convert existing volume to SAN application
tags: create
netapp.ontap.na_ontap_lun:
state: present
name: someVolume
size: 22
size_unit: mb
os_type: linux
space_reserve: false
san_application_template:
name: san-ansibleLUN
igroup_name: testme_igroup
lun_count: 3
protection_type:
local_policy: default
scope: application
hostname: "{{ netapp_hostname }}"
username: "{{ netapp_username }}"
password: "{{ netapp_password }}"
"""
RETURN = """
"""
import copy
import traceback
from ansible.module_utils.basic import AnsibleModule
from ansible.module_utils._text import to_native
import ansible_collections.netapp.ontap.plugins.module_utils.netapp as netapp_utils
from ansible_collections.netapp.ontap.plugins.module_utils.netapp_module import NetAppModule
from ansible_collections.netapp.ontap.plugins.module_utils.rest_application import RestApplication
from ansible_collections.netapp.ontap.plugins.module_utils.netapp import OntapRestAPI
from ansible_collections.netapp.ontap.plugins.module_utils import rest_volume
from ansible_collections.netapp.ontap.plugins.module_utils import rest_generic
HAS_NETAPP_LIB = netapp_utils.has_netapp_lib()
class NetAppOntapLUN:
''' create, modify, delete LUN '''
def __init__(self):
self.argument_spec = netapp_utils.na_ontap_host_argument_spec()
self.argument_spec.update(dict(
state=dict(required=False, type='str', choices=['present', 'absent'], default='present'),
name=dict(required=True, type='str'),
from_name=dict(required=False, type='str'),
size=dict(type='int'),
size_unit=dict(default='gb',
choices=['bytes', 'b', 'kb', 'mb', 'gb', 'tb',
'pb', 'eb', 'zb', 'yb'], type='str'),
comment=dict(required=False, type='str'),
force_resize=dict(type='bool'),
force_remove=dict(required=False, type='bool', default=False),
force_remove_fenced=dict(type='bool'),
flexvol_name=dict(type='str'),
vserver=dict(required=True, type='str'),
os_type=dict(required=False, type='str', aliases=['ostype']),
qos_policy_group=dict(required=False, type='str'),
qos_adaptive_policy_group=dict(required=False, type='str'),
space_reserve=dict(required=False, type='bool', default=True),
space_allocation=dict(required=False, type='bool'),
use_exact_size=dict(required=False, type='bool', default=True),
san_application_template=dict(type='dict', options=dict(
use_san_application=dict(type='bool', default=True),
exclude_aggregates=dict(type='list', elements='str'),
name=dict(required=True, type='str'),
igroup_name=dict(type='str'),
lun_count=dict(type='int'),
protection_type=dict(type='dict', options=dict(
local_policy=dict(type='str'),
)),
storage_service=dict(type='str', choices=['value', 'performance', 'extreme']),
tiering=dict(type='dict', options=dict(
control=dict(type='str', choices=['required', 'best_effort', 'disallowed']),
policy=dict(type='str', choices=['all', 'auto', 'none', 'snapshot-only']),
object_stores=dict(type='list', elements='str') # create only
)),
total_size=dict(type='int'),
total_size_unit=dict(choices=['bytes', 'b', 'kb', 'mb', 'gb', 'tb',
'pb', 'eb', 'zb', 'yb'], type='str'),
scope=dict(type='str', choices=['application', 'auto', 'lun'], default='auto'),
))
))
self.module = AnsibleModule(
argument_spec=self.argument_spec,
supports_check_mode=True,
mutually_exclusive=[('qos_policy_group', 'qos_adaptive_policy_group')]
)
# set up state variables
self.na_helper = NetAppModule()
self.parameters = self.na_helper.set_parameters(self.module.params)
if self.parameters.get('size') is not None:
self.parameters['size'] *= netapp_utils.POW2_BYTE_MAP[self.parameters['size_unit']]
if self.na_helper.safe_get(self.parameters, ['san_application_template', 'total_size']) is not None:
unit = self.na_helper.safe_get(self.parameters, ['san_application_template', 'total_size_unit'])
if unit is None:
unit = self.parameters['size_unit']
self.parameters['san_application_template']['total_size'] *= netapp_utils.POW2_BYTE_MAP[unit]
self.debug = {}
self.uuid = None
# self.debug['got'] = 'empty' # uncomment to enable collecting data
self.rest_api = OntapRestAPI(self.module)
# use_exact_size is defaulted to true, but not supported with REST. To get around this we will ignore the variable in rest.
unsupported_rest_properties = ['force_resize', 'force_remove_fenced']
partially_supported_rest_properties = [['san_application_template', (9, 7)],
['space_allocation', (9, 10)]]
self.use_rest = self.rest_api.is_rest_supported_properties(self.parameters, unsupported_rest_properties,
partially_supported_rest_properties)
if self.use_rest:
self.parameters.pop('use_exact_size')
if self.parameters.get('qos_adaptive_policy_group') is not None:
self.parameters['qos_policy_group'] = self.parameters.pop('qos_adaptive_policy_group')
else:
if not netapp_utils.has_netapp_lib():
self.module.fail_json(msg=netapp_utils.netapp_lib_is_required())
self.server = netapp_utils.setup_na_ontap_zapi(module=self.module, vserver=self.parameters['vserver'])
# set default value for ZAPI only supported options.
if self.parameters.get('force_resize') is None:
self.parameters['force_resize'] = False
if self.parameters.get('force_remove_fenced') is None:
self.parameters['force_remove_fenced'] = False
# REST API for application/applications if needed
self.rest_app = self.setup_rest_application()
def setup_rest_application(self):
use_application_template = self.na_helper.safe_get(self.parameters, ['san_application_template', 'use_san_application'])
rest_app = None
if self.use_rest:
if use_application_template:
if self.parameters.get('flexvol_name') is not None:
self.module.fail_json(msg="'flexvol_name' option is not supported when san_application_template is present")
name = self.na_helper.safe_get(self.parameters, ['san_application_template', 'name'], allow_sparse_dict=False)
rest_app = RestApplication(self.rest_api, self.parameters['vserver'], name)
elif self.parameters.get('flexvol_name') is None:
self.module.fail_json(msg="flexvol_name option is required when san_application_template is not present")
else:
if use_application_template:
self.module.fail_json(msg="Error: using san_application_template requires ONTAP 9.7 or later and REST must be enabled.")
if self.parameters.get('flexvol_name') is None:
self.module.fail_json(msg="Error: 'flexvol_name' option is required when using ZAPI.")
return rest_app
def get_luns(self, lun_path=None):
"""
Return list of LUNs matching vserver and volume names.
:return: list of LUNs in XML format.
:rtype: list
"""
if self.use_rest:
return self.get_luns_rest(lun_path)
luns = []
tag = None
query_details = netapp_utils.zapi.NaElement('lun-info')
query_details.add_new_child('vserver', self.parameters['vserver'])
if lun_path is not None:
query_details.add_new_child('lun_path', lun_path)
else:
query_details.add_new_child('volume', self.parameters['flexvol_name'])
query = netapp_utils.zapi.NaElement('query')
query.add_child_elem(query_details)
while True:
lun_info = netapp_utils.zapi.NaElement('lun-get-iter')
lun_info.add_child_elem(query)
if tag:
lun_info.add_new_child('tag', tag, True)
try:
result = self.server.invoke_successfully(lun_info, enable_tunneling=True)
except netapp_utils.zapi.NaApiError as exc:
self.module.fail_json(msg="Error fetching luns for %s: %s" %
(self.parameters['flexvol_name'] if lun_path is None else lun_path, to_native(exc)),
exception=traceback.format_exc())
if result.get_child_by_name('num-records') and int(result.get_child_content('num-records')) >= 1:
attr_list = result.get_child_by_name('attributes-list')
luns.extend(attr_list.get_children())
tag = result.get_child_content('next-tag')
if tag is None:
break
return luns
def get_lun_details(self, lun):
"""
Extract LUN details, from XML to python dict
:return: Details about the lun
:rtype: dict
"""
if self.use_rest:
return lun
return_value = {'size': int(lun.get_child_content('size'))}
bool_attr_map = {
'is-space-alloc-enabled': 'space_allocation',
'is-space-reservation-enabled': 'space_reserve'
}
for attr in bool_attr_map:
value = lun.get_child_content(attr)
if value is not None:
return_value[bool_attr_map[attr]] = self.na_helper.get_value_for_bool(True, value)
str_attr_map = {
'comment': 'comment',
'multiprotocol-type': 'os_type',
'name': 'name',
'path': 'path',
'qos-policy-group': 'qos_policy_group',
'qos-adaptive-policy-group': 'qos_adaptive_policy_group',
}
for attr in str_attr_map:
value = lun.get_child_content(attr)
if value is None and attr in ('comment', 'qos-policy-group', 'qos-adaptive-policy-group'):
value = ''
if value is not None:
return_value[str_attr_map[attr]] = value
return return_value
def find_lun(self, luns, name, lun_path=None):
"""
Return lun record matching name or path
:return: lun record
:rtype: XML for ZAPI, dict for REST, or None if not found
"""
if luns:
for lun in luns:
path = lun['path']
if lun_path is None:
if name == path:
return lun
_rest, _splitter, found_name = path.rpartition('/')
if found_name == name:
return lun
elif lun_path == path:
return lun
return None
def get_lun(self, name, lun_path=None):
"""
Return details about the LUN
:return: Details about the lun
:rtype: dict
"""
luns = self.get_luns(lun_path)
lun = self.find_lun(luns, name, lun_path)
if lun is not None:
return self.get_lun_details(lun)
return None
def get_luns_from_app(self):
app_details, error = self.rest_app.get_application_details()
self.fail_on_error(error)
if app_details is not None:
app_details['paths'] = self.get_lun_paths_from_app()
return app_details
def get_lun_paths_from_app(self):
"""Get luns path for SAN application"""
backing_storage, error = self.rest_app.get_application_component_backing_storage()
self.fail_on_error(error)
# {'luns': [{'path': '/vol/ansibleLUN/ansibleLUN_1', ...
if backing_storage is not None:
return [lun['path'] for lun in backing_storage.get('luns', [])]
return None
def get_lun_path_from_backend(self, name):
"""returns lun path matching name if found in backing_storage
retruns None if not found
"""
lun_paths = self.get_lun_paths_from_app()
match = "/%s" % name
return next((path for path in lun_paths if path.endswith(match)), None)
def create_san_app_component(self, modify):
'''Create SAN application component'''
if modify:
required_options = ['name']
action = 'modify'
if 'lun_count' in modify:
required_options.append('total_size')
else:
required_options = ('name', 'total_size')
action = 'create'
for option in required_options:
if self.parameters.get(option) is None:
self.module.fail_json(msg="Error: '%s' is required to %s a san application." % (option, action))
application_component = dict(name=self.parameters['name'])
if not modify:
application_component['lun_count'] = 1 # default value for create, may be overriden below
for attr in ('igroup_name', 'lun_count', 'storage_service'):
if not modify or attr in modify:
value = self.na_helper.safe_get(self.parameters, ['san_application_template', attr])
if value is not None:
application_component[attr] = value
for attr in ('os_type', 'qos_policy_group', 'qos_adaptive_policy_group', 'total_size'):
if not self.rest_api.meets_rest_minimum_version(True, 9, 8, 0) and attr in (
'os_type',
'qos_policy_group',
'qos_adaptive_policy_group',
):
# os_type and qos are not supported in 9.7 for the SAN application_component
continue
if not modify or attr in modify:
value = self.na_helper.safe_get(self.parameters, [attr])
if value is not None:
# only one of them can be present at most
if attr in ('qos_policy_group', 'qos_adaptive_policy_group'):
attr = 'qos'
value = dict(policy=dict(name=value))
application_component[attr] = value
tiering = self.na_helper.safe_get(self.parameters, ['san_application_template', 'tiering'])
if tiering is not None and not modify:
application_component['tiering'] = {}
for attr in ('control', 'policy', 'object_stores'):
value = tiering.get(attr)
if attr == 'object_stores' and value is not None:
value = [dict(name=x) for x in value]
if value is not None:
application_component['tiering'][attr] = value
return application_component
def create_san_app_body(self, modify=None):
'''Create body for san template'''
# TODO:
# Should we support new_igroups?
# It may raise idempotency issues if the REST call fails if the igroup already exists.
# And we already have na_ontap_igroups.
san = {
'application_components': [self.create_san_app_component(modify)],
}
for attr in ('protection_type',):
if not modify or attr in modify:
value = self.na_helper.safe_get(self.parameters, ['san_application_template', attr])
if value is not None:
# we expect value to be a dict, but maybe an empty dict
value = self.na_helper.filter_out_none_entries(value)
if value:
san[attr] = value
for attr in ('exclude_aggregates',):
if modify is None: # only used for create
values = self.na_helper.safe_get(self.parameters, ['san_application_template', attr])
if values:
san[attr] = [dict(name=name) for name in values]
for attr in ('os_type',):
if not modify: # not supported for modify operation, but required at application component level for create
value = self.na_helper.safe_get(self.parameters, [attr])
if value is not None:
san[attr] = value
body, error = self.rest_app.create_application_body('san', san)
return body, error
def create_san_application(self):
'''Use REST application/applications san template to create one or more LUNs'''
body, error = self.create_san_app_body()
self.fail_on_error(error)
dummy, error = self.rest_app.create_application(body)
self.fail_on_error(error)
def modify_san_application(self, modify):
'''Use REST application/applications san template to add one or more LUNs'''
body, error = self.create_san_app_body(modify)
self.fail_on_error(error)
# these cannot be present when using PATCH
body.pop('name')
body.pop('svm')
body.pop('smart_container')
dummy, error = self.rest_app.patch_application(body)
self.fail_on_error(error)
def convert_to_san_application(self, scope):
'''First convert volume to smart container using POST
Second modify app to add new luns using PATCH
'''
# dummy modify, so that we don't fill in the body
modify = dict(dummy='dummy')
body, error = self.create_san_app_body(modify)
self.fail_on_error(error)
dummy, error = self.rest_app.create_application(body)
self.fail_on_error(error)
app_current, error = self.rest_app.get_application_uuid()
self.fail_on_error(error)
if app_current is None:
self.module.fail_json(msg='Error: failed to create smart container for %s' % self.parameters['name'])
app_modify, app_modify_warning = self.app_changes(scope)
if app_modify_warning is not None:
self.module.warn(app_modify_warning)
if app_modify:
self.modify_san_application(app_modify)
def delete_san_application(self):
'''Use REST application/applications san template to delete one or more LUNs'''
dummy, error = self.rest_app.delete_application()
self.fail_on_error(error)
def create_lun(self):
"""
Create LUN with requested name and size
"""
if self.use_rest:
return self.create_lun_rest()
path = '/vol/%s/%s' % (self.parameters['flexvol_name'], self.parameters['name'])
options = {'path': path,
'size': str(self.parameters['size']),
'space-reservation-enabled': self.na_helper.get_value_for_bool(False, self.parameters['space_reserve']),
'use-exact-size': str(self.parameters['use_exact_size'])}
if self.parameters.get('space_allocation') is not None:
options['space-allocation-enabled'] = self.na_helper.get_value_for_bool(False, self.parameters['space_allocation'])
if self.parameters.get('comment') is not None:
options['comment'] = self.parameters['comment']
if self.parameters.get('os_type') is not None:
options['ostype'] = self.parameters['os_type']
if self.parameters.get('qos_policy_group') is not None:
options['qos-policy-group'] = self.parameters['qos_policy_group']
if self.parameters.get('qos_adaptive_policy_group') is not None:
options['qos-adaptive-policy-group'] = self.parameters['qos_adaptive_policy_group']
lun_create = netapp_utils.zapi.NaElement.create_node_with_children(
'lun-create-by-size', **options)
try:
self.server.invoke_successfully(lun_create, enable_tunneling=True)
except netapp_utils.zapi.NaApiError as exc:
self.module.fail_json(msg="Error provisioning lun %s of size %s: %s"
% (self.parameters['name'], self.parameters['size'], to_native(exc)),
exception=traceback.format_exc())
def delete_lun(self, path):
"""
Delete requested LUN
"""
if self.use_rest:
return self.delete_lun_rest()
lun_delete = netapp_utils.zapi.NaElement.create_node_with_children(
'lun-destroy', **{'path': path,
'force': str(self.parameters['force_remove']),
'destroy-fenced-lun':
str(self.parameters['force_remove_fenced'])})
try:
self.server.invoke_successfully(lun_delete, enable_tunneling=True)
except netapp_utils.zapi.NaApiError as exc:
self.module.fail_json(msg="Error deleting lun %s: %s" % (path, to_native(exc)),
exception=traceback.format_exc())
def resize_lun(self, path):
"""
Resize requested LUN
:return: True if LUN was actually re-sized, false otherwise.
:rtype: bool
"""
if self.use_rest:
return self.resize_lun_rest()
lun_resize = netapp_utils.zapi.NaElement.create_node_with_children(
'lun-resize', **{'path': path,
'size': str(self.parameters['size']),
'force': str(self.parameters['force_resize'])})
try:
self.server.invoke_successfully(lun_resize, enable_tunneling=True)
except netapp_utils.zapi.NaApiError as exc:
if to_native(exc.code) == "9042":
# Error 9042 denotes the new LUN size being the same as the
# old LUN size. This happens when there's barely any difference
# in the two sizes. For example, from 8388608 bytes to
# 8194304 bytes. This should go away if/when the default size
# requested/reported to/from the controller is changed to a
# larger unit (MB/GB/TB).
return False
else:
self.module.fail_json(msg="Error resizing lun %s: %s" % (path, to_native(exc)),
exception=traceback.format_exc())
return True
def set_lun_value(self, path, key, value):
key_to_zapi = dict(
comment=('lun-set-comment', 'comment'),
# The same ZAPI is used for both QOS attributes
qos_policy_group=('lun-set-qos-policy-group', 'qos-policy-group'),
qos_adaptive_policy_group=('lun-set-qos-policy-group', 'qos-adaptive-policy-group'),
space_allocation=('lun-set-space-alloc', 'enable'),
space_reserve=('lun-set-space-reservation-info', 'enable')
)
if key in key_to_zapi:
zapi, option = key_to_zapi[key]
else:
self.module.fail_json(msg="option %s cannot be modified to %s" % (key, value))
options = dict(path=path)
if option == 'enable':
options[option] = self.na_helper.get_value_for_bool(False, value)
else:
options[option] = value
lun_set = netapp_utils.zapi.NaElement.create_node_with_children(zapi, **options)
try:
self.server.invoke_successfully(lun_set, enable_tunneling=True)
except netapp_utils.zapi.NaApiError as exc:
self.module.fail_json(msg="Error setting lun option %s: %s" % (key, to_native(exc)),
exception=traceback.format_exc())
return
def modify_lun(self, path, modify):
"""
update LUN properties (except size or name)
"""
if self.use_rest:
return self.modify_lun_rest(modify)
for key in sorted(modify):
self.set_lun_value(path, key, modify[key])
def rename_lun(self, path, new_path):
"""
rename LUN
"""
if self.use_rest:
return self.rename_lun_rest(new_path)
lun_move = netapp_utils.zapi.NaElement.create_node_with_children(
'lun-move', **{'path': path,
'new-path': new_path})
try:
self.server.invoke_successfully(lun_move, enable_tunneling=True)
except netapp_utils.zapi.NaApiError as exc:
self.module.fail_json(msg="Error moving lun %s: %s" % (path, to_native(exc)),
exception=traceback.format_exc())
def fail_on_error(self, error, stack=False):
if error is None:
return
elements = dict(msg="Error: %s" % error)
if stack:
elements['stack'] = traceback.format_stack()
self.module.fail_json(**elements)
def set_total_size(self, validate):
# fix total_size attribute, report error if total_size is missing (or size is missing)
attr = 'total_size'
value = self.na_helper.safe_get(self.parameters, ['san_application_template', attr])
if value is not None or not validate:
self.parameters[attr] = value
return
lun_count = self.na_helper.safe_get(self.parameters, ['san_application_template', 'lun_count'])
value = self.parameters.get('size')
if value is not None and (lun_count is None or lun_count == 1):
self.parameters[attr] = value
return
self.module.fail_json(msg="Error: 'total_size' is a required SAN application template attribute when creating a LUN application")
def validate_app_create(self):
# fix total_size attribute
self.set_total_size(validate=True)
def validate_app_changes(self, modify, warning):
saved_modify = dict(modify)
errors = [
"Error: the following application parameter cannot be modified: %s. Received: %s."
% (key, str(modify))
for key in modify
if key not in ('igroup_name', 'os_type', 'lun_count', 'total_size')
]
extra_attrs = tuple()
if 'lun_count' in modify:
extra_attrs = ('total_size', 'os_type', 'igroup_name')
else:
ignored_keys = [key for key in modify if key not in ('total_size',)]
for key in ignored_keys:
self.module.warn(
"Ignoring: %s. This application parameter is only relevant when increasing the LUN count. Received: %s."
% (key, str(saved_modify)))
modify.pop(key)
for attr in extra_attrs:
value = self.parameters.get(attr)
if value is None:
value = self.na_helper.safe_get(self.parameters['san_application_template'], [attr])
if value is None:
errors.append('Error: %s is a required parameter when increasing lun_count.' % attr)
else:
modify[attr] = value
if errors:
self.module.fail_json(msg='\n'.join(errors))
if 'total_size' in modify:
self.set_total_size(validate=False)
if warning and 'lun_count' not in modify:
# can't change total_size, let's ignore it
self.module.warn(warning)
modify.pop('total_size')
saved_modify.pop('total_size')
if modify and not self.rest_api.meets_rest_minimum_version(True, 9, 8):
self.module.fail_json(
msg='Error: modifying %s is not supported on ONTAP 9.7' % ', '.join(saved_modify.keys()))
def fail_on_large_size_reduction(self, app_current, desired, provisioned_size):
""" Error if a reduction of size > 10% is requested.
Warn for smaller reduction and ignore it, to protect against 'rounding' errors.
"""
total_size = app_current['total_size']
desired_size = desired.get('total_size')
warning = None
if desired_size is not None:
details = "total_size=%d, provisioned=%d, requested=%d" % (total_size, provisioned_size, desired_size)
if desired_size < total_size:
# * 100 to get a percentage, and .0 to force float conversion
reduction = round((total_size - desired_size) * 100.0 / total_size, 1)
if reduction > 10:
self.module.fail_json(msg="Error: can't reduce size: %s" % details)
else:
warning = "Ignoring small reduction (%.1f %%) in total size: %s" % (reduction, details)
elif desired_size > total_size and desired_size < provisioned_size:
# we can't increase, but we can't say it is a problem, as the size is already bigger!
warning = "Ignoring increase: requested size is too small: %s" % details
return warning
def get_luns_rest(self, lun_path=None):
if lun_path is None and self.parameters.get('flexvol_name') is None:
return []
api = 'storage/luns'
query = {
'svm.name': self.parameters['vserver'],
'fields': "comment,lun_maps,name,os_type,qos_policy.name,space"}
if lun_path is not None:
query['name'] = lun_path
else:
query['location.volume.name'] = self.parameters['flexvol_name']
record, error = rest_generic.get_0_or_more_records(self.rest_api, api, query)
if error:
if lun_path is not None:
self.module.fail_json(msg="Error getting lun_path %s: %s" % (lun_path, to_native(error)),
exception=traceback.format_exc())
else:
self.module.fail_json(
msg="Error getting LUN's for flexvol %s: %s" % (self.parameters['flexvol_name'], to_native(error)),
exception=traceback.format_exc())
return self.format_get_luns(record)
def format_get_luns(self, records):
luns = []
if not records:
return None
for record in records:
# TODO: Check that path and name are the same in Rest
lun = {
'uuid': self.na_helper.safe_get(record, ['uuid']),
'name': self.na_helper.safe_get(record, ['name']),
'path': self.na_helper.safe_get(record, ['name']),
'size': self.na_helper.safe_get(record, ['space', 'size']),
'comment': self.na_helper.safe_get(record, ['comment']),
'flexvol_name': self.na_helper.safe_get(record, ['location', 'volume', 'name']),
'os_type': self.na_helper.safe_get(record, ['os_type']),
'qos_policy_group': self.na_helper.safe_get(record, ['qos_policy', 'name']),
'space_reserve': self.na_helper.safe_get(record, ['space', 'guarantee', 'requested']),
'space_allocation': self.na_helper.safe_get(record,
['space', 'scsi_thin_provisioning_support_enabled']),
}
luns.append(lun)
return luns
def create_lun_rest(self):
name = self.create_lun_path_rest()
api = 'storage/luns'
body = {
'svm.name': self.parameters['vserver'],
'name': name,
}
if self.parameters.get('flexvol_name') is not None:
body['location.volume.name'] = self.parameters['flexvol_name']
if self.parameters.get('os_type') is not None:
body['os_type'] = self.parameters['os_type']
if self.parameters.get('size') is not None:
body['space.size'] = self.parameters['size']
if self.parameters.get('space_reserve') is not None:
body['space.guarantee.requested'] = self.parameters['space_reserve']
if self.parameters.get('space_allocation') is not None:
body['space.scsi_thin_provisioning_support_enabled'] = self.parameters['space_allocation']
if self.parameters.get('comment') is not None:
body['comment'] = self.parameters['comment']
if self.parameters.get('qos_policy_group') is not None:
body['qos_policy.name'] = self.parameters['qos_policy_group']
dummy, error = rest_generic.post_async(self.rest_api, api, body)
if error:
self.module.fail_json(msg="Error creating LUN %s: %s" % (self.parameters['name'], to_native(error)),
exception=traceback.format_exc())
def create_lun_path_rest(self):
""" ZAPI accepts just a name, while REST expects a path. We need to convert a name in to a path for backward compatibility
If the name start with a slash we will assume it a path and use it as the name
"""
if not self.parameters['name'].startswith('/') and self.parameters.get('flexvol_name') is not None:
# if it dosn't start with a slash and we have a flexvol name we will use it to build the path
return '/vol/%s/%s' % (self.parameters['flexvol_name'], self.parameters['name'])
return self.parameters['name']
def delete_lun_rest(self):
if self.uuid is None:
self.module.fail_json(msg="Error deleting LUN %s: UUID not found" % self.parameters['name'])
api = 'storage/luns'
query = {'allow_delete_while_mapped': self.parameters['force_remove']}
dummy, error = rest_generic.delete_async(self.rest_api, api, self.uuid, query)
if error:
self.module.fail_json(msg="Error deleting LUN %s: %s" % (self.parameters['name'], to_native(error)),
exception=traceback.format_exc())
def rename_lun_rest(self, new_path):
if self.uuid is None:
self.module.fail_json(msg="Error renaming LUN %s: UUID not found" % self.parameters['name'])
api = 'storage/luns'
body = {'name': new_path}
dummy, error = rest_generic.patch_async(self.rest_api, api, self.uuid, body)
if error:
self.module.fail_json(msg="Error renaming LUN %s: %s" % (self.parameters['name'], to_native(error)),
exception=traceback.format_exc())
def resize_lun_rest(self):
if self.uuid is None:
self.module.fail_json(msg="Error resizing LUN %s: UUID not found" % self.parameters['name'])
api = 'storage/luns'
body = {'space.size': self.parameters['size']}
dummy, error = rest_generic.patch_async(self.rest_api, api, self.uuid, body)
if error:
if 'New LUN size is the same as the old LUN size' in error:
return False
self.module.fail_json(msg="Error resizing LUN %s: %s" % (self.parameters['name'], to_native(error)),
exception=traceback.format_exc())
return True
def modify_lun_rest(self, modify):
local_modify = modify.copy()
if self.uuid is None:
self.module.fail_json(msg="Error modifying LUN %s: UUID not found" % self.parameters['name'])
api = 'storage/luns'
body = {}
if local_modify.get('space_reserve') is not None:
body['space.guarantee.requested'] = local_modify.pop('space_reserve')
if local_modify.get('space_allocation') is not None:
body['space.scsi_thin_provisioning_support_enabled'] = local_modify.pop('space_allocation')
if local_modify.get('comment') is not None:
body['comment'] = local_modify.pop('comment')
if local_modify.get('qos_policy_group') is not None:
body['qos_policy.name'] = local_modify.pop('qos_policy_group')
if local_modify != {}:
self.module.fail_json(
msg="Error modifying LUN %s: Unknown parameters: %s" % (self.parameters['name'], local_modify))
dummy, error = rest_generic.patch_async(self.rest_api, api, self.uuid, body)
if error:
self.module.fail_json(msg="Error modifying LUN %s: %s" % (self.parameters['name'], to_native(error)),
exception=traceback.format_exc())
def check_for_errors(self, lun_cd_action, current, modify):
errors = []
if lun_cd_action == 'create':
if self.parameters.get('flexvol_name') is None:
errors.append("The flexvol_name parameter is required for creating a LUN.")
if self.use_rest and self.parameters.get('os_type') is None:
errors.append("The os_type parameter is required for creating a LUN with REST.")
if self.parameters.get('size') is None:
self.module.fail_json(msg="size is a required parameter for create.")
elif modify and 'os_type' in modify:
self.module.fail_json(msg="os_type cannot be modified: current: %s, desired: %s" % (current['os_type'], modify['os_type']))
if errors:
self.module.fail_json(msg=' '.join(errors))
def set_uuid(self, current):
if self.use_rest and current is not None and current.get('uuid') is not None:
self.uuid = current['uuid']
def app_changes(self, scope):
# find and validate app changes
app_current, error = self.rest_app.get_application_details('san')
self.fail_on_error(error)
# save application name, as it is overriden in the flattening operation
app_name = app_current['name']
# there is an issue with total_size not reflecting the real total_size, and some additional overhead
provisioned_size = self.na_helper.safe_get(app_current, ['statistics', 'space', 'provisioned'])
if provisioned_size is None:
provisioned_size = 0
if self.debug:
self.debug['app_current'] = app_current # will be updated below as it is mutable
self.debug['got'] = copy.deepcopy(app_current) # fixed copy
# flatten
app_current = app_current['san'] # app template
app_current.update(app_current['application_components'][0]) # app component
del app_current['application_components']
# if component name does not match, assume a change at LUN level
comp_name = app_current['name']
if comp_name != self.parameters['name']:
msg = "desired component/volume name: %s does not match existing component name: %s" % (self.parameters['name'], comp_name)
if scope == 'application':
self.module.fail_json(msg='Error: ' + msg + ". scope=%s" % scope)
return None, msg + ". scope=%s, assuming 'lun' scope." % scope
# restore app name
app_current['name'] = app_name
# ready to compare, except for a quirk in size handling
desired = dict(self.parameters['san_application_template'])
warning = self.fail_on_large_size_reduction(app_current, desired, provisioned_size)
# preserve change state before calling modify in case an ignorable total_size change is the only change
changed = self.na_helper.changed
app_modify = self.na_helper.get_modified_attributes(app_current, desired)
self.validate_app_changes(app_modify, warning)
if not app_modify:
self.na_helper.changed = changed
app_modify = None
return app_modify, None
def get_app_apply(self):
scope = self.na_helper.safe_get(self.parameters, ['san_application_template', 'scope'])
app_current, error = self.rest_app.get_application_uuid()
self.fail_on_error(error)
if scope == 'lun' and app_current is None:
self.module.fail_json(msg='Application not found: %s. scope=%s.' %
(self.na_helper.safe_get(self.parameters, ['san_application_template', 'name']),
scope))
return scope, app_current
def app_actions(self, app_current, scope, actions, results):
app_modify, app_modify_warning = None, None
app_cd_action = self.na_helper.get_cd_action(app_current, self.parameters)
if app_cd_action == 'create':
# check if target volume already exists
cp_volume_name = self.parameters['name']
volume, error = rest_volume.get_volume(self.rest_api, self.parameters['vserver'], cp_volume_name)
self.fail_on_error(error)
if volume is not None:
if scope == 'application':
# volume already exists, but not as part of this application
app_cd_action = 'convert'
if not self.rest_api.meets_rest_minimum_version(True, 9, 8, 0):
msg = 'Error: converting a LUN volume to a SAN application container requires ONTAP 9.8 or better.'
self.module.fail_json(msg=msg)
else:
# default name already in use, ask user to clarify intent
msg = "Error: volume '%s' already exists. Please use a different group name, or use 'application' scope. scope=%s"
self.module.fail_json(msg=msg % (cp_volume_name, scope))
if app_cd_action is not None:
actions.append('app_%s' % app_cd_action)
if app_cd_action == 'create':
self.validate_app_create()
if app_cd_action is None and app_current is not None:
app_modify, app_modify_warning = self.app_changes(scope)
if app_modify:
actions.append('app_modify')
results['app_modify'] = dict(app_modify)
return app_cd_action, app_modify, app_modify_warning
def lun_actions(self, app_current, actions, results, scope, app_modify, app_modify_warning):
# actions at LUN level
lun_cd_action, lun_modify, lun_rename = None, None, None
lun_path, from_lun_path = None, None
from_name = self.parameters.get('from_name')
if self.rest_app and app_current:
# For LUNs created using a SAN application, we're getting lun paths from the backing storage
lun_path = self.get_lun_path_from_backend(self.parameters['name'])
if from_name is not None:
from_lun_path = self.get_lun_path_from_backend(from_name)
current = self.get_lun(self.parameters['name'], lun_path)
self.set_uuid(current)
if current is not None and lun_path is None:
lun_path = current['path']
lun_cd_action = self.na_helper.get_cd_action(current, self.parameters)
if lun_cd_action == 'create' and from_name is not None:
# create by renaming existing LUN, if it exists
old_lun = self.get_lun(from_name, from_lun_path)
lun_rename = self.na_helper.is_rename_action(old_lun, current)
if lun_rename is None:
self.module.fail_json(msg="Error renaming lun: %s does not exist" % from_name)
if lun_rename:
current = old_lun
if from_lun_path is None:
from_lun_path = current['path']
head, _sep, tail = from_lun_path.rpartition(from_name)
if tail:
self.module.fail_json(
msg="Error renaming lun: %s does not match lun_path %s" % (from_name, from_lun_path))
self.set_uuid(current)
lun_path = head + self.parameters['name']
lun_cd_action = None
actions.append('lun_rename')
app_modify_warning = None # reset warning as we found a match
if lun_cd_action is not None:
actions.append('lun_%s' % lun_cd_action)
if lun_cd_action is None and self.parameters['state'] == 'present':
# we already handled rename if required
current.pop('name', None)
lun_modify = self.na_helper.get_modified_attributes(current, self.parameters)
if lun_modify:
actions.append('lun_modify')
results['lun_modify'] = dict(lun_modify)
app_modify_warning = None # reset warning as we found a match
if lun_cd_action and self.rest_app and app_current:
msg = 'This module does not support %s a LUN by name %s a SAN application.' % \
('adding', 'to') if lun_cd_action == 'create' else ('removing', 'from')
if scope == 'auto':
# ignore LUN not found, as name can be a group name
self.module.warn(msg + ". scope=%s, assuming 'application'" % scope)
if not app_modify:
self.na_helper.changed = False
elif scope == 'lun':
self.module.fail_json(msg=msg + ". scope=%s." % scope)
lun_cd_action = None
self.check_for_errors(lun_cd_action, current, lun_modify)
return lun_path, from_lun_path, lun_cd_action, lun_rename, lun_modify, app_modify_warning
def lun_modify_after_app_update(self, lun_path, results):
# modify at LUN level, as app modify does not set some LUN level options (eg space_reserve)
if lun_path is None:
lun_path = self.get_lun_path_from_backend(self.parameters['name'])
current = self.get_lun(self.parameters['name'], lun_path)
self.set_uuid(current)
# we already handled rename if required
current.pop('name', None)
lun_modify = self.na_helper.get_modified_attributes(current, self.parameters)
if lun_modify:
results['lun_modify_after_app_update'] = dict(lun_modify)
self.check_for_errors(None, current, lun_modify)
return lun_modify
def apply(self):
results = {}
app_cd_action, app_modify, lun_cd_action, lun_modify, lun_rename = None, None, None, None, None
app_modify_warning, app_current, lun_path, from_lun_path = None, None, None, None
actions = []
if self.rest_app:
scope, app_current = self.get_app_apply()
else:
# no application template, fall back to LUN only
scope = 'lun'
if self.rest_app and scope != 'lun':
app_cd_action, app_modify, app_modify_warning = self.app_actions(app_current, scope, actions, results)
if app_cd_action is None and scope != 'application':
lun_path, from_lun_path, lun_cd_action, lun_rename, lun_modify, app_modify_warning = \
self.lun_actions(app_current, actions, results, scope, app_modify, app_modify_warning)
if self.na_helper.changed and not self.module.check_mode:
if app_cd_action == 'create':
self.create_san_application()
elif app_cd_action == 'convert':
self.convert_to_san_application(scope)
elif app_cd_action == 'delete':
self.rest_app.delete_application()
elif lun_cd_action == 'create':
self.create_lun()
elif lun_cd_action == 'delete':
self.delete_lun(lun_path)
else:
if app_modify:
self.modify_san_application(app_modify)
if lun_rename:
self.rename_lun(from_lun_path, lun_path)
if app_modify:
# space_reserve will be set to True
# To match input parameters, lun_modify is recomputed.
lun_modify = self.lun_modify_after_app_update(lun_path, results)
size_changed = False
if lun_modify and 'size' in lun_modify:
# Ensure that size was actually changed. Please
# read notes in 'resize_lun' function for details.
size_changed = self.resize_lun(lun_path)
lun_modify.pop('size')
if lun_modify:
self.modify_lun(lun_path, lun_modify)
if not lun_modify and not lun_rename and not app_modify:
# size may not have changed
self.na_helper.changed = size_changed
if app_modify_warning:
self.module.warn(app_modify_warning)
result = netapp_utils.generate_result(self.na_helper.changed, actions,
extra_responses={'debug': self.debug} if self.debug else None)
self.module.exit_json(**result)
def main():
lun = NetAppOntapLUN()
lun.apply()
if __name__ == '__main__':
main()