Adding Common files for NetApp ElementSW release (#43727)
* MVP2 Post ElementSW OSRB sync * Revert "MVP2 Post ElementSW OSRB sync" This reverts commit c13db2ad962cd56bffce052c2891c558a2240c72.
This commit is contained in:
parent
7a4517a067
commit
479408330e
5 changed files with 349 additions and 18 deletions
|
@ -33,6 +33,9 @@ from ansible.module_utils.six.moves.urllib.error import HTTPError
|
||||||
from ansible.module_utils.urls import open_url
|
from ansible.module_utils.urls import open_url
|
||||||
from ansible.module_utils.api import basic_auth_argument_spec
|
from ansible.module_utils.api import basic_auth_argument_spec
|
||||||
|
|
||||||
|
import os
|
||||||
|
import ssl
|
||||||
|
|
||||||
HAS_NETAPP_LIB = False
|
HAS_NETAPP_LIB = False
|
||||||
try:
|
try:
|
||||||
from netapp_lib.api.zapi import zapi
|
from netapp_lib.api.zapi import zapi
|
||||||
|
@ -81,7 +84,9 @@ def na_ontap_host_argument_spec():
|
||||||
hostname=dict(required=True, type='str'),
|
hostname=dict(required=True, type='str'),
|
||||||
username=dict(required=True, type='str', aliases=['user']),
|
username=dict(required=True, type='str', aliases=['user']),
|
||||||
password=dict(required=True, type='str', aliases=['pass'], no_log=True),
|
password=dict(required=True, type='str', aliases=['pass'], no_log=True),
|
||||||
https=dict(required=False, type='bool', default=False)
|
https=dict(required=False, type='bool', default=False),
|
||||||
|
validate_certs=dict(required=False, type='bool', default=True),
|
||||||
|
http_port=dict(required=False, type='int')
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@ -114,6 +119,8 @@ def setup_na_ontap_zapi(module, vserver=None):
|
||||||
username = module.params['username']
|
username = module.params['username']
|
||||||
password = module.params['password']
|
password = module.params['password']
|
||||||
https = module.params['https']
|
https = module.params['https']
|
||||||
|
validate_certs = module.params['validate_certs']
|
||||||
|
port = module.params['http_port']
|
||||||
|
|
||||||
if HAS_NETAPP_LIB:
|
if HAS_NETAPP_LIB:
|
||||||
# set up zapi
|
# set up zapi
|
||||||
|
@ -123,14 +130,22 @@ def setup_na_ontap_zapi(module, vserver=None):
|
||||||
if vserver:
|
if vserver:
|
||||||
server.set_vserver(vserver)
|
server.set_vserver(vserver)
|
||||||
# Todo : Replace hard-coded values with configurable parameters.
|
# Todo : Replace hard-coded values with configurable parameters.
|
||||||
server.set_api_version(major=1, minor=21)
|
server.set_api_version(major=1, minor=110)
|
||||||
# default is HTTP
|
# default is HTTP
|
||||||
if https is True:
|
if https:
|
||||||
server.set_port(443)
|
if port is None:
|
||||||
server.set_transport_type('HTTPS')
|
port = 443
|
||||||
|
transport_type = 'HTTPS'
|
||||||
|
# HACK to bypass certificate verification
|
||||||
|
if validate_certs is True:
|
||||||
|
if not os.environ.get('PYTHONHTTPSVERIFY', '') and getattr(ssl, '_create_unverified_context', None):
|
||||||
|
ssl._create_default_https_context = ssl._create_unverified_context
|
||||||
else:
|
else:
|
||||||
server.set_port(80)
|
if port is None:
|
||||||
server.set_transport_type('HTTP')
|
port = 80
|
||||||
|
transport_type = 'HTTP'
|
||||||
|
server.set_transport_type(transport_type)
|
||||||
|
server.set_port(port)
|
||||||
server.set_server_type('FILER')
|
server.set_server_type('FILER')
|
||||||
return server
|
return server
|
||||||
else:
|
else:
|
||||||
|
@ -150,7 +165,7 @@ def setup_ontap_zapi(module, vserver=None):
|
||||||
if vserver:
|
if vserver:
|
||||||
server.set_vserver(vserver)
|
server.set_vserver(vserver)
|
||||||
# Todo : Replace hard-coded values with configurable parameters.
|
# Todo : Replace hard-coded values with configurable parameters.
|
||||||
server.set_api_version(major=1, minor=21)
|
server.set_api_version(major=1, minor=110)
|
||||||
server.set_port(80)
|
server.set_port(80)
|
||||||
server.set_server_type('FILER')
|
server.set_server_type('FILER')
|
||||||
server.set_transport_type('HTTP')
|
server.set_transport_type('HTTP')
|
||||||
|
|
156
lib/ansible/module_utils/netapp_elementsw_module.py
Normal file
156
lib/ansible/module_utils/netapp_elementsw_module.py
Normal file
|
@ -0,0 +1,156 @@
|
||||||
|
# This code is part of Ansible, but is an independent component.
|
||||||
|
# This particular file snippet, and this file snippet only, is BSD licensed.
|
||||||
|
|
||||||
|
HAS_SF_SDK = False
|
||||||
|
try:
|
||||||
|
import solidfire.common
|
||||||
|
|
||||||
|
HAS_SF_SDK = True
|
||||||
|
except:
|
||||||
|
HAS_SF_SDK = False
|
||||||
|
|
||||||
|
|
||||||
|
def has_sf_sdk():
|
||||||
|
return HAS_SF_SDK
|
||||||
|
|
||||||
|
|
||||||
|
class NaElementSWModule(object):
|
||||||
|
|
||||||
|
def __init__(self, elem):
|
||||||
|
self.elem_connect = elem
|
||||||
|
self.parameters = dict()
|
||||||
|
|
||||||
|
def get_volume(self, volume_id):
|
||||||
|
"""
|
||||||
|
Return volume details if volume exists for given volume_id
|
||||||
|
|
||||||
|
:param volume_id: volume ID
|
||||||
|
:type volume_id: int
|
||||||
|
:return: Volume dict if found, None if not found
|
||||||
|
:rtype: dict
|
||||||
|
"""
|
||||||
|
volume_list = self.elem_connect.list_volumes(volume_ids=[volume_id])
|
||||||
|
for volume in volume_list.volumes:
|
||||||
|
if volume.volume_id == volume_id:
|
||||||
|
if str(volume.delete_time) == "":
|
||||||
|
return volume
|
||||||
|
return None
|
||||||
|
|
||||||
|
def get_volume_id(self, vol_name, account_id):
|
||||||
|
"""
|
||||||
|
Return volume id from the given (valid) account_id if found
|
||||||
|
Return None if not found
|
||||||
|
|
||||||
|
:param vol_name: Name of the volume
|
||||||
|
:type vol_name: str
|
||||||
|
:param account_id: Account ID
|
||||||
|
:type account_id: int
|
||||||
|
|
||||||
|
:return: Volume ID of the first matching volume if found. None if not found.
|
||||||
|
:rtype: int
|
||||||
|
"""
|
||||||
|
volume_list = self.elem_connect.list_volumes_for_account(account_id=account_id)
|
||||||
|
for volume in volume_list.volumes:
|
||||||
|
if volume.name == vol_name:
|
||||||
|
# return volume_id
|
||||||
|
if str(volume.delete_time) == "":
|
||||||
|
return volume.volume_id
|
||||||
|
return None
|
||||||
|
|
||||||
|
def volume_id_exists(self, volume_id):
|
||||||
|
"""
|
||||||
|
Return volume_id if volume exists for given volume_id
|
||||||
|
|
||||||
|
:param volume_id: volume ID
|
||||||
|
:type volume_id: int
|
||||||
|
:return: Volume ID if found, None if not found
|
||||||
|
:rtype: int
|
||||||
|
"""
|
||||||
|
volume_list = self.elem_connect.list_volumes(volume_ids=[volume_id])
|
||||||
|
for volume in volume_list.volumes:
|
||||||
|
if volume.volume_id == volume_id:
|
||||||
|
if str(volume.delete_time) == "":
|
||||||
|
return volume.volume_id
|
||||||
|
return None
|
||||||
|
|
||||||
|
def volume_exists(self, volume, account_id):
|
||||||
|
"""
|
||||||
|
Return volume_id if exists, None if not found
|
||||||
|
|
||||||
|
:param volume: Volume ID or Name
|
||||||
|
:type volume: str
|
||||||
|
:param account_id: Account ID (valid)
|
||||||
|
:type account_id: int
|
||||||
|
:return: Volume ID if found, None if not found
|
||||||
|
"""
|
||||||
|
# If volume is an integer, get_by_id
|
||||||
|
if str(volume).isdigit():
|
||||||
|
volume_id = int(volume)
|
||||||
|
try:
|
||||||
|
if self.volume_id_exists(volume_id):
|
||||||
|
return volume_id
|
||||||
|
except solidfire.common.ApiServerError:
|
||||||
|
# don't fail, continue and try get_by_name
|
||||||
|
pass
|
||||||
|
# get volume by name
|
||||||
|
volume_id = self.get_volume_id(volume, account_id)
|
||||||
|
return volume_id
|
||||||
|
|
||||||
|
def get_snapshot(self, snapshot_id, volume_id):
|
||||||
|
"""
|
||||||
|
Return snapshot details if found
|
||||||
|
|
||||||
|
:param snapshot_id: Snapshot ID or Name
|
||||||
|
:type snapshot_id: str
|
||||||
|
:param volume_id: Account ID (valid)
|
||||||
|
:type volume_id: int
|
||||||
|
:return: Snapshot dict if found, None if not found
|
||||||
|
:rtype: dict
|
||||||
|
"""
|
||||||
|
# mandate src_volume_id although not needed by sdk
|
||||||
|
snapshot_list = self.elem_connect.list_snapshots(
|
||||||
|
volume_id=volume_id)
|
||||||
|
for snapshot in snapshot_list.snapshots:
|
||||||
|
# if actual id is provided
|
||||||
|
if str(snapshot_id).isdigit() and snapshot.snapshot_id == int(snapshot_id):
|
||||||
|
return snapshot
|
||||||
|
# if snapshot name is provided
|
||||||
|
elif snapshot.name == snapshot_id:
|
||||||
|
return snapshot
|
||||||
|
return None
|
||||||
|
|
||||||
|
def account_exists(self, account):
|
||||||
|
"""
|
||||||
|
Return account_id if account exists for given account id or name
|
||||||
|
Raises an exception if account does not exist
|
||||||
|
|
||||||
|
:param account: Account ID or Name
|
||||||
|
:type account: str
|
||||||
|
:return: Account ID if found, None if not found
|
||||||
|
"""
|
||||||
|
# If account is an integer, get_by_id
|
||||||
|
if account.isdigit():
|
||||||
|
account_id = int(account)
|
||||||
|
try:
|
||||||
|
result = self.elem_connect.get_account_by_id(account_id=account_id)
|
||||||
|
if result.account.account_id == account_id:
|
||||||
|
return account_id
|
||||||
|
except solidfire.common.ApiServerError:
|
||||||
|
# don't fail, continue and try get_by_name
|
||||||
|
pass
|
||||||
|
# get account by name, the method returns an Exception if account doesn't exist
|
||||||
|
result = self.elem_connect.get_account_by_name(username=account)
|
||||||
|
return result.account.account_id
|
||||||
|
|
||||||
|
def set_element_attributes(self, source):
|
||||||
|
"""
|
||||||
|
Return telemetry attributes for the current execution
|
||||||
|
|
||||||
|
:param source: name of the module
|
||||||
|
:type source: str
|
||||||
|
:return: a dict containing telemetry attributes
|
||||||
|
"""
|
||||||
|
attributes = {}
|
||||||
|
attributes['config-mgmt'] = 'ansible'
|
||||||
|
attributes['event-source'] = source
|
||||||
|
return(attributes)
|
149
lib/ansible/module_utils/netapp_module.py
Normal file
149
lib/ansible/module_utils/netapp_module.py
Normal file
|
@ -0,0 +1,149 @@
|
||||||
|
# This code is part of Ansible, but is an independent component.
|
||||||
|
# This particular file snippet, and this file snippet only, is BSD licensed.
|
||||||
|
# Modules you write using this snippet, which is embedded dynamically by Ansible
|
||||||
|
# still belong to the author of the module, and may assign their own license
|
||||||
|
# to the complete work.
|
||||||
|
#
|
||||||
|
# Copyright (c) 2018, Laurent Nicolas <laurentn@netapp.com>
|
||||||
|
# All rights reserved.
|
||||||
|
#
|
||||||
|
# Redistribution and use in source and binary forms, with or without modification,
|
||||||
|
# are permitted provided that the following conditions are met:
|
||||||
|
#
|
||||||
|
# * Redistributions of source code must retain the above copyright
|
||||||
|
# notice, this list of conditions and the following disclaimer.
|
||||||
|
# * Redistributions in binary form must reproduce the above copyright notice,
|
||||||
|
# this list of conditions and the following disclaimer in the documentation
|
||||||
|
# and/or other materials provided with the distribution.
|
||||||
|
#
|
||||||
|
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
|
||||||
|
# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
||||||
|
# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
|
||||||
|
# IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
|
||||||
|
# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
|
||||||
|
# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
||||||
|
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
|
||||||
|
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE
|
||||||
|
# USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||||
|
|
||||||
|
''' Support class for NetApp ansible modules '''
|
||||||
|
|
||||||
|
|
||||||
|
def cmp(a, b):
|
||||||
|
"""
|
||||||
|
Python 3 does not have a cmp function, this will do the cmp.
|
||||||
|
:param a: first object to check
|
||||||
|
:param b: second object to check
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
|
return (a > b) - (a < b)
|
||||||
|
|
||||||
|
|
||||||
|
class NetAppModule(object):
|
||||||
|
'''
|
||||||
|
Common class for NetApp modules
|
||||||
|
set of support functions to derive actions based
|
||||||
|
on the current state of the system, and a desired state
|
||||||
|
'''
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self.log = list()
|
||||||
|
self.changed = False
|
||||||
|
self.parameters = {'name': 'not intialized'}
|
||||||
|
|
||||||
|
def set_parameters(self, ansible_params):
|
||||||
|
self.parameters = dict()
|
||||||
|
for param in ansible_params:
|
||||||
|
if ansible_params[param] is not None:
|
||||||
|
self.parameters[param] = ansible_params[param]
|
||||||
|
return self.parameters
|
||||||
|
|
||||||
|
def get_cd_action(self, current, desired):
|
||||||
|
''' takes a desired state and a current state, and return an action:
|
||||||
|
create, delete, None
|
||||||
|
eg:
|
||||||
|
is_present = 'absent'
|
||||||
|
some_object = self.get_object(source)
|
||||||
|
if some_object is not None:
|
||||||
|
is_present = 'present'
|
||||||
|
action = cd_action(current=is_present, desired = self.desired.state())
|
||||||
|
'''
|
||||||
|
if 'state' in desired:
|
||||||
|
desired_state = desired['state']
|
||||||
|
else:
|
||||||
|
desired_state = 'present'
|
||||||
|
|
||||||
|
if current is None and desired_state == 'absent':
|
||||||
|
return None
|
||||||
|
if current is not None and desired_state == 'present':
|
||||||
|
return None
|
||||||
|
# change in state
|
||||||
|
self.changed = True
|
||||||
|
if current is not None:
|
||||||
|
return 'delete'
|
||||||
|
return 'create'
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def check_keys(current, desired):
|
||||||
|
''' TODO: raise an error if keys do not match
|
||||||
|
with the exception of:
|
||||||
|
new_name, state in desired
|
||||||
|
'''
|
||||||
|
pass
|
||||||
|
|
||||||
|
def get_modified_attributes(self, current, desired):
|
||||||
|
''' takes two lists of attributes and return a list of attributes that are
|
||||||
|
not in the desired state
|
||||||
|
It is expected that all attributes of interest are listed in current and
|
||||||
|
desired.
|
||||||
|
|
||||||
|
NOTE: depending on the attribute, the caller may need to do a modify or a
|
||||||
|
different operation (eg move volume if the modified attribute is an
|
||||||
|
aggregate name)
|
||||||
|
'''
|
||||||
|
# if the object does not exist, we can't modify it
|
||||||
|
modified = dict()
|
||||||
|
if current is None:
|
||||||
|
return modified
|
||||||
|
|
||||||
|
# error out if keys do not match
|
||||||
|
self.check_keys(current, desired)
|
||||||
|
|
||||||
|
# collect changed attributes
|
||||||
|
for key, value in current.items():
|
||||||
|
if key in desired and desired[key] is not None:
|
||||||
|
if type(value) is list:
|
||||||
|
value.sort()
|
||||||
|
desired[key].sort()
|
||||||
|
if cmp(value, desired[key]) != 0:
|
||||||
|
modified[key] = desired[key]
|
||||||
|
if modified:
|
||||||
|
self.changed = True
|
||||||
|
return modified
|
||||||
|
|
||||||
|
def is_rename_action(self, source, target):
|
||||||
|
''' takes a source and target object, and returns True
|
||||||
|
if a rename is required
|
||||||
|
eg:
|
||||||
|
source = self.get_object(source_name)
|
||||||
|
target = self.get_object(target_name)
|
||||||
|
action = is_rename_action(source, target)
|
||||||
|
:return: None for error, True for rename action, False otherwise
|
||||||
|
'''
|
||||||
|
if source is None and target is None:
|
||||||
|
# error, do nothing
|
||||||
|
# cannot rename an non existent resource
|
||||||
|
# alternatively we could create B
|
||||||
|
return None
|
||||||
|
if source is not None and target is not None:
|
||||||
|
# error, do nothing
|
||||||
|
# idempotency (or) new_name_is_already_in_use
|
||||||
|
# alternatively we could delete B and rename A to B
|
||||||
|
return False
|
||||||
|
if source is None and target is not None:
|
||||||
|
# do nothing, maybe the rename was already done
|
||||||
|
return False
|
||||||
|
# source is not None and target is None:
|
||||||
|
# rename is in order
|
||||||
|
self.changed = True
|
||||||
|
return True
|
|
@ -39,7 +39,7 @@ options:
|
||||||
required: true
|
required: true
|
||||||
description:
|
description:
|
||||||
- This can be a Cluster-scoped or SVM-scoped account, depending on whether a Cluster-level or SVM-level API is required.
|
- This can be a Cluster-scoped or SVM-scoped account, depending on whether a Cluster-level or SVM-level API is required.
|
||||||
For more information, please read the documentation U(https://goo.gl/BRu78Z).
|
For more information, please read the documentation U(https://mysupport.netapp.com/NOW/download/software/nmsdk/9.4/).
|
||||||
aliases: ['user']
|
aliases: ['user']
|
||||||
password:
|
password:
|
||||||
required: true
|
required: true
|
||||||
|
@ -48,9 +48,20 @@ options:
|
||||||
aliases: ['pass']
|
aliases: ['pass']
|
||||||
https:
|
https:
|
||||||
description:
|
description:
|
||||||
- Enable and disabled https
|
- Enable and disable https
|
||||||
type: bool
|
type: bool
|
||||||
default: false
|
default: false
|
||||||
|
validate_certs:
|
||||||
|
description:
|
||||||
|
- If set to C(False), the SSL certificates will not be validated.
|
||||||
|
- This should only set to C(False) used on personally controlled sites using self-signed certificates.
|
||||||
|
default: true
|
||||||
|
type: bool
|
||||||
|
http_port:
|
||||||
|
description:
|
||||||
|
- Override the default port (80 or 443) with this port
|
||||||
|
type: int
|
||||||
|
|
||||||
|
|
||||||
requirements:
|
requirements:
|
||||||
- A physical or virtual clustered Data ONTAP system. The modules were developed with Clustered Data ONTAP 9.3
|
- A physical or virtual clustered Data ONTAP system. The modules were developed with Clustered Data ONTAP 9.3
|
||||||
|
@ -74,7 +85,7 @@ options:
|
||||||
required: true
|
required: true
|
||||||
description:
|
description:
|
||||||
- This can be a Cluster-scoped or SVM-scoped account, depending on whether a Cluster-level or SVM-level API is required.
|
- This can be a Cluster-scoped or SVM-scoped account, depending on whether a Cluster-level or SVM-level API is required.
|
||||||
For more information, please read the documentation U(https://goo.gl/BRu78Z).
|
For more information, please read the documentation U(https://mysupport.netapp.com/NOW/download/software/nmsdk/9.4/).
|
||||||
aliases: ['user']
|
aliases: ['user']
|
||||||
password:
|
password:
|
||||||
required: true
|
required: true
|
||||||
|
@ -101,17 +112,21 @@ options:
|
||||||
username:
|
username:
|
||||||
required: true
|
required: true
|
||||||
description:
|
description:
|
||||||
- Please ensure that the user has the adequate permissions. For more information, please read the official documentation U(https://goo.gl/ddJa4Q).
|
- Please ensure that the user has the adequate permissions. For more information, please read the official documentation
|
||||||
|
U(https://mysupport.netapp.com/documentation/docweb/index.html?productID=62636&language=en-US).
|
||||||
|
aliases: ['user']
|
||||||
password:
|
password:
|
||||||
required: true
|
required: true
|
||||||
description:
|
description:
|
||||||
- Password for the specified user.
|
- Password for the specified user.
|
||||||
|
aliases: ['pass']
|
||||||
|
|
||||||
requirements:
|
requirements:
|
||||||
- solidfire-sdk-python (1.1.0.92)
|
- The modules were developed with SolidFire 10.1
|
||||||
|
- solidfire-sdk-python (1.1.0.92) or greater. Install using 'pip install solidfire-sdk-python'
|
||||||
|
|
||||||
notes:
|
notes:
|
||||||
- The modules prefixed with C(sf\\_) are built to support the SolidFire storage platform.
|
- The modules prefixed with na\\_elementsw are built to support the SolidFire storage platform.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
|
@ -1126,11 +1126,7 @@ lib/ansible/modules/storage/netapp/netapp_e_volume_copy.py E323
|
||||||
lib/ansible/modules/storage/netapp/netapp_e_volume_copy.py E324
|
lib/ansible/modules/storage/netapp/netapp_e_volume_copy.py E324
|
||||||
lib/ansible/modules/storage/netapp/netapp_e_volume_copy.py E325
|
lib/ansible/modules/storage/netapp/netapp_e_volume_copy.py E325
|
||||||
lib/ansible/modules/storage/netapp/netapp_e_volume_copy.py E326
|
lib/ansible/modules/storage/netapp/netapp_e_volume_copy.py E326
|
||||||
lib/ansible/modules/storage/netapp/sf_account_manager.py E322
|
|
||||||
lib/ansible/modules/storage/netapp/sf_check_connections.py E322
|
|
||||||
lib/ansible/modules/storage/netapp/sf_snapshot_schedule_manager.py E322
|
|
||||||
lib/ansible/modules/storage/netapp/sf_snapshot_schedule_manager.py E325
|
lib/ansible/modules/storage/netapp/sf_snapshot_schedule_manager.py E325
|
||||||
lib/ansible/modules/storage/netapp/sf_volume_access_group_manager.py E322
|
|
||||||
lib/ansible/modules/storage/netapp/sf_volume_manager.py E322
|
lib/ansible/modules/storage/netapp/sf_volume_manager.py E322
|
||||||
lib/ansible/modules/storage/netapp/sf_volume_manager.py E325
|
lib/ansible/modules/storage/netapp/sf_volume_manager.py E325
|
||||||
lib/ansible/modules/storage/purestorage/purefb_fs.py E324
|
lib/ansible/modules/storage/purestorage/purefb_fs.py E324
|
||||||
|
|
Loading…
Reference in a new issue