summaryrefslogtreecommitdiffstats
path: root/ansible_collections/netapp/azure/plugins
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-13 12:04:41 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-13 12:04:41 +0000
commit975f66f2eebe9dadba04f275774d4ab83f74cf25 (patch)
tree89bd26a93aaae6a25749145b7e4bca4a1e75b2be /ansible_collections/netapp/azure/plugins
parentInitial commit. (diff)
downloadansible-975f66f2eebe9dadba04f275774d4ab83f74cf25.tar.xz
ansible-975f66f2eebe9dadba04f275774d4ab83f74cf25.zip
Adding upstream version 7.7.0+dfsg.upstream/7.7.0+dfsg
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'ansible_collections/netapp/azure/plugins')
-rw-r--r--ansible_collections/netapp/azure/plugins/doc_fragments/azure.py129
-rw-r--r--ansible_collections/netapp/azure/plugins/doc_fragments/azure_tags.py31
-rw-r--r--ansible_collections/netapp/azure/plugins/doc_fragments/netapp.py43
-rw-r--r--ansible_collections/netapp/azure/plugins/module_utils/azure_rm_netapp_common.py156
-rw-r--r--ansible_collections/netapp/azure/plugins/module_utils/netapp_module.py271
-rw-r--r--ansible_collections/netapp/azure/plugins/modules/azure_rm_netapp_account.py404
-rw-r--r--ansible_collections/netapp/azure/plugins/modules/azure_rm_netapp_capacity_pool.py259
-rw-r--r--ansible_collections/netapp/azure/plugins/modules/azure_rm_netapp_snapshot.py226
-rw-r--r--ansible_collections/netapp/azure/plugins/modules/azure_rm_netapp_volume.py399
9 files changed, 1918 insertions, 0 deletions
diff --git a/ansible_collections/netapp/azure/plugins/doc_fragments/azure.py b/ansible_collections/netapp/azure/plugins/doc_fragments/azure.py
new file mode 100644
index 000000000..49467db70
--- /dev/null
+++ b/ansible_collections/netapp/azure/plugins/doc_fragments/azure.py
@@ -0,0 +1,129 @@
+# -*- coding: utf-8 -*-
+
+# Copyright: (c) 2016 Matt Davis, <mdavis@ansible.com>
+# Copyright: (c) 2016 Chris Houseknecht, <house@redhat.com>
+# 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
+
+
+class ModuleDocFragment(object):
+
+ # Azure doc fragment
+ DOCUMENTATION = r'''
+
+options:
+ ad_user:
+ description:
+ - Active Directory username. Use when authenticating with an Active Directory user rather than service
+ principal.
+ type: str
+ password:
+ description:
+ - Active Directory user password. Use when authenticating with an Active Directory user rather than service
+ principal.
+ type: str
+ profile:
+ description:
+ - Security profile found in ~/.azure/credentials file.
+ type: str
+ subscription_id:
+ description:
+ - Your Azure subscription Id.
+ type: str
+ client_id:
+ description:
+ - Azure client ID. Use when authenticating with a Service Principal.
+ type: str
+ secret:
+ description:
+ - Azure client secret. Use when authenticating with a Service Principal.
+ type: str
+ tenant:
+ description:
+ - Azure tenant ID. Use when authenticating with a Service Principal.
+ type: str
+ cloud_environment:
+ description:
+ - For cloud environments other than the US public cloud, the environment name (as defined by Azure Python SDK, eg, C(AzureChinaCloud),
+ C(AzureUSGovernment)), or a metadata discovery endpoint URL (required for Azure Stack). Can also be set via credential file profile or
+ the C(AZURE_CLOUD_ENVIRONMENT) environment variable.
+ type: str
+ default: AzureCloud
+ version_added: '0.0.1'
+ adfs_authority_url:
+ description:
+ - Azure AD authority url. Use when authenticating with Username/password, and has your own ADFS authority.
+ type: str
+ version_added: '0.0.1'
+ cert_validation_mode:
+ description:
+ - Controls the certificate validation behavior for Azure endpoints. By default, all modules will validate the server certificate, but
+ when an HTTPS proxy is in use, or against Azure Stack, it may be necessary to disable this behavior by passing C(ignore). Can also be
+ set via credential file profile or the C(AZURE_CERT_VALIDATION) environment variable.
+ type: str
+ choices: [ ignore, validate ]
+ version_added: '0.0.1'
+ auth_source:
+ description:
+ - Controls the source of the credentials to use for authentication.
+ - Can also be set via the C(ANSIBLE_AZURE_AUTH_SOURCE) environment variable.
+ - When set to C(auto) (the default) the precedence is module parameters -> C(env) -> C(credential_file) -> C(cli).
+ - When set to C(env), the credentials will be read from the environment variables
+ - When set to C(credential_file), it will read the profile from C(~/.azure/credentials).
+ - When set to C(cli), the credentials will be sources from the Azure CLI profile. C(subscription_id) or the environment variable
+ C(AZURE_SUBSCRIPTION_ID) can be used to identify the subscription ID if more than one is present otherwise the default
+ az cli subscription is used.
+ - When set to C(msi), the host machine must be an azure resource with an enabled MSI extension. C(subscription_id) or the
+ environment variable C(AZURE_SUBSCRIPTION_ID) can be used to identify the subscription ID if the resource is granted
+ access to more than one subscription, otherwise the first subscription is chosen.
+ - The C(msi) was added in Ansible 2.6.
+ type: str
+ default: auto
+ choices:
+ - auto
+ - cli
+ - credential_file
+ - env
+ - msi
+ version_added: '0.0.1'
+ api_profile:
+ description:
+ - Selects an API profile to use when communicating with Azure services. Default value of C(latest) is appropriate for public clouds;
+ future values will allow use with Azure Stack.
+ type: str
+ default: latest
+ version_added: '0.0.1'
+ log_path:
+ description:
+ - Parent argument.
+ type: str
+ log_mode:
+ description:
+ - Parent argument.
+ type: str
+requirements:
+ - python >= 2.7
+ - The host that executes this module must have the azure.azcollection collection installed via galaxy
+ - All python packages listed in collection's requirements-azure.txt must be installed via pip on the host that executes modules from azure.azcollection
+ - Full installation instructions may be found https://galaxy.ansible.com/azure/azcollection
+
+notes:
+ - For authentication with Azure you can pass parameters, set environment variables, use a profile stored
+ in ~/.azure/credentials, or log in before you run your tasks or playbook with C(az login).
+ - Authentication is also possible using a service principal or Active Directory user.
+ - To authenticate via service principal, pass subscription_id, client_id, secret and tenant or set environment
+ variables AZURE_SUBSCRIPTION_ID, AZURE_CLIENT_ID, AZURE_SECRET and AZURE_TENANT.
+ - To authenticate via Active Directory user, pass ad_user and password, or set AZURE_AD_USER and
+ AZURE_PASSWORD in the environment.
+ - "Alternatively, credentials can be stored in ~/.azure/credentials. This is an ini file containing
+ a [default] section and the following keys: subscription_id, client_id, secret and tenant or
+ subscription_id, ad_user and password. It is also possible to add additional profiles. Specify the profile
+ by passing profile or setting AZURE_PROFILE in the environment."
+
+seealso:
+ - name: Sign in with Azure CLI
+ link: https://docs.microsoft.com/en-us/cli/azure/authenticate-azure-cli?view=azure-cli-latest
+ description: How to authenticate using the C(az login) command.
+ '''
diff --git a/ansible_collections/netapp/azure/plugins/doc_fragments/azure_tags.py b/ansible_collections/netapp/azure/plugins/doc_fragments/azure_tags.py
new file mode 100644
index 000000000..8edb80eed
--- /dev/null
+++ b/ansible_collections/netapp/azure/plugins/doc_fragments/azure_tags.py
@@ -0,0 +1,31 @@
+# -*- coding: utf-8 -*-
+
+# Copyright: (c) 2016, Matt Davis, <mdavis@ansible.com>
+# Copyright: (c) 2016, Chris Houseknecht, <house@redhat.com>
+# 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
+
+
+class ModuleDocFragment(object):
+
+ # Azure doc fragment
+ DOCUMENTATION = r'''
+options:
+ tags:
+ description:
+ - Dictionary of string:string pairs to assign as metadata to the object.
+ - Metadata tags on the object will be updated with any provided values.
+ - To remove tags set append_tags option to false.
+ - Currently, Azure DNS zones and Traffic Manager services also don't allow the use of spaces in the tag.
+ - Azure Front Door doesn't support the use of # in the tag name.
+ - Azure Automation and Azure CDN only support 15 tags on resources.
+ type: dict
+ append_tags:
+ description:
+ - Use to control if tags field is canonical or just appends to existing tags.
+ - When canonical, any tags not found in the tags parameter will be removed from the object's metadata.
+ type: bool
+ default: yes
+ '''
diff --git a/ansible_collections/netapp/azure/plugins/doc_fragments/netapp.py b/ansible_collections/netapp/azure/plugins/doc_fragments/netapp.py
new file mode 100644
index 000000000..18e9cc2a2
--- /dev/null
+++ b/ansible_collections/netapp/azure/plugins/doc_fragments/netapp.py
@@ -0,0 +1,43 @@
+# -*- coding: utf-8 -*-
+
+# Copyright: (c) 2019, NetApp Ansible Team ng-ansibleteam@netapp.com
+# 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
+
+
+class ModuleDocFragment(object):
+
+ DOCUMENTATION = r'''
+options:
+ - See respective platform section for more details
+requirements:
+ - See respective platform section for more details
+notes:
+ - Ansible modules are available for the following NetApp Storage Platforms: E-Series, ONTAP, SolidFire
+'''
+
+ # Documentation fragment for Cloud Volume Services on Azure NetApp (azure_rm_netapp)
+ AZURE_RM_NETAPP = r'''
+options:
+ resource_group:
+ description:
+ - Name of the resource group.
+ required: true
+ type: str
+requirements:
+ - python >= 2.7
+ - azure >= 2.0.0
+ - Python azure-mgmt. Install using 'pip install azure-mgmt'
+ - Python azure-mgmt-netapp. Install using 'pip install azure-mgmt-netapp'
+ - For authentication with Azure NetApp log in before you run your tasks or playbook with C(az login).
+
+notes:
+ - The modules prefixed with azure_rm_netapp are built to support the Cloud Volume Services for Azure NetApp Files.
+
+seealso:
+ - name: Sign in with Azure CLI
+ link: https://docs.microsoft.com/en-us/cli/azure/authenticate-azure-cli?view=azure-cli-latest
+ description: How to authenticate using the C(az login) command.
+ '''
diff --git a/ansible_collections/netapp/azure/plugins/module_utils/azure_rm_netapp_common.py b/ansible_collections/netapp/azure/plugins/module_utils/azure_rm_netapp_common.py
new file mode 100644
index 000000000..716e4fb95
--- /dev/null
+++ b/ansible_collections/netapp/azure/plugins/module_utils/azure_rm_netapp_common.py
@@ -0,0 +1,156 @@
+# (c) 2019, NetApp, Inc
+# GNU General Public License v3.0+
+# (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+'''
+azure_rm_netapp_common
+Wrapper around AzureRMModuleBase base class
+'''
+
+from __future__ import (absolute_import, division, print_function)
+__metaclass__ = type
+
+import sys
+
+HAS_AZURE_COLLECTION = True
+NEW_STYLE = None
+COLLECTION_VERSION = "21.10.0"
+IMPORT_ERRORS = []
+SDK_VERSION = "0.0.0"
+
+if 'pytest' in sys.modules:
+ from ansible_collections.netapp.azure.plugins.module_utils.netapp_module import AzureRMModuleBaseMock as AzureRMModuleBase
+else:
+ try:
+ from ansible_collections.azure.azcollection.plugins.module_utils.azure_rm_common import AzureRMModuleBase
+ except ImportError as exc:
+ IMPORT_ERRORS.append(str(exc))
+ HAS_AZURE_COLLECTION = False
+ except SyntaxError as exc:
+ # importing Azure collection fails with python 2.6
+ if sys.version_info < (2, 8):
+ IMPORT_ERRORS.append(str(exc))
+ from ansible_collections.netapp.azure.plugins.module_utils.netapp_module import AzureRMModuleBaseMock as AzureRMModuleBase
+ HAS_AZURE_COLLECTION = False
+ else:
+ raise
+
+try:
+ from azure.mgmt.netapp import NetAppManagementClient # 1.0.0 or newer
+ NEW_STYLE = True
+except ImportError as exc:
+ IMPORT_ERRORS.append(str(exc))
+ try:
+ from azure.mgmt.netapp import AzureNetAppFilesManagementClient # 0.10.0 or older
+ NEW_STYLE = False
+ except ImportError as exc:
+ IMPORT_ERRORS.append(str(exc))
+
+try:
+ from azure.mgmt.netapp import VERSION as SDK_VERSION
+except ImportError as exc:
+ IMPORT_ERRORS.append(str(exc))
+
+
+class AzureRMNetAppModuleBase(AzureRMModuleBase):
+ ''' Wrapper around AzureRMModuleBase base class '''
+ def __init__(self, derived_arg_spec, required_if=None, supports_check_mode=False, supports_tags=True):
+ self._netapp_client = None
+ self._new_style = NEW_STYLE
+ self._sdk_version = SDK_VERSION
+ super(AzureRMNetAppModuleBase, self).__init__(derived_arg_spec=derived_arg_spec,
+ required_if=required_if,
+ supports_check_mode=supports_check_mode,
+ supports_tags=supports_tags)
+ if not HAS_AZURE_COLLECTION:
+ self.fail_when_import_errors(IMPORT_ERRORS)
+
+ def get_mgmt_svc_client(self, client_type, base_url=None, api_version=None):
+ if not self._new_style:
+ return super(AzureRMNetAppModuleBase, self).get_mgmt_svc_client(client_type, base_url, api_version)
+ self.log('Getting management service client NetApp {0}'.format(client_type.__name__))
+ self.check_client_version(client_type)
+
+ if not base_url:
+ # most things are resource_manager, don't make everyone specify
+ base_url = self.azure_auth._cloud_environment.endpoints.resource_manager
+
+ client_kwargs = dict(credential=self.azure_auth.azure_credentials, subscription_id=self.azure_auth.subscription_id, base_url=base_url)
+
+ return client_type(**client_kwargs)
+
+ @property
+ def netapp_client(self):
+ self.log('Getting netapp client')
+ if self._new_style is None:
+ # note that we always have at least one import error
+ self.fail_when_import_errors(IMPORT_ERRORS)
+ if self._netapp_client is None:
+ if self._new_style:
+ self._netapp_client = self.get_mgmt_svc_client(NetAppManagementClient)
+ else:
+ self._netapp_client = self.get_mgmt_svc_client(AzureNetAppFilesManagementClient,
+ base_url=self._cloud_environment.endpoints.resource_manager,
+ api_version='2018-05-01')
+ return self._netapp_client
+
+ @property
+ def new_style(self):
+ return self._new_style
+
+ @property
+ def sdk_version(self):
+ return self._sdk_version
+
+ def get_method(self, category, name):
+ try:
+ methods = getattr(self.netapp_client, category)
+ except AttributeError as exc:
+ self.module.fail_json('Error: category %s not found for netapp_client: %s' % (category, str(exc)))
+
+ if self._new_style:
+ name = 'begin_' + name
+ try:
+ method = getattr(methods, name)
+ except AttributeError as exc:
+ self.module.fail_json('Error: method %s not found for netapp_client category: %s - %s' % (name, category, str(exc)))
+ return method
+
+ def fail_when_import_errors(self, import_errors, has_azure_mgmt_netapp=True):
+ if has_azure_mgmt_netapp and not import_errors:
+ return
+ msg = ''
+ if not has_azure_mgmt_netapp:
+ msg = "The python azure-mgmt-netapp package is required. "
+ if hasattr(self, 'module'):
+ msg += 'Import errors: %s' % str(import_errors)
+ self.module.fail_json(msg=msg)
+ msg += str(import_errors)
+ raise ImportError(msg)
+
+ def has_feature(self, feature_name):
+ feature = self.get_feature(feature_name)
+ if isinstance(feature, bool):
+ return feature
+ self.module.fail_json(msg="Error: expected bool type for feature flag: %s" % feature_name)
+
+ def get_feature(self, feature_name):
+ ''' if the user has configured the feature, use it
+ otherwise, use our default
+ '''
+ default_flags = dict(
+ # TODO: review need for these
+ # trace_apis=False, # if true, append REST requests/responses to /tmp/azure_apis.log
+ # check_required_params_for_none=True,
+ # deprecation_warning=True,
+ # show_modified=True,
+ #
+ # preview features in ANF
+ ignore_change_ownership_mode=True
+ )
+
+ if self.parameters.get('feature_flags') is not None and feature_name in self.parameters['feature_flags']:
+ return self.parameters['feature_flags'][feature_name]
+ if feature_name in default_flags:
+ return default_flags[feature_name]
+ self.module.fail_json(msg="Internal error: unexpected feature flag: %s" % feature_name)
diff --git a/ansible_collections/netapp/azure/plugins/module_utils/netapp_module.py b/ansible_collections/netapp/azure/plugins/module_utils/netapp_module.py
new file mode 100644
index 000000000..9ee758c01
--- /dev/null
+++ b/ansible_collections/netapp/azure/plugins/module_utils/netapp_module.py
@@ -0,0 +1,271 @@
+# 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 '''
+
+from __future__ import (absolute_import, division, print_function)
+__metaclass__ = type
+
+from ansible.module_utils import basic
+
+
+class AzureRMModuleBaseMock():
+ ''' Mock for sanity tests when azcollection is not installed '''
+ def __init__(self, derived_arg_spec, required_if=None, supports_check_mode=False, supports_tags=True, **kwargs):
+ if supports_tags:
+ derived_arg_spec.update(dict(tags=dict()))
+ self.module = basic.AnsibleModule(
+ argument_spec=derived_arg_spec,
+ required_if=required_if,
+ supports_check_mode=supports_check_mode
+ )
+ self.module.warn('Running in Unit Test context!')
+ # the following is done in exec_module()
+ self.parameters = dict([item for item in self.module.params.items() if item[1] is not None])
+ # remove values with a default of None (not required)
+ self.module_arg_spec = dict([item for item in self.module_arg_spec.items() if item[0] in self.parameters])
+
+ def update_tags(self, tags):
+ self.module.log('update_tags called with:', tags)
+ return None, None
+
+
+def cmp(obj1, obj2):
+ """
+ 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:
+ """
+ # convert to lower case for string comparison.
+ if obj1 is None:
+ return -1
+ if isinstance(obj1, str) and isinstance(obj2, str):
+ obj1 = obj1.lower()
+ obj2 = obj2.lower()
+ # if list has string element, convert string to lower case.
+ if isinstance(obj1, list) and isinstance(obj2, list):
+ obj1 = [x.lower() if isinstance(x, str) else x for x in obj1]
+ obj2 = [x.lower() if isinstance(x, str) else x for x in obj2]
+ obj1.sort()
+ obj2.sort()
+ if isinstance(obj1, dict) and isinstance(obj2, dict):
+ return 0 if obj1 == obj2 else 1
+ return (obj1 > obj2) - (obj1 < obj2)
+
+
+class NetAppModule():
+ '''
+ 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 = []
+ self.changed = False
+ self.parameters = {'name': 'not intialized'}
+ self.zapi_string_keys = dict()
+ self.zapi_bool_keys = dict()
+ self.zapi_list_keys = {}
+ self.zapi_int_keys = {}
+ self.zapi_required = {}
+
+ def set_parameters(self, ansible_params):
+ self.parameters = {}
+ 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())
+ '''
+ desired_state = desired['state'] if 'state' in desired else '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'
+
+ def compare_and_update_values(self, current, desired, keys_to_compare):
+ updated_values = {}
+ is_changed = False
+ for key in keys_to_compare:
+ if key in current:
+ if key in desired and desired[key] is not None:
+ if current[key] != desired[key]:
+ updated_values[key] = desired[key]
+ is_changed = True
+ else:
+ updated_values[key] = current[key]
+ else:
+ updated_values[key] = current[key]
+
+ return updated_values, is_changed
+
+ @staticmethod
+ def check_keys(current, desired):
+ ''' TODO: raise an error if keys do not match
+ with the exception of:
+ new_name, state in desired
+ '''
+
+ @staticmethod
+ def compare_lists(current, desired, get_list_diff):
+ ''' compares two lists and return a list of elements that are either the desired elements or elements that are
+ modified from the current state depending on the get_list_diff flag
+ :param: current: current item attribute in ONTAP
+ :param: desired: attributes from playbook
+ :param: get_list_diff: specifies whether to have a diff of desired list w.r.t current list for an attribute
+ :return: list of attributes to be modified
+ :rtype: list
+ '''
+ desired_diff_list = [item for item in desired if item not in current] # get what in desired and not in current
+ current_diff_list = [item for item in current if item not in desired] # get what in current but not in desired
+
+ if desired_diff_list or current_diff_list:
+ # there are changes
+ if get_list_diff:
+ return desired_diff_list
+ else:
+ return desired
+ else:
+ return []
+
+ def get_modified_attributes(self, current, desired, get_list_diff=False):
+ ''' takes two dicts of attributes and return a dict of attributes that are
+ not in the current state
+ It is expected that all attributes of interest are listed in current and
+ desired.
+ :param: current: current attributes in ONTAP
+ :param: desired: attributes from playbook
+ :param: get_list_diff: specifies whether to have a diff of desired list w.r.t current list for an attribute
+ :return: dict of attributes to be modified
+ :rtype: dict
+
+ 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 = {}
+ 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 isinstance(value, list):
+ modified_list = self.compare_lists(value, desired[key], get_list_diff) # get modified list from current and desired
+ if modified_list:
+ modified[key] = modified_list
+ elif 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:
+ # 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
+
+ def filter_out_none_entries(self, list_or_dict):
+ """take a dict or list as input and return a dict/list without keys/elements whose values are None
+ skip empty dicts or lists.
+ """
+
+ if isinstance(list_or_dict, dict):
+ result = {}
+ for key, value in list_or_dict.items():
+ if isinstance(value, (list, dict)):
+ sub = self.filter_out_none_entries(value)
+ if sub:
+ # skip empty dict or list
+ result[key] = sub
+ elif value is not None:
+ # skip None value
+ result[key] = value
+ return result
+
+ if isinstance(list_or_dict, list):
+ alist = []
+ for item in list_or_dict:
+ if isinstance(item, (list, dict)):
+ sub = self.filter_out_none_entries(item)
+ if sub:
+ # skip empty dict or list
+ alist.append(sub)
+ elif item is not None:
+ # skip None value
+ alist.append(item)
+ return alist
+
+ raise TypeError('unexpected type %s' % type(list_or_dict))
+
+ @staticmethod
+ def get_not_none_values_from_dict(parameters, keys):
+ # python 2.6 does not support dict comprehension using k: v
+ return dict((key, value) for key, value in parameters.items() if key in keys and value is not None)
diff --git a/ansible_collections/netapp/azure/plugins/modules/azure_rm_netapp_account.py b/ansible_collections/netapp/azure/plugins/modules/azure_rm_netapp_account.py
new file mode 100644
index 000000000..c09ade0df
--- /dev/null
+++ b/ansible_collections/netapp/azure/plugins/modules/azure_rm_netapp_account.py
@@ -0,0 +1,404 @@
+#!/usr/bin/python
+#
+# (c) 2019, NetApp, Inc
+# GNU General Public License v3.0+
+# (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+'''
+azure_rm_netapp_account
+'''
+
+from __future__ import absolute_import, division, print_function
+__metaclass__ = type
+
+DOCUMENTATION = '''
+---
+module: azure_rm_netapp_account
+
+short_description: Manage NetApp Azure Files Account
+version_added: 19.10.0
+author: NetApp Ansible Team (@carchi8py) <ng-ansibleteam@netapp.com>
+
+description:
+ - Create and delete NetApp Azure account.
+ Provide the Resource group name for the NetApp account to be created.
+extends_documentation_fragment:
+ - netapp.azure.azure
+ - netapp.azure.azure_tags
+ - netapp.azure.netapp.azure_rm_netapp
+
+options:
+ name:
+ description:
+ - The name of the NetApp account.
+ required: true
+ type: str
+ location:
+ description:
+ - Resource location.
+ - Required for create.
+ type: str
+
+ active_directories:
+ description:
+ - list of active directory dictionaries.
+ - The list is currently limited to a single active directory (ANF or Azure limit of one AD per subscription).
+ type: list
+ elements: dict
+ version_added: 21.2.0
+ suboptions:
+ active_directory_id:
+ description: not used for create. Not needed for join.
+ type: str
+ dns:
+ description: list of DNS addresses. Required for create or join.
+ type: list
+ elements: str
+ domain:
+ description: Fully Qualified Active Directory DNS Domain Name. Required for create or join.
+ type: str
+ site:
+ description: The Active Directory site the service will limit Domain Controller discovery to.
+ type: str
+ smb_server_name:
+ description: Prefix for creating the SMB server's computer account name in the Active Directory domain. Required for create or join.
+ type: str
+ organizational_unit:
+ description: LDAP Path for the Organization Unit where SMB Server machine accounts will be created (i.e. OU=SecondLevel,OU=FirstLevel).
+ type: str
+ username:
+ description: Credentials that have permissions to create SMB server machine account in the AD domain. Required for create or join.
+ type: str
+ password:
+ description: see username. If password is present, the module is not idempotent, as we cannot check the current value. Required for create or join.
+ type: str
+ aes_encryption:
+ description: If enabled, AES encryption will be enabled for SMB communication.
+ type: bool
+ ldap_signing:
+ description: Specifies whether or not the LDAP traffic needs to be signed.
+ type: bool
+ ad_name:
+ description: Name of the active directory machine. Used only while creating kerberos volume.
+ type: str
+ version_added: 21.3.0
+ kdc_ip:
+ description: kdc server IP addresses for the active directory machine. Used only while creating kerberos volume.
+ type: str
+ version_added: 21.3.0
+ server_root_ca_certificate:
+ description:
+ - When LDAP over SSL/TLS is enabled, the LDAP client is required to have base64 encoded Active Directory Certificate Service's
+ self-signed root CA certificate, this optional parameter is used only for dual protocol with LDAP user-mapping volumes.
+ type: str
+ version_added: 21.3.0
+ state:
+ description:
+ - State C(present) will check that the NetApp account exists with the requested configuration.
+ - State C(absent) will delete the NetApp account.
+ default: present
+ choices:
+ - absent
+ - present
+ type: str
+ debug:
+ description: output details about current account if it exists.
+ type: bool
+ default: false
+
+'''
+EXAMPLES = '''
+
+- name: Create NetApp Azure Account
+ netapp.azure.azure_rm_netapp_account:
+ resource_group: myResourceGroup
+ name: testaccount
+ location: eastus
+ tags: {'abc': 'xyz', 'cba': 'zyx'}
+
+- name: Modify Azure NetApp account (Join AD)
+ netapp.azure.azure_rm_netapp_account:
+ resource_group: myResourceGroup
+ name: testaccount
+ location: eastus
+ active_directories:
+ - site: ln
+ dns: 10.10.10.10
+ domain: domain.com
+ smb_server_name: dummy
+ password: xxxxxx
+ username: laurentn
+
+- name: Delete NetApp Azure Account
+ netapp.azure.azure_rm_netapp_account:
+ state: absent
+ resource_group: myResourceGroup
+ name: testaccount
+ location: eastus
+
+- name: Create Azure NetApp account (with AD)
+ netapp.azure.azure_rm_netapp_account:
+ resource_group: laurentngroupnodash
+ name: tests-netapp11
+ location: eastus
+ tags:
+ creator: laurentn
+ use: Ansible
+ active_directories:
+ - site: ln
+ dns: 10.10.10.10
+ domain: domain.com
+ smb_server_name: dummy
+ password: xxxxxx
+ username: laurentn
+'''
+
+RETURN = '''
+'''
+
+import traceback
+
+HAS_AZURE_MGMT_NETAPP = False
+IMPORT_ERRORS = list()
+
+try:
+ from msrestazure.azure_exceptions import CloudError
+ from azure.core.exceptions import AzureError, ResourceNotFoundError
+except ImportError as exc:
+ IMPORT_ERRORS.append(str(exc))
+
+try:
+ from azure.mgmt.netapp.models import NetAppAccount, NetAppAccountPatch, ActiveDirectory
+ HAS_AZURE_MGMT_NETAPP = True
+except ImportError as exc:
+ IMPORT_ERRORS.append(str(exc))
+
+from ansible.module_utils.basic import to_native
+from ansible_collections.netapp.azure.plugins.module_utils.azure_rm_netapp_common import AzureRMNetAppModuleBase
+from ansible_collections.netapp.azure.plugins.module_utils.netapp_module import NetAppModule
+
+
+class AzureRMNetAppAccount(AzureRMNetAppModuleBase):
+ ''' create, modify, delete account, including joining AD domain
+ '''
+ def __init__(self):
+
+ self.module_arg_spec = dict(
+ resource_group=dict(type='str', required=True),
+ name=dict(type='str', required=True),
+ location=dict(type='str', required=False),
+ state=dict(choices=['present', 'absent'], default='present', type='str'),
+ active_directories=dict(type='list', elements='dict', options=dict(
+ active_directory_id=dict(type='str'),
+ dns=dict(type='list', elements='str'),
+ domain=dict(type='str'),
+ site=dict(type='str'),
+ smb_server_name=dict(type='str'),
+ organizational_unit=dict(type='str'),
+ username=dict(type='str'),
+ password=dict(type='str', no_log=True),
+ aes_encryption=dict(type='bool'),
+ ldap_signing=dict(type='bool'),
+ ad_name=dict(type='str'),
+ kdc_ip=dict(type='str'),
+ server_root_ca_certificate=dict(type='str', no_log=True),
+ )),
+ debug=dict(type='bool', default=False)
+ )
+
+ self.na_helper = NetAppModule()
+ self.parameters = dict()
+ self.debug = list()
+ self.warnings = list()
+
+ # import errors are handled in AzureRMModuleBase
+ super(AzureRMNetAppAccount, self).__init__(derived_arg_spec=self.module_arg_spec,
+ required_if=[('state', 'present', ['location'])],
+ supports_check_mode=True)
+
+ def get_azure_netapp_account(self):
+ """
+ Returns NetApp Account object for an existing account
+ Return None if account does not exist
+ """
+ try:
+ account_get = self.netapp_client.accounts.get(self.parameters['resource_group'], self.parameters['name'])
+ except (CloudError, ResourceNotFoundError): # account does not exist
+ return None
+ account = vars(account_get)
+ ads = None
+ if account.get('active_directories') is not None:
+ ads = list()
+ for each_ad in account.get('active_directories'):
+ ad_dict = vars(each_ad)
+ dns = ad_dict.get('dns')
+ if dns is not None:
+ ad_dict['dns'] = sorted(dns.split(','))
+ ads.append(ad_dict)
+ account['active_directories'] = ads
+ return account
+
+ def create_account_request_body(self, modify=None):
+ """
+ Create an Azure NetApp Account Request Body
+ :return: None
+ """
+ options = dict()
+ location = None
+ for attr in ('location', 'tags', 'active_directories'):
+ value = self.parameters.get(attr)
+ if attr == 'location' and modify is None:
+ location = value
+ continue
+ if value is not None:
+ if modify is None or attr in modify:
+ if attr == 'active_directories':
+ ads = list()
+ for ad_dict in value:
+ if ad_dict.get('dns') is not None:
+ # API expects a string of comma separated elements
+ ad_dict['dns'] = ','.join(ad_dict['dns'])
+ ads.append(ActiveDirectory(**self.na_helper.filter_out_none_entries(ad_dict)))
+ value = ads
+ options[attr] = value
+ if modify is None:
+ if location is None:
+ self.module.fail_json(msg="Error: 'location' is a required parameter")
+ return NetAppAccount(location=location, **options)
+ return NetAppAccountPatch(**options)
+
+ def create_azure_netapp_account(self):
+ """
+ Create an Azure NetApp Account
+ :return: None
+ """
+ account_body = self.create_account_request_body()
+ try:
+ response = self.get_method('accounts', 'create_or_update')(body=account_body,
+ resource_group_name=self.parameters['resource_group'],
+ account_name=self.parameters['name'])
+ while response.done() is not True:
+ response.result(10)
+ except (CloudError, AzureError) as error:
+ self.module.fail_json(msg='Error creating Azure NetApp account %s: %s'
+ % (self.parameters['name'], to_native(error)),
+ exception=traceback.format_exc())
+
+ def update_azure_netapp_account(self, modify):
+ """
+ Create an Azure NetApp Account
+ :return: None
+ """
+ account_body = self.create_account_request_body(modify)
+ try:
+ response = self.get_method('accounts', 'update')(body=account_body,
+ resource_group_name=self.parameters['resource_group'],
+ account_name=self.parameters['name'])
+ while response.done() is not True:
+ response.result(10)
+ except (CloudError, AzureError) as error:
+ self.module.fail_json(msg='Error creating Azure NetApp account %s: %s'
+ % (self.parameters['name'], to_native(error)),
+ exception=traceback.format_exc())
+
+ def delete_azure_netapp_account(self):
+ """
+ Delete an Azure NetApp Account
+ :return: None
+ """
+ try:
+ response = self.get_method('accounts', 'delete')(resource_group_name=self.parameters['resource_group'],
+ account_name=self.parameters['name'])
+ while response.done() is not True:
+ response.result(10)
+ except (CloudError, AzureError) as error:
+ self.module.fail_json(msg='Error deleting Azure NetApp account %s: %s'
+ % (self.parameters['name'], to_native(error)),
+ exception=traceback.format_exc())
+
+ def get_changes_in_ads(self, current, desired):
+ c_ads = current.get('active_directories')
+ d_ads = desired.get('active_directories')
+ if not c_ads:
+ return desired.get('active_directories'), None
+ if not d_ads:
+ return None, current.get('active_directories')
+ if len(c_ads) > 1 or len(d_ads) > 1:
+ msg = 'Error checking for AD, currently only one AD is supported.'
+ if len(c_ads) > 1:
+ msg += ' Current: %s.' % str(c_ads)
+ if len(d_ads) > 1:
+ msg += ' Desired: %s.' % str(d_ads)
+ self.module.fail_json(msg='Error checking for AD, currently only one AD is supported')
+ changed = False
+ d_ad = d_ads[0]
+ c_ad = c_ads[0]
+ for key, value in c_ad.items():
+ if key == 'password':
+ if d_ad.get(key) is None:
+ continue
+ self.warnings.append("module is not idempotent if 'password:' is present")
+ if d_ad.get(key) is None:
+ d_ad[key] = value
+ elif d_ad.get(key) != value:
+ changed = True
+ self.debug.append("key: %s, value %s" % (key, value))
+ if changed:
+ return [d_ad], None
+ return None, None
+
+ def exec_module(self, **kwargs):
+
+ # unlikely
+ self.fail_when_import_errors(IMPORT_ERRORS, HAS_AZURE_MGMT_NETAPP)
+
+ # set up parameters according to our initial list
+ for key in list(self.module_arg_spec):
+ self.parameters[key] = kwargs[key]
+ # and common parameter
+ for key in ['tags']:
+ if key in kwargs:
+ self.parameters[key] = kwargs[key]
+
+ current = self.get_azure_netapp_account()
+ modify = None
+ cd_action = self.na_helper.get_cd_action(current, self.parameters)
+ self.debug.append('current: %s' % str(current))
+ if current is not None and cd_action is None:
+ ads_to_add, ads_to_delete = self.get_changes_in_ads(current, self.parameters)
+ self.parameters.pop('active_directories', None)
+ if ads_to_add:
+ self.parameters['active_directories'] = ads_to_add
+ if ads_to_delete:
+ self.module.fail_json(msg="Error: API does not support unjoining an AD", debug=self.debug)
+ modify = self.na_helper.get_modified_attributes(current, self.parameters)
+ if 'tags' in modify:
+ dummy, modify['tags'] = self.update_tags(current.get('tags'))
+
+ if self.na_helper.changed:
+ if self.module.check_mode:
+ pass
+ else:
+ if cd_action == 'create':
+ self.create_azure_netapp_account()
+ elif cd_action == 'delete':
+ self.delete_azure_netapp_account()
+ elif modify:
+ self.update_azure_netapp_account(modify)
+ results = dict(
+ changed=self.na_helper.changed,
+ modify=modify
+ )
+ if self.warnings:
+ results['warnings'] = self.warnings
+ if self.parameters['debug']:
+ results['debug'] = self.debug
+ self.module.exit_json(**results)
+
+
+def main():
+ AzureRMNetAppAccount()
+
+
+if __name__ == '__main__':
+ main()
diff --git a/ansible_collections/netapp/azure/plugins/modules/azure_rm_netapp_capacity_pool.py b/ansible_collections/netapp/azure/plugins/modules/azure_rm_netapp_capacity_pool.py
new file mode 100644
index 000000000..9d099a03f
--- /dev/null
+++ b/ansible_collections/netapp/azure/plugins/modules/azure_rm_netapp_capacity_pool.py
@@ -0,0 +1,259 @@
+#!/usr/bin/python
+#
+# (c) 2019, NetApp, Inc
+# GNU General Public License v3.0+
+# (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+"""
+azure_rm_netapp_capacity_pool
+"""
+
+from __future__ import absolute_import, division, print_function
+__metaclass__ = type
+
+DOCUMENTATION = '''
+---
+module: azure_rm_netapp_capacity_pool
+
+short_description: Manage NetApp Azure Files capacity pool
+version_added: 19.10.0
+author: NetApp Ansible Team (@carchi8py) <ng-ansibleteam@netapp.com>
+
+description:
+ - Create and delete NetApp Azure capacity pool.
+ Provide the Resource group name for the capacity pool to be created.
+ - Resize NetApp Azure capacity pool
+extends_documentation_fragment:
+ - netapp.azure.azure
+ - netapp.azure.azure_tags
+ - netapp.azure.netapp.azure_rm_netapp
+
+options:
+ name:
+ description:
+ - The name of the capacity pool.
+ required: true
+ type: str
+ account_name:
+ description:
+ - The name of the NetApp account.
+ required: true
+ type: str
+ location:
+ description:
+ - Resource location.
+ - Required for create.
+ type: str
+ size:
+ description:
+ - Provisioned size of the pool (in chunks). Allowed values are in 4TiB chunks.
+ - Provide number to be multiplied to 4TiB.
+ - Required for create.
+ default: 1
+ type: int
+ service_level:
+ description:
+ - The service level of the file system.
+ - Required for create.
+ choices: ['Standard', 'Premium', 'Ultra']
+ type: str
+ version_added: "20.5.0"
+ state:
+ description:
+ - State C(present) will check that the capacity pool exists with the requested configuration.
+ - State C(absent) will delete the capacity pool.
+ default: present
+ choices: ['present', 'absent']
+ type: str
+
+'''
+EXAMPLES = '''
+
+- name: Create Azure NetApp capacity pool
+ netapp.azure.azure_rm_netapp_capacity_pool:
+ resource_group: myResourceGroup
+ account_name: tests-netapp
+ name: tests-pool
+ location: eastus
+ size: 2
+ service_level: Standard
+
+- name: Resize Azure NetApp capacity pool
+ netapp.azure.azure_rm_netapp_capacity_pool:
+ resource_group: myResourceGroup
+ account_name: tests-netapp
+ name: tests-pool
+ location: eastus
+ size: 3
+ service_level: Standard
+
+- name: Delete Azure NetApp capacity pool
+ netapp.azure.azure_rm_netapp_capacity_pool:
+ state: absent
+ resource_group: myResourceGroup
+ account_name: tests-netapp
+ name: tests-pool
+
+'''
+
+RETURN = '''
+'''
+
+import traceback
+
+AZURE_OBJECT_CLASS = 'NetAppAccount'
+HAS_AZURE_MGMT_NETAPP = False
+IMPORT_ERRORS = list()
+SIZE_POOL = 4398046511104
+
+try:
+ from msrestazure.azure_exceptions import CloudError
+ from azure.core.exceptions import AzureError, ResourceNotFoundError
+except ImportError as exc:
+ IMPORT_ERRORS.append(str(exc))
+
+try:
+ from azure.mgmt.netapp.models import CapacityPool
+ HAS_AZURE_MGMT_NETAPP = True
+except ImportError as exc:
+ IMPORT_ERRORS.append(str(exc))
+
+from ansible.module_utils.basic import to_native
+from ansible_collections.netapp.azure.plugins.module_utils.azure_rm_netapp_common import AzureRMNetAppModuleBase
+from ansible_collections.netapp.azure.plugins.module_utils.netapp_module import NetAppModule
+
+
+class AzureRMNetAppCapacityPool(AzureRMNetAppModuleBase):
+ """ create, modify, delete a capacity pool """
+ def __init__(self):
+
+ self.module_arg_spec = dict(
+ resource_group=dict(type='str', required=True),
+ name=dict(type='str', required=True),
+ account_name=dict(type='str', required=True),
+ location=dict(type='str', required=False),
+ state=dict(choices=['present', 'absent'], default='present', type='str'),
+ size=dict(type='int', required=False, default=1),
+ service_level=dict(type='str', required=False, choices=['Standard', 'Premium', 'Ultra']),
+ )
+
+ self.na_helper = NetAppModule()
+ self.parameters = dict()
+
+ # import errors are handled in AzureRMModuleBase
+ super(AzureRMNetAppCapacityPool, self).__init__(derived_arg_spec=self.module_arg_spec,
+ required_if=[('state', 'present', ['location', 'service_level'])],
+ supports_check_mode=True)
+
+ def get_azure_netapp_capacity_pool(self):
+ """
+ Returns capacity pool object for an existing pool
+ Return None if capacity pool does not exist
+ """
+ try:
+ capacity_pool_get = self.netapp_client.pools.get(self.parameters['resource_group'],
+ self.parameters['account_name'], self.parameters['name'])
+ except (CloudError, ResourceNotFoundError): # capacity pool does not exist
+ return None
+ return capacity_pool_get
+
+ def create_azure_netapp_capacity_pool(self):
+ """
+ Create a capacity pool for the given Azure NetApp Account
+ :return: None
+ """
+ options = self.na_helper.get_not_none_values_from_dict(self.parameters, ['location', 'service_level', 'size', 'tags'])
+ capacity_pool_body = CapacityPool(**options)
+ try:
+ response = self.get_method('pools', 'create_or_update')(body=capacity_pool_body, resource_group_name=self.parameters['resource_group'],
+ account_name=self.parameters['account_name'],
+ pool_name=self.parameters['name'])
+ while response.done() is not True:
+ response.result(10)
+
+ except (CloudError, AzureError) as error:
+ self.module.fail_json(msg='Error creating capacity pool %s for Azure NetApp account %s: %s'
+ % (self.parameters['name'], self.parameters['account_name'], to_native(error)),
+ exception=traceback.format_exc())
+
+ def modify_azure_netapp_capacity_pool(self, modify):
+ """
+ Modify a capacity pool for the given Azure NetApp Account
+ :return: None
+ """
+ options = self.na_helper.get_not_none_values_from_dict(self.parameters, ['location', 'service_level', 'size', 'tags'])
+ capacity_pool_body = CapacityPool(**options)
+ try:
+ response = self.get_method('pools', 'update')(body=capacity_pool_body, resource_group_name=self.parameters['resource_group'],
+ account_name=self.parameters['account_name'],
+ pool_name=self.parameters['name'])
+ while response.done() is not True:
+ response.result(10)
+
+ except (CloudError, AzureError) as error:
+ self.module.fail_json(msg='Error modifying capacity pool %s for Azure NetApp account %s: %s'
+ % (self.parameters['name'], self.parameters['account_name'], to_native(error)),
+ exception=traceback.format_exc())
+
+ def delete_azure_netapp_capacity_pool(self):
+ """
+ Delete a capacity pool for the given Azure NetApp Account
+ :return: None
+ """
+ try:
+ response = self.get_method('pools', 'delete')(resource_group_name=self.parameters['resource_group'],
+ account_name=self.parameters['account_name'], pool_name=self.parameters['name'])
+ while response.done() is not True:
+ response.result(10)
+
+ except (CloudError, AzureError) as error:
+ self.module.fail_json(msg='Error deleting capacity pool %s for Azure NetApp account %s: %s'
+ % (self.parameters['name'], self.parameters['name'], to_native(error)),
+ exception=traceback.format_exc())
+
+ def exec_module(self, **kwargs):
+
+ # unlikely
+ self.fail_when_import_errors(IMPORT_ERRORS, HAS_AZURE_MGMT_NETAPP)
+
+ # set up parameters according to our initial list
+ for key in list(self.module_arg_spec):
+ self.parameters[key] = kwargs[key]
+ # and common parameter
+ for key in ['tags']:
+ if key in kwargs:
+ self.parameters[key] = kwargs[key]
+ if 'size' in self.parameters:
+ self.parameters['size'] *= SIZE_POOL
+
+ modify = {}
+ current = self.get_azure_netapp_capacity_pool()
+ cd_action = self.na_helper.get_cd_action(current, self.parameters)
+ if cd_action is None and self.parameters['state'] == 'present':
+ current = vars(current)
+ # get_azure_netapp_capacity_pool() returns pool name with account name appended in front of it like 'account/pool'
+ current['name'] = self.parameters['name']
+ modify = self.na_helper.get_modified_attributes(current, self.parameters)
+ if 'tags' in modify:
+ dummy, modify['tags'] = self.update_tags(current.get('tags'))
+
+ if self.na_helper.changed:
+ if self.module.check_mode:
+ pass
+ else:
+ if cd_action == 'create':
+ self.create_azure_netapp_capacity_pool()
+ elif cd_action == 'delete':
+ self.delete_azure_netapp_capacity_pool()
+ elif modify:
+ self.modify_azure_netapp_capacity_pool(modify)
+
+ self.module.exit_json(changed=self.na_helper.changed, modify=modify)
+
+
+def main():
+ AzureRMNetAppCapacityPool()
+
+
+if __name__ == '__main__':
+ main()
diff --git a/ansible_collections/netapp/azure/plugins/modules/azure_rm_netapp_snapshot.py b/ansible_collections/netapp/azure/plugins/modules/azure_rm_netapp_snapshot.py
new file mode 100644
index 000000000..212f10861
--- /dev/null
+++ b/ansible_collections/netapp/azure/plugins/modules/azure_rm_netapp_snapshot.py
@@ -0,0 +1,226 @@
+#!/usr/bin/python
+#
+# (c) 2019, NetApp, Inc
+# GNU General Public License v3.0+
+# (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+"""
+azure_rm_netapp_snapshot
+"""
+
+from __future__ import absolute_import, division, print_function
+__metaclass__ = type
+
+DOCUMENTATION = '''
+---
+module: azure_rm_netapp_snapshot
+
+short_description: Manage NetApp Azure Files Snapshot
+version_added: 19.10.0
+author: NetApp Ansible Team (@carchi8py) <ng-ansibleteam@netapp.com>
+
+description:
+ - Create and delete NetApp Azure Snapshot.
+extends_documentation_fragment:
+ - netapp.azure.azure
+ - netapp.azure.netapp.azure_rm_netapp
+
+options:
+ name:
+ description:
+ - The name of the snapshot.
+ required: true
+ type: str
+ volume_name:
+ description:
+ - The name of the volume.
+ required: true
+ type: str
+ pool_name:
+ description:
+ - The name of the capacity pool.
+ required: true
+ type: str
+ account_name:
+ description:
+ - The name of the NetApp account.
+ required: true
+ type: str
+ location:
+ description:
+ - Resource location.
+ - Required for create.
+ type: str
+ state:
+ description:
+ - State C(present) will check that the snapshot exists with the requested configuration.
+ - State C(absent) will delete the snapshot.
+ default: present
+ choices:
+ - absent
+ - present
+ type: str
+
+'''
+EXAMPLES = '''
+
+- name: Create Azure NetApp Snapshot
+ netapp.azure.azure_rm_netapp_snapshot:
+ resource_group: myResourceGroup
+ account_name: tests-netapp
+ pool_name: tests-pool
+ volume_name: tests-volume2
+ name: tests-snapshot
+ location: eastus
+
+- name: Delete Azure NetApp Snapshot
+ netapp.azure.azure_rm_netapp_snapshot:
+ state: absent
+ resource_group: myResourceGroup
+ account_name: tests-netapp
+ pool_name: tests-pool
+ volume_name: tests-volume2
+ name: tests-snapshot
+
+'''
+
+RETURN = '''
+'''
+
+import traceback
+
+AZURE_OBJECT_CLASS = 'NetAppAccount'
+HAS_AZURE_MGMT_NETAPP = False
+IMPORT_ERRORS = list()
+
+try:
+ from msrestazure.azure_exceptions import CloudError
+ from azure.core.exceptions import AzureError, ResourceNotFoundError
+except ImportError as exc:
+ IMPORT_ERRORS.append(str(exc))
+
+try:
+ from azure.mgmt.netapp.models import Snapshot
+ HAS_AZURE_MGMT_NETAPP = True
+except ImportError as exc:
+ IMPORT_ERRORS.append(str(exc))
+
+from ansible.module_utils.basic import to_native
+from ansible_collections.netapp.azure.plugins.module_utils.azure_rm_netapp_common import AzureRMNetAppModuleBase
+from ansible_collections.netapp.azure.plugins.module_utils.netapp_module import NetAppModule
+
+
+class AzureRMNetAppSnapshot(AzureRMNetAppModuleBase):
+ """ crate or delete snapshots """
+ def __init__(self):
+
+ self.module_arg_spec = dict(
+ resource_group=dict(type='str', required=True),
+ name=dict(type='str', required=True),
+ volume_name=dict(type='str', required=True),
+ pool_name=dict(type='str', required=True),
+ account_name=dict(type='str', required=True),
+ location=dict(type='str', required=False),
+ state=dict(choices=['present', 'absent'], default='present', type='str')
+ )
+ self.na_helper = NetAppModule()
+ self.parameters = dict()
+
+ # import errors are handled in AzureRMModuleBase
+ super(AzureRMNetAppSnapshot, self).__init__(derived_arg_spec=self.module_arg_spec,
+ required_if=[('state', 'present', ['location'])],
+ supports_check_mode=True,
+ supports_tags=False)
+
+ def get_azure_netapp_snapshot(self):
+ """
+ Returns snapshot object for an existing snapshot
+ Return None if snapshot does not exist
+ """
+ try:
+ snapshot_get = self.netapp_client.snapshots.get(self.parameters['resource_group'], self.parameters['account_name'],
+ self.parameters['pool_name'], self.parameters['volume_name'],
+ self.parameters['name'])
+ except (CloudError, ResourceNotFoundError): # snapshot does not exist
+ return None
+ return snapshot_get
+
+ def create_azure_netapp_snapshot(self):
+ """
+ Create a snapshot for the given Azure NetApp Account
+ :return: None
+ """
+ kw_args = dict(
+ resource_group_name=self.parameters['resource_group'],
+ account_name=self.parameters['account_name'],
+ pool_name=self.parameters['pool_name'],
+ volume_name=self.parameters['volume_name'],
+ snapshot_name=self.parameters['name']
+ )
+ if self.new_style:
+ kw_args['body'] = Snapshot(
+ location=self.parameters['location']
+ )
+ else:
+ kw_args['location'] = self.parameters['location']
+ try:
+ result = self.get_method('snapshots', 'create')(**kw_args)
+ # waiting till the status turns Succeeded
+ while result.done() is not True:
+ result.result(10)
+
+ except (CloudError, AzureError) as error:
+ self.module.fail_json(msg='Error creating snapshot %s for Azure NetApp account %s: %s'
+ % (self.parameters['name'], self.parameters['account_name'], to_native(error)),
+ exception=traceback.format_exc())
+
+ def delete_azure_netapp_snapshot(self):
+ """
+ Delete a snapshot for the given Azure NetApp Account
+ :return: None
+ """
+ try:
+ result = self.get_method('snapshots', 'delete')(resource_group_name=self.parameters['resource_group'],
+ account_name=self.parameters['account_name'],
+ pool_name=self.parameters['pool_name'],
+ volume_name=self.parameters['volume_name'],
+ snapshot_name=self.parameters['name'])
+ # waiting till the status turns Succeeded
+ while result.done() is not True:
+ result.result(10)
+
+ except (CloudError, AzureError) as error:
+ self.module.fail_json(msg='Error deleting snapshot %s for Azure NetApp account %s: %s'
+ % (self.parameters['name'], self.parameters['account_name'], to_native(error)),
+ exception=traceback.format_exc())
+
+ def exec_module(self, **kwargs):
+
+ # unlikely
+ self.fail_when_import_errors(IMPORT_ERRORS, HAS_AZURE_MGMT_NETAPP)
+
+ # set up parameters according to our initial list
+ for key in list(self.module_arg_spec):
+ self.parameters[key] = kwargs[key]
+
+ current = self.get_azure_netapp_snapshot()
+ cd_action = self.na_helper.get_cd_action(current, self.parameters)
+
+ if self.na_helper.changed:
+ if self.module.check_mode:
+ pass
+ else:
+ if cd_action == 'create':
+ self.create_azure_netapp_snapshot()
+ elif cd_action == 'delete':
+ self.delete_azure_netapp_snapshot()
+
+ self.module.exit_json(changed=self.na_helper.changed)
+
+
+def main():
+ AzureRMNetAppSnapshot()
+
+
+if __name__ == '__main__':
+ main()
diff --git a/ansible_collections/netapp/azure/plugins/modules/azure_rm_netapp_volume.py b/ansible_collections/netapp/azure/plugins/modules/azure_rm_netapp_volume.py
new file mode 100644
index 000000000..487787ee7
--- /dev/null
+++ b/ansible_collections/netapp/azure/plugins/modules/azure_rm_netapp_volume.py
@@ -0,0 +1,399 @@
+#!/usr/bin/python
+#
+# (c) 2019, NetApp, Inc
+# GNU General Public License v3.0+
+# (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+'''
+azure_rm_netapp_volume
+'''
+
+from __future__ import absolute_import, division, print_function
+__metaclass__ = type
+
+DOCUMENTATION = '''
+---
+module: azure_rm_netapp_volume
+
+short_description: Manage NetApp Azure Files Volume
+version_added: 19.10.0
+author: NetApp Ansible Team (@carchi8py) <ng-ansibleteam@netapp.com>
+
+description:
+ - Create and delete NetApp Azure volume.
+extends_documentation_fragment:
+ - netapp.azure.azure
+ - netapp.azure.azure_tags
+ - netapp.azure.netapp.azure_rm_netapp
+
+options:
+ name:
+ description:
+ - The name of the volume.
+ required: true
+ type: str
+ file_path:
+ description:
+ - A unique file path for the volume. Used when creating mount targets.
+ type: str
+ pool_name:
+ description:
+ - The name of the capacity pool.
+ required: true
+ type: str
+ account_name:
+ description:
+ - The name of the NetApp account.
+ required: true
+ type: str
+ location:
+ description:
+ - Resource location.
+ - Required for create.
+ type: str
+ subnet_name:
+ description:
+ - Azure resource name for a delegated subnet. Must have the delegation Microsoft.NetApp/volumes.
+ - Provide name of the subnet ID.
+ - Required for create.
+ type: str
+ aliases: ['subnet_id']
+ version_added: 21.1.0
+ virtual_network:
+ description:
+ - The name of the virtual network required for the subnet to create a volume.
+ - Required for create.
+ type: str
+ service_level:
+ description:
+ - The service level of the file system.
+ - default is Premium.
+ type: str
+ choices: ['Premium', 'Standard', 'Ultra']
+ vnet_resource_group_for_subnet:
+ description:
+ - Only required if virtual_network to be used is of different resource_group.
+ - Name of the resource group for virtual_network and subnet_name to be used.
+ type: str
+ version_added: "20.5.0"
+ size:
+ description:
+ - Provisioned size of the volume (in GiB).
+ - Minimum size is 100 GiB. Upper limit is 100TiB
+ - default is 100GiB.
+ version_added: "20.5.0"
+ type: int
+ protocol_types:
+ description:
+ - Protocol types - NFSv3, NFSv4.1, CIFS (for SMB).
+ type: list
+ elements: str
+ version_added: 21.2.0
+ state:
+ description:
+ - State C(present) will check that the volume exists with the requested configuration.
+ - State C(absent) will delete the volume.
+ default: present
+ choices: ['present', 'absent']
+ type: str
+ feature_flags:
+ description:
+ - Enable or disable a new feature.
+ - This can be used to enable an experimental feature or disable a new feature that breaks backward compatibility.
+ - Supported keys and values are subject to change without notice. Unknown keys are ignored.
+ type: dict
+ version_added: 21.9.0
+notes:
+ - feature_flags is setting ignore_change_ownership_mode to true by default to bypass a 'change ownership mode' issue with azure-mgmt-netapp 4.0.0.
+'''
+EXAMPLES = '''
+
+- name: Create Azure NetApp volume
+ netapp.azure.azure_rm_netapp_volume:
+ resource_group: myResourceGroup
+ account_name: tests-netapp
+ pool_name: tests-pool
+ name: tests-volume2
+ location: eastus
+ file_path: tests-volume2
+ virtual_network: myVirtualNetwork
+ vnet_resource_group_for_subnet: myVirtualNetworkResourceGroup
+ subnet_name: test
+ service_level: Ultra
+ size: 100
+
+- name: Delete Azure NetApp volume
+ netapp.azure.azure_rm_netapp_volume:
+ state: absent
+ resource_group: myResourceGroup
+ account_name: tests-netapp
+ pool_name: tests-pool
+ name: tests-volume2
+
+'''
+
+RETURN = '''
+mount_path:
+ description: Returns mount_path of the Volume
+ returned: always
+ type: str
+
+'''
+
+import traceback
+
+AZURE_OBJECT_CLASS = 'NetAppAccount'
+HAS_AZURE_MGMT_NETAPP = False
+IMPORT_ERRORS = []
+ONE_GIB = 1073741824
+
+try:
+ from msrestazure.azure_exceptions import CloudError
+ from msrest.exceptions import ValidationError
+ from azure.core.exceptions import AzureError, ResourceNotFoundError
+except ImportError as exc:
+ IMPORT_ERRORS.append(str(exc))
+
+try:
+ from azure.mgmt.netapp.models import Volume, ExportPolicyRule, VolumePropertiesExportPolicy, VolumePatch
+ HAS_AZURE_MGMT_NETAPP = True
+except ImportError as exc:
+ IMPORT_ERRORS.append(str(exc))
+
+from ansible.module_utils.basic import to_native
+from ansible_collections.netapp.azure.plugins.module_utils.azure_rm_netapp_common import AzureRMNetAppModuleBase
+from ansible_collections.netapp.azure.plugins.module_utils.netapp_module import NetAppModule
+
+
+class AzureRMNetAppVolume(AzureRMNetAppModuleBase):
+ ''' create or delete a volume '''
+
+ def __init__(self):
+
+ self.module_arg_spec = dict(
+ resource_group=dict(type='str', required=True),
+ name=dict(type='str', required=True),
+ file_path=dict(type='str', required=False),
+ pool_name=dict(type='str', required=True),
+ account_name=dict(type='str', required=True),
+ location=dict(type='str', required=False),
+ state=dict(choices=['present', 'absent'], default='present', type='str'),
+ subnet_name=dict(type='str', required=False, aliases=['subnet_id']),
+ virtual_network=dict(type='str', required=False),
+ size=dict(type='int', required=False),
+ vnet_resource_group_for_subnet=dict(type='str', required=False),
+ service_level=dict(type='str', required=False, choices=['Premium', 'Standard', 'Ultra']),
+ protocol_types=dict(type='list', elements='str'),
+ feature_flags=dict(type='dict')
+ )
+ self.na_helper = NetAppModule()
+ self.parameters = {}
+
+ # import errors are handled in AzureRMModuleBase
+ super(AzureRMNetAppVolume, self).__init__(derived_arg_spec=self.module_arg_spec,
+ required_if=[('state', 'present', ['location', 'file_path', 'subnet_name', 'virtual_network']),
+ ],
+ supports_check_mode=True)
+
+ @staticmethod
+ def dict_from_volume_object(volume_object):
+
+ def replace_list_of_objects_with_list_of_dicts(adict, key):
+ if adict.get(key):
+ adict[key] = [vars(x) for x in adict[key]]
+
+ current_dict = vars(volume_object)
+ attr = 'subnet_id'
+ if attr in current_dict:
+ current_dict['subnet_name'] = current_dict.pop(attr).split('/')[-1]
+ attr = 'mount_targets'
+ replace_list_of_objects_with_list_of_dicts(current_dict, attr)
+ attr = 'export_policy'
+ if current_dict.get(attr):
+ attr_dict = vars(current_dict[attr])
+ replace_list_of_objects_with_list_of_dicts(attr_dict, 'rules')
+ current_dict[attr] = attr_dict
+ return current_dict
+
+ def get_azure_netapp_volume(self):
+ """
+ Returns volume object for an existing volume
+ Return None if volume does not exist
+ """
+ try:
+ volume_get = self.netapp_client.volumes.get(self.parameters['resource_group'], self.parameters['account_name'],
+ self.parameters['pool_name'], self.parameters['name'])
+ except (CloudError, ResourceNotFoundError): # volume does not exist
+ return None
+ return self.dict_from_volume_object(volume_get)
+
+ def get_export_policy_rules(self):
+ # ExportPolicyRule(rule_index: int=None, unix_read_only: bool=None, unix_read_write: bool=None,
+ # kerberos5_read_only: bool=False, kerberos5_read_write: bool=False, kerberos5i_read_only: bool=False,
+ # kerberos5i_read_write: bool=False, kerberos5p_read_only: bool=False, kerberos5p_read_write: bool=False,
+ # cifs: bool=None, nfsv3: bool=None, nfsv41: bool=None, allowed_clients: str=None, has_root_access: bool=True
+ ptypes = self.parameters.get('protocol_types')
+ if ptypes is None:
+ return None
+ ptypes = [x.lower() for x in ptypes]
+ if 'nfsv4.1' in ptypes:
+ ptypes.append('nfsv41')
+ # only create a policy when NFSv4 is used (for now)
+ if 'nfsv41' not in ptypes:
+ return None
+ options = dict(
+ rule_index=1,
+ allowed_clients='0.0.0.0/0',
+ unix_read_write=True)
+ if self.has_feature('ignore_change_ownership_mode') and self.sdk_version >= '4.0.0':
+ # https://github.com/Azure/azure-sdk-for-python/issues/20356
+ options['chown_mode'] = None
+ for protocol in ('cifs', 'nfsv3', 'nfsv41'):
+ options[protocol] = protocol in ptypes
+ return VolumePropertiesExportPolicy(rules=[ExportPolicyRule(**options)])
+
+ def create_azure_netapp_volume(self):
+ """
+ Create a volume for the given Azure NetApp Account
+ :return: None
+ """
+ options = self.na_helper.get_not_none_values_from_dict(self.parameters, ['protocol_types', 'service_level', 'tags', 'usage_threshold'])
+ rules = self.get_export_policy_rules()
+ if rules is not None:
+ # TODO: other options to expose ?
+ # options['throughput_mibps'] = 1.6
+ # options['encryption_key_source'] = 'Microsoft.NetApp'
+ # options['security_style'] = 'Unix'
+ # options['unix_permissions'] = '0770'
+ # required for NFSv4
+ options['export_policy'] = rules
+ subnet_id = '/subscriptions/%s/resourceGroups/%s/providers/Microsoft.Network/virtualNetworks/%s/subnets/%s'\
+ % (self.azure_auth.subscription_id,
+ self.parameters['resource_group'] if self.parameters.get('vnet_resource_group_for_subnet') is None
+ else self.parameters['vnet_resource_group_for_subnet'],
+ self.parameters['virtual_network'],
+ self.parameters['subnet_name'])
+ volume_body = Volume(
+ location=self.parameters['location'],
+ creation_token=self.parameters['file_path'],
+ subnet_id=subnet_id,
+ **options
+ )
+ try:
+ result = self.get_method('volumes', 'create_or_update')(body=volume_body, resource_group_name=self.parameters['resource_group'],
+ account_name=self.parameters['account_name'],
+ pool_name=self.parameters['pool_name'], volume_name=self.parameters['name'])
+ # waiting till the status turns Succeeded
+ while result.done() is not True:
+ result.result(10)
+ except (CloudError, ValidationError, AzureError) as error:
+ self.module.fail_json(msg='Error creating volume %s for Azure NetApp account %s and subnet ID %s: %s'
+ % (self.parameters['name'], self.parameters['account_name'], subnet_id, to_native(error)),
+ exception=traceback.format_exc())
+
+ def modify_azure_netapp_volume(self):
+ """
+ Modify a volume for the given Azure NetApp Account
+ :return: None
+ """
+ options = self.na_helper.get_not_none_values_from_dict(self.parameters, ['tags', 'usage_threshold'])
+ volume_body = VolumePatch(
+ **options
+ )
+ try:
+ result = self.get_method('volumes', 'update')(body=volume_body, resource_group_name=self.parameters['resource_group'],
+ account_name=self.parameters['account_name'],
+ pool_name=self.parameters['pool_name'], volume_name=self.parameters['name'])
+ # waiting till the status turns Succeeded
+ while result.done() is not True:
+ result.result(10)
+ except (CloudError, ValidationError, AzureError) as error:
+ self.module.fail_json(msg='Error modifying volume %s for Azure NetApp account %s: %s'
+ % (self.parameters['name'], self.parameters['account_name'], to_native(error)),
+ exception=traceback.format_exc())
+
+ def delete_azure_netapp_volume(self):
+ """
+ Delete a volume for the given Azure NetApp Account
+ :return: None
+ """
+ try:
+ result = self.get_method('volumes', 'delete')(resource_group_name=self.parameters['resource_group'],
+ account_name=self.parameters['account_name'],
+ pool_name=self.parameters['pool_name'], volume_name=self.parameters['name'])
+ # waiting till the status turns Succeeded
+ while result.done() is not True:
+ result.result(10)
+ except (CloudError, AzureError) as error:
+ self.module.fail_json(msg='Error deleting volume %s for Azure NetApp account %s: %s'
+ % (self.parameters['name'], self.parameters['account_name'], to_native(error)),
+ exception=traceback.format_exc())
+
+ def validate_modify(self, modify, current):
+ disallowed = dict(modify)
+ disallowed.pop('tags', None)
+ disallowed.pop('usage_threshold', None)
+ if disallowed:
+ self.module.fail_json(msg="Error: the following properties cannot be modified: %s. Current: %s" % (repr(disallowed), repr(current)))
+
+ def exec_module(self, **kwargs):
+
+ # unlikely
+ self.fail_when_import_errors(IMPORT_ERRORS, HAS_AZURE_MGMT_NETAPP)
+
+ # set up parameters according to our initial list
+ for key in list(self.module_arg_spec):
+ self.parameters[key] = kwargs[key]
+ # and common parameter
+ for key in ['tags']:
+ if key in kwargs:
+ self.parameters[key] = kwargs[key]
+
+ # API is using 'usage_threshold' for 'size', and the unit is bytes
+ if self.parameters.get('size') is not None:
+ self.parameters['usage_threshold'] = ONE_GIB * self.parameters.pop('size')
+
+ modify = None
+ current = self.get_azure_netapp_volume()
+ cd_action = self.na_helper.get_cd_action(current, self.parameters)
+ if cd_action is None and current:
+ # ignore change in name
+ name = current.pop('name', None)
+ modify = self.na_helper.get_modified_attributes(current, self.parameters)
+ if name is not None:
+ current['name'] = name
+ if 'tags' in modify:
+ dummy, modify['tags'] = self.update_tags(current.get('tags'))
+ self.validate_modify(modify, current)
+
+ if self.na_helper.changed and not self.module.check_mode:
+ if cd_action == 'create':
+ self.create_azure_netapp_volume()
+ elif cd_action == 'delete':
+ self.delete_azure_netapp_volume()
+ elif modify:
+ self.modify_azure_netapp_volume()
+
+ def get_mount_info(return_info):
+ if return_info is not None and return_info.get('mount_targets'):
+ return '%s:/%s' % (return_info['mount_targets'][0]['ip_address'], return_info['creation_token'])
+ return None
+
+ mount_info = ''
+ if self.parameters['state'] == 'present':
+ return_info = self.get_azure_netapp_volume()
+ if return_info is None and not self.module.check_mode:
+ self.module.fail_json(msg='Error: volume %s was created successfully, but cannot be found.' % self.parameters['name'])
+ mount_info = get_mount_info(return_info)
+ if mount_info is None and not self.module.check_mode:
+ self.module.fail_json(msg='Error: volume %s was created successfully, but mount target(s) cannot be found - volume details: %s.'
+ % (self.parameters['name'], str(return_info)))
+ self.module.exit_json(changed=self.na_helper.changed, mount_path=mount_info, modify=modify)
+
+
+def main():
+ AzureRMNetAppVolume()
+
+
+if __name__ == '__main__':
+ main()