diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-13 12:04:41 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-13 12:04:41 +0000 |
commit | 975f66f2eebe9dadba04f275774d4ab83f74cf25 (patch) | |
tree | 89bd26a93aaae6a25749145b7e4bca4a1e75b2be /ansible_collections/netapp/azure/plugins | |
parent | Initial commit. (diff) | |
download | ansible-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')
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() |